Harvest Festival · cider_press.js
シードルの搾り機
縁の上がった厚い台座と 2 本の支柱、砕いたリンゴを詰めた籠、木製の桶、梁の上の赤いリンゴを備えた、48×64 の木製スクリュー式圧搾機です。16 コマの idle(待機)では注ぎ口から果汁の滴が桶へと落ちて波紋を作り、果肉が溝を滑り落ちます。16 コマの press ではバーが半回転して果汁が流れ出し、12 コマの full では泡立つ桶の周りをハチが飛び回ります。
6×
動くスプライト素材: Harvest Festival(itch.io)Vol.6・収穫祭の小物 12 点・37 アニメ
冒頭のコメント。これが指示書です。サイズ、視点、光の向き、すべての変種が書かれています。
cider_press.js · 1–12
// Cider press — 48x64 floor prop (tall), top-down 3/4, key light top-left.// A wooden screw press: a thick plank bed with a raised rim, two posts carrying a crossbeam, a big threaded wooden// screw through the beam with a hub and a long turning bar on top, a pressing block and round plate bearing down on a// slatted basket (two iron hoops) full of crushed apples. Cider runs across the bed into a carved spout and pours into// a staved wooden bucket standing in front. A red apple sits on the end of the beam.// Variants:// idle 16f loop a drop of cider swells at the spout and falls into the bucket (ring on the surface), a bit of// apple pulp slides down the spout channel// press 16f loop the bar turns half a revolution around the screw (across, angled, end-on toward the viewer,// angled back), the threads wind down, the plate sits 2px lower in the basket, pomace squeezes up,// juice runs down between the slats and streams steadily from the spout// full 12f loop the bucket is brimming, a head of froth heaves and pops, a slow drip, a wasp buzzes around it最後の数行。submit でキャンバスの大きさと、描画エンジンが書き出す変種の一覧を指定します。
cider_press.js · 393–403
submit(CiderPress, { size: [W, H], origin: "top-left", kind: "icon", scale: 1, animate: { fps: 10, duration: N_IDLE / 10, loop: true }, anchor: { x: CX, y: GROUND }, args: ["idle"], variants: [ { name: "idle", args: ["idle"] }, { name: "press", args: ["press"], animate: { fps: 10, duration: N_PRESS / 10, loop: true } }, { name: "full", args: ["full"], animate: { fps: 10, duration: N_FULL / 10, loop: true } }, ],});プログラム全体を表示 (403 行)
cider_press.js · 403 行
// Cider press — 48x64 floor prop (tall), top-down 3/4, key light top-left.// A wooden screw press: a thick plank bed with a raised rim, two posts carrying a crossbeam, a big threaded wooden// screw through the beam with a hub and a long turning bar on top, a pressing block and round plate bearing down on a// slatted basket (two iron hoops) full of crushed apples. Cider runs across the bed into a carved spout and pours into// a staved wooden bucket standing in front. A red apple sits on the end of the beam.// Variants:// idle 16f loop a drop of cider swells at the spout and falls into the bucket (ring on the surface), a bit of// apple pulp slides down the spout channel// press 16f loop the bar turns half a revolution around the screw (across, angled, end-on toward the viewer,// angled back), the threads wind down, the plate sits 2px lower in the basket, pomace squeezes up,// juice runs down between the slats and streams steadily from the spout// full 12f loop the bucket is brimming, a head of froth heaves and pops, a slow drip, a wasp buzzes around itconst W = 48, H = 64, GROUND = 60, CX = 24;const N_IDLE = 16, N_PRESS = 16, N_FULL = 12;const BX = 18; // press axis (screw, plate, basket centre)const BRX = 10, BTY = 34, BBY = 48, BRY = 3; // basketconst HUB_Y = 6; // bar axis rowconst BKX = 41, BKCY = 54; // bucket centre (rim ellipse)const e2 = (x, y, cx, cy, rx, ry) => ((x + 0.5 - cx) / rx) ** 2 + ((y + 0.5 - cy) / ry) ** 2;const cl = (v, a, b) => Math.max(a, Math.min(b, v));const WOOD = ["w0", "w1", "w2", "w3", "w4"], IRON = ["n0", "n1", "n2", "n3", "n4"];// a pixel map ringed with ink by hand (outlined() only inks empty pixels and these parts overlap each other)function stamp(g, name, m, opts = {}) { const ink = new Set(); for (const i of m.keys()) { const x = i % W, y = (i - x) / W; for (const [nx, ny] of [[x + 1, y], [x - 1, y], [x, y + 1], [x, y - 1]]) { if (nx < 0 || ny < 0 || nx >= W || ny >= H) continue; const k = ny * W + nx; if (!m.has(k) && !(opts.skip && opts.skip(nx, ny))) ink.add(k); } } let x0 = W, x1 = 0, y0 = H, y1 = 0; for (const i of [...m.keys(), ...ink]) { const x = i % W, y = (i - x) / W; x0 = Math.min(x0, x); x1 = Math.max(x1, x); y0 = Math.min(y0, y); y1 = Math.max(y1, y); } if (x0 > x1) return; g.part(name, () => g.field({ x: [x0, x1], y: [y0, y1] }, (x, y) => { const i = y * W + x; return m.has(i) ? m.get(i) : ink.has(i) ? "ink" : null; }));}const pmap = () => { const m = new Map(); m.P = (x, y, k) => { if (x >= 0 && y >= 0 && x < W && y < H) m.set(y * W + x, k); }; return m; };// ---- bed (tray) ------------------------------------------------------------------------------------------------const TX0 = 2, TX1 = 34, TTOP = 44, TFRONT = 52, TBOT = 59;function bed(g, S) { g.part("bed", () => g.field({ x: [TX0, TX1], y: [TTOP, TBOT] }, (x, y) => { if (y <= TFRONT) { // top face: rim around a recessed floor if (y === TTOP) return x < 12 ? "w4" : "w3"; if (x === TX0) return "w4"; if (x === TX1) return "w2"; if (y === TFRONT) return x < 8 ? "w4" : "w3"; if (y === TTOP + 1) return "w1"; // far inner wall in shadow if (x === TX0 + 1) return "w1"; // floor: juice film collects toward the spout corner (right), more of it while pressing const wet = S.mode === "idle" ? 0.55 : 0.25; if ((x - TX0) / (TX1 - TX0) > wet + (TFRONT - y) * 0.03) return y === TTOP + 2 ? "g0" : (x + y) % 7 === 0 ? "g2" : "g1"; return (x * 3 + y) % 11 === 0 ? "w1" : "w2"; } // front face: two planks, lit left end, darker toward the floor if (x === TX0) return y === TBOT ? "w2" : "w3"; if (x === TX1) return "w0"; if (y === TFRONT + 1) return "w3"; if (y === 56) return "w1"; // plank seam if (y === TBOT) return "w1"; if ((y === 54 && x % 9 === 4) || (y === 58 && x % 7 === 2)) return "w1"; // grain return "w2"; })); g.part("bedIron", () => { // corner straps with nails for (const x of [TX0 + 1, TX1 - 2]) { g.pixel(x, 54, "n2"); g.pixel(x + 1, 54, "n1"); g.pixel(x, 57, "n2"); g.pixel(x + 1, 57, "n1"); } g.pixel(TX0 + 1, 54, "n4"); g.pixel(TX0 + 1, 57, "n3"); }); g.part("pulp", () => { // spilled pomace on the bed rim and floor for (const [x, y, k] of [[4, 50, "r1"], [5, 50, "g1"], [31, 51, "e2"], [32, 50, "r1"], [6, TFRONT, "r1"], [7, TFRONT, "g1"], [25, TFRONT, "e1"]]) g.pixel(x, y, k); });}function spout(g, S, t) { const m = pmap(); for (let x = 35; x <= 39; x++) { m.P(x, 46, x === 35 ? "w4" : "w3"); // lit lip const flow = S.mode === "press" ? ((x - Math.floor(t * N_PRESS * 1.5)) % 3 + 3) % 3 : ((x - Math.floor(t * 8)) % 4 + 4) % 4; m.P(x, 47, flow === 0 ? "g2" : "g1"); // cider in the channel m.P(x, 48, "w1"); } m.P(40, 47, "w2"); m.P(40, 48, "w0"); // the tip if (S.mode === "idle") { // a bit of pulp slides down the channel const p = t; if (p < 0.6) m.P(35 + Math.floor(p / 0.6 * 5), 47, "r1"); } stamp(g, "spout", m, { skip: (x) => x <= TX1 });}// ---- posts, beam, screw, bar ---------------------------------------------------------------------------------function frame(g) { g.part("posts", () => { for (const px of [4, 30]) { for (let y = 16; y <= 47; y++) { g.pixel(px, y, "w3"); g.pixel(px + 1, y, "w2"); g.pixel(px + 2, y, "w1"); } g.pixel(px + 1, 22, "w1"); g.pixel(px + 1, 23, "w1"); g.pixel(px, 31, "w4"); g.pixel(px + 1, 39, "w1"); // grain, a lit knot g.line([px, 16], [px + 2, 16], "w0"); // under the beam g.line([px, 47], [px + 2, 47], "w0"); // foot in its mortise g.pixel(px + 1, 18, "w4"); g.pixel(px + 2, 18, "w2"); // through-peg } }); g.part("beam", () => g.field({ x: [1, 35], y: [10, 15] }, (x, y) => { if (x === 1) return y <= 11 ? "w4" : y === 15 ? "w2" : "w3"; // end grain, lit if (x === 35) return y <= 11 ? "w2" : "w0"; if (y === 10) return x < 14 ? "w4" : "w3"; if (y === 11) return "w3"; if (y === 12) return x < 10 ? "w4" : "w3"; if (y === 15) return "w1"; if ((y === 13 && x % 8 === 3) || (y === 14 && x % 11 === 6)) return "w1"; return "w2"; })); g.part("beamHole", () => { g.pixel(BX - 2, 10, "w0"); g.pixel(BX + 2, 11, "w1"); g.pixel(BX - 2, 11, "w0"); g.pixel(BX + 2, 10, "w1"); });}function screw(g, y0, y1, shift) { // 3px threaded shaft: lit left column, threads as a diagonal helix that winds down with `shift` g.part("screw", () => { for (let y = y0; y <= y1; y++) for (let c = 0; c < 3; c++) { const lv = [3, 2, 1][c]; const th = ((y - shift - c) % 3 + 3) % 3; let k = th === 0 ? lv - 1 : th === 1 ? lv + 1 : lv; if (y === y0 && y0 > 12) k = 0; // shadow under the beam g.pixel(BX - 1 + c, y, WOOD[cl(k, 0, 4)]); } });}function plateAndBlock(g, py) { // round plate (top face + 2px thickness) with a pressing block on it g.part("plate", () => g.field({ x: [BX - 7, BX + 7], y: [py - 3, py + 4] }, (x, y) => { const top = e2(x, y, BX, py, 7, 2.2) <= 1; if (top) { const nx = (x + 0.5 - BX) / 7, ny = (y + 0.5 - py) / 2.2; const a = -0.7 * nx - 0.5 * ny; if (e2(x, y - 1, BX, py, 7, 2.2) > 1 && nx < 0.2) return "w4"; return a > 0.25 ? "w4" : a > -0.35 ? "w3" : "w2"; } for (const d of [1, 2]) if (e2(x, y - d, BX, py, 7, 2.2) <= 1) { const nx = (x + 0.5 - BX) / 7; return d === 2 ? "w0" : nx < -0.4 ? "w2" : "w1"; } return null; })); g.part("block", () => { const yt = py - 4; for (let x = BX - 3; x <= BX + 3; x++) { g.pixel(x, yt, x < BX ? "w4" : "w3"); for (let y = yt + 1; y <= py; y++) g.pixel(x, y, x === BX - 3 ? "w3" : x === BX + 3 ? "w1" : y === py ? "w1" : "w2"); } g.pixel(BX + 1, yt + 2, "w1"); g.pixel(BX - 1, yt + 3, "w1"); // grain });}function hub(g) { const m = pmap(); for (let x = BX - 3; x <= BX + 3; x++) { m.P(x, 3, x < BX ? "w4" : "w3"); m.P(x, 4, x < BX + 1 ? "w3" : "w2"); for (let y = 5; y <= 8; y++) m.P(x, y, x === BX - 3 ? "w3" : x >= BX + 2 ? "w1" : y === 8 ? "w1" : "w2"); } m.P(BX - 3, 3, "w3"); m.P(BX + 3, 3, "w2"); for (let y = 9; y <= 9; y++) for (let c = 0; c < 3; c++) m.P(BX - 1 + c, y, ["w2", "w1", "w0"][c]); // collar stamp(g, "hub", m);}function barHalf(g, th, sgn, near) { // one half of the bar, from the hub axis out to its end; th in degrees (0 = across the view) const L = 13, a = (th * Math.PI) / 180; const ex = BX + sgn * L * Math.cos(a), ey = HUB_Y + sgn * L * 0.48 * Math.sin(a); const steep = Math.abs(Math.cos(a)) < 0.45; const m = pmap(); // the bar passes through the hub: the near half emerges from the hub's faces, it never paints across them if (near) { const P0 = m.P; m.P = (x, y, k) => { if (!(x >= BX - 3 && x <= BX + 3 && y >= 3 && y <= 9)) P0(x, y, k); }; } const n = Math.ceil(Math.max(Math.abs(ex - BX), Math.abs(ey - HUB_Y))); for (let i = 0; i <= n; i++) { const x = Math.round(BX + ((ex - BX) * i) / n), y = Math.round(HUB_Y + ((ey - HUB_Y) * i) / n); if (steep) { m.P(x, y, near ? "w3" : "w2"); m.P(x + 1, y, near ? "w2" : "w1"); } else { m.P(x, y, near ? "w3" : "w2"); m.P(x, y + 1, near ? "w1" : "w0"); } } // the end: lit end grain on the near end, a worn grip const xe = Math.round(ex), ye = Math.round(ey); if (near) { m.P(xe, ye, "w4"); if (steep) m.P(xe, ye + 1, "w3"); } stamp(g, sgn > 0 ? "barR" : "barL", m);}function bar(g, th) { const s = Math.sin((th * Math.PI) / 180); if (Math.abs(s) < 0.2) { barHalf(g, th, -1, true); barHalf(g, th, 1, false); hub(g); return; } const nearSgn = s > 0 ? 1 : -1; barHalf(g, th, -nearSgn, false); hub(g); barHalf(g, th, nearSgn, true);}function beamApple(g) { const m = pmap(); const R = [[1, 3], [0, 4], [0, 4], [1, 3]]; R.forEach(([a, b], r) => { for (let c = a; c <= b; c++) { const i = litIndex(c, r, 2.5, 2, 2.6, 2.2, 3, 0); m.P(3 + c, 6 + r, ["r0", "r1", "r2"][Math.max(0, i)]); } }); m.P(4, 7, "r3"); m.P(4, 6, "n5"); m.P(5, 5, "w1"); m.P(6, 5, "e2"); stamp(g, "apple", m);}// ---- basket ------------------------------------------------------------------------------------------------------const topFront = (x) => BTY + BRY * Math.sqrt(Math.max(0, 1 - ((x + 0.5 - BX) / BRX) ** 2));function basket(g, S, t, py) { const press = S.mode === "press"; const tick = Math.floor(t * N_PRESS); g.part("basket", () => g.field({ x: [BX - BRX, BX + BRX], y: [BTY - BRY, BBY + BRY] }, (x, y) => { const nx = (x + 0.5 - BX) / BRX; if (Math.abs(nx) > 1) return null; const outerTop = e2(x, y, BX, BTY, BRX, BRY) <= 0.99; const innerTop = e2(x, y, BX, BTY, BRX - 1.3, BRY - 1) <= 0.99; const tf = topFront(x); if (outerTop && !innerTop) { // slat ends (rim ring) const ny = (y + 0.5 - BTY) / BRY; if (ny < 0) return nx < 0.3 ? "w3" : "w2"; return nx < -0.2 ? "w4" : nx < 0.5 ? "w3" : "w2"; } if (innerTop) { // inside: pomace around the plate const plateHere = e2(x, y, BX, py, 7, 2.2) <= 1 || e2(x, y - 1, BX, py, 7, 2.2) <= 1 || e2(x, y - 2, BX, py, 7, 2.2) <= 1; if (plateHere) return null; const h = hash(x, y); if (press && h < 0.3) return "g2"; // juice welling up return h < 0.18 ? "r1" : h < 0.32 ? "e2" : h < 0.45 ? "w2" : h < 0.6 ? "g0" : "g1"; } const bottom = BBY + BRY * Math.sqrt(Math.max(0, 1 - nx * nx)); if (y <= tf || y > bottom) return null; const dy = y - Math.floor(tf); // slats: 2px slat, 1px gap, laid out along the arc so they crowd toward the sides const arc = Math.asin(cl(nx, -0.999, 0.999)) * BRX + 30; const gap = arc % 3 < 0.9; const lv = nx < -0.6 ? 3 : nx < 0.1 ? 2 : nx < 0.65 ? 1 : 0; if (dy === 2 || dy === 3 || y === Math.floor(bottom) - 2 || y === Math.floor(bottom) - 1) { // iron hoops const topRow = dy === 2 || y === Math.floor(bottom) - 2; if (topRow && [-0.7, 0, 0.6].some((u) => Math.round(BX + u * BRX - 0.5) === x)) return lv >= 2 ? "n4" : "n3"; return IRON[cl(lv + (topRow ? 1 : 0), 0, 4)]; } if (gap) { if (press) { const b = ((y - tick) % 4 + 4) % 4; return b === 0 ? "g2" : b === 1 ? "g1" : "g0"; } // juice running down between the slats return dy < 6 && hash(x, 3) < 0.5 ? "r0" : "w0"; } let k = lv + 1; if (dy === 1) k -= 1; // under the rim if (y >= Math.floor(bottom)) k -= 1; if (dy === 4) k -= 1; // hoop shadow return WOOD[cl(k, 0, 4)]; })); if (press) g.part("ooze", () => { // pomace squeezed up around the plate for (const [dx, k] of [[-8, "r1"], [-7, "g1"], [7, "e2"], [8, "g2"]]) g.pixel(BX + dx, py - 1, k); g.pixel(BX - 6, py - 2, "g2"); g.pixel(BX + 5, py - 2, "r1"); });}function basketDrips(g, t) { // pressing: juice drips from under the basket onto the bed g.detached("drips", () => { for (const [x, ph] of [[11, 0], [16, 0.5], [24, 0.25]]) { const p = (t * 2 + ph) % 1; const y = 50 + Math.floor(p * 3); if (p < 0.75) g.pixel(x, y, p < 0.35 ? "g2" : "g1"); } });}// ---- bucket ------------------------------------------------------------------------------------------------------function bucket(g, S, t) { const m = pmap(); const rx = 5; for (let y = BKCY - 2; y <= 61; y++) for (let x = BKX - rx; x <= BKX + rx; x++) { const nx = (x + 0.5 - BKX) / (rx + 0.5); const inRim = e2(x, y, BKX, BKCY, rx + 0.5, 2.2) <= 1; const inMouth = e2(x, y, BKX, BKCY, rx - 0.5, 1.4) <= 1; if (inRim && !inMouth) { const ny = (y + 0.5 - BKCY) / 2.2; m.P(x, y, ny < 0 ? (nx < 0.2 ? "w3" : "w2") : nx < -0.2 ? "w4" : nx < 0.5 ? "w3" : "w2"); continue; } if (inMouth) { // cider: far strip in shade, glint const far = e2(x, y - 1, BKX, BKCY, rx - 0.5, 1.4) > 1; m.P(x, y, far ? "g0" : "g1"); continue; } if (y <= BKCY) continue; const hw = y <= 58 ? rx : rx - 1; // tapers toward the foot if (Math.abs(x - BKX) > hw || (y === 61 && Math.abs(x - BKX) >= hw)) continue; const u = (x - BKX) / hw; if (y === 57 || y === 60) { m.P(x, y, u < -0.5 ? "n3" : u < 0.4 ? "n2" : "n1"); continue; } // hoops let k = u < -0.6 ? 3 : u < 0.2 ? 2 : u < 0.7 ? 1 : 0; if ((x === BKX - 2 || x === BKX + 2) && k === 2) k = 1; // stave seams if (y === 61 || y === BKCY + 2 - 0) k -= 0; if (y === 61) k -= 1; m.P(x, y, WOOD[cl(k, 0, 4)]); } m.P(BKX - 2, BKCY, "g3"); m.P(BKX - 1, BKCY, "g2"); // glint if (S.mode === "full") { // brimming: a head of froth over the rim const heave = Math.round(Math.sin(TAU * t) * 0.6 + 0.4); for (let x = BKX - 4; x <= BKX + 4; x++) { const ht = Math.round(2.2 * Math.sqrt(Math.max(0, 1 - ((x - BKX) / 4.6) ** 2))) + (Math.abs(x - BKX) < 2 ? heave : 0); for (let k = 0; k <= ht; k++) { const y = BKCY + 1 - k; m.P(x, y, k === ht ? (x < BKX + 2 ? "n5" : "g3") : x > BKX + 2 ? "g2" : k === 0 ? "g2" : "g3"); } } m.P(BKX + 4, BKCY + 2, "g3"); m.P(BKX + 4, BKCY + 3, "g3"); // froth spills over the lip m.P(BKX + 4, BKCY + 4, "g2"); } stamp(g, "bucket", m); g.part("bucketBail", () => { // rope handle ears g.pixel(BKX - 5, BKCY + 2, "n2"); g.pixel(BKX + 5, BKCY + 2, "n1"); });}function froth(g, t) { // bubbles on the head: rise, swell, pop g.detached("bubbles", () => { for (const [dx, dy, ph] of [[-2, -2, 0], [1, -3, 0.33], [3, -1, 0.66]]) { const p = (t + ph) % 1, x = BKX + dx, y = BKCY + dy; if (p < 0.4) g.pixel(x, y, "g3"); else if (p < 0.6) { g.pixel(x, y, "n5"); g.pixel(x, y - 1, "g3"); } else if (p < 0.72) { g.pixel(x - 1, y - 1, "n5"); g.pixel(x + 1, y - 1, "n5"); } } });}function wasp(g, t) { // gold-and-ink wasp on a figure-of-eight around the bucket; wings blur every frame const f = Math.round(t * N_FULL) % N_FULL; const x = BKX + Math.round(Math.sin(TAU * t) * 5), y = 44 + Math.round(Math.sin(2 * TAU * t) * 3) + Math.round(Math.cos(TAU * t) * 2); const dir = Math.cos(TAU * t) >= 0 ? 1 : -1; g.detached("wasp", () => { g.pixel(x, y, "g2"); // abdomen g.pixel(x + dir, y, "ink"); // head g.pixel(x - dir, y, f % 2 ? "ink" : "g1"); // stinger stripe if (f % 2 === 0) { g.pixel(x, y - 1, "n5"); g.pixel(x - dir, y - 1, "n4"); } else { g.pixel(x, y - 1, "n4"); g.pixel(x + dir, y - 1, "n5"); } });}// ---- cider in motion -------------------------------------------------------------------------------------------------const TIP = [40, 49], SURF = 54;function drip(g, t, n) { // one drop per loop: swell at the tip, fall, ring on the surface const f = Math.round(t * n) % n, fall = n - 6; g.detached("drip", () => { const [x, y] = TIP; if (f < fall - 3) g.pixel(x, y, "g1"); else if (f < fall) { g.pixel(x, y, "g2"); g.pixel(x, y + 1, "g1"); } else if (f === fall) { g.pixel(x, y + 1, "g2"); g.pixel(x, y + 2, "g3"); } else if (f === fall + 1) { g.pixel(x, y + 3, "g3"); } else if (f === fall + 2) { g.pixel(x - 1, SURF, "g3"); g.pixel(x + 1, SURF, "g3"); g.pixel(x, SURF - 1, "g2"); } else if (f === fall + 3) { g.pixel(x - 2, SURF, "g2"); g.pixel(x + 2, SURF, "g2"); } });}function stream(g, t) { g.detached("stream", () => { const tick = Math.floor(t * N_PRESS); for (let y = TIP[1]; y <= SURF; y++) { const b = ((y - tick) % 3 + 3) % 3; g.pixel(TIP[0], y, b === 0 ? "g3" : "g2"); g.pixel(TIP[0] + 1, y, b === 1 ? "g2" : "g1"); } const k = tick % 2; // splash at the surface g.pixel(TIP[0] - 1 - k, SURF, "g3"); g.pixel(TIP[0] + 2 + k, SURF, "n5"); g.pixel(TIP[0] - 1 + 3 * k, SURF + 1, "g2"); });}function stateAt(state, t) { t = ((t % 1) + 1) % 1; return { mode: state, t };}const CiderPress = sprite(pal, (g, t0, state) => { const S = stateAt(state, t0), t = S.t; const press = S.mode === "press"; const py = press ? BTY : BTY - 2; // plate centre row const th = press ? 180 * t : 0; const shift = press ? Math.floor(t * 6) : 0; contactShadow(g, 18, GROUND, 18, 1); contactShadow(g, BKX, 62, 6, 1); g.outlined("ink", () => { bed(g, S); frame(g); screw(g, 16, py - 5, shift); basket(g, S, t, py); plateAndBlock(g, py); }); screw(g, 9, 9, shift); spout(g, S, t); bar(g, th); beamApple(g); if (press) basketDrips(g, t); bucket(g, S, t); if (S.mode === "idle") drip(g, t, N_IDLE); if (press) stream(g, t); if (S.mode === "full") { drip(g, t, N_FULL); froth(g, t); wasp(g, t); }});submit(CiderPress, { size: [W, H], origin: "top-left", kind: "icon", scale: 1, animate: { fps: 10, duration: N_IDLE / 10, loop: true }, anchor: { x: CX, y: GROUND }, args: ["idle"], variants: [ { name: "idle", args: ["idle"] }, { name: "press", args: ["press"], animate: { fps: 10, duration: N_PRESS / 10, loop: true } }, { name: "full", args: ["full"], animate: { fps: 10, duration: N_FULL / 10, loop: true } }, ],});掲載されたコードでは、ビルドスクリプトが自動付与する共通部分を省略しています。具体的には 1 行のランタイムヘッダー、共有パレット、および汎用ヘルパー関数(contactShadow、litIndex、flame、hash、chk、TAU)です。表示されている行番号は実際のファイルと一致しています。これらのプログラムは AIエージェントが生成し、描画されたコマの仕上がりを確認しながら修正を加えたものです。
エージェントに、こうしたプログラムを書かせる。
Tessarune は描画 API のリファレンス、25 冊の制作マニュアル、結果を確かめるための描画エンジンをエージェントに渡します。