Tessarune
Harvest Festival · harvest_bonfire.js

収穫祭のかがり火

64×64 · JavaScript 370 行

64×64 の収穫祭のかがり火で、野石の輪の中の灰の上に薪を円錐に組み、根元に焚きつけの藁、足元に藁束を 2 つ置いています。16 コマの idle(待機)では炎が立ち上り、16 コマの embers では燠火が脈動します。10 コマ一回再生の ignite(点火)で火が燃え広がり、8 コマの unlit(消灯)では古い灰から薄い煙が漂います。炎は一つの階調(fl0..3)なので、差し替え一つで色を変えられます。

収穫祭のかがり火 — 描画結果6×
このプログラムで描画(変種「ignite」) · 64×64 · 10 コマ
Harvest Festival動くスプライト素材: Harvest Festival(itch.io)Vol.6・収穫祭の小物 12 点・37 アニメ

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

harvest_bonfire.js · 1–12
// Harvest bonfire — 64x64 floor prop (large), top-down 3/4, key light top-left.// A big festival bonfire on open ground: a teepee of split and bark-on logs (they cross and stick out at the apex)// stacked on an ash bed inside a ring of fieldstones, a tuft of straw kindling at its base, and two tied straw// sheaves standing at its foot. The fire is the fl0..3 family, so swap("fl","soul") etc. would recolour it.// Variants://   idle     16f loop: tall flames lick up through the logs and shed tongues, sparks rise and cool, heat shimmer//                      wavers above the tips, the ash bed and the stones facing the fire glow and breathe//   embers   16f loop: burned down — charred stubs and two fallen logs on a heap of glowing coals whose cracks//                      pulse, small low flames, thin smoke curling up, a rare spark//   ignite   10f once: a spark in the straw kindling, the straw catches, flames climb the logs, flare and settle;//                      frame 0 equals unlit frame 0, the last frame equals idle frame 0//   unlit     8f loop: fresh logs and straw on old ash, a faint wisp of smoke from the kindling

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

