Tessarune
Witch's Hut · hut_door.js

魔女の小屋の扉

48×64 · JavaScript 432 行

48×64 の床置きの小物で、正面を向いています。たわんだ藁ぶきの軒の下に編み枝と土壁の小屋の壁の一部があり、鉄の帯の蝶番で留めた反った板の扉、蹄鉄、吊るしたお守り、右に吊りランタン、左に立てかけた箒、踏み段に彫ったかぼちゃが並びます。割れ目と扉の下から暖かい火の光が漏れます。closed(12 コマ)はお守りが揺れて光が脈打ちます。open(10 コマの一回再生)は扉が内側へ開き、大釜の緑の光が見える火の灯る部屋が現れます。open-idle(12 コマ)は戸口から湯気が上がります。close(10 コマの一回再生)はドンと閉まって踏み段から土ぼこりが立ちます。

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

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

hut_door.js · 1–13
// Witch's hut door — 48x64 wall-stub floor prop, top-down 3/4, key light top-left. Camera-facing.// A piece of a wattle-and-daub hut wall under a sagging, crooked thatch eave: a rough timber door frame with a// slanted lintel, a door of warped planks with iron strap hinges, a horseshoe and a hanging charm (a bundle of// twigs bound with red thread, a bone dangling below). A lantern hangs from a bracket on the right, a broom leans// on the left, a carved pumpkin grins on the step. Warm firelight leaks through a crack and under the warped door.// Variants://   closed     12f loop  the charm sways, the lantern and the pumpkin flicker, firelight pulses in the cracks//   open       10f once  the door swings inward (overshoots, settles), revealing a firelit room: a cauldron's//                        green glow over a fire, herbs hanging from the rafters; light spills onto the step;//                        ends on open-idle frame 0//   open-idle  12f loop  the fire flickers inside, steam curls out of the doorway and up past the eave//   close      10f once  the door swings shut with a thud (dust puffs off the step, the charm jolts);//                        ends on closed frame 0

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

