Witch's Hut · hut_door.js
魔女の小屋の扉
48×64 の床置きの小物で、正面を向いています。たわんだ藁ぶきの軒の下に編み枝と土壁の小屋の壁の一部があり、鉄の帯の蝶番で留めた反った板の扉、蹄鉄、吊るしたお守り、右に吊りランタン、左に立てかけた箒、踏み段に彫ったかぼちゃが並びます。割れ目と扉の下から暖かい火の光が漏れます。closed(12 コマ)はお守りが揺れて光が脈打ちます。open(10 コマの一回再生)は扉が内側へ開き、大釜の緑の光が見える火の灯る部屋が現れます。open-idle(12 コマ)は戸口から湯気が上がります。close(10 コマの一回再生)はドンと閉まって踏み段から土ぼこりが立ちます。
6×
動くスプライト素材: 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 冊の制作マニュアル、結果を確かめるための描画エンジンをエージェントに渡します。