harvest_bonfire.js · 359–370
submit(Bonfire, {  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: "embers", args: ["embers"] },    { name: "ignite", args: ["ignite"], animate: { fps: 10, duration: N_IGN / 10, loop: false } },    { name: "unlit", args: ["unlit"], animate: { fps: 10, duration: N_UNLIT / 10, loop: true } },  ],});
プログラム全体を表示 (370 行)
harvest_bonfire.js · 370 行
// Harvest bonfire — 64x64 floor prop (large), top-down 3/4, key light top-left.// A big festival bonfire on open ground: a teepee of split and bark-on logs (they cross and stick out at the apex)// stacked on an ash bed inside a ring of fieldstones, a tuft of straw kindling at its base, and two tied straw// sheaves standing at its foot. The fire is the fl0..3 family, so swap("fl","soul") etc. would recolour it.// Variants://   idle     16f loop: tall flames lick up through the logs and shed tongues, sparks rise and cool, heat shimmer//                      wavers above the tips, the ash bed and the stones facing the fire glow and breathe//   embers   16f loop: burned down — charred stubs and two fallen logs on a heap of glowing coals whose cracks//                      pulse, small low flames, thin smoke curling up, a rare spark//   ignite   10f once: a spark in the straw kindling, the straw catches, flames climb the logs, flare and settle;//                      frame 0 equals unlit frame 0, the last frame equals idle frame 0//   unlit     8f loop: fresh logs and straw on old ash, a faint wisp of smoke from the kindlingconst W = 64, H = 64, CX = 32, GROUND = 61;const N_LOOP = 16, N_UNLIT = 8, N_IGN = 10;const BED = { cx: 32, cy: 53, rx: 18, ry: 5 };const RING = { cx: 32, cy: 53.5, rx: 23, ry: 6 };const APEX = { x: 32, y: 31 };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;// ---- logs ------------------------------------------------------------------------------------------------------// a log is a tapered capsule A (foot) -> B (top), shaded as a cylinder across its width with the light top-left.// style: "split" (pale split face on the lit half, bark on the shaded half) | "bark" (bark all round) | "char"const SPLIT = ["w1", "w3", "w3", "w4"], BARK = ["w0", "w1", "w2", "w3"];function logField(g, L, glow, pulse) {  const { A, B, r0, r1, style } = L;  const dx = B[0] - A[0], dy = B[1] - A[1], len = Math.hypot(dx, dy);  const nx = -dy / len, ny = dx / len;                 // across-normal for s > 0  const lsign = -0.7 * nx - 0.7 * ny >= 0 ? 1 : -1;     // which side faces the key light  const pad = Math.max(r0, r1) + 1;  g.field({ x: [Math.floor(Math.min(A[0], B[0]) - pad), Math.ceil(Math.max(A[0], B[0]) + pad)],            y: [Math.floor(Math.min(A[1], B[1]) - pad), Math.ceil(Math.max(A[1], B[1]) + pad)] }, (x, y) => {    const px = x + 0.5 - A[0], py = y + 0.5 - A[1];    const u = (px * dx + py * dy) / (len * len);    if (u < 0 || u > 1) return null;    const r = r0 + (r1 - r0) * u;    const s = px * nx + py * ny;    if (Math.abs(s) > r) return null;    const q = (s / r) * lsign;                          // +1 lit edge .. -1 shaded edge    const endGrain = (1 - u) * len < 1.6;               // the sawn top end catches the light    if (style === "char") {      if (endGrain) return pulse > 0.5 ? "r2" : "r1";   // the burning broken end      if (glow > 0 && q < -0.35 && Math.round(u * len) % 3 === 0) return pulse > 0.6 ? "r2" : "r1";   // glowing checks      return q > 0.45 ? "n2" : q > -0.2 ? "n1" : "n0";    }    if (endGrain) return Math.abs(s) < r * 0.45 ? "w3" : "w4";    if (glow > 0 && u < 0.42 && q < -0.55) return glow > 0.7 ? "r2" : "r1";   // firelight on the edge facing the fire    if (style === "split") {      if (q > 0.6) return "w4";                         // sharp lit arris of the split face      if (q > -0.1) return "w3";                        // flat split face      return q > -0.6 ? "w1" : "w0";                    // bark    }    return BARK[q > 0.6 ? 2 : q > -0.15 ? 1 : 0];  });  // countable marks: grain along the split face, bark ridges  const mark = (uu, ss, k) => g.pixel(Math.round(A[0] + dx * uu + nx * ss * lsign - 0.5), Math.round(A[1] + dy * uu + ny * ss * lsign - 0.5), k);  if (style === "split") { mark(0.3, 0.5, "w2"); mark(0.55, 0.3, "w2"); mark(0.72, 0.55, "w2"); }  else if (style === "bark") { mark(0.3, 0.1, "w0"); mark(0.5, -0.1, "w2"); mark(0.7, 0.1, "w0"); }}// teepee: feet on an inner ellipse around the bed, all leaning into the apex and crossing past itfunction teepee() {  const feet = [[214, "bark", 0.1], [270, "split", -0.6], [326, "bark", 0.5],                [14, "split", 0.3], [62, "bark", -0.4], [118, "split", 0.4], [166, "bark", -0.3]];  return feet.map(([deg, style, jit], i) => {    const a = (deg * Math.PI) / 180;    const A = [32 + 17.5 * Math.cos(a), 53 + 4.6 * Math.sin(a)];    const top = [APEX.x + jit * 2, APEX.y + jit];    const B = [top[0] + (top[0] - A[0]) * 0.24, top[1] + (top[1] - A[1]) * 0.24];    return { i, A, B, r0: 2.6, r1: 1.8, style, back: Math.sin(a) < 0.05 };  });}const LOGS = teepee();// burned down: four charred stubs still leaning in, two logs fallen across the coalsconst STUBS = [  { A: [16, 53], B: [24, 44], r0: 2.4, r1: 1.9, style: "char", back: true },  { A: [46, 51], B: [39, 43], r0: 2.4, r1: 1.9, style: "char", back: true },  { A: [29, 50], B: [33, 41], r0: 2.2, r1: 1.7, style: "char", back: true },  { A: [15, 55], B: [47, 52], r0: 2.4, r1: 2.2, style: "char", back: false },  { A: [21, 58], B: [44, 56], r0: 2.3, r1: 2.0, style: "char", back: false },];// ---- ground: ash bed, stones, straw ---------------------------------------------------------------------------function bed(g, glow, heap, t) {  const { cx, cy, rx, ry } = BED;  g.part("bed", () => g.field({ x: [cx - rx, cx + rx], y: [cy - ry - 3, cy + ry] }, (x, y) => {    const nx = (x + 0.5 - cx) / rx, ny = (y + 0.5 - cy) / ry;    const bump = heap * Math.max(0, 1 - nx * nx * 1.6) * 0.75;   // a coal heap mounds up in the middle    const r = Math.sqrt(nx * nx + (ny + bump * (ny < 0 ? 1 : 0)) * (ny + bump * (ny < 0 ? 1 : 0)));    if (nx * nx + ny * ny > 1 && !(ny < 0 && r <= 1)) return null;    const rr = Math.sqrt(nx * nx + ny * ny * 0.8);    if (glow > 0) {      const g2 = glow * (0.9 + 0.1 * Math.sin(TAU * 2 * t));      if (rr < 0.38 * g2) return "fl3";      if (rr < 0.62 * g2) return "fl2";      if (rr < 0.85 * g2) return "fl1";      return rr < 0.95 ? "r0" : "n0";    }    if (rr > 0.86) return "n0";    return nx < -0.25 && ny < 0.2 ? "n2" : "n1";                   // old grey ash  }));}function coals(g, t) {  // lumps on the heap, each crack pulsing on its own phase  const LUMPS = [[22, 52], [27, 50], [33, 49], [39, 50], [44, 52], [25, 54], [31, 53], [37, 54], [19, 55], [42, 56]];  g.part("coals", () => LUMPS.forEach(([lx, ly], i) => {    g.ellipse({ x: [lx - 2, lx + 1], y: [ly - 1, ly] }, "n0", { fill: "n0" });    g.line([lx - 1, ly - 1], [lx, ly - 1], "n1");    const p = 0.5 + 0.5 * Math.sin(TAU * (t + hash(i, 3)));    g.pixel(lx + (i % 2), ly, p > 0.7 ? "fl3" : p > 0.35 ? "fl2" : "fl1");  }));}const STONES = Array.from({ length: 15 }, (_, i) => {  const a = TAU * (i / 15) + 0.12;  const w = 3 + Math.round(hash(i, 1) * 1.6), h = 2 + Math.round(hash(i, 2));  return { i, a, x: RING.cx + RING.rx * Math.cos(a), y: RING.cy + RING.ry * Math.sin(a), w, h, tone: i % 4 === 1 ? 1 : 0 };});function stone(g, s, warm) {  const { x: cx, y: cy, w, h, i } = s;  const RAMP = s.tone ? ["n1", "n2", "n3", "n4"] : ["n0", "n1", "n2", "n3"];  const facesFire = Math.sin(s.a) < -0.2;                           // back stones show the camera their fire side  g.outlined("ink", () => g.part("stone" + i, () => g.field({ x: [Math.floor(cx - w - 1), Math.ceil(cx + w + 1)], y: [Math.floor(cy - h - 1), Math.ceil(cy + h)] }, (x, y) => {    const k = litIndex(x, y, cx, cy, w, h, 4, 0.05);    if (k < 0) return null;    const ny = (y + 0.5 - cy) / h;    if (warm > 0 && facesFire && ny > 0.15) return warm > 0.75 ? "r2" : "r1";    return RAMP[k];  })));  g.pixel(Math.round(cx - w * 0.4), Math.round(cy - h * 0.5), s.tone ? "n5" : "n4");          // a glint  if (i % 5 === 2) g.pixel(Math.round(cx + 1), Math.round(cy - h * 0.6), "e1");              // a fleck of moss}function sheaf(g, name, cx, base, h, flip) {  const tie = base - Math.round(h * 0.42);  const hw = (y) => {    if (y >= tie) return 1.8 + (y - tie) / (base - tie) * 2.6;      // splays to the ground    const k = (tie - y) / (h - (base - tie));    return 1.8 + Math.sqrt(Math.max(0, k)) * 3.9;                   // fans out to the ears  };  g.outlined("ink", () => g.part(name, () => {    g.field({ x: [cx - 6, cx + 6], y: [base - h - 2, base] }, (x, y) => {      const d = x + 0.5 - cx, w = hw(y);      if (Math.abs(d) > w) return null;      const col = Math.round(d);      const topY = base - h + (Math.abs(col) * 0.5 | 0) + (hash(col + cx, 5) > 0.55 ? 1 : 0);   // ragged ear line      if (y < topY) return null;      const u = d / w;                                              // -1 lit .. +1 shaded      if (y <= topY + 1) return u < -0.2 ? "g2" : u < 0.5 ? "g1" : "g0";   // grain heads      if (u > 0.55) return "g0";      if (u < -0.55) return (col & 1) ? "g2" : "g3";      return (col & 1) ? "g1" : "g2";                               // straw stalks: alternating stripes    });    // the twine band around the waist    for (let x = Math.ceil(cx - 2.2); x <= Math.floor(cx + 2.2); x++) {      g.pixel(x, tie, x < cx - 1 ? "w3" : x < cx + 1 ? "w2" : "w1");      g.pixel(x, tie + 1, "w1");    }    g.pixel(Math.round(cx + (flip ? -3 : 3)), tie + 2, "w2");      // twine end    const top = base - h;                                           // a few stray stalks poke out of the ears    for (const [dx, dy, k] of [[-5, -1, "g1"], [-2, -2, "g2"], [3, -2, "g1"], [5, 0, "g0"]]) {      const sx = Math.round(cx + dx * 0.6), sy = top + 2;      g.line([sx, sy], [Math.round(cx + dx * (flip ? 0.9 : 1)), top + dy], k);    }  }));}function kindling(g) {  g.part("kindling", () => {    const tips = [[-5, -3, "g1"], [-3, -5, "g2"], [-1, -6, "g1"], [1, -5, "g2"], [3, -4, "g1"], [5, -2, "g0"], [0, -3, "g3"], [-4, -1, "g0"]];    for (const [dx, dy, k] of tips) g.line([32 + Math.sign(dx), 56], [32 + dx, 56 + dy], k);    g.line([28, 56], [36, 56], "g0");  });}// ---- fire ------------------------------------------------------------------------------------------------------const LAYERS = [["fl0", 1, 1], ["fl1", 0.86, 0.72], ["fl2", 0.6, 0.48], ["fl3", 0.34, 0.26]];const profile = (k) => (k < 0.2 ? 0.72 + 0.28 * Math.sin((k / 0.2) * Math.PI / 2) : Math.pow((1 - k) / 0.8, 0.85));const FLAMES = [  { name: "flameFarL", x: 21, base: 53, h: 15, hw: 3.6, lean: -2.5, seed: 0.83 },  { name: "flameFarR", x: 43, base: 53, h: 17, hw: 3.6, lean: 2.5, seed: 0.47 },  { name: "flameL", x: 25, base: 53, h: 34, hw: 7, lean: -4, seed: 0.31 },  { name: "flameR", x: 39, base: 53, h: 37, hw: 7, lean: 4, seed: 0.64 },  { name: "flameC", x: 32, base: 54, h: 50, hw: 10.5, lean: 0, seed: 0.05 },];const LOW = [  { name: "lowL", x: 25, base: 52, h: 8, hw: 2.6, lean: -1, seed: 0.2 },  { name: "lowC", x: 32, base: 51, h: 11, hw: 3.2, lean: 0.5, seed: 0.55 },  { name: "lowR", x: 39, base: 52, h: 7, hw: 2.4, lean: 1, seed: 0.9 },];const fh = (f, t, s) => s * f.h * (1 + 0.08 * Math.sin(TAU * (2 * t + f.seed)) + 0.05 * Math.sin(TAU * (3 * t + 2 * f.seed)));const fax = (f, t, k) => f.x + 0.5 + (f.lean + 1.6 * Math.sin(TAU * (t + f.seed))) * k * k + 1.1 * Math.sin(TAU * (1.2 * k - 2 * t + f.seed)) * k;function tongue(g, f, t, s) {  const hh = fh(f, t, s);  if (hh < 2) return;  const ws = Math.min(1, 0.45 + 0.55 * s);  g.part(f.name, () => g.field({ x: [0, W - 1], y: [Math.max(0, Math.floor(f.base - hh - 1)), f.base] }, (x, y) => {    const k = (f.base + 1 - (y + 0.5)) / hh;    if (k < 0 || k > 1) return null;    const lick = 1 + 0.2 * Math.sin(TAU * (2.3 * k - 2 * t + 1.7 * f.seed));   // bulges travel upward    const dx = Math.abs(x + 0.5 - fax(f, t, k));    let key = null;    for (const [c, hf, wf] of LAYERS) {      const kj = k / hf;      if (kj > 1) break;      if (dx <= f.hw * ws * wf * profile(kj) * lick) key = c; else break;    }    return key;  }));}function tongues(g, list, t, s) {  // tongues tear off the tips and rise  g.detached("tongues", () => {    for (const f of list) {      const p = (2 * t + f.seed) % 1, tip = f.base - fh(f, t, s);      const x = Math.round(fax(f, t, 1) - 0.5), y = Math.round(tip - 1 - p * 8);      if (y < 1 || s < 0.5) continue;      if (p < 0.4) { g.rect({ x: [x, x + 1], y: [y, y + 1] }, "fl1", { fill: "fl1" }); g.pixel(x + 1, y, "fl0"); g.pixel(x, y + 1, "fl0"); }      else if (p < 0.7) g.pixel(x, y, "fl0");    }  });}function sparks(g, t, n, amt) {  g.detached("sparks", () => {    for (let i = 0; i < n; i++) {      const p = (t * (1 + (i % 2)) + i / n) % 1;      if (p > amt) continue;      const x0 = 32 + (hash(i, 4) - 0.5) * 22;      const x = Math.round(x0 + Math.sin(TAU * (p * 1.3 + hash(i, 6))) * 3 + (x0 - 32) * p * 0.4);      const y = Math.round(30 - hash(i, 8) * 8 - p * 30);      if (y < 0) continue;      if (p < 0.25) { g.pixel(x, y, "fl3"); g.pixel(x, y + 1, "fl2"); }      else if (p < 0.55) g.pixel(x, y, "fl2");      else if (p < 0.8) g.pixel(x, y, "fl1");      else g.pixel(x, y, "fl0");    }  });}function shimmer(g, t, amt) {  // heat haze: three wavering ribbons of dotted light climbing above the flame tips  if (amt <= 0) return;  g.detached("shimmer", () => {    for (const [cx, y0, y1, ph] of [[27, 2, 15, 0.1], [33, 0, 9, 0.45], [39, 3, 14, 0.75]]) {      for (let y = y0; y <= y1; y++) {        if ((y + Math.floor(t * N_LOOP)) % 3 !== 0) continue;        const x = Math.round(cx + 1.4 * Math.sin(y * 0.7 - TAU * 2 * t + ph * TAU));        g.pixel(x, y, y < y0 + 4 ? "fl0" : "fl1");      }    }  });}function pool(g, t, amt) {  // firelight spilling on the ground around the ring (dithered, so it sits on any floor)  if (amt <= 0) return;  const R = 1 + 0.04 * Math.sin(TAU * 2 * t);  g.detached("firelight", () => g.field({ x: [0, W - 1], y: [44, 63] }, (x, y) => {    const nx = (x + 0.5 - 32) / (31 * R), ny = (y + 0.5 - 55) / (8.5 * R);    const r = Math.sqrt(nx * nx + ny * ny);    if (r > 1) return null;    return dith(x, y, 0.42 * amt * (1 - r)) ? (r < 0.75 ? "fl1" : "fl0") : null;  }));}function smoke(g, t, n, x0, y0, rise, amt) {  g.detached("smoke", () => {    for (let i = 0; i < n; i++) {      const p = (t + i / n) % 1;      const x = Math.round(x0 + Math.sin(TAU * (p * 1.2 + i * 0.37)) * (1 + p * 3) + p * 3);      const y = Math.round(y0 - p * rise);      if (y < 0) continue;      if (p < 0.35) { g.rect({ x: [x, x + 1], y: [y, y + 1] }, "n3", { fill: "n3" }); g.pixel(x, y, "n4"); g.pixel(x + 1, y, "n4"); }      else if (p < 0.7) { g.pixel(x, y, "n4"); g.pixel(x + 1, y, "n3"); g.pixel(x + 1, y - 1, "n3"); }      else if (p < amt) { g.pixel(x, y, "n3"); if (chk(x, y)) g.pixel(x + 1, y - 1, "n2"); }    }  });}// ---- scenes ----------------------------------------------------------------------------------------------------// st: { t, fire (flame height scale), glow (bed + stone firelight 0..1), sparkAmt, shimmer, pool }function stones(g, back, warm) {  for (const s of STONES) if ((Math.sin(s.a) < 0) === back) stone(g, s, warm);}function teepeeScene(g, st) {  const { t, fire, glow } = st;  const pulse = 0.5 + 0.5 * Math.sin(TAU * 2 * t);  pool(g, t, st.pool);  contactShadow(g, CX, 59, 29, 3);  stones(g, true, glow);  bed(g, glow, 0, t);  for (const L of LOGS) if (L.back) g.outlined("ink", () => g.part("log" + L.i, () => logField(g, L, 0, pulse)));  if (fire < 0.9) kindling(g);  for (const f of FLAMES) tongue(g, f, t, fire);  for (const L of LOGS) if (!L.back) g.outlined("ink", () => g.part("log" + L.i, () => logField(g, L, glow, pulse)));  stones(g, false, 0);  sheaf(g, "sheafL", 8, 61, 16, false);  sheaf(g, "sheafR", 57, 61, 12, true);  if (fire > 0.4) tongues(g, FLAMES.slice(2), t, fire);  sparks(g, t, 10, st.sparkAmt);  shimmer(g, t, st.shimmer);}function emberScene(g, t) {  const pulse = 0.5 + 0.5 * Math.sin(TAU * 2 * t);  pool(g, t, 0.45);  contactShadow(g, CX, 59, 29, 3);  stones(g, true, 0.5);  bed(g, 0.7, 1, t);  coals(g, t);  for (const L of STUBS) if (L.back) g.outlined("ink", () => g.part("stub", () => logField(g, L, 1, (pulse + hash(L.A[0], 1)) % 1)));  for (const f of LOW) tongue(g, f, t, 1);  for (const L of STUBS) if (!L.back) g.outlined("ink", () => g.part("fallen", () => logField(g, L, 1, (pulse + hash(L.A[0], 2)) % 1)));  stones(g, false, 0);  sheaf(g, "sheafL", 8, 61, 16, false);  sheaf(g, "sheafR", 57, 61, 12, true);  sparks(g, t, 3, 0.7);  smoke(g, t, 8, 32, 42, 40, 0.95);}function unlitScene(g, t) {  teepeeScene(g, { t, fire: 0, glow: 0, sparkAmt: -1, shimmer: 0, pool: 0 });  smoke(g, t, 3, 32, 47, 36, 0.9);}const IGN_FIRE = [0, 0, 0.12, 0.24, 0.38, 0.55, 0.75, 0.98, 1.1, 1];const IGN_GLOW = [0, 0, 0.2, 0.35, 0.5, 0.65, 0.8, 1, 1, 1];function igniteFrame(g, f) {  if (f >= N_IGN - 1) return idleFrame(g, 0);  if (f === 0) return unlitScene(g, 0);  const t = f / N_LOOP;  teepeeScene(g, { t, fire: IGN_FIRE[f], glow: IGN_GLOW[f], sparkAmt: f >= 6 ? (f - 5) / 3 : -1, shimmer: f >= 7 ? 1 : 0, pool: IGN_GLOW[f] });  if (f <= 3) g.detached("catch", () => {                           // the spark in the straw, flaring    const k = ["fl3", "fl3", "fl2"][f - 1];    if (f === 1) {                                                  // a bright four-point spark      for (const [ox, oy] of [[1, 0], [-1, 0], [0, -1], [0, 1]]) g.pixel(32 + ox, 53 + oy, "fl2");      for (const [ox, oy] of [[2, -2], [-2, -2], [2, 1], [-2, 1]]) g.pixel(32 + ox, 53 + oy, "fl1");    }    g.pixel(32, 53, k);    if (f > 1) for (const [ox, oy] of [[-2, -1], [2, -2], [0, -3], [-3, 1]]) g.pixel(32 + ox * f, 53 + oy * f, "fl1");  });}function idleFrame(g, t) {  teepeeScene(g, { t, fire: 1, glow: 1, sparkAmt: 1, shimmer: 1, pool: 1 });}const Bonfire = sprite(pal, (g, t, mode) => {  if (mode === "ignite") return igniteFrame(g, Math.min(N_IGN - 1, Math.round(t * N_IGN)));  t = ((t % 1) + 1) % 1;  if (mode === "embers") return emberScene(g, t);  if (mode === "unlit") return unlitScene(g, t);  idleFrame(g, t);});submit(Bonfire, {  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: "embers", args: ["embers"] },    { name: "ignite", args: ["ignite"], animate: { fps: 10, duration: N_IGN / 10, loop: false } },    { name: "unlit", args: ["unlit"], animate: { fps: 10, duration: N_UNLIT / 10, loop: true } },  ],});

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

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

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