Witch's Hut · black_cat.js
黒猫
32×32 の床置きの小物で、魔女の黒猫が刺繍入りの小さなクッション(赤いベルベット、金の組紐、四隅の房)に座っています。毛並みはほぼ黒で、左上の縁に紫の光が入り、目は縦長の瞳の黄緑、赤い首輪に真鍮の鈴が付いています。変種は 4 つで、尻尾を振って一度まばたきする idle(16 コマ)、目を閉じて上げた前足をなめる groom(12 コマ)、脚を突っ張って毛を逆立てる一回再生の hiss(8 コマ)、鼻先と尻尾をくっつけて丸まり寝息を立てる sleep(20 コマ)です。
6×
動くスプライト素材: Witch's Hut(itch.io)Vol.5・ハロウィンの小物 12 点・42 アニメ
冒頭のコメント。これが指示書です。サイズ、視点、光の向き、すべての変種が書かれています。
black_cat.js · 1–10
// Black cat — 32x32 floor prop, top-down 3/4, key light top-left.// A witch's black cat sitting on a small embroidered cushion (red velvet, gold braid, corner tassels). The fur is// near-black (n0 / v0) with a violet rim light on the top-left edges, yellow-green eyes with slit pupils, and a// red collar with a brass bell.// Variants:// idle 16f loop: the tail swishes, an ear twitches, one blink// groom 12f loop: head tilted down, licks a raised paw (tongue flicks), eyes shut// hiss once, 8f: stands up on stiff legs, fur bristles, ears flatten, eyes flare, mouth opens on the fangs;// settles back; the last frame equals idle frame 0// sleep 20f loop: curled up nose-to-tail, breathing, a small 'z' drifts up最後の数行。submit でキャンバスの大きさと、描画エンジンが書き出す変種の一覧を指定します。
black_cat.js · 256–267
submit(BlackCat, { size: [W, 32], origin: "top-left", kind: "icon", scale: 1, animate: { fps: 10, duration: N_IDLE / 10, loop: true }, anchor: { x: CX, y: GROUND }, args: ["idle"], variants: [ { name: "idle", args: ["idle"] }, { name: "groom", args: ["groom"], animate: { fps: 10, duration: N_GROOM / 10, loop: true } }, { name: "hiss", args: ["hiss"], animate: { fps: 10, duration: N_HISS / 10, loop: false } }, { name: "sleep", args: ["sleep"], animate: { fps: 8, duration: N_SLEEP / 8, loop: true } }, ],});プログラム全体を表示 (267 行)
black_cat.js · 267 行
// Black cat — 32x32 floor prop, top-down 3/4, key light top-left.// A witch's black cat sitting on a small embroidered cushion (red velvet, gold braid, corner tassels). The fur is// near-black (n0 / v0) with a violet rim light on the top-left edges, yellow-green eyes with slit pupils, and a// red collar with a brass bell.// Variants:// idle 16f loop: the tail swishes, an ear twitches, one blink// groom 12f loop: head tilted down, licks a raised paw (tongue flicks), eyes shut// hiss once, 8f: stands up on stiff legs, fur bristles, ears flatten, eyes flare, mouth opens on the fangs;// settles back; the last frame equals idle frame 0// sleep 20f loop: curled up nose-to-tail, breathing, a small 'z' drifts upconst W = 32, CX = 16, GROUND = 30;const N_IDLE = 16, N_GROOM = 12, N_HISS = 8, N_SLEEP = 20;const FUR = ["n0", "n0", "n0", "v0"];// ---- cushion ------------------------------------------------------------------------------------------------const CU = { cx: 16, cy: 24, rx: 12, ry: 3.6 };function cushion(g) { const { cx, cy, rx, ry } = CU; g.part("cushion", () => { // the plump front side, then the lit top face g.field({ x: [cx - rx, cx + rx], y: [cy - 1, cy + ry + 3] }, (x, y) => { const nx = (x + 0.5 - cx) / rx; if (Math.abs(nx) > 1) return null; const yb = cy + ry * Math.sqrt(1 - nx * nx) + 2.6 - 0.8 * nx * nx; const yt = cy; if (y + 0.5 < yt || y + 0.5 > yb) return null; return nx < -0.55 ? "r1" : nx < 0.6 ? "r0" : "r0"; }); g.field({ x: [cx - rx, cx + rx], y: [cy - ry - 1, cy + ry + 1] }, (x, y) => { const nx = (x + 0.5 - cx) / rx, ny = (y + 0.5 - cy) / ry; const r = nx * nx + ny * ny; if (r > 1) return null; if (r > 0.7 && ny > 0.3) return nx < -0.3 ? "g2" : "g1"; // gold braid along the front edge const a = -0.6 * nx - 0.5 * ny; return a > 0.45 ? "r2" : "r1"; }); // embroidered gold diamonds on the top face (mostly hidden under the cat; they show at the sides) for (const [x, y] of [[7, 24], [25, 24]]) { g.pixel(x, y - 1, "g1"); g.pixel(x - 1, y, "g0"); g.pixel(x + 1, y, "g0"); g.pixel(x, y + 1, "g0"); } // a seam along the side's bottom g.line([cx - rx + 3, cy + ry + 2], [cx + rx - 3, cy + ry + 2], "w0"); }); g.part("tassels", () => { for (const [x, s] of [[cx - rx - 1, -1], [cx + rx + 1, 1]]) { g.pixel(x, cy + 1, "g2"); g.pixel(x, cy + 2, s < 0 ? "g2" : "g1"); g.pixel(x + s, cy + 3, "g1"); g.pixel(x, cy + 3, s < 0 ? "g3" : "g1"); g.pixel(x, cy + 4, "g0"); } });}// ---- fur shading -------------------------------------------------------------------------------------------function furBlob(g, name, cx, cy, rx, ry, bias, inkEdge) { g.part(name, () => g.field({ x: [Math.floor(cx - rx), Math.ceil(cx + rx)], y: [Math.floor(cy - ry), Math.ceil(cy + ry)] }, (x, y) => { const i = litIndex(x, y, cx, cy, rx, ry, 4, bias || 0); if (i < 0) return null; // separation from a body part behind it: ink on the right/bottom boundary if (inkEdge && (litIndex(x + 1, y, cx, cy, rx, ry, 4, 0) < 0 || litIndex(x, y + 1, cx, cy, rx, ry, 4, 0) < 0)) return "ink"; const nx = (x + 0.5 - cx) / rx, ny = (y + 0.5 - cy) / ry; const r = Math.sqrt(nx * nx + ny * ny); // violet rim light: the top-left edge if (r > 0.72 && -0.7 * nx - 0.7 * ny > 0.55) return "v1"; return FUR[i]; }));}// pose: { lift, puff, hx, hy (head offset), earL, earR (0 up .. 1 flat), eyes, mouth, tail:[...path], tailW, paw }function cat(g, p) { const L = p.lift, P = p.puff; const hx = 15 + p.hx, hy = 10 - L + p.hy; g.outlined("ink", () => { // tail first (behind the haunch on the right) g.part("tail", () => g.sweep(p.tail, { w0: p.tailW, w1: p.tailW * 0.75 }, (u, v) => (v < -0.2 && u < 0.85 ? "v0" : u > 0.86 ? "v0" : "n0"))); if (P > 0.6) g.part("tailFluff", () => { for (let i = 1; i < p.tail.length; i++) { const [x, y] = p.tail[i]; g.pixel(x + (i & 1 ? 2 : -2), y, "n0"); } }); furBlob(g, "haunches", 16, 21 - L * 0.6, 7 + P, 4.6 + P * 0.4, 0.05); furBlob(g, "chest", 15, 17 - L, 4.6 + P * 0.8, 4.6, 0.1); if (P > 0.6) g.part("hackles", () => { // bristling fur along the back/shoulders: a row of spikes the outline turns into a jagged silhouette for (let k = 0; k < 6; k++) { const a = Math.PI * (1.1 + k * 0.16); const x = Math.round(16 + Math.cos(a) * (8 + P)), y = Math.round(21 - L * 0.6 + Math.sin(a) * (5.4 + P * 0.4)); g.pixel(x, y, k < 3 ? "v0" : "n0"); } }); // front legs: stiff columns down to the cushion, the left one lit g.part("legs", () => { const top = 18 - L; for (const [x0, lit] of [[12, true], [17, false]]) { if (!lit && p.paw) continue; // raised for grooming (drawn separately) for (let y = top; y <= 25; y++) { g.pixel(x0, y, lit ? "v0" : "n0"); g.pixel(x0 + 1, y, lit ? "n0" : "n0"); } g.pixel(x0, 25, lit ? "n1" : "v0"); g.pixel(x0 + 1, 25, "n0"); // paws: lit toes g.pixel(x0 - 1, 25, "n0"); } g.line([14, 20 - L], [14, 25], "ink"); // gap between the legs }); g.part("head", () => { furBlob(g, "skull", hx, hy, 5.4, 4.3, 0.12); // ears: lean back as earX -> 1 const ear = (s, f) => { const base0 = [hx + s * 5, hy - 0.5 + f * 1.5], base1 = [hx + s * 1.5, hy - 3.2 + f]; const tip = [hx + s * (4 + f * 3), hy - 6.5 + f * 4.5]; g.fillPoly([base0, tip, base1].map(([a, b]) => [Math.round(a), Math.round(b)]), s < 0 ? "v0" : "n0"); const ix = Math.round(hx + s * (3.4 + f * 1.6)), iy = Math.round(hy - 3.6 + f * 2.4); g.pixel(ix, iy, "r0"); if (f < 0.5) g.pixel(ix, iy + 1, "r0"); if (s < 0) g.pixel(Math.round(tip[0]), Math.round(tip[1]), "v1"); }; ear(-1, p.earL); ear(1, p.earR); }); }); face(g, hx, hy, p); g.part("collar", () => { const cy = hy + 4; for (let x = hx - 3; x <= hx + 3; x++) g.pixel(x, cy + (Math.abs(x - hx) > 2 ? -1 : 0), x < hx ? "r2" : "r1"); if (!p.paw) { g.pixel(hx, cy + 1, "g2"); g.pixel(hx - 1, cy + 1, "g3"); g.pixel(hx, cy + 2, "g0"); } }); g.part("chestTuft", () => { g.pixel(hx, hy + 7 - (p.paw ? 1 : 0), "n1"); g.pixel(hx - 1, hy + 8, "v0"); }); g.outlined("ink", () => { if (p.paw) g.part("pawRaised", () => { // the right forepaw lifted to the mouth (drawn in front of the chest) const [px, py] = p.paw; g.rect({ x: [px, px + 1], y: [py, 21] }, "v0", { fill: "v0" }); g.line([px + 2, py], [px + 2, 21], "ink"); // the foreleg's shaded edge against the chest g.line([px - 1, py + 1], [px - 1, 21], "ink"); g.rect({ x: [px - 1, px + 1], y: [py - 1, py] }, "n0", { fill: "n0" }); g.pixel(px - 1, py - 1, "v1"); g.pixel(px, py - 1, "n1"); g.pixel(px + 1, py - 1, "v0"); g.line([px - 1, py + 1 - 0], [px - 1, py + 1], "ink"); g.pixel(px + 2, py - 1, "ink"); g.pixel(px + 2, py, "ink"); }); });}function face(g, hx, hy, p) { g.part("eyes", () => { for (const s of [-1, 1]) { const x0 = hx + (s < 0 ? -4 : 2), y = hy; if (p.eyes === "closed") { g.line([x0, y + 1], [x0 + 2, y + 1], "n2"); g.pixel(x0 + 1, y, "n0"); continue; } if (p.eyes === "half") { g.line([x0, y], [x0 + 2, y], "n0"); g.line([x0, y + 1], [x0 + 2, y + 1], "e2"); g.pixel(x0 + 1, y + 1, "ink"); continue; } if (p.eyes === "wide") { g.rect({ x: [x0, x0 + 2], y: [y - 1, y + 1] }, "e3", { fill: "e3" }); g.pixel(x0, y - 1, "g3"); g.pixel(x0 + 1, y - 1, "g3"); g.pixel(x0 + 1, y, "ink"); // a pinpoint pupil continue; } g.pixel(x0, y, s < 0 ? "g3" : "e3"); g.pixel(x0 + 1, y, "ink"); g.pixel(x0 + 2, y, "e3"); g.pixel(x0, y + 1, "e2"); g.pixel(x0 + 1, y + 1, "ink"); g.pixel(x0 + 2, y + 1, "e1"); } }); g.part("muzzle", () => { g.pixel(hx, hy + 2, "r0"); // nose if (p.mouth === "hiss") { g.rect({ x: [hx - 1, hx + 1], y: [hy + 3, hy + 4] }, "r1", { fill: "r1" }); g.pixel(hx - 1, hy + 3, "n5"); g.pixel(hx + 1, hy + 3, "n5"); g.pixel(hx, hy + 4, "r0"); } else if (p.mouth === "lick") { g.pixel(hx - 1, hy + 3, "r2"); g.pixel(hx - 1, hy + 4, "r3"); } else { g.pixel(hx - 1, hy + 3, "n1"); g.pixel(hx + 1, hy + 3, "n1"); } }); g.detached("whiskers", () => { g.line([hx - 7, hy + 2], [hx - 6, hy + 2], "n3"); g.pixel(hx - 7, hy + 3, "n2"); g.line([hx + 6, hy + 2], [hx + 7, hy + 2], "n2"); });}// ---- poses --------------------------------------------------------------------------------------------------function idlePose(t) { const sw = Math.sin(TAU * t), sw2 = Math.sin(TAU * (t + 0.12)); const f = Math.round(t * N_IDLE) % N_IDLE; return { lift: 0, puff: 0, hx: 0, hy: 0, earL: 0, earR: f === 5 || f === 6 ? 0.5 : 0, // the right ear flicks eyes: f === 10 || f === 12 ? "half" : f === 11 ? "closed" : "open", mouth: "", tailW: 1.3, tail: [[21, 24], [24, 25], [26.5, 23.5], [27.5 + sw * 0.6, 20.5], [27 + sw2 * 2, 17.5]], };}function groomPose(t) { const f = Math.round(t * N_GROOM) % N_GROOM; const bob = Math.round(0.5 + 0.5 * Math.sin(TAU * 2 * t)); // two licks per loop const p = idlePose(0); p.hx = -1; p.hy = 1 + bob; p.eyes = "closed"; p.mouth = bob ? "" : "lick"; p.paw = [16, 15 + (1 - bob)]; p.tail = [[21, 24], [24, 25], [26.5, 24], [28, 22.5 + Math.sin(TAU * t)]]; return p;}const HISS = [ // lift, puff, ears, eyes, mouth [1, 0.4, 0.4, "wide", ""], [2, 1.0, 1.0, "wide", "hiss"], [3, 1.5, 1.0, "wide", "hiss"], [3, 1.6, 1.0, "wide", "hiss"], [2, 1.0, 0.8, "wide", "hiss"], [1, 0.5, 0.4, "open", ""], [0, 0.2, 0.2, "open", ""],];function hissPose(t) { const f = Math.min(N_HISS - 1, Math.round(t * N_HISS)); if (f === N_HISS - 1) return idlePose(0); const [lift, puff, ears, eyes, mouth] = HISS[f]; const p = idlePose(0); Object.assign(p, { lift, puff, earL: ears, earR: ears, eyes, mouth }); // tail goes up like a bottle brush const up = Math.min(1, puff / 1.2); p.tail = [[21, 24], [24 - up, 24 - up * 3], [26 - up, 22 - up * 5], [27 - up * 0.5, 19 - up * 7], [27 + up * 0.5, 17.5 - up * 8]]; p.tailW = 1.3 + puff * 0.7; return p;}// ---- sleep: curled up nose-to-tail --------------------------------------------------------------------------function sleep(g, t) { const br = 0.5 - 0.5 * Math.cos(TAU * t); // 0 exhaled .. 1 inhaled const ry = 4.3 + br * 0.9, cy = 26 - ry; g.outlined("ink", () => { furBlob(g, "body", 17.5, cy, 8.5, ry, 0.05); g.part("tail", () => g.sweep([[25, 23.5], [23, 25.6], [17, 26.2], [11, 25.6]], { w0: 1.4, w1: 1.1 }, (u, v) => (v < 0 ? "v0" : "n0"))); }); g.outlined("ink", () => { furBlob(g, "head", 10, 22.2, 4.4, 3.5, 0.15, true); g.part("ears", () => { g.fillPoly([[7, 20], [7, 17], [9, 19]], "v0"); g.pixel(7, 17, "v1"); g.fillPoly([[11, 19], [13, 17], [13, 20]], "n0"); g.pixel(12, 19, "r0"); }); }); g.part("sleepFace", () => { g.line([7, 23], [8, 23], "n2"); g.line([11, 23], [12, 23], "n2"); // shut eyes g.pixel(10, 24, "r0"); g.pixel(9, 25, "n1"); g.pixel(10, 25, "v0"); g.pixel(12, 26, "n1"); g.pixel(13, 26, "v0"); // tucked forepaws }); g.part("collar", () => { g.pixel(13, 25, "r2"); g.pixel(14, 25, "r1"); g.pixel(14, 26, "g2"); }); g.part("spine", () => { // a faint ridge of rim light along the back, rising and falling with the breath const yy = Math.round(cy - ry + 1); g.line([14, yy], [19, yy], "v1"); g.pixel(20, yy, "v0"); }); g.detached("zzz", () => { const p = t; if (p > 0.85) return; const x = Math.round(6 + p * 4 + Math.sin(TAU * p) * 1), y = Math.round(14 - p * 12); const k = p < 0.55 ? "n4" : "n3"; for (const [dx, dy] of [[0, 0], [1, 0], [2, 0], [3, 0], [2, 1], [1, 2], [0, 3], [1, 3], [2, 3], [3, 3]]) g.pixel(x + dx, y + dy, k); });}const BlackCat = sprite(pal, (g, t, mode) => { t = ((t % 1) + 1) % 1; contactShadow(g, CX, GROUND, 13, 1); cushion(g); if (mode === "sleep") return sleep(g, t); cat(g, mode === "groom" ? groomPose(t) : mode === "hiss" ? hissPose(t) : idlePose(t));});submit(BlackCat, { size: [W, 32], origin: "top-left", kind: "icon", scale: 1, animate: { fps: 10, duration: N_IDLE / 10, loop: true }, anchor: { x: CX, y: GROUND }, args: ["idle"], variants: [ { name: "idle", args: ["idle"] }, { name: "groom", args: ["groom"], animate: { fps: 10, duration: N_GROOM / 10, loop: true } }, { name: "hiss", args: ["hiss"], animate: { fps: 10, duration: N_HISS / 10, loop: false } }, { name: "sleep", args: ["sleep"], animate: { fps: 8, duration: N_SLEEP / 8, loop: true } }, ],});掲載されたコードでは、ビルドスクリプトが自動付与する共通部分を省略しています。具体的には 1 行のランタイムヘッダー、共有パレット、および汎用ヘルパー関数(contactShadow、litIndex、flame、hash、chk、TAU)です。表示されている行番号は実際のファイルと一致しています。これらのプログラムは AIエージェントが生成し、描画されたコマの仕上がりを確認しながら修正を加えたものです。
エージェントに、こうしたプログラムを書かせる。
Tessarune は描画 API のリファレンス、25 冊の制作マニュアル、結果を確かめるための描画エンジンをエージェントに渡します。