Skip to content

Pixel Art Palette Editor — Indexed Colour and Lospec

A palette editor that works on the sprite itself. Convert a picture to indexed colour, then reorder, recolour or replace palette entries while every frame keeps its look, pull a named palette from Lospec, and write the palette in the formats Aseprite, GIMP and Photoshop read.

A pixel-art palette is a handful of ramps: each ramp is one material (skin, cloth, metal) going from dark to light in 3–6 steps, and good ramps shift hue as they go, cooler in the shadows and warmer in the lights. In Nerulio's Pixel workspace, Pixel › Hue-shifted ramp… builds such a ramp around the foreground colour (5 colours, 24° shift by default, the base kept exactly), Colour mode… turns the sprite into indexed colour with the nearest colour chosen perceptually, and the Palette menu saves the result as .gpl, .pal, .hex, .ase, .act or .json.

Drop a sprite, frames, a GIF or an .aseprite file to paint on — or an upscaled picture to clean up
Runs in your browser. Files are not uploaded.
Studio Pixel workspace: a CC0 sprite in indexed colour mode with its palette, the transparent index and the colour audit per frame
The real Studio with a CC0 asset (OpenGameArt).
Exports to
  • .asepriteVerified· Aseprite 1.3.18
  • Palette filesMeasured· read back
  • Engine bundlesVerified· Godot 4.7.2 · Unity 6000.5
  • Frame PNG · .nerulioPlain files

Ramps, hue shifting and indexed colour

A ramp that only changes lightness looks grey and muddy in the shadows. Painters shift hue along it instead: the darkest step moves toward blue-violet and loses saturation, the lightest moves toward yellow and also loses some saturation, and the middle keeps the richest colour. Nerulio does this in OkLCh, a colour space where equal lightness steps look equal: shadows move toward hue 265° (blue-violet), lights toward 85° (yellow-orange), each end by up to the Hue shift you type, and the chroma fades toward both ends.

Converting to indexed colour replaces every pixel by a palette index. The choice of "nearest" matters: Nerulio measures distance in Oklab, which follows how the eye sees colour, while Aseprite measures RGB distance. For the green #3e8948 and the PICO-8 palette, Oklab picks PICO-8's green #008751, whereas plain RGB distance picks the grey-brown #5f574f. Dithering is off by default, because the noise it adds reads as texture in pixel art.

In an indexed sprite the transparent entry is a palette slot of its own, and an opaque pixel is never matched to it. That is why a palette that starts with black (PICO-8 does) keeps its black pixels black here: the conversion adds a transparent slot in front instead of turning index 0 into the mask.

Ramp
An ordered set of colours for one material, dark to light.
Hue shift
Rotating the hue along a ramp: shadows cooler, highlights warmer.
OkLCh / Oklab
Perceptual colour spaces: L lightness, C chroma (saturation), h hue angle.
Shading ink
A brush that moves each pixel one step along the selected ramp.
Index map
The renumbering applied to every cel when entries move, so the picture stays the same.

Example: the ramp Nerulio builds from #3e8948

Pixel › Hue-shifted ramp… with the default settings (Colours 5, Hue shift 24, Darkest 18 %, Lightest 94 %). Values measured back from the 8-bit colours:

step  hex      L      C      h
0     #001a0d  0.19   0.044  159°   shadow: darker, hue turned toward blue
1     #005129  0.38   0.097  154°
2     #3e8948  0.57   0.123  146°   base colour, kept exactly
3     #91be77  0.75   0.108  135°
4     #e3f3c1  0.94   0.068  122°   light: brighter, hue turned toward yellow
lightness steps 0.19 → 0.38 → 0.57 → 0.75 → 0.94 (about 0.19 apart)
chroma peaks at the base (0.123) and fades at both ends

The lights moved the full 24° (146° → 122°); the darkest step came back at 159° instead of 170° because a colour that dark has little room left for hue in 8-bit sRGB. Treat the ramp as a starting point: nudge a step by double-clicking it in the Palette panel.

