Procedural generation of pixel-art sprites: characters, creatures, items, props, icons, tiles
Notes compiled 2026-11-02. Target: plain JavaScript, phone browser, small internal resolutions (64x64 to 640x360), deterministic seeds. Where a claim comes from my own engineering knowledge rather than a fetched source, it sits under Inferences, not Cited Findings. Research budget was about 18 tool calls, so some games named in the brief (Moonring, Enter the Gungeon, Noita, Shapez, No Man’s Sky, Spore, Dwarf Fortress) have no fetched primary sources here. They are listed under Gaps.
1. Mask/template generators (Bollinger, zfedoran, mirrored masks, cellular automata): how they keep silhouettes readable
Takeaway
The whole family comes down to this: a hand-authored mask (per-cell rules: always empty, always solid, random solid/empty, random body/outline), randomized per seed, mirrored on one axis, then given an outline pass (any empty 4-neighbor of a body cell becomes outline). Readability comes from the mask, which acts as the body plan, plus symmetry and the outline, not from the noise. Bollinger also curated the output: he patched known-bad sub-patterns and rejected weak results. Cellular-automata variants (Sprator) swap the mask for a few Life-like smoothing steps on a tiny noise grid, then mirror and outline it the same way.
Cited Findings
Dave Bollinger, Pixel Spaceships (c. 2006, original page archived 2008; newer write-up at davebollinger.us)
- Work area is a 14x14 grid (196 px). Strict horizontal (left/right) symmetry cuts the editable area to 7x14 = 98 px. A mask nicknamed “the avocado” leaves 62 pixels free to vary (2^62, about 4e18 ships). — Bollinger, Pixel Spaceships write-up
- Generation applies adjacency constraints, using “about 2 bits per pixel on average”. Structure is stored in four 32-bit values (128 bits), color in one optional 32-bit value, and styling in about a dozen bits of another. Six 32-bit words define a ship. — Bollinger
- Coloring uses ten color/shade strategies that imitate early arcade bit depths (1-bit mono plus a bold variant, 2- and 3-bit grayscale, 2-, 3- and 4-bit color), each shown positive (on white) or negative (on black). The page also says “twenty” elsewhere, so the count is inconsistent. — Bollinger
- Auto-curation: certain “potentially problematic” sub-patterns (the example is “bug eyes”) trigger extra mitigation steps during generation. A second review then discards and regenerates deficient ships, especially for spotlight use. — Bollinger
- Original page: http://web.archive.org/web/20080228054410/http://www.davebollinger.com/works/pixelspaceships/ (cited by a Python port that uses a set of always-filled pixels and a list of candidate positions) — discatte gist. A CoffeeScript port builds ships from fixed solid, cockpit and body blocks, with optional blocks chosen at random and mirrored — goatslacker gist. Bollinger later released “Pixel Spaceships Solos” (generative NFT, single ships) — raster.art. Java derivative: stewsters/shipwright.
zfedoran/pixel-sprite-generator (JavaScript, credits Bollinger): the exact algorithm, from the source file
- Mask cell values: -1 = always border (outline), 0 = always empty, 1 = random empty/body, 2 = random border/body. — pixel-sprite-generator.js
new Mask(data, w, h, mirrorX=true, mirrorY=true). The sprite is the mask doubled on each mirrored axis.mirrorXcopies left columns to the right,mirrorYcopies top rows down. The grid starts filled with -1, andapplyMaskcopies mask data into the top-left quadrant. — sourcegenerateRandomSample: a value of 1 becomes1 * Math.round(Math.random())(50% body). A value of 2 becomes body ifMath.random() > 0.5, else border (-1). — sourcegenerateEdges: for every body cell (>0), each orthogonal neighbor that is empty (0) becomes border (-1). Diagonals are not checked. — sourcerenderPixelDatacoloring:isVerticalGradient = random > 0.5picks the gradient axis u. One random hue per sprite. Saturation israndom * options.saturation, clamped to [0,1]. Per line u, the code averages three random values in [-1,1] and takes the absolute value (biased low). If that is >1 - colorVariations, the hue is re-rolled, which gives occasional color bands. Brightness issin((u/ulen)*PI) * (1 - brightnessNoise) + random * brightnessNoise, which is bright in the middle and dark at the ends, so it reads as volume. Border pixels get RGB timesedgeBrightness. Empty pixels are white. — source- Defaults:
colored: true, edgeBrightness: 0.3, colorVariations: 0.2, brightnessNoise: 0.3, saturation: 0.5. — source - The library uses
Math.random()directly, so it is not seedable as shipped. — source
Sprator (yurkth): cellular-automata sprites
- Steps: (1) 4x8 white noise; (2) one Life-like generation where a live cell with 2 or 3 live neighbors survives, a dead cell with 0 or 1 live neighbors is born, and all other cells die or stay dead; (3) repeat step 2 “several times” (the count is user-adjustable in the app); (4) flip (mirror) and add an outline. MIT licensed, C++/OpenSiv3D. — yurkth/sprator README
- Ports: TypeScript npm package
sprator— npm; a Go gist that reimplements it with a seed and random or simplex initial states — peterhellberg gist; LJ Miranda’s Python “Cellular Sprites”; and Sprites-as-a-Service (Game of Life sprites behind a REST API) — stjordanis/sprites-as-a-service.
Inferences
- Why the masks read well. Bilateral symmetry is the strongest single cue the eye reads as “creature/ship/face”, which is why every generator mirrors. The 0 and -1 cells of a mask fix the body plan (where the cockpit, wings and spine can and cannot be), so the noise only varies the details inside a plan that is already readable. The orthogonal-only outline gives a closed one-pixel contour. Sprator’s unusual birth rule (born with 0 or 1 neighbors) fills holes and isolated gaps, and the survival rule removes isolated specks. After 2 to 4 steps you get chunky, blobby shapes rather than noise.
- A JS implementation sketch (seeded, Int8Array):
rng = mulberry32(hash(seed, 'sprite', kind)).grid = Int8Array(w*h). For each half-mask cell: 0 becomes 0, -1 becomes -1, 1 becomesrng() < pFill ? 1 : 0, 2 becomesrng() < pBody ? 1 : -1. Raising pFill above 0.5 for “core” cells and lowering it near the edges gives a soft silhouette falloff.- Optional CA smoothing (Sprator rule) before mirroring.
- Mirror on X, sometimes on Y as well. Mirroring on X only gives side-view creatures and front-view robots and faces. X+Y gives top-down ships and icons.
- Outline pass: any empty cell 4-adjacent to body becomes -1. 8-adjacency gives thicker, rounder corners.
- Rejection tests (the automated version of Bollinger’s curation): reject if body pixel count is below about 25% or above about 75% of the mask, if the body has more than one connected component (flood fill), if the bounding box is under about 60% of the mask in either dimension, or if there are lone 1-pixel holes. Then re-seed deterministically (
seed+1,seed+2, and so on), so the same seed still maps to the same accepted sprite. - Color with a palette ramp (section 4) instead of zfedoran’s HSL rolls, so the output respects a fixed ink limit.
- Masks for different body plans: a “humanoid” mask (head blob, torso column, 2-pixel leg stubs fixed as 1 or 2 cells), a “beast” mask (wide horizontal body, 4 leg columns), an “item” mask (vertical blade, crossguard row, grip), an “icon” mask (circle-ish -1 border with 1-cells inside). Each mask is a short array literal in code, a few hundred bytes.
- Animation from masks: bob the upper half by 1 px, or swap leg cells between two fixed patterns. Wing cells can flip between 1 and 0 on alternate frames. Because everything derives from the seed, frames stay consistent.
Gaps
- The full Bollinger mask (exact avocado cell layout), the “robots” variant (Pixel Robots, also by Bollinger) and the exact adjacency constraints were not on the fetched page. The Wayback archive URL above is the likely source.
- No primary source was found for the specific tutorials titled “Procedural pixel creatures” or “Tiny procedural sprites”.
- The default generation count and neighborhood type for Sprator are not stated in its README.
2. Part/kit-based (paper-doll) generation, palette swapping, and how specific games vary characters and items
Takeaway
Kit-based generation (layer pre-drawn parts in a fixed z-order, then recolor each layer through a palette ramp) is the most reliable route to hand-made quality procedural characters and items, and it is cheap at runtime. The LPC generator stores its layer metadata (groups, z-index) in JSON, and its recolors break wherever assets don’t share a common ramp. Caves of Qud shows the extreme low-cost form: each tile is authored in 2 or 3 index colors (foreground, detail, background), and the game assigns real colors from a fixed 18-color palette at runtime. Item generators (swords, spellbooks, Starbound-style guns) stack slot parts (blade/guard/handle; butt/middle/barrel) with palette swaps.
Cited Findings
- Universal LPC Spritesheet Character Generator (GitHub LiberatedPixelCup/Universal-LPC-Spritesheet-Character-Generator, hosted at liberatedpixelcup.github.io; originally by Sander Frenken). The maintainers keep most asset “metadata” in JSON files, including group membership and z-index ordering. — OpenGameArt forum, Vitruvian Studio thread
- LPC recolors are uneven: some packs were recolored automatically with the
lpctoolsscripts and others weren’t, so not all assets share palettes. One third-party project reduced sprites to at most 6 shades per material and recolored them through Eliza’s “Liberated Palette”. Sprites with too many colors left spots that didn’t recolor. Users asked for per-layer hue shifts. — OpenGameArt: color-customizable LPC generator - Caves of Qud tiles are 16x24 px and use up to three of the game’s 18 fixed palette colors: primary (foreground), detail and background. In the tile PNG, black opaque pixels take the foreground color, white opaque pixels take the detail color, and transparent pixels take the background. A few tiles use a fourth color, computed as a weighted mix of the tile and detail colors, with the weight taken from that pixel’s red channel. — Caves of Qud wiki: Modding:Tiles; Visual Style
- Dead Cells reuses parts at the model level: new items such as armor are attached to existing models, and old monster components are reused on new characters (3D kit, rendered to pixels; see section 3). — Vasseur, Game Developer, 2018
- Starbound: most weapons are procedurally generated with differing stats. — Starbound Wiki: Weapons. A dev blog post describes the procedural guns sorted into classes (rocket launchers, pistols, assault rifles) — playstarbound.com dev post (only a summary was seen). A mod’s gun config has an
animationPartsblock withbutt,middle,barrelandmuzzleFlashentries, and the middle part references a palette-swapped image. This shows the part-plus-palette structure, but it is a mod and may not match vanilla exactly. — GTG3000/GunsNAmmo - Layered item generators: a hobbyist Unity tool builds swords by stacking Blade (top), Guard (middle) and Handle (bottom) sprites — RunicPixels Weapon Generator. A ProcJam “Seeds” piece generates spellbook sprites from “a series of image layers, each of which can have one or more possible sprite, colored and drawn on top of each other”, and plans the same for weapons, armor and potions — ProcJam Seeds: Procedurally Generated Spellbook Sprites.
- Spelunky generates levels by rearranging premade room templates, not by generating sprites procedurally. — Wikipedia: List of games using procedural generation
Inferences
- The data structure for a kit:
parts = { slot: [ {id, img(Uint8Array of palette indexes), w, h, anchor:{x,y}, sockets:{name:{x,y}}, tags:[...], weight} ] }, plusorder = ['back','legs','body','arms','head','hair','hat','held']. Generation:for slot of order: part = weightedPick(rng, parts[slot].filter(compatible(tags))), blit at the parent socket offset, and remap indexes through the slot’s chosen ramp. Sockets (hand, head-top, belt) let held items and hats line up without per-combination art. This is the same idea as LPC’s z-index JSON. - Author parts in “material index” space, not color. Use Qud-style semantics: index 1 = material A dark, 2 = A mid, 3 = A light, 4 to 6 = material B, 0 = transparent, 7 = outline. A ramp table
ramps[material] = [dark, mid, light](indexes into the global palette) then gives palette swaps for free, and recoloring is a 256-entry lookup per pixel. The LPC experience (some assets with too many shades failed to recolor) argues for enforcing a fixed number of shades per material at authoring time. - Affix to visual mapping (Diablo-like): map item properties onto part choice and ramp. Base type picks the blade or barrel family. Material (“iron”, “obsidian”) picks a ramp. Elemental affix picks an accent ramp for the gem or guard plus an overlay part (flame, frost crystals). Rarity sets the number of decorative sockets filled and adds a 1-px colored outline or glint. Item level scales blade length within a part family (pick from a longer subset). Since the name is generated from the same affix list, the picture and the name always agree.
- Potions: a bottle silhouette from a small set of masks (round, tall, flask), cork part, liquid fill height =
rng, liquid ramp from the affix, plus a 1-px highlight on the upper-left of the glass. Gems: an n-gon in a 7x7 to 11x11 box with facet lines drawn from the center to vertices, shading per facet from the light direction (section 4). - How the remaining games in the brief do it, from general knowledge, unverified here: Brogue uses glyphs with procedural colors for items and monsters. Dwarf Fortress describes procedurally generated creatures (forgotten beasts, titans) in text and uses tile and color in classic mode. Enter the Gungeon’s guns are hand-drawn. These need confirming before the final report relies on them.
Gaps
- No primary sources fetched for Moonring, Enter the Gungeon, Dead Cells’ item variation, Terraria, No Man’s Sky creature body plans, Spore, Noita items, Shapez, Brogue or Dwarf Fortress. Search results did not cover them, and the budget ran out.
- Vanilla Starbound’s exact part-selection and palette-swap rules (
paletteSwapsin .activeitem configs) were not confirmed from the official modding docs.
3. Rendering-based pipelines: 3D/voxel to pixel art, sprite stacking, SDF/vector rasterization, L-systems, metaballs
Takeaway
Dead Cells is the canonical production case: simple low-poly 3D models in 3ds Max, rendered by an in-house tool at tiny size with no anti-aliasing, exported per frame as PNG plus a normal map, then shaded by a toon shader. Flicker was never fully solved. Sprite stacking (NIUM) gives a pseudo-3D look at runtime from horizontal slices drawn with 1-px vertical offsets and rotated together. For a phone JS engine, the realistic runtime options are sprite stacking, SDF/shape rasterization into palette indexes, and L-system or metaball generators drawn straight into an index buffer. Full 3D-to-pixel rendering is better done offline.
Cited Findings
- Dead Cells pipeline (Thomas Vasseur, Motion Twin, Game Developer, January 25, 2018): (1) a 2D pixel model sheet; (2) a rough 3D model and skeleton in 3ds Max, exported as FBX, kept deliberately simple because the character is about 50 px tall in game; (3) a custom in-house tool “renders the mesh in a very small size and without antialiasing”; (4) pose-to-pose keyframe animation, with in-betweens added only before or after keys, never between, and VFX carrying the motion; (5) each frame exported as PNG plus a normal map, with “a basic toon shader” using the normal map for volume; (6) retiming means moving keys, done “dozens of times in minutes”; (7) reusing assets saved “hundreds of hours”. Known limitation: pixel flicker was never fully solved, and lower detail was accepted for speed. — Vasseur, Game Developer
- The tool was never released. Community recreations exist for Houdini — fatalexit, Pixelart renderer for SideFX Houdini — and for Blender, Godot and VRoid — midgelf, 3D to 2D animations. There is also a case study video write-up — 80.lv, Dead Cells character art pipeline.
- Sprite stacking (popularized by Moppin’s NIUM): horizontal cross-sections drawn bottom to top, each offset 1 px upward, with the whole stack rotated (each slice rotated about its center) to sell the 3D effect. PICO-8 versions use
rspr-style rotated sprite helpers, and GameMaker versions usedraw_sprite_pos. SpriteStack.io is an authoring tool. MagicaVoxel can export slices (as a vertical PNG strip). — Lexaloffle BBS thread; Lexaloffle post; Sprite stacking with perspective, GameMaker; gamefromscratch
Inferences
- Sprite stacking in plain JS: keep a voxel model as
Uint8Array(w*d*h)of palette indexes, which can itself be procedurally generated (extrude a mask, revolve a profile for pots and barrels, stack shrinking discs for trees). For each angle θ in 8 or 16 steps, pre-render a cached canvas: for z from 0 to h, for each slice pixel, rotate (x,y) by θ and plot at (rx, ry - z). Cache per angle so the per-frame cost is onedrawImageper object. That fits CLAUDE.md’s rule that onlydrawImageis used per frame. Nearest-neighbor rotation of each slice creates holes, so do inverse mapping (for each destination pixel, rotate back and sample) rather than forward plotting. - Mini 3D-to-pixel at runtime (the Dead Cells idea scaled down): rasterize simple primitives (spheres, capsules, boxes) as SDFs at the target resolution. For each pixel inside (d < 0), estimate the normal from the SDF gradient (central differences), then
shade = dot(n, L)quantized into 3 or 4 ramp steps (toon), and add an outline where d < 0 and any 4-neighbor has d ≥ 0. Sample once at the pixel center with no AA. This is the Dead Cells “tiny size, no anti-aliasing, toon via normals” recipe done analytically. A 32x32 creature from 5 to 10 capsules costs about 10k SDF evaluations, trivial on a phone if done once and cached. - Metaballs for slimes, clouds and bushes:
f(p) = Σ r_i² / |p - c_i|², inside if f > 1. Threshold, then use the gradient for normals, quantize, and outline as above. Seed the centers and radii from the rng. Animate by moving the centers slightly over 2 to 4 frames and caching each frame. - L-systems for plants and trees at pixel scale: axiom
X, rules likeX → F[+X][-X]FX,F → FF, angle 20 to 30 degrees, 3 to 4 iterations. Draw with a turtle using integer Bresenham lines into an index buffer, then thicken the trunk by depth, add leaf clusters as small metaball or disc blobs at the branch tips, then outline and shade. At 16 to 48 px, keep the iterations low or it turns to noise. Applying the “pixel-perfect” corner cleanup (section 4) to the branch lines matters. - Isometric note (for Elsewhere specifically): voxel or SDF props can be rendered once into Elsewhere’s 2:1 isometric projection (x’ = x - y, y’ = (x + y)/2 - z) using the same palette-index buffers that
Inkuses. Procedural props then obey the eight-ink rule automatically.
Gaps
- No primary source fetched for the t3ssel8r technique (3D scenes rendered at low res with pixel-snapped cameras and outline shaders), NIUM’s own write-up, or Dead Cells’ render resolution and per-frame counts (the article gives neither).
4. Post-passes that make generated sprites look hand-made
Takeaway
The passes that matter most, in order: (1) quantize to a fixed palette of hue-shifted ramps (shift hue, not just value, between shades; Slynyrd caps shifts around 20 degrees); (2) outline, either 1-px outer, or “selective” outline colored by the adjacent fill’s darkest ramp step; (3) light-direction shading (top-left light: darken bottom-right edges, highlight top-left); (4) cleanup: remove orphan pixels, single-pixel holes and L-shaped corner pixels (“pixel-perfect”), and keep cluster sizes reasonable; (5) optional ordered (Bayer) dithering for gradients only. Pixel Composer ships a “De-Corner” filter, and Krita has worked on a pixel-perfect stroke mode.
Cited Findings
- Hue-shifted ramps: beginners make flat ramps that change only brightness and saturation. Raymond Schlitter (Slynyrd) prefers large hue shifts for harmony across ramps but tops out around 20 degrees, and notes that uniform saturation and brightness steps do not reliably look good. — Slynyrd Pixelblog 1: Color Palettes (2018)
- Ramp tools built on those principles: PalettePainter CLI (hue-shift and hue-spectrum settings) — chickensoft-games/PalettePainter; the Aseprite “Smart Palette” extension, which adds an OKHSL option for even lightness across hues — Aseprite community; ColorRampGenerator. The usual rule is cooler shadows and warmer highlights — aseprite-mcp generate_color_ramp; see also LPC: Some insights about pixel art color ramps.
- zfedoran’s shading: brightness follows
sin(πu)across the gradient axis (a cylinder-like volume cue), and outline color = fill times 0.3 (a colored, darkened outline rather than pure black). — source - Corner (L-shape) removal: Pixel Composer’s “De-Corner” node defines a corner as a same-color cluster arranged in an L shape and removes it, with tolerance and iteration settings — Pixel Composer docs: De-Corner. Krita’s pixel-perfect work removes corner pixels from L-shapes. A freehand approach holds back a “waiting pixel” and drops it when the next pixel leaves the horizontal/vertical line, but a corner pixel may show until a third pixel arrives — Krita MR 2158; Ken Lo, week 9 recap (2024-07-29). PixaPencil has a feature request for the same mode (remove the middle pixel of an L) — PixaPencil issue 25.
- Dead Cells toon-shades from normal maps instead of painting shading, so volume and light come from geometry. — Vasseur
- Bollinger’s auto-curation (mitigating “bug eyes” patterns, rejecting duds) acts as a quality post-pass. — Bollinger
Inferences
Implementable passes, all on an Int8Array/Uint8Array index buffer b[w*h] (0 = transparent):
- Ramp generation (deterministic, seedable): pick base hue H and n = 3 to 5 steps. For step i in [0, n-1], with t = i/(n-1):
L = lerp(0.18, 0.92, t);h = H + lerp(-shift, +shift, t)where shift ≈ 10 to 20 degrees, shifting toward blue/purple in shadows and yellow in highlights (move hue toward 60° for highlights and toward 240° for shadows rather than adding a fixed sign);Speaks in the mid-tones (S = base * (1 - 0.5*|2t-1|)). Compute in OKLCH/OKHSL for even perceived lightness. - Outer outline:
out[i] = OUTLINEifb[i]==0and any 4-neighbor is nonzero. Use 8-neighbors for a heavier look. Selective outline: the outline pixel takesramp(material of neighbor)[0](darkest step), or one step darker than that. Top-left-lit sprites often lighten the outline on the lit side. - Inner outline / rim: fill pixels adjacent to transparency on the side away from the light are set to the ramp’s darkest step. On the lit side, set them to the lightest step (a rim light).
- Light-direction shading from a mask alone (no normals): compute a distance-to-edge field (two-pass chamfer distance transform on the mask). Estimate the normal as
n = normalize(-∇dist, k)where k sets how domed it looks.shade = clamp(dot(n, L), 0, 1), quantized to ramp steps. A cheaper version:shade = base + (dist toward top-left edge < 1 ? +1 : 0) + (dist toward bottom-right edge < 2 ? -1 : 0). - Orphan removal: a nonzero pixel with 0 same-material 8-neighbors is set to the majority neighbor (or to 0 at the silhouette). A transparent pixel with 4 nonzero 4-neighbors is filled (closes pinholes). Iterate 1 or 2 times.
- Pixel-perfect L-corner removal on 1-px lines: for each line pixel p whose two line neighbors are one horizontal and one vertical (an L), and where those two neighbors are diagonal to each other, delete p. For stroke lists, walk the point sequence: if
pts[i-1]andpts[i+1]differ by (±1, ±1) andpts[i]is orthogonally adjacent to both, droppts[i]. - Jaggies: line segments should use consistent run lengths (1-1-1, 2-2-2, 1-2-1-2), not 3-1-2. When drawing generated contours, use Bresenham at slopes that are ratios of small integers, or resample the run lengths.
- Cluster cleanup: flood-fill each same-color region. Merge regions under 2 px into the adjacent region with the closest ramp step, which removes “noise”, matching the house rule “flat color, a few bold marks, no noise”.
- Dithering: apply a 2x2 or 4x4 Bayer matrix only on large flat gradients (sky, big shadows), never on small sprites, where it reads as noise.
idx = floor(v*n + bayer[x&3][y&3]/16 - 0.5). - Low-res anti-aliasing: add one intermediate ramp step at concave corners of steep curves (manual AA). Usually skipped on outlined sprites, since the outline already defines the edge.
Gaps
- No primary source fetched for Aseprite’s own “Pixel-perfect” brush algorithm or for published selective-outline algorithms. The passes above are standard practice reconstructed from knowledge, not from a fetched spec.
5. ML/AI approaches (as of late 2026)
Takeaway
Diffusion-based pixel-art services (PixelLab, Retro Diffusion) are the practical state of the art for offline asset generation: text or image to sprite, directional rotations, skeleton-driven animation frames, and palette and size controls, priced per image or one-time. Academic work focuses on generating missing directions and poses (Pix2Pix and imputation GANs) and on pose-conditioned sprite-sheet diffusion, using small datasets (hundreds of sequences). None of these is a sensible on-device runtime generator for a small phone game. The usual pattern is to generate offline, then clean up, re-quantize to the game palette and snap to the grid by hand or with scripts.
Cited Findings
- PixelLab: a skeleton animation tool controls character motion. Inpainting and init images improve control, and skeletons can be saved (as images or Aseprite files) and reused on other characters. — PixelLab docs: skeleton animation. The API page lists models “Pixflux” ($0.0084 per 64x64, transparent background) and “Bitforge” ($0.01433 per 64x64), with “Animate with text” marked coming soon on the official page (that page may be stale). — PixelLab API. Third-party tool listings include
animate_with_skeleton(frames from a reference image plus per-frame skeleton keypoints),estimate_skeletonandstart_text_animation. — OOMOL PixelLab skill; pixellab-mcp, npm (unofficial). A September 2026 comparison guide lists PixelLab at $12/mo and warns that prices change often. — Cinevva: Best AI pixel art generators 2026 - Retro Diffusion (Astropulse): an Aseprite extension with a purpose-trained pixel-art model, smart color reduction, text-guided palette creation, “Neural Pixelate” (image to pixel art), “Neural Resize” and “Neural Detail”. It is a one-time purchase (Gumroad listed $65, moving to itch.io; a $20 “Lite” version exists). The website and the extension use different models. The Lite version got an improved 16x16 model. — Astropulse, Gumroad; Retro Diffusion Lite, itch.io; Lite 16x16 devlog. A June 2026 comparison describes it as a pixel-art specialist with pay-as-you-go or one-time pricing. — SpriteCook comparison (2026)
- Sprite Sheet Diffusion (Hsieh, Zhang, Yan; CMU; arXiv 2412.03685, Dec 2024): conditions on a reference image plus a pose sequence to produce animation frames. The dataset (from GameArt2D and SpriteDatabase) has 152 paired action sequences and 916 paired frames. — arXiv 2412.03685
- Coutinho & Chaimowicz (UFMG): Pix2Pix generates a character facing a target direction from a source-direction sprite, evaluated visually and with FID — arXiv 2208.06413; AIIDE 2022, “On the Challenges of Generating Pixel Art Character Sprites Using GANs”. Follow-up “A Missing Data Imputation GAN for Character Sprite Generation” (2024): imputes a missing pose (e.g. front) from the available ones (left, right, back) — arXiv 2409.10721.
- Serpa & Rodrigues (SBGames 2019): pixel-art sprites from line-art sketches via image translation. — SBGames 2019 paper
Inferences
- Feasibility on device: diffusion models are hundreds of MB to GB and need a GPU. WebGPU makes in-browser inference possible on high-end phones but it is slow, power-hungry and adds megabytes to download. For a game that “installs nothing” and must be deterministic across devices, on-device ML is impractical: floating-point results differ across GPUs, so the same seed would not reliably give the same pixels. A tiny GAN or VAE decoder (about 1 MB, 16x16 output) is technically runnable in JS, but determinism across devices and quality are both doubtful. Use ML offline only.
- Known limitations to plan for (consistent with the tool features above, which exist because of them): off-palette colors (hence “smart color reduction” and palette controls), off-grid or “fake pixel” outputs that need re-snapping (hence “Neural Pixelate” and “Neural Resize”), and inconsistency across frames and directions (hence skeletons and reference-image conditioning). The pipeline: generate, downscale with nearest-neighbor to the true grid (detect the scale from the run lengths), quantize to the game palette by nearest in OKLab, run the section-4 cleanup, then store as palette indexes.
- A good hybrid: use AI offline to author kit parts (section 2), then let the deterministic kit system generate the variety at runtime.
Gaps
- Current (Nov 2026) PixelLab and Retro Diffusion model names, resolutions and prices could not be verified from first-party pages. The official PixelLab roadmap appears older than third-party tool listings. No information found on on-device pixel-art models in 2026.
6. Programmatic authoring tools: Aseprite Lua, PICO-8 idioms, icon generators, pixel DSLs (“pixel recipes”)
Takeaway
For a plain-JS engine, the most useful idea is “sprites as code”: tiny strings or arrays of palette indexes (PICO-8 style hex strings, ASCII art maps) plus procedural modifiers, held in the source and painted at load time. Aseprite’s Lua API and tools such as Retro Diffusion and PalettePainter fit an offline authoring step. Few authoritative sources were fetched on this topic in the budget.
Cited Findings
- Aseprite extensions do palette and ramp generation (Smart Palette, with OKHSL) — Aseprite community. Retro Diffusion integrates generation, color reduction and palette tools into Aseprite — Astropulse. An Aseprite MCP server exposes a
generate_color_ramptool — aseprite-mcp. - PICO-8 sprite-stacking code uses rotated-sprite helpers (
rspr). PICO-8 angles run clockwise, with 0.0 pointing right and 0.25 pointing down. — Lexaloffle BBS - Sprator exports cellular-automaton sprites as icons in chosen colors and sizes, and has an npm/Express port (a “sprite as a service” pattern). — yurkth/sprator; npm sprator
Inferences
- A pixel recipe DSL for JS (an engineer’s sketch): an ASCII map where each character is a material slot, e.g.
const SWORD = ` ....o ...oa ..oa. bob.. .bb..`; // o=outline, a=blade ramp, b=guard ramp, .=emptyThe parser builds a
Uint8Arrayof slot indexes. A recipe can also be a function,(rng, p) => ops[], with ops likerect,disc,line,mirrorX,outline,shade(light)andramp('blade', 'steel'), all executed against an index buffer. Recipes keep art diffable, tiny and seedable. They also print again in another palette, which is exactly Elsewhere’sInkmodel of palette sheets. - PICO-8 idioms worth borrowing: a 16-color global palette, sprites as hex nybble strings (
"0077700..."),pal(a,b)remapping for palette swaps and flashes, andfillpdither patterns for 2-tone shading. - Aseprite Lua for offline batch work: iterate the cels, map colors to indexes, export sheets plus JSON. That can turn hand-authored kit parts into the index-buffer format the runtime uses.
Gaps
- Aseprite API reference pages, PICO-8 manual pages, and specific procedural icon generators (e.g. game-icons style generators) were not fetched. The text-to-pixel DSLs named in the brief have no sources here.
7. Determinism: same seed gives the same sprite
Takeaway
None of the classic JS generators are seedable out of the box (zfedoran calls Math.random() throughout), but they are easy to make so: replace every random call with a seeded PRNG stream derived from (world seed, sprite kind, id), and keep everything in integer palette-index buffers. Bollinger’s design, where a ship is six 32-bit numbers, is the ideal: the sprite is a pure function of its seed bits.
Cited Findings
- Bollinger: one ship is fully defined by six 32-bit values (four for structure, one for color, part of one for styling). — Bollinger
- zfedoran uses
Math.random()for masks, gradient axis, hue, saturation and brightness noise, so it is not deterministic as shipped. — source - A Go reimplementation of Sprator takes an explicit seed (and random or simplex initial states). — peterhellberg gist
- Dead Cells notes flickering pixels as an unsolved artifact of rendering 3D at low res without AA, an argument against runtime 3D rasterization if pixel-stable output matters. — Vasseur
Inferences
- Recipe:
seed32 = hash32(worldSeed + ':' + kind + ':' + id)(e.g. FNV-1a or xmur3 over the string), then a fast 32-bit PRNG (mulberry32 / sfc32 / splitmix32). Use separate named streams per stage (shape,color,detail) so tuning one stage doesn’t reshuffle the others. That matches Elsewhere’s existingctx.rng(name)convention. - Avoid floating-point divergence: do all generation in integers or at least in IEEE-exact operations.
Math.sin,Math.powandMath.expare not guaranteed bit-identical across JS engines, so zfedoran’ssinbrightness could, in rare edge cases, quantize differently on different browsers. Use lookup tables or integer approximations wherever a value is quantized to a palette index. Never generate through canvas drawing APIs that anti-alias (arc, lineTo with AA) and then read back pixels, because results vary by browser and GPU. Write to typed arrays, thenputImageDataonce into an offscreen canvas. - Rejection loops must be deterministic: retry with
seed32 + attemptand cap the attempts, with a fallback mask, so every seed terminates and always maps to the same sprite. - Cache key = (recipe version, seed). Bump the recipe version when the generator changes, and pin golden images (the Elsewhere repo already has
test/golden.json) to catch accidental drift. - Saves: store only the seed and parameters (plain JSON), never pixels. The sprite regenerates identically on load.
Gaps
- No fetched source documents cross-browser bit-exactness of
Math.sinand friends. The ECMAScript spec leaves them “implementation-approximated”, which comes from knowledge, not a fetched page.