Skip to content

Pixel Art Palette Lock — One Palette for Every Frame

Several frames exported from different tools rarely share a palette. The Pixel Lab builds one palette from all of them, locks every frame to it and proves it: every exported colour is in the palette.

Drop your sprite frames (PNG) — several frames share one palette
Open the Pixel Lab
Runs in your browser. Files are not uploaded.
Pixel Lab: CC0 sprite frames locked to one extracted palette, with the palette and the share of the pixels in each colour
The real Pixel Lab with a CC0 asset (OpenGameArt).
Outputs
  • PNG framesMeasured· Pillow
  • PaletteMeasured· re-import
  • pixel-lab.jsonUNVERIFIED

What it does

  • One alpha-weighted palette from all frames (Oklab median cut), 2–256 colours, or a classic palette (Game Boy, PICO-8, Sweetie 16, Endesga 32, NES).
  • Dither: None, Floyd–Steinberg, Atkinson, Bayer 2×2/4×4/8×8 — ordered dithering does not flicker between frames, and the page warns when error diffusion might.
  • Palette editing: sort by lightness/hue/saturation/frequency, lock colours that survive re-extraction.
  • Export PNG frames at 1×–8× nearest, the palette as .gpl, and a JSON envelope.

How to do it

  1. Drop the frames.
  2. Choose the colour count or a classic palette.
  3. Pick a dither mode (None is the default for animations).
  4. Export the ZIP.

Outputs and how each was checked

"Measured" means the downloaded file was opened again outside the page (Pillow, numpy or an independent parser) and the property was measured. Loading these files in a game engine was not tested.

TargetFilesCheck
PNG framesanim_N.png · 1×–8× nearestMeasured PillowEvery exported colour re-read from the PNGs lies inside the locked palette.
Palette.gpl · .hex (Lospec) · .jsonMeasured re-importEach format was exported and imported back through the UI: 16 → 16 colours.
pixel-lab.jsongeneric frames + paletteUNVERIFIEDGeneric JSON; not loaded in any engine.

Evidence

8 anti-aliased frames → one 16-colour palette with 0 exported colours outside it; an unchanged region identical in all 8 frames with dithering off and with Bayer 4×4; the anti-alias remover took 271 colours to 16 without moving a single silhouette pixel; integer scale, offset and 2.5× non-integer scaling detected exactly.

Limits

  • Frames larger than 4096 × 4096 are refused: every pass runs on the main thread, and there is no cancel button yet.
  • A sprite sheet is treated as one image; cut it into frames first (sprite sheet cutter).
  • With a dithering mode on, the cleanup counts include dither pixels; set dithering to None first.
  • The exported JSON is generic; importing it into Godot or Unity was not tested.

Questions

Why does my animation flicker after dithering?

Error diffusion carries error across the whole image, so one moving pixel changes later decisions. Ordered (Bayer) dithering depends only on position and does not flicker; the Lab defaults to None.

Can I use a Lospec palette?

Yes: paste or import a Lospec HEX list, .gpl or JSON, and the frames lock to it.

Is it free, and do I need an account?

Everything on these pages is free and works without an account. There is no watermark and no export that is held back.

Are my sprites uploaded?

No. The Studio decodes, packs and exports in this browser tab. Projects are autosaved in the browser (IndexedDB) and can be saved as a .nerulio file.

Does it need Aseprite or an engine plugin installed?

No. .aseprite files are read and written in the browser, and each engine bundle carries what that engine needs (for Godot, the import settings and scene; for Unity and the Godot TileSet, a small editor script).