Tessarune
Witch's Hut · toadstool_ring.js

毒キノコの輪

48×32 · JavaScript 224 行

48×32 の床置きの小物で、苔むした小山の上に、白い斑点の赤い傘と淡い紫に光る傘が高さを変えて輪に並びます。輪に沿った草は濃く、内側の空き地は青々としています。idle(12 コマ)では光る傘がずれた拍子で脈打ち、その光が苔の上で息づきます。spores(12 コマ)では傘から胞子が吹き出して昇り、消えていきます。awaken(10 コマの一回再生)では光が輪を一周し、通るたびにキノコを一つずつ灯します。キノコは g.depth の中で手前から描くので、一つずつ輪郭線が保たれます。

毒キノコの輪 — 描画結果6×
このプログラムで描画(変種「awaken」) · 48×32 · 10 コマ
Witch's Hut動くスプライト素材: Witch's Hut(itch.io)Vol.5・ハロウィンの小物 12 点・42 アニメ

冒頭のコメント。これが指示書です。サイズ、視点、光の向き、すべての変種が書かれています。

toadstool_ring.js · 1–9
// Toadstool ring — 48x32 floor prop, top-down 3/4, key light top-left.// A fairy/witch ring on a mossy mound: red caps with white spots and pale violet glowing caps of varied heights stand// in a circle; the grass along the ring is darker (the "fairy ring"), the clearing inside is lush.// Variants://   idle    12f loop: the glowing caps pulse softly, each out of phase; their light breathes on the moss//   spores  12f loop: the glowing caps puff spores that drift up, sway and fade//   awaken  10f once: a spark of light runs around the ring, lighting each toadstool as it passes, then every glow//           flares and settles; ends on spores f0// Toadstools are drawn nearest-first inside g.depth so each keeps its own ink outline against the ones behind it.

最後の数行。submit でキャンバスの大きさと、描画エンジンが書き出す変種の一覧を指定します。

