Witch's hut door
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.
6×
Get 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.
// 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 0The last lines. submit declares the canvas and lists the variants the renderer exports.
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)
// 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.