Skip to content

Pixel Sprite Editor

An in-editor pixel art and frame animation tool for Godot 4.7. Pure GDScript, no external dependencies.

model/ Document, layer, palette, tag, selection, the store, edit
commands, autosave and playback. No editor dependencies.
paint/ Raster, flood fill, selection edits, onion skin, recent colours.
format/ Aseprite in and out, .gpl, spritesheet slicing, export.
runtime/ PixelSprite2D, PixelSpriteTexture, PixelSpriteFrames. What a
game opens; an export carries the whole of addons/jamchip/.
panel/ The tab itself.
strip/ The shell strip: the document chip and the menus.
bar/ The tool strip and the context bar.
stage/ The canvas.
lane/ The timeline.
rack/ Palette and Tags.
host/ What the plugin loads: the inspector button, the FileSystem
menu and the document previews.
icons/ The PixelSprite2D node icon.
palettes/ default.palette.tres, the palette new documents open on until
the project names its own.
tests/ Headless suite and editor layout checks. Not shipped.

Tools

Tool Key Notes
Pencil B Square or round brush, 1 to 32 pixels, no antialiasing. Pixel-perfect at size 1; Shift+click draws a line from where the last stroke ended
Eraser E Writes transparency, same brush, same pixel-perfect and Shift+click
Fill G Four connected, with tolerance and a whole-layer scope
Pick I Samples the flattened image; Scope on the context bar switches it to the layer in hand
Marquee M A rectangle. Drag inside to move, Delete to erase, Esc to drop
Lasso Q Drag a loop; what it encloses is selected
Wand W Click a colour; the patch of it under the pointer is selected, at the fill’s tolerance. Scope on the context bar takes every pixel of that colour on the layer instead
Line L Drawn with the brush. Shift snaps to flat, upright or diagonal
Rectangle R Outline or filled. Shift draws a square
Ellipse O Outline or filled. Shift draws a circle

The palette holds two colours, a foreground and a background, shown as a pair in the Palette header. The left button paints the foreground and the right button the background, with every tool: the pencil, the fill and the shapes draw with it, the eraser and the marquee do what they do with the left. X swaps the two. Left-click a swatch to set the foreground, right-click to set the background. Alt+click samples a colour with any tool, into the foreground; Alt+right-click samples into the background, and so does the picker on the right button.

The rectangle and the ellipse share one button, and so do the three selection tools. A button’s face shows the tool in hand, a right-click on it lists the others, and each key reaches its tool directly. The line has its own.

A selection is any shape, and the three tools build one between them: Shift held when a drag or a click starts adds to what is selected, Ctrl (Command on a Mac) takes away, and neither replaces it. Every edit works on the shape: move by dragging inside it with the marquee, Delete, cut, copy, paste, flip and quarter turn, and painting with any tool stays inside it. All layers on the context bar, shown with any selection tool, makes a drag move the selection on every layer of the frame at once; one Ctrl+Z puts them all back, and the outline with them.

With the pencil or the eraser at size 1, Pixel-perfect, the check item under Square and Round in the brush shape menu on the context bar, drops the corner pixel of every L a hand-drawn stroke makes, so a curve stays one pixel wide. It is on by default, applies to the two tools and no other, and a wider brush is not changed by it. Shift+click paints a straight line from the last pixel of the previous stroke on the same layer and frame to the clicked pixel, as one undo step; with no stroke before it on that cel, Shift+click is one stamp. Shift+drag is a plain stroke.

The brush size is a count of pixels, not a radius, and an even size extends right and down from the cursor. The round brush is a plus at 3 and does nothing at 1 or 2. Outlines are stamped with the brush; filled shapes ignore it. A shape is drawn from the pixel the drag started on to the one it is over, and an ellipse of a given size is the same pixels as a round brush of that size.

Fill tolerance is how far each channel may differ from the clicked pixel, out of 255. On flat palette art it does nothing until it does everything; it is for shaded art. Scope chooses between the region under the cursor and every matching pixel on the layer. A fill covers the current layer and frame only.

