@pebbly/aseprite-ai-artist 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/CHANGELOG.md +170 -0
  2. package/LICENSE +21 -0
  3. package/README.md +169 -0
  4. package/agents/animation-director.md +65 -0
  5. package/agents/palette-smith.md +57 -0
  6. package/agents/pixel-critic.md +69 -0
  7. package/agents/rig-builder.md +46 -0
  8. package/bin/aseprite-ai-artist +6 -0
  9. package/bin/aseprite-ai-artist.mjs +44 -0
  10. package/dist/bridge/bridge.d.ts +52 -0
  11. package/dist/bridge/bridge.js +230 -0
  12. package/dist/bridge/bridge.js.map +1 -0
  13. package/dist/bridge/client.d.ts +54 -0
  14. package/dist/bridge/client.js +214 -0
  15. package/dist/bridge/client.js.map +1 -0
  16. package/dist/cli.d.ts +10 -0
  17. package/dist/cli.js +272 -0
  18. package/dist/cli.js.map +1 -0
  19. package/dist/extension.d.ts +21 -0
  20. package/dist/extension.js +65 -0
  21. package/dist/extension.js.map +1 -0
  22. package/dist/install.d.ts +35 -0
  23. package/dist/install.js +185 -0
  24. package/dist/install.js.map +1 -0
  25. package/dist/lib/color.d.ts +66 -0
  26. package/dist/lib/color.js +170 -0
  27. package/dist/lib/color.js.map +1 -0
  28. package/dist/lib/protocol.d.ts +78 -0
  29. package/dist/lib/protocol.js +72 -0
  30. package/dist/lib/protocol.js.map +1 -0
  31. package/dist/lib/render.d.ts +62 -0
  32. package/dist/lib/render.js +134 -0
  33. package/dist/lib/render.js.map +1 -0
  34. package/dist/lib/skills.d.ts +43 -0
  35. package/dist/lib/skills.js +112 -0
  36. package/dist/lib/skills.js.map +1 -0
  37. package/dist/lib/version.d.ts +4 -0
  38. package/dist/lib/version.js +25 -0
  39. package/dist/lib/version.js.map +1 -0
  40. package/dist/server.d.ts +15 -0
  41. package/dist/server.js +144 -0
  42. package/dist/server.js.map +1 -0
  43. package/dist/tools/assets.d.ts +3 -0
  44. package/dist/tools/assets.js +181 -0
  45. package/dist/tools/assets.js.map +1 -0
  46. package/dist/tools/craft.d.ts +12 -0
  47. package/dist/tools/craft.js +143 -0
  48. package/dist/tools/craft.js.map +1 -0
  49. package/dist/tools/draw.d.ts +3 -0
  50. package/dist/tools/draw.js +256 -0
  51. package/dist/tools/draw.js.map +1 -0
  52. package/dist/tools/escape.d.ts +12 -0
  53. package/dist/tools/escape.js +49 -0
  54. package/dist/tools/escape.js.map +1 -0
  55. package/dist/tools/kit.d.ts +29 -0
  56. package/dist/tools/kit.js +85 -0
  57. package/dist/tools/kit.js.map +1 -0
  58. package/dist/tools/look.d.ts +7 -0
  59. package/dist/tools/look.js +215 -0
  60. package/dist/tools/look.js.map +1 -0
  61. package/dist/tools/palette.d.ts +3 -0
  62. package/dist/tools/palette.js +232 -0
  63. package/dist/tools/palette.js.map +1 -0
  64. package/dist/tools/session.d.ts +3 -0
  65. package/dist/tools/session.js +231 -0
  66. package/dist/tools/session.js.map +1 -0
  67. package/dist/tools/structure.d.ts +3 -0
  68. package/dist/tools/structure.js +218 -0
  69. package/dist/tools/structure.js.map +1 -0
  70. package/examples/mcp-configs/claude-code.json +9 -0
  71. package/examples/mcp-configs/codex.toml +13 -0
  72. package/examples/mcp-configs/cursor.json +9 -0
  73. package/examples/mcp-configs/gemini-cli.json +9 -0
  74. package/examples/mcp-configs/vscode.json +10 -0
  75. package/extension/ai-artist.lua +3291 -0
  76. package/extension/package.json +14 -0
  77. package/hooks/hooks.json +25 -0
  78. package/hooks/look-nudge.mjs +46 -0
  79. package/hooks/session-status.mjs +47 -0
  80. package/knowledge/palettes.json +60 -0
  81. package/package.json +69 -0
  82. package/rules/00-core-principles.md +57 -0
  83. package/rules/01-palette-and-color.md +51 -0
  84. package/rules/02-shading-and-light.md +56 -0
  85. package/rules/03-silhouette-and-form.md +60 -0
  86. package/rules/04-outlines-and-edges.md +46 -0
  87. package/rules/05-animation.md +62 -0
  88. package/rules/06-layers-and-rigging.md +52 -0
  89. package/rules/07-review-checklist.md +51 -0
  90. package/rules/README.md +16 -0
  91. package/skills/README.md +29 -0
  92. package/skills/pixel-animate/SKILL.md +91 -0
  93. package/skills/pixel-brief/SKILL.md +61 -0
  94. package/skills/pixel-draw/SKILL.md +93 -0
  95. package/skills/pixel-export/SKILL.md +80 -0
  96. package/skills/pixel-fix/SKILL.md +91 -0
  97. package/skills/pixel-new/SKILL.md +70 -0
  98. package/skills/pixel-palette/SKILL.md +80 -0
  99. package/skills/pixel-review/SKILL.md +81 -0
  100. package/skills/pixel-rig/SKILL.md +76 -0
  101. package/skills/pixel-shade/SKILL.md +77 -0
  102. package/skills/pixel-tileset/SKILL.md +104 -0
  103. package/templates/AGENTS.section.md +33 -0
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "aseprite-ai-artist",
3
+ "displayName": "Aseprite AI Artist",
4
+ "description": "Lets a coding agent draw in this Aseprite session over a local WebSocket link. Connects to 127.0.0.1 only; nothing leaves the machine.",
5
+ "version": "0.1.0",
6
+ "author": "with-pebbly",
7
+ "publisher": "with-pebbly",
8
+ "license": "MIT",
9
+ "categories": ["Scripts"],
10
+ "contributes": {
11
+ "scripts": [{ "path": "./ai-artist.lua" }]
12
+ },
13
+ "api": 1
14
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "hooks": {
3
+ "SessionStart": [
4
+ {
5
+ "hooks": [
6
+ {
7
+ "type": "command",
8
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/session-status.mjs\""
9
+ }
10
+ ]
11
+ }
12
+ ],
13
+ "PostToolUse": [
14
+ {
15
+ "matcher": "mcp__plugin_aseprite-ai-artist_aseprite__draw|mcp__plugin_aseprite-ai-artist_aseprite__recolor|mcp__plugin_aseprite-ai-artist_aseprite__transform",
16
+ "hooks": [
17
+ {
18
+ "type": "command",
19
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/look-nudge.mjs\""
20
+ }
21
+ ]
22
+ }
23
+ ]
24
+ }
25
+ }
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * After a mutating pixel operation, remind the model to look at the result.
4
+ *
5
+ * Not looking is the single most common failure mode: the tool result says
6
+ * "412 pixels changed" and the model reports a finished sprite it has never
7
+ * seen. The nudge fires once per session so it stays a reminder rather than
8
+ * noise the model learns to skip.
9
+ */
10
+
11
+ import { readFileSync, existsSync, writeFileSync, mkdirSync } from "node:fs";
12
+ import path from "node:path";
13
+ import os from "node:os";
14
+
15
+ let payload = {};
16
+ try {
17
+ payload = JSON.parse(readFileSync(0, "utf8"));
18
+ } catch {
19
+ process.exit(0);
20
+ }
21
+
22
+ const sessionId = payload.session_id || "unknown";
23
+ const stateDir = path.join(
24
+ process.env.CLAUDE_PLUGIN_DATA || path.join(os.tmpdir(), "aseprite-ai-artist"),
25
+ "look-nudge",
26
+ );
27
+ const marker = path.join(stateDir, `${sessionId}.seen`);
28
+
29
+ if (existsSync(marker)) process.exit(0);
30
+
31
+ try {
32
+ mkdirSync(stateDir, { recursive: true });
33
+ writeFileSync(marker, String(Date.now()));
34
+ } catch {
35
+ // A hook that cannot write its marker should still nudge once, not crash.
36
+ }
37
+
38
+ process.stdout.write(
39
+ JSON.stringify({
40
+ hookSpecificOutput: {
41
+ hookEventName: "PostToolUse",
42
+ additionalContext:
43
+ "You just changed pixels. Call `look` before deciding whether it worked — op 'preview' for the overall read, op 'ascii' for exact pixel positions. A tool result saying pixels changed is not evidence that the sprite is right.",
44
+ },
45
+ }),
46
+ );
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Tells the session whether Aseprite is actually reachable, before the model
4
+ * tries to draw and gets a confusing failure five tool calls in.
5
+ *
6
+ * Deliberately cheap: a raw TCP connect to the bridge's control port, no
7
+ * dependencies, hard 400ms budget. A hook that slows session start is a hook
8
+ * people disable.
9
+ */
10
+
11
+ import net from "node:net";
12
+
13
+ const CONTROL_PORT = Number(process.env.ASEPRITE_AI_CONTROL_PORT || 9932);
14
+ const PLUGIN_PORT = Number(process.env.ASEPRITE_AI_PLUGIN_PORT || 9931);
15
+ const BUDGET_MS = 400;
16
+
17
+ function probe(port) {
18
+ return new Promise((resolve) => {
19
+ const socket = net.connect({ host: "127.0.0.1", port });
20
+ const done = (open) => {
21
+ socket.destroy();
22
+ resolve(open);
23
+ };
24
+ socket.setTimeout(BUDGET_MS);
25
+ socket.once("connect", () => done(true));
26
+ socket.once("timeout", () => done(false));
27
+ socket.once("error", () => done(false));
28
+ });
29
+ }
30
+
31
+ const [bridge, plugin] = await Promise.all([probe(CONTROL_PORT), probe(PLUGIN_PORT)]);
32
+
33
+ // The plugin port being open means the bridge is listening for Aseprite, not
34
+ // that Aseprite has connected — only `preflight` can answer that, so say so
35
+ // rather than implying a readiness this probe cannot see.
36
+ const message = bridge
37
+ ? `aseprite-ai-artist: bridge is up on :${CONTROL_PORT}. Call preflight before any drawing to confirm Aseprite itself is attached.`
38
+ : `aseprite-ai-artist: no bridge on :${CONTROL_PORT} yet — it starts on the first tool call. If drawing fails, run \`npx @pebbly/aseprite-ai-artist doctor\`.`;
39
+
40
+ process.stdout.write(
41
+ JSON.stringify({
42
+ hookSpecificOutput: {
43
+ hookEventName: "SessionStart",
44
+ additionalContext: message,
45
+ },
46
+ }),
47
+ );
@@ -0,0 +1,60 @@
1
+ {
2
+ "$comment": "Bundled palette presets. Values are transcribed from each palette's published definition; see docs/PALETTES.md for provenance and how to add more. Anything not listed here can still be loaded from a .gpl/.hex/.pal/.png file via palette op 'load'.",
3
+ "presets": {
4
+ "pico8": {
5
+ "name": "PICO-8",
6
+ "author": "Lexaloffle",
7
+ "size": 16,
8
+ "notes": "Fantasy-console fixed palette. Strong, saturated, forgiving — a good default when a user says 'retro' without more detail.",
9
+ "colors": [
10
+ "#000000", "#1d2b53", "#7e2553", "#008751",
11
+ "#ab5236", "#5f574f", "#c2c3c7", "#fff1e8",
12
+ "#ff004d", "#ffa300", "#ffec27", "#00e436",
13
+ "#29adff", "#83769c", "#ff77a8", "#ffccaa"
14
+ ]
15
+ },
16
+ "gameboy": {
17
+ "name": "Game Boy (DMG)",
18
+ "author": "Nintendo",
19
+ "size": 4,
20
+ "notes": "The original green LCD. Four values only — everything is silhouette and value contrast, no colour to hide behind.",
21
+ "colors": ["#0f380f", "#306230", "#8bac0f", "#9bbc0f"]
22
+ },
23
+ "gameboy-pocket": {
24
+ "name": "Game Boy Pocket",
25
+ "author": "Nintendo",
26
+ "size": 4,
27
+ "notes": "Neutral grey variant of the DMG ramp.",
28
+ "colors": ["#000000", "#555555", "#aaaaaa", "#ffffff"]
29
+ },
30
+ "cga": {
31
+ "name": "CGA (16-colour)",
32
+ "author": "IBM",
33
+ "size": 16,
34
+ "notes": "IBM CGA/EGA text palette. Harsh, high-contrast, unmistakably 1980s PC.",
35
+ "colors": [
36
+ "#000000", "#0000aa", "#00aa00", "#00aaaa",
37
+ "#aa0000", "#aa00aa", "#aa5500", "#aaaaaa",
38
+ "#555555", "#5555ff", "#55ff55", "#55ffff",
39
+ "#ff5555", "#ff55ff", "#ffff55", "#ffffff"
40
+ ]
41
+ },
42
+ "1bit": {
43
+ "name": "1-bit",
44
+ "author": "—",
45
+ "size": 2,
46
+ "notes": "Pure black and white. Forces dithering for every intermediate value.",
47
+ "colors": ["#000000", "#ffffff"]
48
+ },
49
+ "grayscale-8": {
50
+ "name": "Grayscale 8",
51
+ "author": "—",
52
+ "size": 8,
53
+ "notes": "Even value ramp. Useful as a working palette while blocking in form, before colour is chosen.",
54
+ "colors": [
55
+ "#000000", "#242424", "#484848", "#6d6d6d",
56
+ "#919191", "#b6b6b6", "#dadada", "#ffffff"
57
+ ]
58
+ }
59
+ }
60
+ }
package/package.json ADDED
@@ -0,0 +1,69 @@
1
+ {
2
+ "name": "@pebbly/aseprite-ai-artist",
3
+ "version": "0.1.0",
4
+ "description": "Agent-native control layer for Aseprite. MCP server (2025-11-25 spec, designed forward to 2026-07-28) + cross-agent skills for Claude Code, Codex, Gemini CLI and Cursor.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "engines": {
8
+ "node": ">=22.6.0"
9
+ },
10
+ "bin": {
11
+ "aseprite-ai-artist": "dist/cli.js"
12
+ },
13
+ "main": "dist/index.js",
14
+ "types": "dist/index.d.ts",
15
+ "files": [
16
+ "dist",
17
+ "extension",
18
+ "skills",
19
+ "rules",
20
+ "agents",
21
+ "hooks",
22
+ "knowledge",
23
+ "templates",
24
+ "examples",
25
+ "bin",
26
+ "README.md",
27
+ "LICENSE",
28
+ "CHANGELOG.md"
29
+ ],
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "git+https://github.com/with-pebbly/aseprite-ai-artist.git"
33
+ },
34
+ "homepage": "https://github.com/with-pebbly/aseprite-ai-artist#readme",
35
+ "bugs": "https://github.com/with-pebbly/aseprite-ai-artist/issues",
36
+ "keywords": [
37
+ "aseprite",
38
+ "pixel-art",
39
+ "mcp",
40
+ "model-context-protocol",
41
+ "sprites",
42
+ "animation",
43
+ "claude-code",
44
+ "codex",
45
+ "gemini-cli",
46
+ "cursor"
47
+ ],
48
+ "scripts": {
49
+ "build": "tsc -p tsconfig.json",
50
+ "dev": "tsc -p tsconfig.json --watch",
51
+ "clean": "rm -rf dist",
52
+ "typecheck": "tsc -p tsconfig.json --noEmit",
53
+ "test": "node --test --experimental-strip-types tests/color.test.ts tests/render.test.ts tests/bridge.test.ts tests/server.test.ts",
54
+ "lint": "tsc -p tsconfig.json --noEmit",
55
+ "prepublishOnly": "npm run clean && npm run build",
56
+ "test:extension": "aseprite -b --script-param root=. --script tests/extension.test.lua",
57
+ "test:e2e": "node tests/e2e.mjs"
58
+ },
59
+ "dependencies": {
60
+ "@modelcontextprotocol/sdk": "^1.30.0",
61
+ "ws": "^8.21.3",
62
+ "zod": "^3.25.76"
63
+ },
64
+ "devDependencies": {
65
+ "@types/node": "^24.3.0",
66
+ "@types/ws": "^8.18.1",
67
+ "typescript": "^5.9.2"
68
+ }
69
+ }
@@ -0,0 +1,57 @@
1
+ # Core principles
2
+
3
+ Pixel art is not "a small image". It is an image where every pixel was a
4
+ decision. That constraint is the whole medium: an agent that treats a 32×32
5
+ canvas as a low-resolution render will produce something that is technically
6
+ correct and unmistakably wrong.
7
+
8
+ ## The loop
9
+
10
+ Inspect → decide → draw in one batch → look → fix → validate.
11
+
12
+ Skipping "look" is the single most common failure. You cannot tell from a tool
13
+ result whether a sprite reads; you have to see it. `look` op `preview` shows you
14
+ what a human sees; op `ascii` shows you exactly which pixel is where.
15
+
16
+ ## Resolution is a budget, not a limitation
17
+
18
+ At 16×16 a character gets roughly 3 pixels of head, 5 of torso and 4 of legs.
19
+ There is no room for a nose. Decide what the sprite must communicate at a
20
+ glance — a class, a threat, a direction — and spend the pixels there.
21
+
22
+ Common sizes and what fits:
23
+
24
+ | Size | Fits |
25
+ |------|------|
26
+ | 8×8 | An icon or an item. One idea. |
27
+ | 16×16 | A readable character with 2–3 colours per material. |
28
+ | 32×32 | Facial suggestion, distinct armour pieces, believable weapon. |
29
+ | 48×48+ | Real detail; now the risk is noise, not scarcity. |
30
+
31
+ ## Constraints beat cleverness
32
+
33
+ Pick and write down, before drawing: canvas size, palette, light direction,
34
+ outline style, and whether the sprite is side-on, top-down or 3⁄4. Every later
35
+ decision either follows from those or is a mistake.
36
+
37
+ ## What makes generated pixel art look generated
38
+
39
+ In rough order of how quickly a person spots it:
40
+
41
+ 1. **Too many colours.** Ninety near-identical browns instead of a four-step
42
+ ramp. Fix: a palette, and `paletteLock` left on.
43
+ 2. **Flat luminance shading.** Shadow = the same hue, darker. Real shading
44
+ shifts hue (see `rules://02-shading-and-light`).
45
+ 3. **Anti-aliased or semi-transparent pixels.** Usually from a resize or a soft
46
+ brush. At 1× they read as blur, not smoothness.
47
+ 4. **Noise.** Isolated pixels with no neighbour, "detail" that is just dirt.
48
+ 5. **Mushy silhouette.** The shape does not read as anything when filled black.
49
+ 6. **Mechanical timing.** Every animation frame the same duration.
50
+
51
+ `validate` checks 1, 3, 4 and 6 mechanically. The rest you have to look at.
52
+
53
+ ## Never silently change what the user did not ask about
54
+
55
+ Do not resize their canvas, re-index their colour mode, flatten their layers or
56
+ overwrite their file unless they asked. When a task seems to require one of
57
+ those, say so and ask.
@@ -0,0 +1,51 @@
1
+ # Palette and colour
2
+
3
+ ## Decide the palette first
4
+
5
+ A palette chosen after the art is finished means repainting the art. Choose
6
+ before the first pixel, and state it back to the user so they can object early.
7
+
8
+ Reasonable defaults when the user has no preference:
9
+
10
+ - "retro" / "8-bit" / no detail given → **PICO-8** (16 colours, forgiving, reads well)
11
+ - "Game Boy" / "monochrome" → **gameboy** (4 values; pure form study)
12
+ - "NES-like" → a hand-built 12–16 colour palette, 3–4 hues × 4 values
13
+ - A specific game named → sample it: `reference` op `sample_palette`
14
+
15
+ Load one with `palette` op `preset`, or a file the user has with op `load`.
16
+
17
+ ## Size
18
+
19
+ Most sprites want **8–32 colours total**. Per material, 3–5 steps is plenty:
20
+ shadow, base, light, and optionally a deep shadow and a specular.
21
+
22
+ More colours is not more quality. It is more decisions, more inconsistency, and
23
+ a sprite that cannot be recoloured for a palette swap later.
24
+
25
+ ## Ramps, not gradients
26
+
27
+ A **ramp** is an ordered run of colours for one material. Build them with
28
+ `palette` op `ramp`, which hue-shifts as it goes (see
29
+ `rules://02-shading-and-light`). Reuse ramps across materials wherever you can —
30
+ sharing the darkest step between skin and leather ties a sprite together and
31
+ costs nothing.
32
+
33
+ ## Palette lock
34
+
35
+ `draw` and `recolor` snap every colour to the nearest palette entry by
36
+ perceptual (CIELAB) distance, by default. Leave it on.
37
+
38
+ When the report says a colour moved a long way (ΔE > 12), that is the tool
39
+ telling you the palette has no colour for what you asked. Do not turn the lock
40
+ off to force it through — either pick a different colour or tell the user the
41
+ palette needs a new entry and let them decide.
42
+
43
+ ## Contrast is what makes a sprite readable
44
+
45
+ Value contrast, not hue contrast, carries readability. A sprite that is all
46
+ mid-tones disappears against a background. Check by squinting — or run
47
+ `recolor` op `desaturate` on a copy and see whether the shapes survive.
48
+
49
+ Keep the darkest and lightest steps of a ramp genuinely far apart. Two colours
50
+ with ΔE under 3 are the same colour to a viewer; `palette` op `analyze` flags
51
+ those as near-duplicates.
@@ -0,0 +1,56 @@
1
+ # Shading and light
2
+
3
+ ## Pick a light direction and never move it
4
+
5
+ One light source, stated up front — usually upper-left or upper-right. Every
6
+ surface in the sprite is lit from that direction, in every frame of every
7
+ animation. A sprite whose light drifts between frames looks like it is made of
8
+ different materials each frame.
9
+
10
+ ## Hue-shift, always
11
+
12
+ The difference between pixel art and a brightness slider:
13
+
14
+ - **Shadows** rotate toward the ambient — in practice, **toward blue/purple** —
15
+ and lose a little saturation.
16
+ - **Highlights** rotate **toward yellow/orange** and gain a little saturation.
17
+
18
+ A red `#c04030` shades to something like `#8a2f45` (cooler), not `#802b20`
19
+ (the same hue, darker). The second one is the tell.
20
+
21
+ `recolor` op `shade` applies this rule; `palette` op `ramp` builds ramps that
22
+ already obey it. Use them instead of picking darker hex values by hand.
23
+
24
+ ## Order of work
25
+
26
+ 1. **Block in** flat base colours per material. No shading at all yet.
27
+ 2. **Check the silhouette** — see `rules://03-silhouette-and-form`.
28
+ 3. **One shadow step**, on the side away from the light. Stop and look.
29
+ 4. **One light step**, on the side facing it.
30
+ 5. Only then consider a second shadow, a specular, or reflected light.
31
+
32
+ Most sprites are finished at step 4. Detail added before the form is right just
33
+ makes the wrongness harder to see.
34
+
35
+ ## Dithering
36
+
37
+ Dithering buys you an intermediate value you do not have colours for, and it
38
+ adds texture. It is not free: at small sizes it reads as noise.
39
+
40
+ - Use it for large flat areas, skies, gradients on 32×32 and up.
41
+ - Avoid it on a 16×16 character; you do not have the pixels.
42
+ - Prefer ordered patterns (`bayer4`) over `noise`. Random dithering looks like
43
+ dirt at low resolutions.
44
+ - Keep the two dithered colours adjacent in a ramp. Dithering between distant
45
+ colours produces visible speckle.
46
+
47
+ `draw` op `dither` handles the pattern; you choose the two colours and the ratio.
48
+
49
+ ## Anti-aliasing
50
+
51
+ Manual AA — placing one intermediate colour at a hard diagonal — is a legitimate
52
+ technique on larger sprites. It is **not** the same as alpha blending.
53
+
54
+ Never leave semi-transparent pixels in pixel art. They come from soft brushes and
55
+ non-integer resizes, and they make a sprite look blurry and impossible to palette
56
+ swap. `validate` flags them.
@@ -0,0 +1,60 @@
1
+ # Silhouette and form
2
+
3
+ ## The silhouette test
4
+
5
+ Fill the sprite with a single flat colour. Can you still tell what it is? If not,
6
+ no amount of shading will save it.
7
+
8
+ This is the highest-leverage check in pixel art and it takes one call:
9
+ `look` op `preview` after a `recolor` op `replace` on a copy, or just squint at
10
+ the preview. Do it before you shade, not after.
11
+
12
+ Silhouettes fail because of:
13
+
14
+ - **Symmetry.** A perfectly symmetrical pose reads as a statue. Break it: one
15
+ arm forward, weapon on one side, head turned.
16
+ - **Tangents.** An arm whose outline touches the torso outline fuses into one
17
+ blob. Leave a gap, or overlap clearly.
18
+ - **Uniform limb width.** Everything the same thickness reads as a stick figure.
19
+
20
+ ## Proportions
21
+
22
+ Game characters are usually stylised shorter than life. Common ratios, measured
23
+ in head-heights:
24
+
25
+ | Style | Heads tall | Reads as |
26
+ |-------|-----------|----------|
27
+ | 2 | Chibi / cute | Toy, mascot |
28
+ | 3–4 | Classic JRPG / action | The default; safe |
29
+ | 5–6 | Realistic-ish | Needs 48px+ to work |
30
+
31
+ At 16×16 you are at 2–3 heads whether you planned it or not. Plan it.
32
+
33
+ ## The 3⁄4 view
34
+
35
+ Most top-down and isometric games want 3⁄4: the character faces the camera but
36
+ is seen slightly from above. In practice:
37
+
38
+ - The head shows the face **and** a little of the top of the skull.
39
+ - The shoulders are wider than a pure front view suggests.
40
+ - Feet are visible as separate shapes, not a single base.
41
+ - The far-side limb is partly hidden by the torso — that is what sells depth.
42
+
43
+ ## Direction sets
44
+
45
+ A 4-direction set is down / up / left / right; 8 adds the diagonals. Rules:
46
+
47
+ - **Left and right are mirrors** — draw one, use `transform` op `flip` on a
48
+ copied layer. Mirror asymmetric details (a scar, a shoulder pad) back by hand
49
+ if the character has any, or accept the flip and say so.
50
+ - **Up** shows the back of the head and no face. This is the frame agents most
51
+ often get wrong by drawing a face on the back of a head.
52
+ - Keep the **same pixel height and centre of mass** across all directions, or
53
+ the character appears to bob when the player turns.
54
+
55
+ ## Volume consistency
56
+
57
+ Across an animation, a limb must stay the same number of pixels thick unless it
58
+ is foreshortened on purpose. Volume drift — an arm that quietly gains two pixels
59
+ by frame four — is very visible in motion and invisible frame by frame. Review
60
+ with `look` op `filmstrip`, which is exactly what it is for.
@@ -0,0 +1,46 @@
1
+ # Outlines and edges
2
+
3
+ ## Choose one outline style and hold it
4
+
5
+ | Style | What it is | Use when |
6
+ |-------|-----------|----------|
7
+ | **None** | Shapes defined by colour alone | Backgrounds, high-colour art |
8
+ | **Full black** | Every edge outlined in one dark colour | Icons, high-contrast game art |
9
+ | **Selective** | Outline only where the shape meets the background | The usual choice for characters |
10
+ | **Coloured** | Outline is a dark version of the fill it borders | Soft, painterly look |
11
+
12
+ Selective outlining — outline on the outside, none between internal shapes — is
13
+ what most sprites want. It keeps the silhouette crisp without turning the
14
+ interior into a colouring book.
15
+
16
+ Pure black outlines on everything read as harsh and flatten the form. A very dark
17
+ version of the sprite's own colours nearly always looks better.
18
+
19
+ ## Clean lines
20
+
21
+ A pixel line is clean when its segment lengths are consistent: 2,2,2,2 or
22
+ 4,4,4,4 — not 3,2,4,1. Inconsistent runs produce the jagged look people call
23
+ "jaggies", and they are the difference between a hand-drawn-looking curve and a
24
+ computed one.
25
+
26
+ - A 45° line is one pixel per step.
27
+ - A shallow line is a run of N, then N, then N. Keep N the same.
28
+ - Never place a single pixel that breaks an otherwise even run.
29
+
30
+ `draw` op `line` uses Bresenham, which is even by construction. Hand-placed
31
+ `pixels` ops are where runs go wrong — check them with `look` op `ascii`.
32
+
33
+ ## Doubles and corners
34
+
35
+ Avoid **doubled pixels** on a diagonal: two pixels side by side inside a line
36
+ that is otherwise single-width. They read as a lump.
37
+
38
+ At a corner, one pixel is a sharp corner and two is a rounded one. Pick
39
+ deliberately and be consistent across the sprite.
40
+
41
+ ## Banding
42
+
43
+ Banding is when two ramp steps run parallel for a long stretch, creating a
44
+ visible stripe that reads as a contour line rather than a curved surface. Break
45
+ it by letting the boundary between steps wander, or by dithering a short section
46
+ of it.
@@ -0,0 +1,62 @@
1
+ # Animation
2
+
3
+ ## Timing is the performance
4
+
5
+ Frame durations carry more of the life in a cycle than the drawings do. Uniform
6
+ timing reads mechanical no matter how good the poses are.
7
+
8
+ Rules of thumb, at 60fps-equivalent thinking but expressed in milliseconds:
9
+
10
+ | Cycle | Frames | Typical timing |
11
+ |-------|--------|----------------|
12
+ | Idle (breathing) | 2–4 | 200–400ms each, slow |
13
+ | Walk | 4–8 | 100–150ms; hold contact poses longer |
14
+ | Run | 6–8 | 60–100ms |
15
+ | Attack | 3–6 | Fast on the strike (40–60ms), long on the hold (150–250ms) |
16
+
17
+ Set a whole cycle at once: `frame` op `set_duration` with a `durations` array.
18
+
19
+ ## Key poses first
20
+
21
+ Draw the extremes, then the in-betweens. Never animate by nudging pixels frame
22
+ to frame — drift accumulates and by frame six the character is a different size.
23
+
24
+ A walk cycle's canonical poses:
25
+
26
+ 1. **Contact** — front foot lands, back foot pushing off. Widest stance.
27
+ 2. **Down** — weight over the front leg, body at its **lowest**.
28
+ 3. **Pass** — legs together, body at its **highest**. This is the frame people
29
+ forget, and its absence is why a walk looks like sliding.
30
+ 4. **Up** — pushing off, rising.
31
+
32
+ Then mirror all four for the other leg: 8 frames. A 4-frame walk uses contact
33
+ and pass for each leg.
34
+
35
+ ## The principles that survive at 16 pixels
36
+
37
+ - **Anticipation** — a small movement opposite to the action before it. One
38
+ frame of crouch before a jump. Skipping it makes everything feel weightless.
39
+ - **Follow-through** — hair, cloak and weapon keep moving after the body stops.
40
+ - **Squash and stretch** — sparingly, and only where a material would deform.
41
+ - **Arcs** — a hand travels along a curve, never a straight line.
42
+ - **Overlap** — parts do not all start and stop together.
43
+
44
+ Easing does not exist frame-by-frame; you get it from **spacing**. Poses close
45
+ together = slow; far apart = fast. That is the whole trick.
46
+
47
+ ## Working method
48
+
49
+ 1. `rig` the character onto separate layers first (see `rules://06-layers-and-rigging`).
50
+ 2. Block in the key poses on their own frames. Look at them as a filmstrip.
51
+ 3. Add in-betweens only once the keys read.
52
+ 4. Set timing.
53
+ 5. Tag the cycle. Untagged frames are unusable by a game engine.
54
+ 6. Review with `look` op `filmstrip` — a vision model reads only the first
55
+ frame of a GIF, so a filmstrip is the only way to actually see the motion.
56
+
57
+ ## Cross-frame checks
58
+
59
+ - Does the character stay the same height? (Except deliberate squash.)
60
+ - Do limbs keep the same thickness?
61
+ - Does the light stay in the same place?
62
+ - Does the cycle loop — is frame N a plausible predecessor of frame 1?
@@ -0,0 +1,52 @@
1
+ # Layers and rigging
2
+
3
+ ## Split before you animate, not after
4
+
5
+ Pixels baked into one layer cannot be animated without redrawing them. Separating
6
+ a finished sprite into limbs later is more work than building it separated. If
7
+ the sprite will ever move, rig it first.
8
+
9
+ ## The standard character rig
10
+
11
+ Bottom to top in the layer stack, so nearer parts draw over farther ones:
12
+
13
+ ```
14
+ arm-near ← the arm on the camera side
15
+ leg-near
16
+ head
17
+ torso
18
+ arm-far ← partly hidden by the torso; this is what sells depth
19
+ leg-far
20
+ shadow ← ground contact, if the style has one
21
+ ```
22
+
23
+ Add `weapon`, `cape`, `hair-back` and `hair-front` as the design needs. Keep the
24
+ names stable — every later tool call refers to them by name.
25
+
26
+ Build the whole rig in one `layer` call with a `batch` array; that is one undo
27
+ step for the user and one round trip for you.
28
+
29
+ ## Groups
30
+
31
+ Group related layers (`arms`, `legs`) once the count passes about eight. Groups
32
+ hold no pixels themselves — target their children when drawing.
33
+
34
+ ## Cels
35
+
36
+ A **cel** is one layer's image on one frame. Cels are only as big as their
37
+ content, which is why moving a limb between frames is `cel` op `move` (cheap,
38
+ lossless) rather than redrawing it.
39
+
40
+ **Linked cels** share one image across several frames: edit once, every linked
41
+ frame updates. Use them for parts that genuinely do not move in a cycle — a
42
+ static torso under moving arms. Do not link something you will later want to
43
+ differ; unlinking after the fact loses the shared history.
44
+
45
+ ## Non-destructive habits
46
+
47
+ - Keep a `reference` layer locked and semi-transparent while tracing; delete it
48
+ before export.
49
+ - Keep sketch/blockout on its own layer until the clean pass is done.
50
+ - Never merge layers to "simplify" unless the user asked. The layer split is the
51
+ sprite's editability.
52
+ - Hide layers rather than deleting them when trying an alternative.