What it does

  • Colour mode RGB ↔ indexed: from the exact colours of the picture, the current palette or DB32, nearest colour chosen in Oklab, dithering off by default (ordered dither in linear light when you want it). The transparent index is its own entry, so black stays black.
  • Dragging a palette entry to a new slot rewrites every cel of an indexed sprite through the index map, so the picture never changes. Sort by brightness, hue, saturation or use, add, remove, and pick the transparent index.
  • Hue-shifted ramps in OkLCh around a base colour: shadows move toward cool hues and lights toward warm ones, with the step count, hue shift and darkest and lightest levels set in a dialog. The base colour is kept exactly.
  • Team-colour variants: select a ramp in the palette and each team colour gets its own ramp mapped onto every frame and layer, each variant as a new sprite. In an indexed sprite a single palette edit recolours every frame.
  • The Audit panel counts colours per frame against a budget, lists off-palette colours and near duplicates, and can select the stray pixels or snap them to the palette in one step.

How to do it

  1. Drop the sprite, frames or an .aseprite file on this page; the Pixel workspace opens with its palette in the Palette panel.
  2. Open Pixel › Colour mode, choose Indexed and the palette source (the picture's exact colours, the current palette or DB32), and keep dithering off for clean pixel art.
  3. Double-click an entry to edit it, drag entries to reorder, or open Pixel › Lospec palette and type a palette name after the one-time consent.
  4. Export the palette as .gpl, .pal, .hex, .ase, .act or .json from the Palette menu, and save the sprite as an indexed .aseprite.

Check the palette

  • Sort by brightness in the Palette menu: each ramp should run dark to light without a step jumping out of order.
  • Select a ramp with Shift/Ctrl+click and paint with the Shading ink: left click should step lighter, right click darker, one colour at a time.
  • Run the Palette audit: near-duplicate pairs are listed, which usually means two ramps share a colour that could be merged.
  • Export the palette and import it again: each format was written and read back identical in the test suite.

Palette formats in and out

All are read and written by the Palette menu; "checked" means written and read back identical.

FormatMade byReadWrite
.gplGIMP, Lospec, AsepriteYes (also Aseprite's RGBA extension)Yes, checked
.palPaint Shop Pro (JASC), RIFF palettesBoth variantsYes, checked
.hexLospecYesYes, checked
.ase (swatches)Adobe Photoshop, IllustratorYes (a real 16-colour file read)Yes, checked
.actAdobe colour tableYesYes, checked
.json / .asepriteLospec JSON / an Aseprite spriteYes / its paletteJSON, checked

Troubleshooting

SymptomLikely causeHow to checkFix
After converting to indexed, all black pixels turned transparent (seen in files from other tools)The palette's index 0 is black and the tool uses index 0 as the transparent colourIn Aseprite, Sprite › Properties shows the transparent colour indexKeep a separate transparent slot (Nerulio adds one in front); in Aseprite change the transparent colour or add an entry
The indexed result picks different colours than Aseprite didNearest colour is measured in Oklab here and in RGB in AsepriteCompare one pixel: #3e8948 goes to #008751 here, #5f574f by RGB distanceKeep Nerulio's perceptual pick, or convert in Aseprite and open the indexed file here
Editing one colour changed pixels everywhereIn an indexed sprite a palette entry is the colour of every pixel that uses itThe edit dialog says every pixel of this index changes with itAdd a new entry for the new colour and repaint only where you want it
The Shading ink jumps to unrelated coloursNo ramp is selected, so the ink follows the whole palette orderThe options bar says "Shading along the palette order"Shift/Ctrl+click the colours of one ramp, dark to light, before painting
The converted sprite looks noisyA dither pattern was chosen in the Colour mode dialogZoom in: checkerboard or Bayer patterns in flat areasUndo and convert again with Dither: None

Other ways to do it

OptionWhen it is the better choice
Aseprite's palette toolsYou already work there: the colour bar edits entries, Sprite › Properties sets the transparent colour, and the Shading ink uses the colours selected in the colour bar.
A published palette from LospecYou would rather start from a tested palette than build ramps: see using a Lospec palette, including which file each program reads.
Recolouring finished frames without the editorThe sprite is done and only needs a team colour or a status tint: ramp swap and team colours.

Engine exports and how each was checked

"Verified" means the exported files were loaded by the real engine or tool on real CC0 assets and the result was compared with the source. Anything that was not loaded is labelled as such.

TargetFilesCheck
.asepritelayers · blend · opacity · locked · indexed palette · frames · tagsVerified Aseprite 1.3.18The real Aseprite command line reopened the file as INDEXED with 2 layers, Multiply at opacity 160 and the locked layer not editable; the frames are pixel-identical.
Palette files.gpl · .pal · .hex · .ase · .act · .jsonMeasured read backEach format was written and read back identical; Aseprite's own .gpl, .pal, .hex and .act and a real Adobe .ase (16 colours) were read.
Engine bundlesGodot 4 · Unity 6 · Phaser 3/4 · PixiJS 8 … through Pack & ExportVerified Godot 4.7.2 · Unity 6000.5The sprite you drew is packed in Pack & Export; those engine files were loaded in each engine on real CC0 assets (47 of 49 runs pass; the 2 failures are a Phaser 3.90 bug the Phaser 3 preset avoids).
Frame PNG · .nerulioPNG-8 with the palette when an indexed frame fits it · project filePlain filesPlain files for any tool; no engine run is claimed for them.

Evidence

Cleanup benchmark, 69 CC0 cases with a known 1× original: the exact size found for 81 % (±1 px: 90 %) and 74.0 % of pixels exact with the background kept; the best competitor found 30 % of sizes (perfectPixel) and the best exact-pixel score was 44.6 % (unfake.js). An Aseprite-made ×4 nearest upscale came back 100 % exact and a ×3.78 bilinear resize 99.7 %, both with the size found automatically. A 1 px outline and a drop shadow matched Aseprite pixel for pixel (1846 and 705 px). .aseprite files written here reopen in Aseprite 1.3.18 as indexed, with their layers, blend mode and lock.

Limits

  • Ramps are starting points generated from one base colour; they are not art direction.
  • A palette holds at most 256 colours, and there is no grayscale colour mode.
  • Nearest-colour matching is Oklab only; Aseprite's RGB matching cannot be selected.
  • Not in the Pixel workspace (Aseprite has them): layer groups, linked cels, tilemap layers, reference layers, RotSprite and free rotation or scaling of a selection (only flips and 90°), gradient, spray, contour and text tools, custom brushes (square or round, 1–16 px), tiled drawing, Lua scripting and a command line.
  • The symmetry axis is typed, not dragged on the canvas. On touch there is no right button, so the background colour needs the swap button or X.
  • Cleanup: JPEG input comes back close (84 % of pixels within ΔE 0.02 with the background kept) but rarely exact (12 %); simulated generated art gets the exact size 20 % of the time (±1 px: 80 %); none of 7 real generated images was snapped automatically — you type the pixel size the panel suggests.
  • Indexed conversion picks the nearest colour in Oklab, not Aseprite's RGB distance: 68 % of the test sprite's pixels got a different (perceptually closer) pick. The Lospec search is the only network request and asks first.

Versions and sources

Nerulio: ramps, index maps, the Oklab matcher and every palette reader and writer are unit-tested (tests/studio-pixel.test.mjs); the PICO-8 conversion of a 96×144 CC0 sprite gave 100 % of opaque pixels their Oklab-nearest colour with all 1708 black pixels kept black (docs/STUDIO-PIXEL.md §6, T5). The ramp values above were computed with src/studio/pixel/ramps.js. Aseprite's behaviour follows its documentation.

Official documentation used

Questions

Why does the indexed conversion pick different colours than Aseprite?

The nearest colour is chosen in Oklab, a perceptual colour space, while Aseprite measures RGB distance. On the test sprite converted to PICO-8, every opaque pixel got its Oklab-nearest colour, which differed from the RGB-nearest pick on 68 % of pixels.

What does the Lospec search send over the network?

Only the palette name you type, turned into a lospec.com/palette-list/<name>.json request without cookies or a referrer, after a consent dialog. It is the only network request in the whole workspace; declining sends nothing.

Can it read palettes from Photoshop and Paint Shop Pro?

Yes: Adobe .ase swatch exchange and .act files, JASC and RIFF .pal, GIMP .gpl, plain .hex and Lospec JSON, plus the palette inside an .aseprite. Each format was written and read back identical, and a real Adobe .ase with 16 colours was read.

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).