DEV Community

WesLin
WesLin

Posted on

Build Clean Animated GIFs From Sprite Sheets With ffmpeg

TL;DR

If you point ffmpeg straight at your raw sprite frames, you get a bloated 611KB GIF that shimmers on every frame. You can fix both problems with a two-pass ffmpeg command that builds one shared palette:

# Step 1: Generate a single 64-colour palette across all frames
ffmpeg -i frame-%02d.png -vf "palettegen=max_colors=64:stats_mode=diff" -update 1 palette.png

# Step 2: Render the GIF using that palette with dithering disabled
ffmpeg -framerate 12 -i frame-%02d.png -i palette.png -lavfi "paletteuse=dither=none" run-cycle.gif
Enter fullscreen mode Exit fullscreen mode

These two steps shrink your file size by 78%. But palette work only fixes half your problem. If your animation still jitters, your cut is the reason.

The size ladder: why palettegen matters

A standard GIF supports at most 256 colours. A raw image from a model holds about 48,000 colours across subtle shifts in your background.

When you export without a custom palette, ffmpeg picks a separate 256-colour map for every single frame. That makes your background flicker, and it blows up your file size.

Export method File size What changed
Even grid cut, default palette 611,294 bytes The bloated baseline.
Sprite-aligned, default palette 241,987 bytes Alignment alone cuts 60% of the byte weight.
Sprite-aligned, shared 64-colour palette 165,345 bytes The shared palette removes another 31%.
4-frame sheet, aligned, shared palette 130,635 bytes The final production build.

Setting stats_mode=diff tells ffmpeg to build the palette around the pixels that move between frames. Setting dither=none stops speckling on your flat pixel-art surfaces.

The math: why even grid slicing fails

Most developers slice sprite sheets by dividing the canvas into equal boxes. If your image is 1672 pixels wide with 4 columns, you expect each box to be 418 pixels wide.

We tested sheets from the GPT Image 2.5 generator across Flare and Sunburst checkpoints. We asked for a 1280x720 sheet, but every one came back at 1672x941 pixels.

Here's what your actual pixel offsets look like on the raw sheet:

Canvas width: 1672 px
Target columns: 4
Calculated cell width: 1672 / 4 = 418 px

Actual frame start positions (Flare 8-frame):
Frame 1: 145 px
Frame 2: 554 px (gap: 409 px)
Frame 3: 928 px (gap: 374 px)
Frame 4: 1295 px (gap: 367 px)

Drift on frame 4: 418 - 367 = 51 px off-centre
Enter fullscreen mode Exit fullscreen mode

The characters don't sit on an even grid. The gap between frame starts shrinks from 409 pixels down to 367 pixels across the row.

If you slice by fixed 418-pixel steps, your character drifts 104 pixels sideways and bounces 48 pixels up and down. That's half the character's body width in drift.

Eight frames averaged into one image twice: the grid cut on the left is a blur of eight scattered characters, the aligned cut on the right is one sharp character with only the limbs blurred

Align on the baseline, not the cell

To get rid of the bounce, you need to ignore the canvas grid and measure the pixels directly:

  1. Find each sprite's actual box inside the raw image.
  2. Crop tightly to your sprite's edges.
  3. Place each cropped sprite onto one shared canvas.
  4. Centre the crop from left to right, and line up the bottom edge where the feet land.
Slicing approach Horizontal drift Vertical bounce Result
Even 4x2 grid 104 px 48 px Character slides and bounces constantly.
Bounding box, bottom-aligned (8 frames) 19 px 0 px Vertical bounce drops to zero.
Bounding box, bottom-aligned (4 frames) 1 px 0 px Character stays locked in place.

Lining up the feet eliminates the 48-pixel vertical hop entirely.

A four-frame animated GIF of the pixel-art courier running in place with no bounce

Why you should make 4 frames instead of 8

When you prompt for an 8-frame run cycle across two rows, the model duplicates poses.

We measured frame similarity using mean absolute difference (MAD), which scores the average per-pixel difference between two images. A low score means two frames are near copies:

  • Top frame 3 vs bottom frame 7: 4.95 MAD
  • Top frame 2 vs bottom frame 6: 5.62 MAD
  • Top frame 4 vs bottom frame 8: 5.73 MAD
  • Top frame 1 vs bottom frame 5: 7.21 MAD

Genuinely distinct animation frames score 12.36 MAD or higher. The second row of an 8-frame grid merely repeats the first row with minor errors.

On Sunburst sheets, the gaps between frame starts measured 400, 366, and 417 pixels. On Flare, the gaps measured 409, 374, and 367 pixels.

A single-row 4-frame sheet keeps your frame starts within 7 pixels of each other. Sprite widths on the 4-frame sheet varied by only 2 pixels (227 to 229 pixels), compared to a 39-pixel spread on the 8-frame layout.

The video model route vs direct cutting

You can also pass your raw sprite sheet into an image-to-video model like Seedance 2.0 Mini.

We tested this with our 4-frame sheet at 480p. The model produced an 864x496 video with 121 frames in 93 seconds.

Metric Direct sheet crop Video generation route
Generation compute cost 1x about 19x
Frame control 4 exact approved frames 121 generated frames
Palette size per frame 64 colours 7,433 to 8,194 colours
Final GIF size 130,635 bytes 530,249 bytes
Baseline drift 0 px 3 px

The video route keeps your character stable, with just 3 pixels of drift across five seconds.

But the video model redraws your character on every single frame. You lose your clean pixel outlines, and the colour count multiplies by more than a hundred times.

How to make the raw sheet

To try these steps yourself, open the GPT Image 2.5 tool and pick your settings.

OpenAI positions Flare for fast runs and Sunburst for tasks where you need higher editing precision. The composer lets you set resolution tiers (1K, 2K, 4K) and aspect ratios from one menu.

Use this prompt to make your source frames:

A 2D pixel-art sprite sheet of one character: a green-hooded courier with a brown satchel, side view facing right, running cycle, 4 frames in a single horizontal row, evenly spaced with equal margins on a flat mid-grey background, identical character height, proportions and colour palette in every frame, crisp 1px outlines, limited 16-colour palette, no text, no frame numbers, no grid lines, no drop shadows, no background scenery.
Enter fullscreen mode Exit fullscreen mode

Three limits will stay on the raw output:

  • Background colour values shift slightly between runs (such as RGB 122,121,122 vs 127,126,125), so don't use hardcoded hex keys.
  • Files download as JPGs without an alpha layer.
  • Two-row requests duplicate poses across rows.

You can run your prompts directly on GPT Image 2.5 on Seadanse with starter credits upon sign-up.

Workflow checklist

Apply this checklist when you build your next sprite animation:

  1. Ask for 4 frames in a single horizontal row, not 8 frames in a grid.
  2. Read your image dimensions from the file header instead of assuming your requested canvas size.
  3. Crop around individual sprite boxes rather than using equal grid cuts.
  4. Align each cropped frame to a common bottom baseline where the feet land.
  5. Make a shared 64-colour palette with palettegen=stats_mode=diff.
  6. Build your final GIF using paletteuse=dither=none.

We work on Seadanse, which runs GPT Image 2.5, and the tests in this post were run there.

Top comments (0)