hut_door.js · 421–432
submit(Hut, {  size: [W, H], origin: "top-left", kind: "icon", scale: 1,  animate: { fps: 10, duration: N_IDLE / 10, loop: true },  anchor: { x: 24, y: GROUND },  args: ["closed"],  variants: [    { name: "closed", args: ["closed"] },    { name: "open", args: ["open"], animate: { fps: 10, duration: N_ONCE / 10, loop: false } },    { name: "open-idle", args: ["open-idle"] },    { name: "close", args: ["close"], animate: { fps: 10, duration: N_ONCE / 10, loop: false } },  ],});
プログラム全体を表示 (432 行)
hut_door.js · 432 行
// Witch's hut door — 48x64 wall-stub floor prop, top-down 3/4, key light top-left. Camera-facing.// A piece of a wattle-and-daub hut wall under a sagging, crooked thatch eave: a rough timber door frame with a// slanted lintel, a door of warped planks with iron strap hinges, a horseshoe and a hanging charm (a bundle of// twigs bound with red thread, a bone dangling below). A lantern hangs from a bracket on the right, a broom leans// on the left, a carved pumpkin grins on the step. Warm firelight leaks through a crack and under the warped door.// Variants://   closed     12f loop  the charm sways, the lantern and the pumpkin flicker, firelight pulses in the cracks//   open       10f once  the door swings inward (overshoots, settles), revealing a firelit room: a cauldron's//                        green glow over a fire, herbs hanging from the rafters; light spills onto the step;//                        ends on open-idle frame 0//   open-idle  12f loop  the fire flickers inside, steam curls out of the doorway and up past the eave//   close      10f once  the door swings shut with a thud (dust puffs off the step, the charm jolts);//                        ends on closed frame 0const W = 48, H = 64, GROUND = 61;const DX0 = 16, DX1 = 31, DY0 = 20, DY1 = 54;     // door leaf / doorway (inclusive)const LEAF = DX1 - DX0 + 1, RISE = 5;const N_IDLE = 12, N_ONCE = 10, REST = 80;const OPEN_TH = [0, 4, 12, 26, 42, 58, 70, 78, 83, REST];const CLOSE_TH = [REST, 77, 68, 54, 37, 20, 7, 0, 0, 0];const LANT = { x: 40, y: 31 };                    // lantern flameconst BAYER = [0, 8, 2, 10, 12, 4, 14, 6, 3, 11, 1, 9, 15, 7, 13, 5];const dith = (x, y, d) => BAYER[(y & 3) * 4 + (x & 3)] / 16 < d;const WOOD = ["w0", "w1", "w2", "w3", "w4"];const dimW = (k, n) => { const i = WOOD.indexOf(k); return i < 0 || !n ? k : WOOD[Math.max(0, i - n)]; };// ---- thatch eave -------------------------------------------------------------------------------------------------const thTop = (x) => 1 + Math.round(6 * Math.pow((x - 20) / 27, 2));const thBot = (x) => 13 + Math.round(1.5 * Math.sin(Math.PI * x / 47)) - Math.round(x * 2 / 47) + [0, 2, 1, 3, 0, 1, 2, 0, 3, 1, 0][x % 11];   // sags, ragged fringeconst TH = ["w1", "g0", "w2", "w3", "g1", "w4"];function thatch(g) {  g.part("thatch", () => g.field({ x: [0, W - 1], y: [0, 18] }, (x, y) => {    const t0 = thTop(x), b0 = thBot(x);    if (y < t0 || y > b0) return null;    const v = (y - t0) / Math.max(1, b0 - t0);    let l = 1.0 - v * 1.25 - x / 47 * 0.6;    const q = x + Math.floor(y * 0.6);    if (q % 4 === 0) l -= 0.4;                             // straw bundles    else if ((3 * x + y) % 7 === 0) l += 0.35;             // a few bright straws    if (y === t0) l += 0.4;                                // the ridge catches the light    if (y >= b0 - 1) l -= 0.5;                             // the fringe ends are in shade    if (x === 0 || x === W - 1) l -= 0.3;    // moss patches on the straw    if ((x >= 8 && x <= 12 && y >= t0 && y <= t0 + 2) || (x >= 33 && x <= 36 && y >= t0 + 1 && y <= t0 + 3))      return l > 0.4 ? "e2" : "e1";    return TH[Math.max(0, Math.min(5, Math.floor((l + 0.6) * 3)))];  }));}// ---- wall --------------------------------------------------------------------------------------------------------const WX0 = 3, WX1 = 44, WY0 = 11, WY1 = 58;const lintelY = (x) => 17 + Math.round((36 - x) * 2 / 25);          // top row of the slanted lintelfunction wallTone(x, y, L) {  if (y > thBot(x) && y <= thBot(x) + 2) return y === thBot(x) + 1 ? "n0" : "n1";   // shadow under the eave  if (y <= thBot(x)) return null;  if (x <= 5 || x >= 42) {                                            // corner posts    const u = x <= 5 ? x - 3 : x - 42;    return ["w2", "w3", "w1"][u] || "w1";  }  if (y === 51 || y === 52) return y === 51 ? "w2" : "w1";            // sill beam  if (y >= 53) {                                                      // fieldstone footing    const r = y < 56 ? 0 : 1, k = (y - 53) % 3, bx = x + r * 4;    if (k === 2 || bx % 8 === 0) return "n0";    return k === 0 ? (hash(Math.floor(bx / 8), r) < 0.5 ? "n3" : "n2") : "n1";  }  // wattle showing where the daub has fallen away (upper left)  if (x >= 7 && x <= 11 && y >= 17 && y <= 22 && !(x === 7 && y === 17) && !(x === 11 && y === 22)) {    if (x === 7 || y === 17) return "n1";    return ((x + (y >> 1)) & 1) ? "w2" : "w0";  }  // daub: cream, lit by the lantern, with a few cracks and damp streaks  const d = Math.hypot(x - L.x, (y - L.y) * 0.9);  if (d < L.r * 0.55) return "n5";  if (d < L.r && dith(x, y, 1 - (d - L.r * 0.55) / (L.r * 0.45))) return "n5";  if ((x === 9 && y >= 26 && y <= 30) || (x === 10 && y === 31) || (x === 38 && y >= 44 && y <= 48)) return "n2";  if ((x === 21 || x === 26) && y < 17) return "n3";  return x > 36 ? "n3" : "n4";}function wall(g, L) {  g.part("wall", () => g.field({ x: [WX0, WX1], y: [WY0, WY1] }, (x, y) => {    if (x >= DX0 && x <= DX1 && y >= DY0) return null;    return wallTone(x, y, L);  }));}function frame(g) {  g.part("frame", () => {    // jambs: rough posts, the right one leaning a touch    for (let y = 17; y <= 54; y++) {      g.pixel(13, y, "w3"); g.pixel(14, y, "w2"); g.pixel(15, y, "w1");      const o = y < 30 ? -1 : 0;      g.pixel(32 + o, y, "w2"); g.pixel(33 + o, y, "w1"); g.pixel(34 + o, y, "w0");      if (o) g.pixel(34, y, "n3");    }    for (const y of [24, 37, 46]) g.pixel(14, y, "w1");                 // checks in the timber    g.pixel(13, 33, "w4"); g.pixel(13, 34, "w4");    // slanted lintel, overhanging both jambs    for (let x = 11; x <= 36; x++) {      const y = lintelY(x);      g.pixel(x, y, x < 22 ? "w4" : "w3"); g.pixel(x, y + 1, "w2"); g.pixel(x, y + 2, "w1");      if (x >= DX0 && x <= DX1) g.pixel(x, y + 3, "w0");                // shadow on the door head    }    g.pixel(11, lintelY(11) + 2, "w2"); g.pixel(36, lintelY(36), "w2");  });  g.part("step", () => {    // a worn stone slab at the threshold    for (let x = 11; x <= 36; x++) {      g.pixel(x, 55, x < 14 ? "n4" : "n3");      g.pixel(x, 56, x < 20 ? "n4" : x > 33 ? "n2" : "n3");      g.pixel(x, 57, "n2"); g.pixel(x, 58, x > 33 ? "n0" : "n1");    }    g.pixel(23, 56, "n2"); g.pixel(24, 56, "n2"); g.pixel(29, 57, "n1");     // worn dip and a crack    g.pixel(11, 58, "e1"); g.pixel(12, 58, "e2"); g.pixel(36, 57, "e1");     // moss at the ends  });}// ---- interior ----------------------------------------------------------------------------------------------------const FIRE = { x: 22, y: 47 };function interior(g, F) {  const R = 12 + 1.5 * F;  g.part("interior", () => g.field({ x: [DX0, DX1], y: [DY0, DY1] }, (x, y) => {    if (y >= 49) {                                                     // floorboards lit by the hearth      const fd = Math.hypot(x + 0.5 - FIRE.x, (y - 49) * 2.4);      const i = fd < R * 0.3 ? 3 : fd < R * 0.55 ? 2 : fd < R * 0.85 ? 1 : 0;      return WOOD[Math.max(0, i - (y % 2))];    }    // the back wall: logs, glowing around the hearth, dark up in the rafters    const d = Math.hypot(x + 0.5 - FIRE.x, (y + 0.5 - 46) * 1.1);    const seam = y % 5 === 0;    if (d < R * 0.32) return "r2";    if (d < R * 0.5) return seam ? "r1" : dith(x, y, 0.5) ? "r2" : "r1";    if (d < R * 0.7) return seam ? "r0" : "r1";    if (d < R * 0.95) return seam ? "w0" : dith(x, y, 0.5) ? "r1" : "r0";    if (d < R * 1.2) return seam ? "ink" : dith(x, y, 0.5) ? "r0" : "w0";    return seam || y < DY0 + 3 ? "ink" : "w0";  }));  g.part("rafters", () => {    // a shelf of jars on the left, herbs drying from a rafter    g.line([DX0, 34], [21, 34], "w1");    g.pixel(17, 32, "b1"); g.pixel(17, 33, "b1"); g.pixel(17, 31, "n2");    g.pixel(19, 31, "v1"); g.pixel(19, 32, "v2"); g.pixel(19, 33, "v1"); g.pixel(20, 33, "v0");    g.line([DX0, 24], [DX1, 23], "w1");    for (const [x, len] of [[19, 4], [24, 6], [29, 3]]) {      g.line([x, 24], [x, 24 + len - 2], "w0");      g.pixel(x - 1, 24 + len - 1, "e1"); g.pixel(x, 24 + len - 1, "e0"); g.pixel(x + 1, 24 + len - 1, "e1");      g.pixel(x, 24 + len, "e0"); g.pixel(x - 1, 24 + len - 2, "e2");    }  });  g.part("cauldronInside", () => {    // a pot over the hearth in silhouette, rimmed underneath by the flames, its brew glowing green    g.field({ x: [16, 28], y: [40, 49] }, (x, y) => {      const nx = (x + 0.5 - 22) / 5.4, ny = (y + 0.5 - 44.8) / 4.3;      const r = nx * nx + ny * ny;      if (r > 1) return null;      if (ny > 0.45 && r > 0.55) return F > 0.5 ? "r2" : "r1";           // firelit underside      if (nx > 0.6 && r > 0.6) return "r0";      return r < 0.5 && nx < -0.2 && ny < 0 ? "n0" : "ink";    });    g.line([16, 41], [28, 41], "n1"); g.pixel(16, 41, "n0"); g.pixel(28, 41, "n0");    g.line([17, 40], [27, 40], F > 0.5 ? "e2" : "e1");    g.pixel(19, 40, "e3"); g.pixel(23, 40, F > 0.3 ? "e3" : "e2"); g.pixel(25, 40, "e2");    g.pixel(17, 50, "ink"); g.pixel(27, 50, "ink");                    // feet  });  g.detached("brewGlow", () => {    for (let x = 17; x <= 27; x++) for (let y = 37; y <= 39; y++)      if (dith(x, y, (0.55 - (39 - y) * 0.18) * (0.7 + 0.3 * F) * (1 - Math.abs(x - 22) / 7))) g.pixel(x, y, y === 39 ? "e2" : "e1");  });}function hearth(g, ft) {  g.part("hearthFire", () => {    flame(g, 21, 51, 5, 5, ft, 0.2, ["r1", "r2", "r3", "g3"]);    flame(g, 24, 51, 4, 4, ft, 0.7, ["r1", "r2", "r3", "g3"]);  });}// ---- door leaf -----------------------------------------------------------------------------------------------------// the charm: a bundle of twigs tied with red thread and a bone, hanging from a nail under the horseshoeconst CHARM = [  "..t..",  "..t..",  ".TwT.",  ".wTw.",  ".rrr.",  ".TwT.",  ".T.w.",  "..t..",  ".b.b.",  "..b..",  "..B..",  ".B.B.",];const CKEY = { t: "r1", T: "w3", w: "w2", r: "r1", b: "n5", B: "n4" };const SHOE = ["H...H", "H...H", "H...H", "HH.HH", ".HHH."];function doorTex(lx, ly, sway, F) {  // lx 0..15 from the hinge stile, ly in canvas rows  // the warped bottom corner and a split between two planks let the firelight through  if (ly >= 52 && lx >= 13 - (ly - 52)) return null;  if (lx === 11 && ly >= 30 && ly <= 35) return null;  // charm (in front of everything on the door)  const cy = ly - 34;  if (cy >= 0 && cy < CHARM.length) {    const dx = Math.round(sway * cy / 11);    const c = CHARM[cy][lx - 6 - dx];    if (c && c !== ".") return CKEY[c];  }  if (ly === 33 && lx === 8) return "n3";                            // the nail  // horseshoe, points up to hold the luck in  const sy = ly - 27, sx = lx - 6;  if (sy >= 0 && sy < 5 && sx >= 0 && sx < 5 && SHOE[sy][sx] === "H") return sx === 0 ? "n4" : sy === 4 ? "n1" : sx === 4 ? "n1" : "n3";  // strap hinges from the hinge side, ending in a split curl  for (const hy of [24, 49]) {    if ((ly === hy || ly === hy + 1) && lx <= 9) {      if (lx === 2 || lx === 6) return ly === hy ? "n4" : "n0";       // rivets      return ly === hy ? "n3" : "n1";    }    if (lx === 10 && (ly === hy - 1 || ly === hy + 2)) return ly < hy ? "n3" : "n1";  }  // latch: an iron ring on the free side  if (lx === 13 && ly >= 38 && ly <= 41) return ly === 38 ? "n4" : "n2";  if (lx === 14 && (ly === 39 || ly === 40)) return "n1";  if (lx === 12 && (ly === 39 || ly === 40)) return "n3";  // warped planks: seams wander a pixel left and right  const p = Math.floor(lx / 4);  const seam = 4 * p + 3 + Math.round(0.8 * Math.sin(ly * 0.33 + p * 1.9));  if (lx === seam) return "w0";  const inP = lx - 4 * p;  let k = inP === 0 ? "w3" : p % 2 ? "w2" : "w3";  if (inP === 2 && p % 2 === 0) k = "w2";  if ((ly + p * 5) % 9 === 0 && inP === 1) k = "w1";                   // grain checks  if (ly <= DY0 + 1) k = dimW(k, 1);  if (ly >= DY1 - 1) k = dimW(k, 1);  if (lx === LEAF - 1) k = "w1";  return k;}function door(g, th, sway, F) {  const a = th * Math.PI / 180;  const w = Math.max(1, Math.round(LEAF * Math.cos(a)));  const dy = RISE * Math.sin(a);  const shade = th > 60 ? 2 : th > 25 ? 1 : 0;                        // turning away from the key light  g.part("door", () => g.field({ x: [DX0, DX1], y: [DY0, DY1] }, (x, y) => {    const i = x - DX0;    if (i >= w) return null;    const sh = w > 1 ? Math.round(dy * i / (w - 1)) : 0;    const ly = y + sh;    if (ly > DY1) return null;    const lx = Math.min(LEAF - 1, Math.floor((i + 0.5) * LEAF / w));    if (th > 25 && i === w - 1) return F > 0.45 ? "r1" : "r0";           // the leaf's edge, rimmed by the fire    if (th > 25 && i === w - 2) return "w0";    const k = doorTex(lx, ly, sway, F);    if (k === null) return null;    return k[0] === "w" ? dimW(k, shade) : k;  }));}// ---- lantern, broom, pumpkin -------------------------------------------------------------------------------------function lantern(g, fl) {  const { x, y } = LANT;  g.part("bracket", () => {    g.line([34, 25], [x + 1, 25], "n2"); g.line([35, 26], [37, 26], "n1");    g.pixel(35, 27, "n1"); g.pixel(x + 1, 24, "n3");    g.pixel(x, 26, "n3");                                              // hook  });  g.part("lantern", () => {    g.line([x - 2, 27], [x + 2, 27], "n3"); g.pixel(x - 2, 27, "n4");    g.line([x - 3, 28], [x + 3, 28], "n2"); g.pixel(x - 3, 28, "n3"); g.pixel(x + 3, 28, "n1");    for (let yy = 29; yy <= 33; yy++) for (let xx = x - 3; xx <= x + 3; xx++) {      const dx = xx - x, ax = Math.abs(dx);      let k;      if (ax === 3) k = dx < 0 ? "n2" : "n0";      else if (yy === 31 && ax === 2) k = "n1";      else {        const d = Math.hypot(dx, (yy - y) * 0.9);        k = d < 1.2 + fl ? "fl2" : d < 2.4 + fl ? "fl1" : "fl0";      }      g.pixel(xx, yy, k);    }    g.pixel(x - 2, 29, "n5");                                          // glint on the glass    g.line([x - 3, 34], [x + 3, 34], "n1"); g.pixel(x - 3, 34, "n3");    g.pixel(x, 35, "n1");    g.pixel(x, y - 1 + (fl > 0.6 ? 0 : 1), "fl3"); g.pixel(x, y + 1, "fl3");  });}function broom(g) {  g.part("broom", () => {    g.line([5, 26], [9, 47], "w3");    g.line([6, 26], [10, 47], "w2");    g.pixel(5, 25, "w2");    g.fillPoly([[8, 47], [11, 47], [14, 57], [4, 57]], "g1");    for (const [x0, x1, k] of [[8, 5, "g0"], [9, 8, "w3"], [10, 11, "g0"], [11, 13, "w3"]]) g.line([x0, 49], [x1, 57], k);    g.line([8, 48], [11, 48], "r1"); g.line([8, 49], [11, 49], "r0");     // binding    g.pixel(4, 57, "g0"); g.pixel(14, 57, "w2");  });}function pumpkin(g, fl) {  const cx = 39, cy = 55;  g.part("pumpkin", () => {    g.field({ x: [cx - 5, cx + 5], y: [cy - 4, cy + 3] }, (x, y) => {      const nx = (x + 0.5 - cx) / 5.3, ny = (y + 0.5 - cy) / 4.1;      if (nx * nx + ny * ny > 1) return null;      // ribs: lobes split by darker grooves; lit from the top-left      const lob = Math.abs(nx) > 0.62 ? 0 : Math.abs(nx) > 0.22 ? 1 : 2;      const groove = Math.abs(Math.abs(nx) - 0.62) < 0.1 || Math.abs(Math.abs(nx) - 0.22) < 0.09;      const l = -0.8 * nx - 0.6 * ny + (lob === 2 ? 0.2 : 0);      if (groove) return l > 0.3 ? "r1" : "r0";      return l > 0.55 ? "r3" : l > -0.3 ? "r2" : "r1";    });    g.line([cx, cy - 5], [cx + 1, cy - 6], "e1"); g.pixel(cx, cy - 4, "e0");  // stem    g.pixel(cx - 2, cy - 5, "e2");                                             // a curl of vine  });  g.part("pumpkinFace", () => {    const k = fl > 0.6 ? "g3" : "g2", k2 = fl > 0.3 ? "g2" : "g1";    g.pixel(cx - 3, cy - 1, k2); g.pixel(cx - 2, cy - 1, k); g.pixel(cx - 2, cy - 2, k2);      // eyes    g.pixel(cx + 2, cy - 1, k2); g.pixel(cx + 1, cy - 1, k); g.pixel(cx + 1, cy - 2, k2);    g.line([cx - 3, cy + 1], [cx + 2, cy + 1], k);                                             // grin    g.pixel(cx - 4, cy, k2); g.pixel(cx + 3, cy, k2); g.pixel(cx - 1, cy + 2, k2); g.pixel(cx + 1, cy + 2, k2);    g.pixel(cx, cy + 1, "r0");                                                                 // a tooth  });}// ---- detached light, steam, dust ---------------------------------------------------------------------------------function spill(g, amt, F) {  if (amt <= 0.05) return;  g.detached("spill", () => g.field({ x: [DX0 - 5, DX1 + 7], y: [55, H - 1] }, (x, y) => {    // a wedge of firelight thrown out across the step and the ground (not on the step's front face)    if (y === 57 || y === 58) return null;    const u = (y - 55) / 8, half = 7 + u * 6, c = 23.5 + u * 2;    const r = Math.abs(x + 0.5 - c) / half;    if (r > 1) return null;    if (y > 62) return null;    const dens = amt * (y <= 56 ? 0.75 - 0.5 * r : 0.42 - 0.5 * r - 0.2 * u) * (0.85 + 0.15 * F);    if (!dith(x, y, dens)) return null;    if (y <= 56) return r < 0.5 ? "w4" : "r3";    return r < 0.35 && u < 0.7 ? "r3" : "r2";  }));}function puff(g, cx, cy, rx, ry, dens) {  // a soft blob: a solid pale core, a dithered rim  if (dens <= 0.03) return;  for (let y = Math.floor(cy - ry); y <= Math.ceil(cy + ry); y++) for (let x = Math.floor(cx - rx); x <= Math.ceil(cx + rx); x++) {    const nx = (x + 0.5 - cx) / rx, ny = (y + 0.5 - cy) / ry, r2 = nx * nx + ny * ny;    if (r2 > 1) continue;    const v = dens * (1 - r2);    if (v > 0.55) g.pixel(x, y, nx < 0 && ny < 0 ? "n5" : "n4");    else if (v > 0.3) g.pixel(x, y, "n4");    else if (dith(x, y, v * 2.2)) g.pixel(x, y, "n3");  }}function steam(g, st, amt) {  if (amt <= 0) return;  g.detached("steam", () => {    for (let k = 0; k < 4; k++) {      const p = (st + k / 4) % 1;      // up off the brew, out under the lintel, then curling up the face of the eave      const x = 22 + p * 7 + 2.4 * Math.sin(TAU * (p * 1.2 + k * 0.29)), y = 37 - p * 33;      const s = 1.3 + p * 2.2;      const d = amt * Math.min(1, Math.sin(Math.PI * p) * 1.6) * 0.8;      puff(g, x, y, s * 1.3, s * 0.8, d);    }  });}function dust(g, f) {  // the door slams: grit puffs out along the sill  if (f < 7) return;  const k = f - 7;  g.detached("dust", () => {    for (const s of [-1, 1]) puff(g, 24 + s * (6 + k * 3), 56 - k * 0.5, 2.5 + k, 1.3 + k * 0.3, 0.8 - k * 0.25);  });}// ---- states --------------------------------------------------------------------------------------------------------function idleClock(tt) {  // fire/lantern flicker 0..1, two incommensurate wobbles closing on a whole loop  return 0.5 + 0.3 * Math.sin(TAU * 2 * tt) + 0.2 * Math.sin(TAU * (3 * tt + 0.4));}function stateAt(state, t) {  t = ((t % 1) + 1) % 1;  if (state === "closed" || state === "open-idle") {    const f = Math.round(t * N_IDLE) % N_IDLE, tt = f / N_IDLE;    const open = state === "open-idle";    return { th: open ? REST : 0, sway: 1.6 * Math.sin(TAU * tt), F: idleClock(tt), ft: tt, st: tt, steam: open ? 1 : 0, spill: open ? 1 : 0, f: -1 };  }  const f = Math.min(N_ONCE - 1, Math.round(t * N_ONCE));  const opening = state === "open";  if (f === 0) return stateAt(opening ? "closed" : "open-idle", 0);  if (f === N_ONCE - 1) return stateAt(opening ? "open-idle" : "closed", 0);  const tt = (((f - (N_ONCE - 1)) / N_IDLE) % 1 + 1) % 1;            // run the end state's clock backwards  const S = { F: idleClock(tt), ft: tt, st: tt, f };  if (opening) {    const k = f / (N_ONCE - 1);    // the charm swings as the door moves, then settles into the idle swing    return { ...S, th: OPEN_TH[f], sway: 2.5 * Math.sin(TAU * (f - (N_ONCE - 1)) / N_IDLE) * (1 + (1 - k)), steam: Math.max(0, (f - 4) / 5), spill: Math.min(1, f / 6) };  }  const k = f / (N_ONCE - 1);  return { ...S, th: CLOSE_TH[f], sway: (f >= 7 ? 3 : 1.6) * Math.sin(TAU * (f - (N_ONCE - 1)) / N_IDLE), steam: Math.max(0, 1 - f / 4), spill: CLOSE_TH[f] / REST, slam: true };}const Hut = sprite(pal, (g, t, state) => {  const S = stateAt(state, t);  const L = { x: LANT.x, y: LANT.y, r: 6.5 + 1.2 * S.F };  contactShadow(g, 24, GROUND, 22, 2);  g.outlined("ink", () => {    wall(g, L);    interior(g, S.F);    hearth(g, S.ft);    door(g, S.th, S.sway, S.F);    frame(g);    thatch(g);    broom(g);    lantern(g, S.F);    pumpkin(g, S.F);  });  spill(g, S.spill, S.F);  steam(g, S.st, S.steam);  if (S.slam) dust(g, S.f);});submit(Hut, {  size: [W, H], origin: "top-left", kind: "icon", scale: 1,  animate: { fps: 10, duration: N_IDLE / 10, loop: true },  anchor: { x: 24, y: GROUND },  args: ["closed"],  variants: [    { name: "closed", args: ["closed"] },    { name: "open", args: ["open"], animate: { fps: 10, duration: N_ONCE / 10, loop: false } },    { name: "open-idle", args: ["open-idle"] },    { name: "close", args: ["close"], animate: { fps: 10, duration: N_ONCE / 10, loop: false } },  ],});

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

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

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