A selection outlives the tool that made it and bounds every painting tool until Esc drops it. Moving lifts the pixels and leaves transparency behind, and content dragged past an edge is cropped. Each drag is one undo step.

The Edit menu holds copy, cut and paste. Paste lands at the top left of the current selection, or where the block was lifted from, and composites rather than punching through. The clipboard is the editor’s own, not the system one. Flip either way keeps the marquee still. A quarter turn moves the marquee with the pixels, and undo moves it back. Entries grey out when they cannot apply.

Where it lives

The document’s name is the chip on the shell strip. Click it for the File menu. An unsaved document reads Untitled, and unsaved changes show a seed in the warning colour on the chip. The .pixel.tres suffix is left off; the full path is in the tooltip.

New, Open and the inspector button ask before replacing unsaved work. Closing the dock only hides the panel. Quitting the editor names the document in Godot’s save-before-quit dialog, and Save All writes it. A document never saved anywhere is named but left alone.

Unsaved work is also written to a recovery file every thirty seconds, or as often as the Saving page of Preferences says, under user://jamchip/autosave/. If the editor is killed, the next start lists every tab with work waiting in one dialog, by name and time, a row per tab. Recover puts it back as unsaved work on the document it belongs to, or as Untitled when that file is gone; Discard removes the file; Later leaves the rest for next time, and autosave waits until it has been answered. Saving, or discarding a document by choice, removes the file. Nothing under res:// is touched.

The editor is an EditorDock. It opens in the bottom slot, can sit in any slot or float, and the choice is remembered per user. The panel picks its arrangement from its own width. Wide is a column of tools, the canvas, then palette and tags beside it. Narrow is a row of tools above the canvas with the panels stacked under it. Crossing between the two refits the canvas; ordinary resizing leaves your zoom alone.

Controls

Action Input
Draw Left drag with the foreground colour, right drag with the background
Line from the last stroke Shift+click with the pencil or the eraser
Swap the two colours X
Pan Two-finger scroll, middle drag, or Space + drag
Zoom Pinch, Cmd/Ctrl + two-finger scroll, or mouse wheel
Zoom by steps Ctrl+- / Ctrl+=, or Zoom in / Zoom out in the View menu
Jump to a zoom level The Zoom submenu of the View menu
Fit to view Ctrl+0, the Fit button after the zoom steps, or Fit to view in the View menu
Undo / redo Ctrl+Z / Ctrl+Shift+Z (also Ctrl+Y)
Save Ctrl+S, or the File menu
Copy / cut / paste Ctrl+C / Ctrl+X / Ctrl+V, or the Edit menu
Select all / deselect Ctrl+A / Esc, or the Edit menu
Read the shortcuts F1, or Keyboard shortcuts... in the Help menu
Grid Toggle on the context bar, or Ctrl+’; off until asked for. The grid is a cell, any width by any height, and an offset, both saved with the document and set in Grid... in the document’s menu, beside Resize canvas..., and, in the same dialog, the snap setting, which is not saved
Onion skin Toggle on the frames lane beside Play, or Ctrl+;; range and strength are under View
Checkerboard Toggle on the context bar beside the grid, or Checkerboard in the View menu
Symmetry The toggle on the context bar beside the grid, which brings back the mode last picked, or Symmetry in the View menu: Off, Horizontal, Vertical or Both. Each axis is a line on the canvas; drag it to move it, Centre the axes puts it back
Constant rate Toggle on the frames lane beside the onion, or Constant rate in the Frames menu; the rate is under Rate
Preview the animation Play in the frames lane under the canvas
Resize the canvas Resize canvas... in the document’s menu

Plain two-finger scroll pans; pinch and Cmd+scroll zoom, as in the 2D editor. The shortcut dialog, the menus and the tooltips all read one table, so the dialog is always current.

Help, last on the strip, holds the shortcut list, this README, and About JamChip with the version installed.

Getting a document into a scene

PixelSprite2D is the front door. Add one from Create New Node, set its Document, and that is all. It is an AnimatedSprite2D underneath even for a still, so a second frame added later just plays. Only Document is saved with the scene; the frames are rebuilt from it. autoplay is on.

