A sprite sheet and animation clip editor for moth::gfx. Load or import
a sprite sheet image, mark out its cells and their pivots, build animation clips from those cells, and save them as
a .mothsprite project. Games do not load project files: moth::gfx loads a sprite sheet descriptor as a
SpriteSheet.
The editor is a separate application, not a toolkit one: it depends on the toolkit's modules but is not built as part of it.
Sheet editing: draw, move and resize cells on a zoomable view of the sheet image. Select many cells at once with Ctrl+click or a selection box, and move them together.
Cell generation: add a regular grid of cells in one step, or detect cells from the image's pixels by alpha or by a background color, with a live preview before the cells are added.
Pivots: set each cell's pivot in the Pivot mode of a zoomable preview of the cell, by typing it, or with nine presets that apply to every selected cell.
Animation clips: build clips as timelines of steps, each with a cell and a duration. Reorder steps by dragging, change a step's cell by picking it on the sheet, and choose Stop, Reset or Loop playback.
Live preview: play clips inside the editor. Each step is kept on its pivot, so the animation plays in place, as it will in the application.
Undo and redo: every change to cells, pivots, clips and the sheet image can be undone. A text edit or a drag is one undo step.
Editor comforts: recent projects, an unsaved changes prompt, a dockable window layout, and a checkerboard or chosen color behind every preview to show transparency.
AI agents (primarily Claude) are used as tools in this project for tasks such as refactoring, documentation writing, and test implementation. The architecture, design decisions, and direction of the project are human-driven. This is not a vibe-coded project.
Start moth_sprite. It opens with an empty, untitled project.
The editor has four dockable windows. Close and reopen them from the Window menu. Window > Reset Layout restores the default layout and opens every window.
| Window | Content |
|---|---|
| Sheet | The sheet image with every cell drawn over it. The sheet image path with Import Spritesheet... and Export Spritesheet..., New Cell, Fit and 1:1 are above it. |
| Selected Cell | Clip playback buttons, and the prime cell (see Selecting and editing cells) with its pivot. Pivot, Fit and 1:1 are above it. With a clip selected, it previews the clip. |
| Cells | Import... (see Cells from other images), the list of cells, each with a thumbnail, its index, offset and size, and a form for the prime cell below. |
| Clips | Clip playback buttons, a new clip row, and each clip as a timeline of steps. |
In the Sheet and Selected Cell windows, the mouse wheel zooms around the cursor, Fit fits the image in the window, and 1:1 shows it at its real size.
The File menu has:
| Item | Action |
|---|---|
| New | Start an empty, untitled project. |
| Open... | Open a project file (.mothsprite), or import a sprite sheet descriptor (.json). |
| Open Recent | Open one of the last 10 projects. Clear Recent empties the list. |
| Save | Save the project. An untitled project asks for a file name first. |
| Save As... | Save the project to a new file. |
| Export... | Export the sprite sheet descriptor that games load (see Exporting) to the project's export path. The first export asks for a file name. |
| Export As... | Export to a new file name, which becomes the project's export path. |
| Exit | Quit. |
Edit > Import Spritesheet... uses a different sheet image (png, jpg, jpeg or bmp). Cells and clips are
kept. Edit > Export Spritesheet... writes the project's sheet image to another file, chosen in a save dialog. It
writes no JSON, keeps the format of the image, and the exported file becomes the project's sheet image, as one undo
step, so the project has unsaved changes. It is enabled when the project has a sheet image.
Tools > Pack... packs every cell into a new sheet image (see Packing); it is enabled when the project
has cells.
The Import Spritesheet... and Export Spritesheet... buttons in the Sheet window do the same as the two Edit items. Without a sheet image, Import Spritesheet... is the only thing in the window; with one, both are next to the image path.
A project can be saved without a sheet image. The window title shows the project's file name, and * when it has
unsaved changes. New, Open, Open Recent and quitting ask whether to save unsaved changes first.
Tools > Pack... packs every cell, as its own image, into one new image, and makes the project use it: the new image becomes the sheet image, and each cell's rectangle becomes its place in it. Cell order, sizes, pivots and clips do not change. Parts of a cell outside the sheet image are transparent in the packed image. Cells from other images are packed too, and become sheet cells.
The pack dialog has:
| Setting | Content |
|---|---|
| Packed image | The file to write. ... chooses it in a dialog. The first time, it is <project name>_packed.png beside the project (in the last image folder for an untitled project). When the file exists, the dialog warns that it will be overwritten, and Pack still writes it. This includes the sheet image or the image of a cell: undo then restores the project but not the file on disk. |
| Padding (px) | Space around each cell. |
| Padding type | How the padding is filled: Color (with Padding color, which is also the background), Extend, Mirror or Wrap. |
| Best pack | Pack into the smallest image the packer can find, from 1 x 1 up to 16384 x 16384. The size limits below are disabled and not used, but kept for when Best pack is turned off. |
| Min width, Min height, Max width, Max height | The size limits of the packed image, in powers of two. |
| Format | PNG, BMP, TGA or JPEG (with JPEG quality). Changing it changes the path's extension. |
Beside the settings, the dialog previews the packed image that the settings would produce, with its width and height. The preview updates when a setting changes, and shows the reason instead when the cells do not fit or an image cannot be read. The preview writes no files.
Pack writes the image and changes the project as one undo step. Undo restores the previous sheet image, cell rectangles, cells from other images and pack settings; the packed file stays on disk. If the cells do not fit into one image of the maximum size, or an image cannot be read or written, the dialog shows why and nothing changes. The dialog changes nothing until Pack succeeds, and the project saves the settings of its last pack.
Tools > Unpack... writes every cell as its own image file. It is enabled when the project has cells. The dialog asks for the output folder and the image format (PNG, BMP, TGA or JPEG, with the JPEG quality), and says how many files in the folder would be overwritten.
Unpack writes the files in cell order, named after the sheet image with a zero-padded cell number:
hero_000.png, hero_001.png and so on. A project with no sheet image uses the project's name, and an untitled one
uses cells. Files with those names are overwritten. Unpack changes nothing else: the cells, the sheet image and
the clips stay as they are, nothing is added to the undo history, and the project is not marked as unsaved.
If a cell has no image to read, or a size of 0, the dialog says so and no file is written. A file that cannot be written stops the unpack there, and the files written before it stay.
Games do not load project files. File > Export... writes a sprite sheet descriptor (.json) that moth::gfx loads
as a SpriteSheet. It writes no image: the descriptor's image field is the path from the descriptor's folder to
the project's sheet image, or an absolute path when there is no relative one, and the image stays where it is. Only
an export that packs first writes an image, the packed one, beside the descriptor. A descriptor already there is
overwritten. Saving the project does not export.
The project remembers its export path, relative to the project file, so Export... does not ask again. Export As... always asks. Setting a new export path is an undo step; undo does not remove exported files.
A project with cells from other images is unpacked: games need every cell on one sheet. Exporting it asks for the
descriptor path first (on the first Export, and on Export As), then opens the pack dialog with the packed image named
after the descriptor: hero.json packs to hero.png. Nothing is written until Pack succeeds; the export then
continues. Cancelling either dialog cancels the export. Problems that a pack cannot fix, such as a clip with no steps,
are reported before the file dialog opens, so a pack never runs for an export that would be refused.
Export refuses, writes no files, and lists the problems when the project has no sheet image, has a sheet image that could not be read, has no cells, has a cell with no width or height, has a clip with no steps, or has a step with a duration of 0 ms or with no cell. These are the things moth::gfx rejects or skips, so the game data never differs from the project without a warning. The same message shows when the descriptor cannot be written.
- New Cell (Sheet window): drag a rectangle on the sheet to add a cell. The mode stays on, so more cells can be drawn. Esc ends it.
- Tools > Grid Cells...: add a regular grid of cells. Set the cell size, the offset of the first cell, the spacing between cells, and the number of rows and columns. Fit to image sets the rows and columns that fit. A preview shows the grid on the sheet. Add Frames adds the cells.
- Tools > Detect Cells...: find cells from the image's pixels. Choose how the background is found: by Alpha, by the Corner color, or by a chosen Color, with a tolerance. Merge gap joins nearby shapes into one cell, the size filter drops cells that are too small or too large, and Buffer adds space around each cell. A preview shows the cells found. Add Frames adds them.
New cells have their pivot at the top-left corner.
Import... in the Cells window adds cells from image files other than the sheet (png, jpg, jpeg or bmp;
several can be chosen at once). Each image becomes one cell, the whole image, at the end of the cell list. The import
is one undo step, and works in a project with no sheet image. An image that does not load is skipped.
The image stays a separate file, and the project refers to it. Such a cell:
- is drawn from its own image in the Cells list, the Selected Cell window and the Clips window,
- shows its size and its image path in the Cells list, in place of its offset (a long path is shortened from the front),
- is not shown on the Sheet, and the sheet tools (New Cell, Grid Cells, Detect Cells, dragging and resizing on the sheet) do not change it,
- has a pivot that can be edited, and X, Y, W and H fields that are read-only (0, 0 and the image size).
If the image is missing when the project is loaded, the project still loads with a warning, and the cell shows only the preview background. Tools > Pack puts these cells on the sheet, but it cannot read a missing image: it shows an error and leaves the project unchanged. Restore the image, or delete the cell, before packing.
One or more cells can be selected. The prime cell is the last one selected. It has its own border color, the Cells form and the Selected Cell window show it, and only it has resize handles on the sheet.
On the Sheet:
- Click a cell to select only that cell. Click empty space to clear the selection.
- Ctrl+click a cell to add it to the selection, or remove it.
- Drag on empty space to select the cells that are fully inside the box. Hold Ctrl to add them to the selection.
- Drag a selected cell to move every selected cell.
- Drag an edge or a corner of the prime cell to resize it.
In the Cells list:
- Click a row to select only that cell. Ctrl+click adds or removes it. Shift+click selects every cell from the prime cell to that row.
- The x button on a row deletes that cell.
The Cells form below the list edits the prime cell's X, Y, W, H, Pivot X and Pivot Y. It is disabled while a clip plays.
Ctrl+A selects every cell. Delete deletes the selected cells. Esc clears the selection. Deleting a cell updates the clip steps that use the cells after it.
A pivot is a point relative to the cell's top-left corner. Set it in one of these ways:
- Turn on Pivot in the Selected Cell window's toolbar, then click or drag on the cell. The button is highlighted and the toolbar says what a drag does while the mode is on. The mode stays on until the button is pressed again or Esc ends it, and it is off every time the editor starts. With it off, a click on the cell does nothing, so the pivot cannot be moved by accident.
- Type it in the Cells form.
- Use the nine Pivot presets buttons in the Cells form, or Edit > Pivot. These set the pivot of every selected cell, relative to each cell's own size: top left, top center, top right, and so on to bottom right.
In the Clips window:
- + Clip adds a clip with the name typed next to it, or
clip_1,clip_2and so on when the name is empty. With more than one cell selected, the clip gets a step for each selected cell, in the order they were selected. - Each clip has a name, a loop type (Stop, Reset or Loop), its step count, a duration box with Set all to give every step that duration, and X to remove the clip.
- + Step adds the selected cells to the end of the clip, or the prime cell when one cell is selected. New steps get the duration of the clip's last step, or the duration in the Set all box when the clip has no steps. The steps of a new clip get 100 ms, the box's starting value.
- Each step shows its cell number, a thumbnail and its duration in milliseconds. Its x removes it.
- Drag a step onto another step of the same clip to move it there.
- Double-click a step, then select a cell on the sheet or in the Cells list, to change the step's cell. Esc cancels.
- Click a step to go to it. With the Clips window focused, Delete removes the current step.
More than one step of a clip can be selected. A selection is within one clip, and the selected steps are outlined in the selected cell colour:
- Click a step to select it alone, and go to it. Ctrl+click adds or removes a step, and Shift+click selects the range from the last step clicked without a modifier, as the Cells window works.
- Dragging a selected step moves every selected step to where it is dropped, keeping their order. Dragging an unselected step moves that step alone.
- The x of a selected step removes every selected step, and so does Delete while the Clips window is focused.
- Typing a duration under a selected step gives every selected step that duration. Set all still applies to every step of the clip.
A multi-step move, delete or duration change is one undo step.
Play/Pause, Step and Reset are in both the Clips and the Selected Cell windows. Playback selects each step's cell as it plays. The Selected Cell window places each step's cell on its pivot inside the clip's bounds.
Edit > Undo (Ctrl+Z) and Edit > Redo (Ctrl+Y) cover every change to cells, pivots, clips and steps, and Import Spritesheet, Export Spritesheet, Pack, and a change of the export path. Typing in a field, or a drag, is one undo step. New, Open and Open Recent clear the undo history.
Shortcuts do not work while a text field is being edited or a dialog is open.
| Shortcut | Action |
|---|---|
| Ctrl+N | New |
| Ctrl+O | Open |
| Ctrl+S | Save |
| Ctrl+Shift+S | Save As |
| Ctrl+X | Exit |
| Ctrl+Z | Undo |
| Ctrl+Y | Redo |
| Ctrl+A | Select every cell |
| Delete | Delete the selected cells, or the current clip step when the Clips window is focused |
| Esc | Cancel picking a cell for a step, else end New Cell, else end Pivot mode, else clear the selection |
Edit > Preferences sets the normal, selected and prime cell border colors, the border thickness, the preview background color and the preview filtering. A background color with alpha 0 shows a gray and white checkerboard. Preview filtering is Nearest or Linear, and applies to every preview image: the sheet, the selected cell, the cell and clip thumbnails and the pack preview. Nearest, the default, keeps the pixels of a zoomed image sharp. The filtering changes how the editor draws, nothing in the project.
The editor keeps its settings in moth_sprite.json and its window layout in imgui.ini, both in the folder it is
started from.
Help > About shows the version.
A project is a .mothsprite file with JSON content. It is the source for editing, and it keeps data that games do
not load. Games load sprite sheet descriptors, not project files.
{
"version": 1,
"image": "hero.png",
"frames": [
{ "x": 0, "y": 0, "w": 32, "h": 48, "pivot_x": 16, "pivot_y": 48 },
{ "x": 32, "y": 0, "w": 32, "h": 48, "pivot_x": 16, "pivot_y": 48 }
],
"clips": [
{
"name": "walk",
"loop": "loop",
"frames": [
{ "frame": 0, "duration_ms": 100 },
{ "frame": 1, "duration_ms": 100 }
]
}
]
}| Field | Content |
|---|---|
version |
The format version. The editor does not load a file with a version newer than it knows. |
image |
Path to the sheet image, relative to the project file. Optional: a project can have no sheet image. |
export_path |
Path of the last exported descriptor, relative to the project file. Optional. |
pack |
The settings of the last pack. Optional. image (the packed image, relative to the project file), padding, padding_type (color, extend, mirror or wrap), padding_color (RRGGBBAA hex), best_pack, min_width, min_height, max_width, max_height, format (png, bmp, tga or jpeg) and jpeg_quality. |
frames |
The cells. Each has x, y, w and h in pixels, and pivot_x and pivot_y relative to the cell's top-left corner. A cell from another image has image, its image path relative to the project file, instead of x, y, w and h. |
clips |
Each clip has a name, a loop type (stop, reset or loop), and frames: its steps. |
clips[].frames |
Each step has frame, an index into frames, and duration_ms. |
The editor writes the whole file when it saves. A project keeps what games reject: no cells, clips with no steps, and steps with a 0 ms duration are saved and loaded unchanged.
A sprite sheet descriptor is the JSON file that moth::gfx loads as a SpriteSheet, written by
File > Export. It has the image, frames and clips fields above, with no version or
export_path. Frame indices follow the project's cell order. Projects saved before the .mothsprite format are descriptors.
File > Open opens a descriptor (.json) as a new project: the project has no file name and has unsaved changes,
so the first save asks for a .mothsprite file name and never writes over the descriptor. Opened descriptors are
not added to Open Recent.
The editor reads a descriptor itself, so it opens files that games reject: it keeps clips with no steps, 0 ms steps
and steps whose cell does not exist, and it opens a descriptor whose sheet image cannot be read, with the cells,
the clips and the image path, and a warning in the log. It does not open a file it cannot parse, one with no
image field, or one with no cells. A project whose sheet image could not be read cannot be exported until the
image is there again.
moth::gfx loads a descriptor with at least one cell. It skips a clip that has no steps, or a step whose duration is 0 or whose cell does not exist.
Pre-built binaries for Windows and Linux are attached to each GitHub Release if you'd rather not build from source.
Set up a Python virtual environment and install Conan:
# Linux / macOS
python3 -m venv .venv
source .venv/bin/activate
pip install conan
# Windows (PowerShell)
python3 -m venv .venv
.\.venv\Scripts\Activate.ps1
pip install conanC++17 is required. The recipe checks for it. On Linux, Conan's detected profile already uses gnu17. On
Windows, MSVC's detected profile defaults to C++14, so pass -s compiler.cppstd=17 or set it in your Conan profile.
The app renders with Vulkan, so it also needs a Vulkan driver.
moth_sprite depends on the moth_toolkit modules moth_bridge, which
brings in moth_core, moth_graphics and moth_ui, and moth_packer, which packs cells into a new sheet image. These are published to an Artifactory remote rather than Conan
Center. Register the remote once before installing (it is publicly readable, so no login is required):
conan remote add moth https://artifactory.matthewcotton.net/artifactory/api/conan/conan-localThe file dialogs come from a git submodule. Clone with submodules, or initialise them after cloning:
git clone --recursive https://github.com/instinkt900/moth_sprite.git
# or, in an existing clone
git submodule update --initSeveral system packages are required on Linux. GTK3 is needed by nativefiledialog; GLFW, FreeType, and HarfBuzz are
pulled in transitively via moth_graphics (see the moth_toolkit README
for background on why these must come from the system).
Conan installs these through apt when you allow it to manage system packages:
conan install . -s build_type=Release --build=missing \
-c tools.system.package_manager:mode=install -c tools.system.package_manager:sudo=True
cmake --preset conan-release
cmake --build --preset conan-releaseIf you'd rather install them yourself first:
sudo apt install libgtk-3-dev libglfw3-dev libfreetype-dev libharfbuzz-devFor a Debug build, use -s build_type=Debug and the conan-debug preset. The Debug build treats warnings as errors
and runs clang-tidy when it is installed.
conan install . -s compiler.cppstd=17 -s build_type=Release --build=missing
cmake --preset conan-default
cmake --build --preset conan-releaseThe binary is in build/Release (or build/Debug). The editor reads and writes moth_sprite.json and imgui.ini in
the folder it is started from, so start it from a folder where those files belong.
| Project | Description |
|---|---|
| moth_toolkit | The modular 2D engine toolkit this editor builds against |
moth::gfx |
Vulkan-backed 2D renderer, window management, and the platform bootstrap. Loads sprite sheet descriptors as a SpriteSheet |
moth::ui |
Core UI library: node graph, keyframe animation, and event system |
moth::bridge |
Adapts moth::ui onto moth::gfx and provides the application loop |
| moth_editor | Visual layout and animation editor for moth::ui layout files |
| moth_sprite | (this project) Sprite sheet and animation clip editor |
MIT. See LICENSE.