Tessarune
Witch's Hut · black_cat.js

黒猫

32×32 · JavaScript 267 行

32×32 の床置きの小物で、魔女の黒猫が刺繍入りの小さなクッション(赤いベルベット、金の組紐、四隅の房)に座っています。毛並みはほぼ黒で、左上の縁に紫の光が入り、目は縦長の瞳の黄緑、赤い首輪に真鍮の鈴が付いています。変種は 4 つで、尻尾を振って一度まばたきする idle(16 コマ)、目を閉じて上げた前足をなめる groom(12 コマ)、脚を突っ張って毛を逆立てる一回再生の hiss(8 コマ)、鼻先と尻尾をくっつけて丸まり寝息を立てる sleep(20 コマ)です。

黒猫 — 描画結果6×
このプログラムで描画(変種「groom」) · 32×32 · 12 コマ
Witch's Hut動くスプライト素材: 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 冊の制作マニュアル、結果を確かめるための描画エンジンをエージェントに渡します。