Files
Company_Sprite_Gen/AI_PIXEL_ART_GUIDE.md
Gabrieal Jimmy 97a27f9a42 init
2026-10-01 15:15:20 -05:00

109 lines
8.2 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# How to make good pixel art as an AI (no image model)
This guide is for AI coding agents. It describes the method used to build the Henry pixel-art loading animations (`~/Documents/henry_pixel_art_animations`). No diffusion or image generation was involved: every pixel is written as code, rendered, looked at, and fixed.
The reference implementation is in `henry_pixel_art_animations/src/lib/henry/`: `engine/` holds the drawing engine, `henry.ts` the mascot sprite, and `scenes/` one file per animation.
---
## 1. Constraints first
Hard limits are what make the result look like real pixel art instead of blurry clip art. Decide them before drawing anything.
- **Tiny canvas.** Everything is drawn at a low logical resolution (Henry uses 160×120, 4:3), then upscaled. Never draw at screen resolution.
- **Whole pixels only.** Round every coordinate. Sub-pixel positions smear when scaled.
- **One fixed palette.** Use about 16 colours plus a few named extras. Henry uses Sweetie 16 by GrafxKid. Every scene uses only these colours, which is why they all look like one set. Ban ad-hoc hex values, or give each extra colour a name.
- **1px dark outline** (`ink`, the palette's darkest colour) around characters and props, so they read against any background.
- **Upscale with nearest-neighbour.** In canvas, set `imageSmoothingEnabled = false`; in CSS, use `image-rendering: pixelated`. For fractional display scales, use a "sharp bilinear" upscale: scale with nearest-neighbour to the next whole multiple, then do one smooth downscale to the exact size. Pixels then stay evenly sized.
## 2. Sprites are text grids
Write each sprite as an array of equal-length strings. Each character maps to a palette colour, and `.` is transparent.
```ts
const PAL = { k: C.ink, w: C.down, h: C.downLight, s: C.downShade, e: C.ink, o: C.bill, O: C.billShade, f: C.bill };
const STAND = [
'....kkkkk.....',
'...kwwwwwk....',
'..khwwwwwwk...',
'..khwwwwwwwk..',
'..kwwwwwwewk..', // e = eye
'..kwwwwwwewkk.',
'..kwwwwwwwwook', // o = bill
'..kswwwwwwwOk.',
// ...
'.....f..f.....', // feet
'.....ff.ff....',
];
```
- **Validate.** Throw if any row has a different width. This catches most authoring mistakes.
- **Bake once.** Render each sprite once to a small offscreen canvas, plus a horizontally flipped copy, and `drawImage` those every frame.
- **Make new poses by editing rows, not redrawing.** A blink changes the eye cell. Breathing drops one row so the body squashes 1px. A crouch drops two rows. A stride swaps the feet rows. Keeping the head rows identical keeps the character on-model across every pose.
- **Reason row by row** about silhouette and symmetry while writing the grid. Get the outline right first, then add the fill, then one highlight colour and one shade colour.
- **Keep sprites small.** A 14×18 character is plenty; small sprites read more clearly and are easier to get right.
## 3. Draw scenery procedurally
Backgrounds are drawing commands, not grids.
- **Primitives:** `rect`, `px`, a scanline `disc` (a filled circle with no antialiasing), `pill` (a rounded rectangle with 1px corner notches), `clip`, and `sprite`.
- **Dithering:** at colour band edges, use a checkerboard. For example, at a sky band edge, paint the previous band's colour where `(x + y) % 2 === 0`, fading over 2–4 rows.
- **Seeded randomness** for skylines, asphalt specks and windows, with a small PRNG such as `mulberry32(seed)`, so the scenery is identical every run.
- **Pre-render static layers** (background, foreground props) once into offscreen canvases. Each frame, draw only the moving things between them.
- **Layer order:** background, then far movers (clouds, birds, distant cars), then big actors (bus), then foreground props, then the character, then speech bubbles and effects.
- **Ambient life** keeps loops from feeling static: drifting clouds, a passing bird, cars, blinking, flickering lights.
## 4. Animate with timers and state machines
- **Phases.** Each animation is a small state machine, e.g. `intro → waiting (loops forever) → resolving → finished`. Each phase has its own timer.
- **Idle scheduler.** While waiting, the character randomly picks short "acts" (look around, tap foot, hop, think bubble), never the same act twice in a row, with random rests between. This prevents a visibly repeating loop.
- **Motion:** use easing functions (`easeOutCubic`, `easeOutBack`) and a parabolic `hop(t, height)`. Step frame animations by time (`Math.floor(t / 0.16) % 2`) or by distance travelled (wheels, walk cycles).
- **Cartoon cues:** a 1-frame pop on bubbles, dust puffs on landing, a squash before a jump, sparkles on success, wobble or shake for impact.
- **Clamp `dt`** to a range like 0–0.05 s, so a backgrounded tab doesn't fast-forward, and a negative first-frame delta can't break array indexing.
- **Transitions:** the iris (circle) wipe paints everything outside a circle with the palette's darkest colour, one scanline at a time so the edges stay crisp. It closes on the character's position and opens the next scene on its character.
## 5. Render, look, fix — the step that matters most
You cannot see the art while writing the character grids, so you must render it and inspect it. Never ship unviewed art.
1. Run the app (e.g. a Vite dev server) and open it in a headless browser.
2. Grab frames from the canvas **inside one in-page script**, because headless tabs may freeze between tool calls:
```js
// in page.evaluate: sample the canvas every 250 ms, trigger UI via setTimeout in the same script
const out = []; const c = document.querySelector('canvas');
const id = setInterval(() => { out.push(c.toDataURL('image/png')); if (out.length > 40) { clearInterval(id); resolve(out); } }, 250);
```
For exact, repeatable checks, import the scene module directly in the page and step it with a fixed `dt = 1/60`, drawing to an offscreen canvas.
3. Write the frames as PNGs, then tile them into a contact sheet with ffmpeg. Always use nearest-neighbour scaling:
```sh
ffmpeg -framerate 1 -i f%03d.png -vf "scale=160:120:flags=neighbor,crop=W:H:X:Y,scale=320:-1:flags=neighbor,tile=6x4" -frames:v 1 sheet.png
```
4. **Look at the image.** Crop and zoom to 4–10× on details such as the character, icons and contact points. Check whether:
- the silhouette reads, and the icon is recognisable;
- the motion reads across frames;
- things line up (feet on the ground, a bill touching a button, nothing clipping through a wall);
- the character is still visible when the iris closes.
5. Fix and repeat. Expect several rounds on anything important. Real fixes from this project: the bill was one pixel too narrow; the "walk" icon was a blob until it got a gap between head and body; a button was 2px too low for the bill to touch; a peeking pose showed the tail instead of the face until it was flipped.
6. For small icons, preview the grid by itself on a plain background at about 14× before placing it in the scene.
## 6. Scaling up with multiple agents
If several agents each build a scene, keep the style consistent:
- **Write a shared guide first.** Cover the engine API, palette, layout constants, character sprite and helpers, the scene contract, style rules (outlines, dithering, ambient life), and the render-and-look verification loop.
- **Freeze the shared files** (engine, palette, character). Agents put scene-specific colours and poses in their own files, and build new poses from the shared character rows so the design never drifts.
- **Give each agent a way to open its scene directly,** e.g. `?scene=<id>`, and a unique temp directory and browser tab.
- **Review every finished scene yourself.** Step each one with fixed timesteps across early, middle and late "done" triggers to catch crashes and scenes that never finish, then look at the contact sheets.
## Checklist
- [ ] Low fixed resolution, whole pixels, nearest-neighbour upscale
- [ ] One named palette; no stray colours
- [ ] 1px dark outlines on characters and props
- [ ] Sprites as text grids with validated widths; poses made by editing rows
- [ ] Procedural backgrounds with checkerboard dithering and seeded randomness
- [ ] State-machine animation, varied idle acts, ambient motion, clamped `dt`
- [ ] Rendered, sampled, contact-sheeted, zoomed and **looked at** — then fixed and checked again