Pixel Lab

Lock many frames to one palette, clean them up, audit the colour budget and export.

Frames exported from different tools, or resized and re-saved, drift apart: the same shade becomes three slightly different colours, and the animation shimmers. Locking fixes that: build one palette from all frames together (2–256 colours, 16 by default, or a preset such as PICO-8), map every pixel of every frame to it, and keep dithering off or ordered so still areas do not flicker. The Pixel Lab does this in the browser and exports the frames as PNG at 1×–8×, the palette as .gpl and a pixel-lab.json summary; every exported colour is checked to be in the palette.

Drop image files hereProcess many files at once
No upload · processed on this device

Why frames drift, and how one palette and the right dither stop it

Each tool that touches a frame can nudge its colours: resampling blends neighbours, JPEG or lossy WebP adds noise, and a canvas that stores colour premultiplied by alpha shifts semi-transparent pixels. Quantising each frame on its own then gives each frame its own palette. The Lab instead feeds all frames into one histogram weighted by alpha, splits it by median cut in Oklab (so both frequency and visible difference count), and refines the result, which yields one palette shared by every frame.

Dithering decides whether the animation flickers. Ordered (Bayer) dithering adds a bias that depends only on the pixel's position, so an area that does not change gets the same pixels in every frame. Error diffusion (Floyd–Steinberg, Atkinson) carries the rounding error across the whole frame, so one moving pixel changes the decisions of every pixel after it, and a still wall can flicker. The Lab defaults to None and warns when diffusion is chosen with more than one frame loaded.

The Lab writes the PNGs from the RGBA values directly instead of through a canvas, because a canvas rounds semi-transparent colours through premultiplied alpha. Before that change, 68 of 84 exported colours fell outside a 16-colour locked palette; after it, none did.

Palette lock
Mapping every pixel of every frame onto one fixed palette.
Median cut
Splitting the colour histogram repeatedly at the median of its widest axis until the target count is reached.
Ordered (Bayer) dither
A fixed threshold pattern by pixel position; it cannot flicker between frames.
Error diffusion
Dithering that spreads each pixel's rounding error to its neighbours; it can flicker in animation.

Example: eight anti-aliased frames locked to 16 colours

The Lab's browser test, with the exported files re-read by an independent decoder:

input      8 frames × 48×48 px = 18,432 px, anti-aliased edges
extract    one palette of 16 colours from all 8 frames
lock       every pixel → nearest of the 16 in Oklab, dither None
check      colours in all exported PNGs: 16; outside the palette: 0
flicker    an unchanging band: 1 distinct version across 8 frames (None and Bayer 4×4)
counting   exact up to 4 MP of frame area in total; here 0.018 MP
size cap   each frame at most 4096 × 4096 px

The last check is the one that matters for a game: open the exported PNGs, not the preview, and count their colours. The Lab's test does exactly that with Pillow.

Check the locked frames

  • Step through the frames with ← and →: the same shade should show the same swatch in the palette on every frame.
  • Open two exported PNGs in any image editor and sample the same spot of a still area: the values must be identical.
  • Count the colours of one exported frame in another tool: the count must not exceed the palette size.
  • Load the exported .gpl in GIMP or Aseprite: it lists the same colours in the same order.

Troubleshooting

SymptomLikely causeHow to checkFix
The animation flickers after lockingAn error-diffusion dither (Floyd–Steinberg or Atkinson) is onThe Convert stage shows the flicker warningSwitch to None or a Bayer matrix; compare them side by side in the dither grid
A small detail colour disappearedMedian cut weights colours by how many pixels use them; a 2-pixel eye highlight loses to large areasLook for the colour in the Palette stage after extractionLock that colour, then press Extract again, or raise the colour count
A dropped sprite sheet came out as one frameThe Lab treats one image as one frame; it does not slice sheetsThe frame strip shows a single frameSlice the sheet first with the sprite slicer, then drop the frames
The Lab refuses an imageA frame larger than 4096 × 4096 px would freeze the tab, so it is refusedThe message names the size and the limitScale or crop it first, or slice it into frames
The colour counts cover only one frameAbove 4 MP of frame area in total, counting is limited to the visible frameThe panel says so next to the countsLoad fewer frames at once if you need exact totals

Other ways to do it

OptionWhen it is the better choice
Converting inside the Studio's Pixel workspaceThe frames belong to a layered sprite with a timeline: Colour mode… makes it indexed and keeps layers and tags. See the palette editor.
A published palette instead of an extracted oneThe art must match a known palette such as PICO-8 or Endesga 32: see applying a Lospec palette.
Cleaning anti-aliased edges and stray pixelsLocking left odd edge pixels or specks: the Lab's Cleanup stage, described on pixel-art cleanup.

What this page does not cover

  • No animation playback: frames are stepped, not played.
  • Everything runs on the main thread with no cancel button; very large batches block the tab until they finish.
  • Verified in Chromium only.

Versions and sources

Nerulio: docs/PIXEL-LAB.md, measured in Chromium through Playwright on a built site, with every number read back from the downloaded files (Pillow, zipfile, json); unit tests in tests/game-palette.test.mjs. The .gpl written follows the GIMP palette format.

Official documentation used

Pixel Lab

Lock many frames to one palette, clean them up, audit the colour budget and export.

How it works

  1. Add images
  2. Adjust settings and review the result
  3. Download a new file

Supported formats

PNG / JPG / WebP / AVIF / HEIC → PNG + JSON (ZIP) — Input decoding depends on browser support and optional engines.

What this tool does

Bring many frames to one palette, then clean up, audit and export in one place.

Implementation and verified quality

advanced · shared Oklab histogram / locked palette indices / ordered and error-diffusion dithers / indexed recolour. Quality qualification is in progress. Pro/Flagship quality has not been established.

Before you export

It prepares pixels; it does not draw new detail. Error-diffusion dithering can differ between frames, so animations should use None or an ordered matrix.

Frequently asked questions

What are this tool’s limitations?

It prepares pixels; it does not draw new detail. Error-diffusion dithering can differ between frames, so animations should use None or an ordered matrix.

Are my files uploaded?

This tool processes the files you open on your device without uploading them. Images you explicitly attach to community posts are uploaded separately. Optional engines and models are downloaded from third parties; those requests expose connection information such as your IP address.

Will my work be saved automatically?

No. Images and editing state stay in this tab; media output can use local temporary storage. Changing language preserves your work; reloading, closing the tab, or following a link to another page can clear it.