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.

- .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 endsThe 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
- Drop the sprite, frames or an .aseprite file on this page; the Pixel workspace opens with its palette in the Palette panel.
- 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.
- 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.
- 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.
| Format | Made by | Read | Write |
|---|---|---|---|
| .gpl | GIMP, Lospec, Aseprite | Yes (also Aseprite's RGBA extension) | Yes, checked |
| .pal | Paint Shop Pro (JASC), RIFF palettes | Both variants | Yes, checked |
| .hex | Lospec | Yes | Yes, checked |
| .ase (swatches) | Adobe Photoshop, Illustrator | Yes (a real 16-colour file read) | Yes, checked |
| .act | Adobe colour table | Yes | Yes, checked |
| .json / .aseprite | Lospec JSON / an Aseprite sprite | Yes / its palette | JSON, checked |
Troubleshooting
| Symptom | Likely cause | How to check | Fix |
|---|---|---|---|
| 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 colour | In Aseprite, Sprite › Properties shows the transparent colour index | Keep 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 did | Nearest colour is measured in Oklab here and in RGB in Aseprite | Compare one pixel: #3e8948 goes to #008751 here, #5f574f by RGB distance | Keep Nerulio's perceptual pick, or convert in Aseprite and open the indexed file here |
| Editing one colour changed pixels everywhere | In an indexed sprite a palette entry is the colour of every pixel that uses it | The edit dialog says every pixel of this index changes with it | Add a new entry for the new colour and repaint only where you want it |
| The Shading ink jumps to unrelated colours | No ramp is selected, so the ink follows the whole palette order | The 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 noisy | A dither pattern was chosen in the Colour mode dialog | Zoom in: checkerboard or Bayer patterns in flat areas | Undo and convert again with Dither: None |
Other ways to do it
| Option | When it is the better choice |
|---|---|
| Aseprite's palette tools | You 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 Lospec | You 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 editor | The 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.
| Target | Files | Check |
|---|---|---|
| .aseprite | layers · blend · opacity · locked · indexed palette · frames · tags | Verified 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 · .json | Measured 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 bundles | Godot 4 · Unity 6 · Phaser 3/4 · PixiJS 8 … through Pack & Export | Verified 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 · .nerulio | PNG-8 with the palette when an indexed frame fits it · project file | Plain 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).