@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.
- package/CHANGELOG.md +170 -0
- package/LICENSE +21 -0
- package/README.md +169 -0
- package/agents/animation-director.md +65 -0
- package/agents/palette-smith.md +57 -0
- package/agents/pixel-critic.md +69 -0
- package/agents/rig-builder.md +46 -0
- package/bin/aseprite-ai-artist +6 -0
- package/bin/aseprite-ai-artist.mjs +44 -0
- package/dist/bridge/bridge.d.ts +52 -0
- package/dist/bridge/bridge.js +230 -0
- package/dist/bridge/bridge.js.map +1 -0
- package/dist/bridge/client.d.ts +54 -0
- package/dist/bridge/client.js +214 -0
- package/dist/bridge/client.js.map +1 -0
- package/dist/cli.d.ts +10 -0
- package/dist/cli.js +272 -0
- package/dist/cli.js.map +1 -0
- package/dist/extension.d.ts +21 -0
- package/dist/extension.js +65 -0
- package/dist/extension.js.map +1 -0
- package/dist/install.d.ts +35 -0
- package/dist/install.js +185 -0
- package/dist/install.js.map +1 -0
- package/dist/lib/color.d.ts +66 -0
- package/dist/lib/color.js +170 -0
- package/dist/lib/color.js.map +1 -0
- package/dist/lib/protocol.d.ts +78 -0
- package/dist/lib/protocol.js +72 -0
- package/dist/lib/protocol.js.map +1 -0
- package/dist/lib/render.d.ts +62 -0
- package/dist/lib/render.js +134 -0
- package/dist/lib/render.js.map +1 -0
- package/dist/lib/skills.d.ts +43 -0
- package/dist/lib/skills.js +112 -0
- package/dist/lib/skills.js.map +1 -0
- package/dist/lib/version.d.ts +4 -0
- package/dist/lib/version.js +25 -0
- package/dist/lib/version.js.map +1 -0
- package/dist/server.d.ts +15 -0
- package/dist/server.js +144 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/assets.d.ts +3 -0
- package/dist/tools/assets.js +181 -0
- package/dist/tools/assets.js.map +1 -0
- package/dist/tools/craft.d.ts +12 -0
- package/dist/tools/craft.js +143 -0
- package/dist/tools/craft.js.map +1 -0
- package/dist/tools/draw.d.ts +3 -0
- package/dist/tools/draw.js +256 -0
- package/dist/tools/draw.js.map +1 -0
- package/dist/tools/escape.d.ts +12 -0
- package/dist/tools/escape.js +49 -0
- package/dist/tools/escape.js.map +1 -0
- package/dist/tools/kit.d.ts +29 -0
- package/dist/tools/kit.js +85 -0
- package/dist/tools/kit.js.map +1 -0
- package/dist/tools/look.d.ts +7 -0
- package/dist/tools/look.js +215 -0
- package/dist/tools/look.js.map +1 -0
- package/dist/tools/palette.d.ts +3 -0
- package/dist/tools/palette.js +232 -0
- package/dist/tools/palette.js.map +1 -0
- package/dist/tools/session.d.ts +3 -0
- package/dist/tools/session.js +231 -0
- package/dist/tools/session.js.map +1 -0
- package/dist/tools/structure.d.ts +3 -0
- package/dist/tools/structure.js +218 -0
- package/dist/tools/structure.js.map +1 -0
- package/examples/mcp-configs/claude-code.json +9 -0
- package/examples/mcp-configs/codex.toml +13 -0
- package/examples/mcp-configs/cursor.json +9 -0
- package/examples/mcp-configs/gemini-cli.json +9 -0
- package/examples/mcp-configs/vscode.json +10 -0
- package/extension/ai-artist.lua +3291 -0
- package/extension/package.json +14 -0
- package/hooks/hooks.json +25 -0
- package/hooks/look-nudge.mjs +46 -0
- package/hooks/session-status.mjs +47 -0
- package/knowledge/palettes.json +60 -0
- package/package.json +69 -0
- package/rules/00-core-principles.md +57 -0
- package/rules/01-palette-and-color.md +51 -0
- package/rules/02-shading-and-light.md +56 -0
- package/rules/03-silhouette-and-form.md +60 -0
- package/rules/04-outlines-and-edges.md +46 -0
- package/rules/05-animation.md +62 -0
- package/rules/06-layers-and-rigging.md +52 -0
- package/rules/07-review-checklist.md +51 -0
- package/rules/README.md +16 -0
- package/skills/README.md +29 -0
- package/skills/pixel-animate/SKILL.md +91 -0
- package/skills/pixel-brief/SKILL.md +61 -0
- package/skills/pixel-draw/SKILL.md +93 -0
- package/skills/pixel-export/SKILL.md +80 -0
- package/skills/pixel-fix/SKILL.md +91 -0
- package/skills/pixel-new/SKILL.md +70 -0
- package/skills/pixel-palette/SKILL.md +80 -0
- package/skills/pixel-review/SKILL.md +81 -0
- package/skills/pixel-rig/SKILL.md +76 -0
- package/skills/pixel-shade/SKILL.md +77 -0
- package/skills/pixel-tileset/SKILL.md +104 -0
- 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
|
+
}
|
package/hooks/hooks.json
ADDED
|
@@ -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.
|