How to cut and export a sprite sheet
Frame size, spacing, margins and offset — what each one means, why frames drift by a few pixels, and how to handle sheets that do not divide evenly.
Published
A sprite sheet is one image holding many frames. Cutting it up is arithmetic, and almost every problem people hit comes from confusing two of the four numbers involved.
The four numbers, and the formula
Per axis:
- Margin — blank border before the first frame.
- Offset — an extra shift applied after the margin, for sheets whose first frame does not start where the margin implies.
- Frame — the frame’s own size.
- Spacing — the gutter between frames. Not around them.
The position of frame n, counting from zero:
position = margin + offset + n × (frame + spacing)
Note what is multiplied by what. Spacing is multiplied by n, not n+1. A row of four frames has three gutters. Treating it as four is the single most common bug in hand-written sheet cutters, and it produces the characteristic symptom of the first frame being correct and each subsequent one drifting by one more gutter width.
Diagnosing a misaligned grid
Look at how the boxes are wrong, and the answer follows:
Every frame shifted by the same amount → margin or offset. The grid’s spacing is right; its starting point is not.
Error growing across the row → spacing. Each frame accumulates the mistake, so the last frame is worst. If frame 1 is fine and frame 8 is off by 14 pixels with 7 gutters between them, your spacing is 2 pixels out.
Boxes the wrong size entirely → frame width or height. Fix this first; everything else is measured relative to it.
Correct across, wrong down → the axes have different values. Sheets routinely have spacing on one axis and none on the other.
Work in that order — frame size, then spacing, then margin, then offset — and each step stops interfering with the next.
Why auto-detection is a suggestion
Detection generally works by finding rows and columns that are fully transparent, treating those as gutters, and measuring the content runs between them. When it works it is exact and saves real time. It fails predictably in four cases:
- Frames that touch. No gutters means nothing to measure. A single solid block is all detection can see, and it cannot tell whether that is one frame or twelve.
- A solid background colour instead of transparency. Detecting by colour sounds more general and is far less reliable, because a sprite that legitimately contains the background colour splits in half — and no algorithm can distinguish that case from a real gutter using pixel data alone.
- Sprites that do not fill their cells. A character standing in the middle of a 64-pixel cell with 10 pixels of air on each side reports a 44-pixel frame. The frames are still regular; the content is smaller than the cell.
- Irregular sheets, where frames genuinely differ in size. These need metadata, not detection.
Any tool that presents detection as an answer rather than a starting point is overselling it. Every value should stay editable, and the overlay — not the numbers — is the thing to trust.
Sheets that do not divide evenly
A 100 × 32 sheet of 32 × 32 frames gives three whole frames and 4 pixels left over. This is normal, not an error.
Two different situations hide behind a leftover:
- A remainder smaller than one frame is a trailing margin. Ignore it.
- A remainder larger than one frame means whole frames are being missed, and the row or column count is wrong.
Frames that extend past the edge should be flagged and exported at the declared size with transparent padding, not silently cropped. A frame that comes out 28 pixels wide when every other frame is 32 will misalign every animation drawn from the set, and the failure will show up somewhere far from its cause.
Animation timing
Frames per second is how many frames display each second; frame duration is its reciprocal. Common rates: 12 fps for classic hand-drawn animation, 24 for film, 30 or 60 for games. Pixel-art animation often runs much slower — 6 to 10 fps — because a small number of well-drawn frames reads better than a smooth blur.
One detail that surprises people: GIF cannot represent every frame rate. The format stores delays in hundredths of a second, so 12 fps is 8.33 hundredths and rounds to 8, which is 12.5 fps. A tool that silently rounds leaves you wondering why the exported GIF runs slightly fast. A good one shows the effective rate.
Also worth knowing: many browsers historically clamped very short GIF delays (1 or 2 hundredths) to a minimum, so anything above roughly 50 fps in a GIF is unreliable. If you need precise timing, export frames and control the timing in your engine.
Transparent backgrounds
Keep transparency, and export PNG. Two reminders:
JPEG has no alpha channel. Saving a sprite as JPEG replaces transparency with a solid colour, usually black, and adds compression artefacts around exactly the hard edges sprites are made of.
Semi-transparent edge pixels are anti-aliasing. They look fine on the background the sprite was drawn against and produce a visible halo on any other. For pixel art the usual fix is to force alpha fully on or off at a threshold.
A checkerboard behind the preview is not decoration — it is the only way to tell transparent from white, and the difference matters a great deal.
Metadata, and what “trimmed” means
Cutting frames out is half the job; your engine needs to know where they are. The widely-read shape is the TexturePacker JSON format, which Phaser, PixiJS and others read directly:
{
"frames": {
"hero_00.png": {
"frame": { "x": 0, "y": 0, "w": 32, "h": 32 },
"rotated": false,
"trimmed": false,
"spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 },
"sourceSize": { "w": 32, "h": 32 }
}
}
}
The fields that catch people out are trimmed, spriteSourceSize and
sourceSize. Trimming means stripping surrounding transparent pixels to save
atlas space, in which case sourceSize records the original dimensions and
spriteSourceSize records where the trimmed content sat inside them — so the
engine can draw it back in the right place. A tool that only cuts frames must
report trimmed: false with untrimmed values. Claiming otherwise misplaces
every sprite drawn from the file.
One more: zero-pad the frame numbers. frame_10 sorts before frame_2 in
every naive string sort, which will scramble your animation somewhere down the
line.
Repacking
Repacking into a single horizontal or vertical strip is worth doing when your
engine or CSS expects one. CSS sprite animation with steps() needs a strip
with no gutters, because the step function divides the width evenly and any
spacing shifts every frame after the first.
Whatever the layout, frames must be written with no scaling and no smoothing. Any resampling introduces blended edge pixels, and in pixel art that is immediately visible.
A working procedure
- Load the sheet and let detection try.
- Check the overlay against the sprites — trust the boxes, not the numbers.
- Fix frame size first, then spacing, then margin, then offset.
- Deselect frames you do not want; reorder if the sheet order is not the animation order.
- Preview at your intended frame rate.
- Export PNG frames plus JSON, or a GIF to show someone.
- Check the first and last frames specifically — that is where off-by-one errors surface.
Sources and further reading
- W3C — PNG specification, including the alpha channel
- GIF89a specification — Graphic Control Extension and the delay-time field
- Phaser — loading a texture atlas in the TexturePacker JSON format
- PixiJS — spritesheet format and frame metadata
- MDN — the CSS
steps()easing function, used for sprite strip animation - MDN —
image-rendering: pixelated, for scaling sprites without smoothing
Last reviewed 31 July 2026.