Add PixelSprite2D to scene, in the document’s menu, does the same from the editor side. A PixelSprite2D for the open document lands in the scene being edited, named, nearest-neighbour filtered and selected, in the editor’s own undo history. It is greyed out until the document has a file and there is a scene to put it in, and the entry says which is missing.

autoplay means nothing in the editor, so there are three ways to see the animation there. PixelSprite2D runs its own clock, which preview_in_editor turns off. Any AnimatedSprite2D plays from the SpriteFrames panel, which is why sprite_frames stays visible on PixelSprite2D; edits made there are overwritten the next time the document changes. An AnimationPlayer with a track on frame is the third.

The resources are for everything a sprite is not. PixelSpriteTexture is a Texture2D that is one frame of a document, kept in step with it, and PixelSpriteFrames is the SpriteFrames counterpart. Put either anywhere its type goes and set its Document. PixelSprite2D is those two assembled.

Live means the node and the panel hold the same document. Unsaved edits are visible to everything showing it, and closing the dock without saving does not put the artwork back. The document ships with the game rather than a flat PNG. Draw against these and export when the artwork is finished.

Changing a sprite’s palette while the game runs

A document is an ordinary resource, so game code can recolour one. Give recolor the palette the artwork was drawn in and the palette to draw it in now, then ask the node to build itself again:

var player := $Player/Sprite as PixelSprite2D
var summer := load("res://art/summer.palette.tres") as PixelPalette
var winter := load("res://art/winter.palette.tres") as PixelPalette
player.document.recolor(summer, winter)
player.rebuild()

Swatch by swatch: a pixel the colour of summer’s third swatch becomes winter’s third. A colour in neither palette is left where it is, and a shorter second palette leaves the tail of the first alone. Nothing in a document is indexed — cels are RGBA and the swatches sit beside them — which is why the palette the artwork was drawn in has to be handed in. The document’s own swatch list is not touched; set document.palette as well when it should carry the new colours.

rebuild() is on PixelSprite2D, and PixelSpriteTexture and PixelSpriteFrames have had it all along. All three follow a document on their own, coalesced to the next idle frame, so rebuild() is for a change made behind that — and for seeing it in the frame it was made in.

What that costs. One pass over every cel: layers times frames times the canvas, and then the node compositing each frame it draws. It is a call for the moment the colours change, not one for every frame.

The document is the loaded resource, so every node holding that .pixel.tres is recoloured with it.

A save reaches the running game. Run the game from the editor, draw in the Pixel tab, Ctrl+S: every PixelSprite2D, PixelSpriteTexture and PixelSpriteFrames holding that file redraws in place, on the animation and frame it was on, with no restart. A .pixel.tres that changes on disk some other way, a git pull or a tool writing into the project, reaches the game the same way once the editor has noticed the file. It rides the debugger connection the editor already has to the game, so an exported build carries the listener and nothing arrives. To turn it off, the project setting Jamchip ▸ Live ▸ Push To Game.

From a scene back to the artwork

Sprite2D, AnimatedSprite2D and TextureRect get an Edit in JamChip inspector button when there is something of this editor’s to open: a live resource, or a texture with a .pixel.tres beside it. For an AnimatedSprite2D that is the spritesheet its frames cut. It opens the document rather than the flattened PNG. Artwork from elsewhere opens from the FileSystem dock’s right-click menu.

Ways in from the rest of the editor

Double-click a .pixel.tres in the FileSystem dock, where it shows its first frame. Right-click any PNG or document there for Open in JamChip, which is the only way in for a PNG. Ctrl+Shift+P has JamChip: Pixel, which brings the tab forward from anywhere in the editor; the keyed actions themselves are on F1. Save All and the quit dialog reach the document too.

Exporting

Export frame as PNG... writes the current frame, and Save rewrites the PNG from that same frame whichever one is selected then. Export Animation... writes a spritesheet beside a SpriteFrames whose frames are AtlasTexture regions into it; drop the .tres onto an AnimatedSprite2D and it plays. Sheets wrap into rows and frame timings export exactly. Export Aseprite... has its own section below.

