Cider press
This 48×64 floor prop shows a wooden screw press with crushed apples in a slatted basket, a carved spout leading to a wooden bucket, and a red apple on the beam. In the 16-frame idle loop, cider drops into the bucket as pulp slides down the spout. The 16-frame press loop turns the bar half a turn to stream juice, while 12 frames of full feature a brimming bucket and a buzzing wasp.
6×
Get the animated sprites: Harvest Festival on itch.ioVol.6 · 12 harvest-fair props · 37 animations
The header. The comment at the top of the file is the brief: size, view, light direction and every variant.
// 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 itThe last lines. submit declares the canvas and lists the variants the renderer exports.
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 } }, ],});Show the whole program (403 lines)
// 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 } }, ],});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.