init
This commit is contained in:
108
AI_PIXEL_ART_GUIDE.md
Normal file
108
AI_PIXEL_ART_GUIDE.md
Normal file
@@ -0,0 +1,108 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user