Saving rewrites what you last exported. The document remembers its export paths and Save writes them again, so a sprite on an exported PNG follows Ctrl+S. A target that has been deleted is left alone. Exporting marks the document unsaved, since the path is part of it. An exported sheet is reimported rather than rescanned, so a texture already on a node updates.

Open also accepts a PNG, as one layer on one frame, read from the file rather than the imported texture.

Both rack sections fold by their titles. Each section and the frames lane have a grip under them. Drag to give a section more room and double-click to put it back. Sizes are not remembered between sessions.

Exporting a TileSet

The grid is the tile. Set the cell to the tile size in Grid... in the document’s menu, and the offset there if the sheet has a margin. Export TileSet... refuses while the grid is one pixel.

Snap, in the same dialog, makes the selection tools work by the tile: the marquee grows a whole cell at a time, a moved selection lands a whole number of cells over, and a paste lands on the nearest cell corner. It is greyed on the pixel grid and is a mode of the view, not of the document.

File > Export TileSet... writes the current frame as a PNG and a .tileset.tres beside it: one TileSetAtlasSource over the PNG, cut at the cell from the offset, with a tile for every cell that has a pixel in it and none for a blank cell. Drop the .tres on a TileMapLayer and paint with it. Save rewrites both files from the frame they came from, the way it rewrites a PNG.

Collision, navigation and custom data are added in Godot’s TileSet editor, on the tiles the export declared, and survive a re-export: a tile still drawn keeps everything on it, a cell newly drawn gets a tile, and a cell erased loses its tile. Exporting over a set that is there asks first, and says whether it can keep the tiles or has to replace the file whole, which it does when the set’s first source is not an atlas.

A TileSet that follows the document

A TileSet does not need the PNG. Make a PixelSpriteTexture over the sheet’s document, set it as the atlas source’s Texture in Godot’s TileSet editor, and the tiles redraw as the sheet is drawn, the way a Sprite2D on the same texture does. The polygons, navigation and custom data on the tiles are the set’s and stay where they are; only the pixels under them change. An exported set takes the swap as it is: point its sources/0 texture at the live texture instead of the PNG and nothing else in the file moves.

The atlas cuts the live texture at its own Texture Region Size and Margins, which the export set from the grid; the live texture does not read the grid, so a sheet resized or regridded after the swap is set right on the atlas, not on the document. A document that shrinks leaves the tiles past its edge in the set, marked as outside the texture, until Godot’s TileSet editor is asked to clear them; nothing here removes a tile on its own.

Importing a spritesheet

Import spritesheet… replaces the open document, asking first if that loses work.

When the SpriteFrames that Export animation wrote is beside the PNG, the frames, tags and timings come back exactly. You can pick the .tres itself if the PNG was renamed. Four things do not survive: frames no tag covered, which want slicing as a grid instead; the order the tags were made in, which comes back alphabetical; a lone tag called default, which is read as untagged; and layers, since a sheet is flat.

A PNG with nothing beside it asks for a grid: cell size, margin from the top left, and spacing between cells. Frames come out left to right, top to bottom, as one untagged animation at the default hold. The readout under the fields says what the numbers come to. Import is unavailable while a cell size does not divide the sheet, with the leftover named, or while a cell is too large for a canvas, or there are more than 512 frames, or the cel memory budget is passed.

Empty cells at the end are dropped and counted. An empty cell with artwork after it is kept as a blank beat. A wholly empty sheet is refused.

An import does not adopt the sheet as an export target, so your first Ctrl+S cannot overwrite it. Export animation once and the document remembers.

Importing an Aseprite file

Import Aseprite… opens .aseprite or .ase, replacing the open document and asking first.

Layers come across with name, visibility, lock and opacity, frames with their durations, cels with position and links, the palette, and all three colour depths. Frame tags become animation tags with their direction, repeat count and colour: repeat-forever loops, repeat-N plays N times, and a black tag colour reads as none, so the row picks one.

Some things are reported in the Output panel and then approximated. Blend modes other than Normal come in as Normal. Groups are dropped and their layers come in flat with the group’s visibility and opacity folded in. Per-cel opacity is baked into the pixels. An embedded ICC profile is noted and the pixels are read as sRGB. Slices and external files are read past silently, and pixels outside the canvas are clipped.

