Tessarune
Witch's Hut · hut_door.js

Witch's hut door

48×64 · 432 lines of JavaScript

A 48×64 floor prop facing the camera: a piece of wattle-and-daub hut wall under a sagging thatch eave, with a door of warped planks on iron strap hinges, a horseshoe, a hanging charm, a lantern on the right, a broom on the left and a carved pumpkin on the step. Warm firelight leaks through a crack and under the door. “closed” (12 frames) sways the charm and pulses the firelight; “open” (10 frames, once) swings the door inward onto a firelit room with a cauldron’s green glow; “open-idle” (12 frames) curls steam out of the doorway; “close” (10 frames, once) shuts it with a thud and a puff of dust.

Witch's hut door — Rendered output6×
Rendered by this program, variant “open” · 48×64 · 10 frames
Witch's HutGet the animated sprites: Witch's Hut on itch.ioVol.5 · 12 Halloween props · 42 animations

The header. The comment at the top of the file is the brief: size, view, light direction and every variant.

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

The last lines. submit declares the canvas and lists the variants the renderer exports.

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 } },  ],});
Show the whole program (432 lines)
hut_door.js · 432 lines
// 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 } },  ],});

These excerpts omit the boilerplate prepended by the asset pack build script: a single-line runtime header, the shared palette, and common helper utilities (contactShadow, litIndex, flame, hash, chk, TAU). The displayed line numbers match the original source files. Each program was authored by an AI coding agent and adjusted after visual inspection of the rendered frames.

Have your agent write programs like this.

Tessarune gives it the drawing API reference, 25 production manuals and a renderer to check its work against.