toadstool_ring.js · 214–224
submit(ToadstoolRing, {  size: [W, H], origin: "top-left", kind: "icon", scale: 1,  animate: { fps: 10, duration: N_LOOP / 10, loop: true },  anchor: { x: CX, y: GROUND },  args: ["idle"],  variants: [    { name: "idle", args: ["idle"] },    { name: "spores", args: ["spores"] },    { name: "awaken", args: ["awaken"], animate: { fps: 10, duration: N_AWAKE / 10, loop: false } },  ],});
プログラム全体を表示 (224 行)
toadstool_ring.js · 224 行
// Toadstool ring — 48x32 floor prop, top-down 3/4, key light top-left.// A fairy/witch ring on a mossy mound: red caps with white spots and pale violet glowing caps of varied heights stand// in a circle; the grass along the ring is darker (the "fairy ring"), the clearing inside is lush.// Variants://   idle    12f loop: the glowing caps pulse softly, each out of phase; their light breathes on the moss//   spores  12f loop: the glowing caps puff spores that drift up, sway and fade//   awaken  10f once: a spark of light runs around the ring, lighting each toadstool as it passes, then every glow//           flares and settles; ends on spores f0// Toadstools are drawn nearest-first inside g.depth so each keeps its own ink outline against the ones behind it.const W = 48, H = 32, GROUND = 28, CX = 24;const N_LOOP = 12, N_AWAKE = 10;const MOSS = { cx: 24, cy: 20, rx: 22, ry: 7.5 };const RING = { cx: 24, cy: 19.5, rx: 17, ry: 6.5 };const LIGHT = { cx: 24, cy: 19.5, rx: 13, ry: 4.6 };          // the path the spark runs, just inside the ring// [angle deg (0 = right, 90 = front), type, stem height, cap radius, pulse phase]const SHROOMS = [  [196, "glow", 6, 3.0, 0.0],  [236, "red", 8, 3.8, 0],  [274, "glow", 4, 2.4, 0.42],  [312, "red", 6, 3.0, 0],  [350, "glow", 5, 3.0, 0.71],  [33, "red", 3, 2.4, 0],  [82, "glow", 2, 2.4, 0.27],  [122, "red", 3, 2.8, 0],  [158, "glow", 3, 2.2, 0.86],].map(([a, type, h, cr, ph], i) => {  const r = (a * Math.PI) / 180;  return { i, a: r, type, h, cr, ph, bx: Math.round(RING.cx + RING.rx * Math.cos(r)), by: Math.round(RING.cy + RING.ry * Math.sin(r)) };});const BAYER = [0, 8, 2, 10, 12, 4, 14, 6, 3, 11, 1, 9, 15, 7, 13, 5];const dith = (x, y, dens) => BAYER[(y & 3) * 4 + (x & 3)] / 16 < dens;// ---- moss mound -----------------------------------------------------------------------------------------------function moss(g) {  const { cx, cy, rx, ry } = MOSS;  g.part("moss", () => {    // earth lip under the moss, seen on the near side    g.field({ x: [cx - rx, cx + rx], y: [cy - ry, cy + ry + 2] }, (x, y) => {      const nx = (x + 0.5 - cx) / rx;      for (let d = 1; d <= 2; d++) {        const ny = (y + 0.5 - d - cy) / ry;        const ny0 = (y + 0.5 - cy) / ry;        if (nx * nx + ny0 * ny0 <= 1) return null;        if (nx * nx + ny * ny <= 1) return d === 1 ? (nx < -0.5 ? "e1" : "e0") : (nx < -0.5 ? "w1" : "w0");      }      return null;    });    g.field({ x: [cx - rx, cx + rx], y: [cy - ry, cy + ry] }, (x, y) => {      const nx = (x + 0.5 - cx) / rx, ny = (y + 0.5 - cy) / ry;      if (nx * nx + ny * ny > 1) return null;      // distance from the ring line (in ring-normalised units)      const rxn = (x + 0.5 - RING.cx) / RING.rx, ryn = (y + 0.5 - RING.cy) / RING.ry;      const rr = Math.sqrt(rxn * rxn + ryn * ryn);      const lit = -0.6 * nx - 0.5 * ny;      if (Math.abs(rr - 1) < 0.16) return chk(x, y) ? "e0" : "e1";       // the darker fairy-ring band      if (rr < 0.84) {                                                    // lush clearing inside the ring        if (lit > 0.32 && !chk(x, y)) return "e3";        if (lit > 0.45) return "e3";        return lit > -0.35 ? "e2" : "e1";      }      return lit > 0.35 ? "e2" : lit > -0.45 ? "e1" : "e0";             // outer moss    });    // tufts and pebbles: countable marks    for (const [x, y, k] of [[6, 17, "e3"], [7, 16, "e2"], [41, 23, "e2"], [12, 25, "e2"], [35, 26, "n2"], [4, 22, "n3"], [44, 18, "e2"], [20, 20, "e1"], [28, 18, "e1"], [24, 22, "e1"]]) g.pixel(x, y, k);  });}// ---- a toadstool -----------------------------------------------------------------------------------------------function toadstool(g, s, glow) {  const { bx, by, h, cr, type, i } = s;  const capY = by - h;                          // cap rim row  const crY = cr * 0.78;  const sw = cr >= 4 ? 3 : 2;                   // stem width  const x0 = bx - (sw === 3 ? 1 : 1);  g.part("stem" + i, () => {    for (let y = capY + 1; y <= by; y++) {      for (let k = 0; k < sw; k++) {        const x = x0 + k;        const key = k === 0 ? "n5" : k === sw - 1 ? "n3" : "n4";        g.pixel(x, y, y === by ? (k === 0 ? "n3" : "n2") : key);      }    }    if (h >= 5) g.pixel(x0 + sw - 1, capY + 2, "n2");                       // shadow under the cap    if (type === "red" && h >= 5) { g.line([x0 - 1, capY + 3], [x0 + sw, capY + 3], "n4"); g.pixel(x0 + sw, capY + 3, "n3"); } // the ring (annulus)  });  const RED = ["r0", "r1", "r1", "r2"];  const VIO = ["v1", "v2", "v2", "v3"];  g.part("cap" + i, () => g.field({ x: [Math.floor(bx - cr - 1), Math.ceil(bx + cr + 1)], y: [Math.floor(capY - crY - 1), capY + 1] }, (x, y) => {    const nx = (x + 0.5 - (bx + 0.5)) / (cr + 0.5), ny = (y + 0.5 - (capY + 0.5)) / (crY + 0.5);    if (ny > 0.55) return null;    if (nx * nx + ny * ny > 1) return null;    // underside / gills: the bottom row of the cap    if (ny > 0.05) return type === "red" ? (nx < -0.3 ? "n4" : "n3") : (glow > 0.5 ? "v3" : "v2");    if (type === "red") {      let k = litIndex(x, y, bx + 0.5, capY + 0.5, cr + 0.5, crY + 0.5, 4, 0.1);      return RED[Math.max(0, Math.min(3, k))];    }    let k = litIndex(x, y, bx + 0.5, capY + 0.5, cr + 0.5, crY + 0.5, 4, -0.15 + 0.4 * glow);    return VIO[Math.max(0, Math.min(3, k))];  }));  // spots on the red caps (fixed, countable)  if (type === "red") g.part("spots" + i, () => {    const pts = cr >= 4 ? [[-2, -2], [1, -3], [2, -1], [-3, 0], [0, -1]] : cr >= 3 ? [[-1, -2], [1, -1], [-2, 0]] : [[-1, -1], [1, 0]];    for (const [dx, dy] of pts) g.pixel(bx + dx, capY + dy, dx < 1 ? "n5" : "n4");  });  else g.part("glint" + i, () => { g.pixel(bx - 1, capY - Math.round(crY) + 1, "n5"); });}// soft dithered light around a glowing cap and on the moss under itfunction halo(g, s, glow) {  const { bx, by, h, cr } = s;  const cy = by - h - 1;  const R = cr + 1 + 2 * glow;  g.field({ x: [Math.floor(bx - R - 1), Math.ceil(bx + R + 1)], y: [Math.floor(cy - R), Math.ceil(by + 2)] }, (x, y) => {    const d = Math.hypot(x + 0.5 - (bx + 0.5), (y + 0.5 - cy) * 1.1);    const dg = Math.hypot((x + 0.5 - (bx + 0.5)) / (cr + 2 + 2 * glow), (y + 0.5 - by) / (1.6 + glow));    if (dg < 1 && dith(x, y, 0.1 + 0.4 * glow * (1 - dg))) return dg < 0.5 && glow > 0.6 ? "v2" : "v1";   // light pool on the moss    if (d > R || d < cr) return null;    if (!dith(x, y, Math.max(0, 0.3 * glow - 0.05) * (1 - (d - cr) / (R - cr)))) return null;    return d < cr + 1.2 ? "v2" : "v1";  });}function sporesOf(g, s, t) {  const { bx, by, h, cr, ph } = s;  const top = by - h - Math.round(cr * 0.78);  for (let k = 0; k < 3; k++) {    const p = (t + ph + k / 3) % 1;    const x = Math.round(bx + (k - 1) * 1.2 + Math.sin(TAU * (p * 0.9 + ph + k * 0.37)) * (1 + p * 2.5));    const y = Math.round(top - 1 - p * 15);    if (y < 0) continue;    if (p < 0.12) { g.pixel(x, y, "v3"); g.pixel(x + 1, y, "v2"); g.pixel(x, y + 1, "v2"); }   // the puff    else if (p < 0.5) { g.pixel(x, y, "v3"); }    else if (p < 0.78) { g.pixel(x, y, "v2"); }    else if (chk(x, y + Math.floor(p * 12))) g.pixel(x, y, "v1");  }}// ---- scene ----------------------------------------------------------------------------------------------------// glowAt(s) -> 0..1.6 glow of each glowing capfunction scene(g, glowAt, extra) {  contactShadow(g, CX, GROUND, 21, 1);  const order = SHROOMS.slice().sort((a, b) => b.by - a.by);              // nearest first  for (const s of order) {    const gl = s.type === "glow" ? glowAt(s) : 0;    g.depth(-s.by * 2, () => g.outlined("ink", () => toadstool(g, s, Math.min(1, gl))));  }  g.depth(0, () => g.outlined("ink", () => moss(g)));  // light over everything solid but behind nothing important: halos sit in front of the moss and behind the caps  g.depth(-10, () => g.detached("glow", () => {    for (const s of SHROOMS) if (s.type === "glow") halo(g, s, Math.min(1.1, glowAt(s)));  }));  if (extra) g.depth(-10, () => g.detached("fx", extra));}const pulse = (s, t) => 0.5 + 0.5 * Math.sin(TAU * (t + s.ph));const ToadstoolRing = sprite(pal, (g, t, mode) => {  t = ((t % 1) + 1) % 1;  if (mode === "awaken") {    const f = Math.min(N_AWAKE - 1, Math.round(t * N_AWAKE));    if (f === N_AWAKE - 1) {                                              // settle: exactly spores f0      scene(g, (s) => pulse(s, 0), () => { for (const s of SHROOMS) if (s.type === "glow") sporesOf(g, s, 0); });      return;    }    // the spark runs once around the ring in frames 0..5 (from the back-left, clockwise as seen from above)    const run = Math.min(1, f / 5);    const head = (200 * Math.PI) / 180 + run * TAU;    const flare = f >= 6 ? [1.6, 1.25, 0.95][f - 6] : 0;    const lit = (s) => {      const passed = run * TAU >= (((s.a - (200 * Math.PI) / 180) % TAU) + TAU) % TAU;      return passed ? 0.85 : 0.15;    };    scene(g, (s) => (f >= 6 ? flare : lit(s)), () => {      if (f <= 5) {        // trail of light along the ring line        for (let k = 0; k < 14; k++) {          const a = head - k * 0.13;          if (a < (200 * Math.PI) / 180) break;          const x = Math.round(LIGHT.cx + LIGHT.rx * Math.cos(a)), y = Math.round(LIGHT.cy + LIGHT.ry * Math.sin(a));          g.pixel(x, y, k < 3 ? "v3" : k < 8 ? "v2" : "v1");          if (k === 0) { g.pixel(x + 1, y, "v3"); g.pixel(x - 1, y, "v3"); g.pixel(x, y - 1, "v3"); g.pixel(x, y + 1, "v2"); g.pixel(x, y - 2, "v2"); g.pixel(x + 2, y, "v1"); g.pixel(x - 2, y, "v1"); }        }      }      if (f >= 5) {        // the whole ring line lights up, then fades        const key = f === 6 ? "v3" : f === 7 || f === 5 ? "v2" : "v1";        for (let k = 0; k < 140; k++) {          const a = (k / 140) * TAU;          const x = Math.round(LIGHT.cx + LIGHT.rx * Math.cos(a)), y = Math.round(LIGHT.cy + LIGHT.ry * Math.sin(a));          if (f === 8 && !chk(x, y)) continue;          g.pixel(x, y, key);          if (f === 6 && Math.sin(a) > 0) g.pixel(x, y + (Math.sin(a) > 0.5 ? 1 : 0), "v2");        }      }      if (f >= 6) {        // a burst of motes rising from the centre of the ring        for (let k = 0; k < 6; k++) {          const p = (f - 6 + 1) / 3;          const x = Math.round(LIGHT.cx + Math.cos(k * 1.05) * (3 + p * 6)), y = Math.round(LIGHT.cy - 2 - p * (4 + (k % 3) * 2) + Math.sin(k * 1.05) * 2);          g.pixel(x, y, f === 6 ? "v3" : f === 7 ? "v2" : "v1");        }      }    });    return;  }  const f = Math.round(t * N_LOOP) % N_LOOP;  const tt = f / N_LOOP;  scene(g, (s) => pulse(s, tt), mode === "spores" ? () => { for (const s of SHROOMS) if (s.type === "glow") sporesOf(g, s, tt); } : null);});submit(ToadstoolRing, {  size: [W, H], origin: "top-left", kind: "icon", scale: 1,  animate: { fps: 10, duration: N_LOOP / 10, loop: true },  anchor: { x: CX, y: GROUND },  args: ["idle"],  variants: [    { name: "idle", args: ["idle"] },    { name: "spores", args: ["spores"] },    { name: "awaken", args: ["awaken"], animate: { fps: 10, duration: N_AWAKE / 10, loop: false } },  ],});

掲載されたコードでは、ビルドスクリプトが自動付与する共通部分を省略しています。具体的には 1 行のランタイムヘッダー、共有パレット、および汎用ヘルパー関数(contactShadow、litIndex、flame、hash、chk、TAU)です。表示されている行番号は実際のファイルと一致しています。これらのプログラムは AIエージェントが生成し、描画されたコマの仕上がりを確認しながら修正を加えたものです。

エージェントに、こうしたプログラムを書かせる。

Tessarune は描画 API のリファレンス、25 冊の制作マニュアル、結果を確かめるための描画エンジンをエージェントに渡します。