A file is refused with the reason when it has tilemap layers or cels, when it passes the document ceilings of 512 frames, 64 layers, the canvas budget or the cel memory budget, or when it is not an Aseprite file or has been cut short.

An import does not adopt the .aseprite as an export target. Export once to make it this document’s own.

Exporting an Aseprite file

Export Aseprite… writes the open document as .aseprite or .ase. Always 32 bits per pixel. Layers go bottom to top, all Normal, a locked layer locked, with one compressed cel per painted layer per frame and a blank cel written as no cel. Frame durations are in milliseconds. Tags are inclusive at both ends with their direction and colour, loop as repeat-forever and otherwise as their repeat count, so direction and repeat survive both ways. The palette goes in both the modern and the 2015 chunk.

A round trip cannot keep sub-millisecond timing, layer opacity beyond one 255th, a tag naming frames that are gone, or the editor’s own state such as export paths and the current frame. Save rewrites this export too.

Nobody working on this can run Aseprite, so the writer is checked against the format’s documentation, the reader beside it, and published Adler-32 answers. Opening one of these in Aseprite and saying what it looks like is still somebody’s to do.

Palette

The rack holds the colour picker, the document’s swatches and a session list of recently used colours. Click a swatch to make it the foreground, right-click to make it the background; the pair at the top of the section shows both, foreground in front, with the foreground’s hex beside it, and every swatch shows its hex on hover. + adds the foreground colour and - removes the selected swatch. The options menu loads and saves .palette.tres files, imports a GIMP .gpl, loads the project default into the document, saves the document’s swatches as the project default, or clears the recent list.

A colour joins the recent list when it is painted with, not when picked. Palettes are ordinary resources and any .tres holding a PixelPalette loads. New documents open on the project’s default palette: the file the project setting jamchip/pixel/default_palette names, else the one set in your preferences, else palettes/default.palette.tres from the addon. Save palette as project default… asks where in the project to save the document’s swatches, writes them there and points the setting at the file, so the addon’s own copy is never written. Swatch edits are not undoable.

The colour picker is the wheel, the sliders and a hex field. It keeps no presets and no recent colours of its own; the swatches above it and the recent list are where colour lives.

Import GIMP palette (.gpl)… replaces the swatches with the file’s colours, which is the format Lospec palettes come in. It browses outside the project and keeps only the colours. Comments, headers, CRLF, a byte order mark and odd spacing are all fine, and an unreadable line is skipped and counted. A file not starting with GIMP Palette, one with no readable colour, or one with more than 1024 colours is refused.

Layers

The layers are the rows under the frames lane’s thumbnails, topmost first, each with its eye, its padlock and its name at the left and a dot per frame after them. Click the eye to show or hide the layer, click the padlock to lock it, click the name to select it, double-click the name to rename it in place, and drag a row onto another to reorder the stack. The + in the corner above the rows adds a layer above the selected one; so does Shift+N. Right-click a row for the rest: New layer above it, Remove layer, Move up, Move down, Rename, Visible, Locked and Opacity..., which opens a bar under the row. The bar updates live and records one undo step on release. Painting targets the selected layer and export flattens the visible ones. A hidden layer’s row draws faint. Undo and redo of a layer edit restore the selection too.

A locked layer keeps its pixels: the pencil, the eraser, the shapes, the fill, a drag of the selection, Delete, cut, paste, flip and the quarter turns all leave it alone, and the status line at the end of the strip says Locked: and the layer’s name. The picker, copy and selecting still work on it, and so do the layer commands. All layers skips a locked layer. Locking isn’t an undo step, undo never unlocks a layer, and the lock is saved with the document.

Frames

The frames lane under the canvas is the timeline: a strip of thumbnails with Play, the onion toggle and the constant rate toggle above it, and the cel grid under it, a row per layer with a dot per frame. A filled dot is a cel with something drawn on it, a hollow dot an empty one, and the accent ring is the cel in hand; click a dot to select that layer and that frame together. The lane is eighty-four units tall with one layer and grows twenty-two a layer to a hundred and sixty, past which the rows scroll. The lane’s bar folds the timeline away, and the grip under it gives the rows more room. The dashed square at the end adds an empty frame after the current one; Shift-click adds a copy. The Frames menu on the strip offers new, duplicate, remove and the two moves; a right-click on a frame offers all but new; or drag one frame onto another to reorder. Delete removes a focused frame, never the last.

Each frame holds for its own duration in milliseconds, read under its thumbnail beside the frame number and set on the Hold bar on the lane’s bar: drag it, or double-click the number to type one. Hold... in the Frames menu, or a double-click on a frame, opens that field with the keyboard in it. A frame off the default hold shows its number in the ordinary text colour.

The strip scrolls sideways and keeps the current frame in view. Thumbnails follow a stroke as it is drawn. The selected frame has an accent border, and while playing, the frame being shown has the playhead colour instead.

Onion skin ghosts the neighbouring frames under the one being drawn, the previous warm and the next cool, as flat silhouettes. Range is how many frames either side, up to eight, each fainter than the last, and strength is how solid the nearest is. The toggle is on the lane beside Play, or Ctrl+;, and the range and strength are submenus of the View menu.

Animation tags

A tag names an inclusive range of frames, and each becomes a SpriteFrames animation of that name: idle over 1 to 4, run over 5 to 10, and play("run") in the game. They are Aseprite’s frame tags, so an import loses nothing.

Tags are the second section of the rack, under Palette: one row per tag with a chip of its colour, its name, its range and, at the end of the row, how it plays (, once, 3x, ⟳ ping-pong, reverse once). Click a row to select it and it opens into its fields: the name, from and to, the direction as four toggles (forward, reverse, ping-pong, ping-pong reverse), Loops, how many times it plays when it doesn’t loop, and the colour. Click the row again to let go. The section’s + makes a new tag and opens its row, - removes the selected tag. The strip draws a band under the selected tag’s frames in its colour and nothing under the others. The fields count frames from one, matching the strip.

A new tag claims the first contiguous run of untagged frames, which on an untagged document is all of them. Names are unique and ranges may overlap. A tag with no colour picked gets one from its place in the list; set the colour’s alpha to zero to go back to that.

Direction and repeat are expanded on export and in the live SpriteFrames: a reverse tag’s animation holds its frames backwards, a ping-pong’s goes out and back, and a tag that plays three times holds three passes with loop off, so play("hop") does what the row says without the game knowing.

When frames move, a frame inserted inside a tag extends it and one inserted before shifts it. One inserted directly after its last frame leaves the tag alone. A removed frame shrinks the tag, and a tag whose only frame goes is dropped. Dragging is a removal then an insertion.

An untagged document is one animation called default over every frame, exactly as before tags existed. A node opens on the document’s first tag. Each tag is timed by its own frames, so idle and run keep their own speeds.

Playback

Play previews in the canvas at the holds you authored. Switch on the rate toggle beside the onion, or Constant rate in the Frames menu, to preview at a fixed rate instead, pick the rate under Rate, and switch it off to go back. Neither touches what you saved. With a tag selected, Play runs only that tag the way it plays, reverse or ping-pong included, and a tag that does not loop stops after its last pass. Editing stops the preview.

Capacity and resizing

Cels are held decoded, so the limit is a memory budget shared by layers and frames that scales with cel size. A 16×16 sprite holds thousands of cels and a 256×256 one a couple of hundred. The add buttons grey out with a tooltip naming the limit.

Resize crops and pads with an anchor, or resamples with nearest neighbour, across every layer and frame in one undo step. Undo restores what a crop discarded. The canvas limit is 262,144 pixels total rather than a square, with no side over 1024, so 512×512 and 1024×256 both fit and 600×600 does not. New and Resize show the running total and suggest the nearest size that fits.

Tests

The suites live under tests/ in the repository and do not ship. The repository’s own README says how to run them.

JamChip 0.1.0, by GlitterSnack. MIT licence. Built from the source on every merge, artwork and screenshots included, with JamChip.