Feast table
This 64×48 harvest feast table is laden with a cider jug, cornucopia, cheese, a lit candle, bread, wine goblets, and an apple pie over a decorated cloth runner. In the 16-frame idle loop, steam curls from the pie as the candle flickers. The 12-frame toast loop lifts the goblets to clink over the table while a bee loops over the fruit, and cleared displays empty dishes and a tipped goblet over 16 frames.
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.
// Feast table — 64x48 floor prop, top-down 3/4, key light top-left.// A long rustic harvest-feast trestle table: lengthwise planks, two trestle legs with block feet and a pegged// stretcher, a red cloth runner with gold borders whose ends drape over the table ends, and a garland of autumn// leaves swagged along the front edge. The spread (back row): an earthenware cider jug, a wicker cornucopia spilling// grapes, an apple and a small pumpkin, a cheese wheel with a cut wedge, a taper candle in a brass holder. Front row:// a scored bread loaf, two pewter goblets of red wine, grapes, an apple and a striped gourd on the runner, and a// lattice apple pie in a tin dish. The candle pools warm light on the planks around it.// Variants:// idle 16f loop steam curls up from the pie, the candle flame flickers and its light pool breathes// toast 12f loop the goblets lift off the table (shadows stay), meet over the fruit and clink, wine splashes,// the jug's cider sloshes, they set back down; a bee loops over the fruit all the while// cleared 16f loop after the feast: an empty pie dish, a bread heel, a rind of cheese, crumbs, one goblet tipped// over in a wine puddle, the cornucopia emptied, the candle burned to a stub and gutteringThe last lines. submit declares the canvas and lists the variants the renderer exports.
submit(FeastTable, { size: [W, H], origin: "top-left", kind: "icon", scale: 1, anchor: { x: CX, y: GROUND }, variants: [ { name: "idle", args: ["idle"], animate: { fps: 10, duration: N_IDLE / 10, loop: true } }, { name: "toast", args: ["toast"], animate: { fps: 10, duration: N_TOAST / 10, loop: true } }, { name: "cleared", args: ["cleared"], animate: { fps: 10, duration: N_CLEAR / 10, loop: true } }, ],});Show the whole program (424 lines)
// Feast table — 64x48 floor prop, top-down 3/4, key light top-left.// A long rustic harvest-feast trestle table: lengthwise planks, two trestle legs with block feet and a pegged// stretcher, a red cloth runner with gold borders whose ends drape over the table ends, and a garland of autumn// leaves swagged along the front edge. The spread (back row): an earthenware cider jug, a wicker cornucopia spilling// grapes, an apple and a small pumpkin, a cheese wheel with a cut wedge, a taper candle in a brass holder. Front row:// a scored bread loaf, two pewter goblets of red wine, grapes, an apple and a striped gourd on the runner, and a// lattice apple pie in a tin dish. The candle pools warm light on the planks around it.// Variants:// idle 16f loop steam curls up from the pie, the candle flame flickers and its light pool breathes// toast 12f loop the goblets lift off the table (shadows stay), meet over the fruit and clink, wine splashes,// the jug's cider sloshes, they set back down; a bee loops over the fruit all the while// cleared 16f loop after the feast: an empty pie dish, a bread heel, a rind of cheese, crumbs, one goblet tipped// over in a wine puddle, the cornucopia emptied, the candle burned to a stub and gutteringconst W = 64, H = 48, CX = 32, GROUND = 45;const N_IDLE = 16, N_TOAST = 12, N_CLEAR = 16;const TOP = { x0: 3, x1: 60, y0: 20, y1: 30 }; // table top faceconst CANDLE = { x: 57, base: 23 };const WOOD = ["w0", "w1", "w2", "w3", "w4"];// one-character codes for the little stamped objects (fruit, crumbs)const CH = { a: "r3", b: "r2", c: "r1", d: "r0", p: "g3", q: "g2", r: "g1", s: "g0", u: "v3", v: "v2", w: "v1", x: "v0", "0": "e0", "1": "e1", "2": "e2", "3": "e3", W: "w4", V: "w3", U: "w2", T: "w1", S: "w0", N: "n5", M: "n4", L: "n3", J: "n2", I: "n1", H: "n0", K: "ink",};// ---- layers: objects standing ON the table get their own ink ring (g.outlined would melt them into the planks) --function layer() { const m = new Map(); const put = (x, y, k) => { if (x >= 0 && y >= 0 && x < W && y < H) m.set(y * W + x, k); }; const stamp = (x, y, rows) => rows.forEach((row, j) => [...row].forEach((ch, i) => { if (ch !== ".") put(x + i, y + j, CH[ch]); })); return { m, put, stamp };}function emit(g, name, m, ringKey) { const ring = new Map(); let x0 = W, x1 = -1, y0 = H, y1 = -1; const grow = (x, y) => { x0 = Math.min(x0, x); x1 = Math.max(x1, x); y0 = Math.min(y0, y); y1 = Math.max(y1, y); }; for (const i of m.keys()) { const x = i % W, y = (i - x) / W; grow(x, y); 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 || m.has(ny * W + nx)) continue; ring.set(ny * W + nx, ringKey); grow(nx, ny); } } if (x1 < 0) return; g.part(name, () => g.field({ x: [x0, x1], y: [y0, y1] }, (x, y) => m.get(y * W + x) ?? ring.get(y * W + x) ?? null));}// ---- the table ------------------------------------------------------------------------------------------------function trestles(g) { g.part("stretcher", () => { g.rect({ x: [12, 51], y: [36, 38] }, "w1", { fill: "w1" }); g.line([12, 36], [51, 36], "w2"); g.line([12, 38], [51, 38], "w0"); for (const x of [13, 50]) { g.line([x, 35], [x, 39], "w3"); g.pixel(x, 40, "w1"); } // through-wedges }); for (const [x, name] of [[8, "legL"], [52, "legR"]]) { g.part(name, () => { g.field({ x: [x, x + 3], y: [33, 41] }, (px, py) => { const u = px - x; if (py <= 34) return u < 2 ? "w1" : "w0"; // in the table's shadow return ["w3", "w2", "w2", "w1"][u]; }); // block foot: lit top, front face with end grain g.rect({ x: [x - 2, x + 5], y: [41, 44] }, "w2", { fill: "w2" }); g.line([x - 2, 41], [x + 5, 41], "w3"); g.line([x - 2, 44], [x + 5, 44], "w1"); g.pixel(x - 2, 42, "w3"); g.pixel(x + 5, 43, "w1"); g.line([x, 43], [x + 2, 43], "w1"); g.pixel(x + 1, 42, "w3"); }); }}function poolAt(x, y, R) { const d = Math.hypot(x + 0.5 - CANDLE.x, (y + 0.5 - CANDLE.base - 1) * 2); return d < R * 0.5 ? 2 : d < R - 1.2 ? 1 : d < R && chk(x, y) ? 1 : 0;}function tableTop(g, R) { const { x0, x1, y0, y1 } = TOP; g.part("tabletop", () => g.field({ x: [x0, x1], y: [y0, y1 + 3] }, (x, y) => { if (y > y1) return y === y1 + 1 ? (x < 40 ? "w3" : "w2") : y === y1 + 2 ? "w1" : "w0"; // front edge, underside let k = x > 50 ? 2 : 3; if (y === y0 && x < 44) k = 4; // back rim catches the key light if (y === 22 || y === 28) k -= 1; // plank seams if (x === x0) k = 2; if ((y === 21 && (x === 12 || x === 13)) || (y === 29 && x >= 30 && x <= 32) || (y === 25 && x === 46)) k -= 1; // grain return WOOD[Math.max(0, Math.min(4, k + poolAt(x, y, R)))]; }));}function runner(g) { g.part("runner", () => { g.field({ x: [TOP.x0, TOP.x1], y: [23, 27] }, (x, y) => (y === 23 || y === 27 ? "g1" : y === 24 ? (x < 40 ? "r2" : "r1") : y === 26 ? "r0" : "r1")); for (let x = TOP.x0 + 2; x < TOP.x1; x += 4) g.pixel(x, 25, "g1"); // woven dots down the middle // ends draped over the table ends, with fringe for (const s of [-1, 1]) { const ex = s < 0 ? TOP.x0 - 1 : TOP.x1 + 1; g.fillPoly([[ex, 23], [ex, 33], [ex + s, 34], [ex + s, 24]], s < 0 ? "r1" : "r0"); g.line([ex, 24], [ex, 33], s < 0 ? "r2" : "r1"); g.pixel(ex, 35, "g1"); g.pixel(ex + s, 36, "g1"); g.pixel(ex, 36, "g0"); } });}function garland(g) { const keys = [["r2", "r1"], ["g2", "g1"], ["r3", "r2"], ["w3", "w2"], ["r1", "r0"]]; g.part("garland", () => { const anchors = [4, 22, 41, 59]; const yAt = (x) => { const k = x < 22 ? 0 : x < 41 ? 1 : 2, a = anchors[k], b = anchors[k + 1]; return 31 + Math.round(3 * Math.sin(Math.PI * (x - a) / (b - a))); }; for (let x = anchors[0]; x <= anchors[3]; x++) g.pixel(x, yAt(x), "w0"); // the twine for (let x = anchors[0] + 1, i = 0; x < anchors[3]; x += 3, i++) { // leaf clusters const y = yAt(x), k = keys[i % 5]; g.pixel(x, y - 1, k[0]); g.pixel(x + 1, y - 1, k[0]); g.pixel(x, y, k[0]); g.pixel(x + 1, y, k[1]); g.pixel(x + 1, y + 1, k[1]); } for (const x of anchors) { g.pixel(x, 30, "r2"); g.pixel(x - 1, 31, "r2"); g.pixel(x + 1, 31, "r1"); g.pixel(x, 32, "r0"); g.pixel(x, 31, "r1"); } // ribbon bows });}// ---- back row -------------------------------------------------------------------------------------------------function jug(g, slosh) { const L = layer(), put = L.put, cx = 9; const hw = (y) => (y <= 10 ? 2 : y <= 12 ? 1 : y <= 13 ? 2 : y <= 14 ? 3 : y <= 21 ? 4 : y <= 22 ? 3 : 2); for (let y = 10; y <= 23; y++) { const w = hw(y); for (let x = cx - w; x <= cx + w; x++) { const u = (x - (cx - w) + 0.5) / (2 * w + 1); let k = y <= 15 ? (u < 0.22 ? "n5" : u < 0.5 ? "n4" : u < 0.8 ? "n3" : "n2") // cream salt glaze above : (u < 0.22 ? "w4" : u < 0.5 ? "w3" : u < 0.8 ? "w2" : "w1"); // bare stoneware below if (y === 16) k = u < 0.22 ? "r3" : u < 0.55 ? "r2" : u < 0.85 ? "r1" : "r0"; // glaze band if (y === 23) k = u < 0.4 ? "w1" : "w0"; put(x, y, k); } } // mouth: dark rim with the cider inside; a pouring lip on the left put(8, 10, "w0"); put(9, 10, slosh === 1 ? "g2" : "g1"); put(10, 10, slosh === 2 ? "g2" : "w0"); put(6, 10, "w4"); // handle on the shaded side put(12, 12, "w2"); put(13, 13, "w1"); put(13, 14, "w1"); put(13, 15, "w1"); put(12, 16, "w1"); emit(g, "jug", L.m, "ink"); if (slosh === 2) g.detached("ciderDrop", () => { g.pixel(9, 8, "g2"); g.pixel(10, 7, "g3"); });}// the horn: a cone swept along a quadratic curve (tail curling up at the back-left, mouth opening to the right),// built in a layer so it keeps its own ink ring against the jug behind itconst HORN = { p0: [18, 10], p1: [13, 23], p2: [29, 19] };const hornAt = (u) => { const { p0, p1, p2 } = HORN, k = 1 - u; return [k * k * p0[0] + 2 * k * u * p1[0] + u * u * p2[0], k * k * p0[1] + 2 * k * u * p1[1] + u * u * p2[1]];};function horn(g, full) { const LD = [-0.74, -0.67]; const S = []; for (let i = 0; i <= 80; i++) { const u = i / 80; S.push([u, ...hornAt(u)]); } const H = layer(); for (let y = 7; y <= 25; y++) for (let x = 11; x <= 32; x++) { let best = null, bd = 1e9; for (const s of S) { const d = (x + 0.5 - s[1]) ** 2 + (y + 0.5 - s[2]) ** 2; if (d < bd) { bd = d; best = s; } } const u = best[0], r = 0.7 + 3.5 * Math.pow(u, 1.3); if (Math.sqrt(bd) > r) continue; const [ax, ay] = hornAt(Math.max(0, u - 0.01)), [bx, by] = hornAt(Math.min(1, u + 0.01)); const tl = Math.hypot(bx - ax, by - ay) || 1, nx = -(by - ay) / tl, ny = (bx - ax) / tl; const v = ((x + 0.5 - best[1]) * nx + (y + 0.5 - best[2]) * ny) / r; // -1..1 across the horn let l = v * (nx * LD[0] + ny * LD[1]) * 1.3 + 0.25 - 0.25 * v * v; const ring = (u * 10) % 1; if (ring < 0.2) l -= 0.6; // woven rings else if ((Math.floor(u * 10) + Math.floor((v + 1) * 2)) % 2) l -= 0.3; // basket weave H.put(x, y, WOOD[l > 0.75 ? 4 : l > 0.2 ? 3 : l > -0.35 ? 2 : l > -0.8 ? 1 : 0]); } // the mouth, seen from the side: a lit rim around the dark inside for (let y = 15; y <= 23; y++) for (let x = 27; x <= 32; x++) { const ex = (x + 0.5 - 29.8) / 2.4, ey = (y + 0.5 - 19.3) / 4.4; const r = ex * ex + ey * ey; if (r > 1) continue; H.put(x, y, r > 0.45 ? (ey < -0.2 || ex < -0.5 ? "w4" : "w2") : ey < 0 ? "ink" : "w0"); } emit(g, "cornucopia", H.m, "ink"); const F = layer(); if (full) { F.stamp(30, 14, ["..1..", ".uvvw", "uvwvw", ".wvwx", "..wx."]); // grapes hanging over the rim F.stamp(33, 18, [".T..", "Nbbc", "bbcd", ".cd."]); // apple F.stamp(36, 19, ["..1..", ".abc.", "abbcd", ".ccd."]); // small pumpkin } else { F.stamp(31, 20, ["T.", "vw", ".x"]); // two grapes left on a stem } emit(g, "hornFruit", F.m, "ink");}function cheese(g, whole) { const L = layer(), put = L.put; if (whole) { const cx = 47, cy = 18.5, rx = 4.6, ry = 1.9; for (let y = 16; y <= 23; y++) for (let x = 42; x <= 52; x++) { const nx = (x + 0.5 - cx) / rx; if (Math.abs(nx) > 1) continue; const s = Math.sqrt(1 - nx * nx), top = cy - ry * s, bot = cy + ry * s; if (y + 0.5 < top || y + 0.5 > bot + 3) continue; // the wedge cut out of the front-right: the cut faces show paler paste if (nx > 0.05 && y + 0.5 > cy - 0.2) { if (nx < 0.35 || y + 0.5 > bot + 2.2) put(x, y, nx < 0.35 ? "g3" : "g2"); continue; } if (y + 0.5 <= bot) { const ny = (y + 0.5 - cy) / ry; put(x, y, -0.6 * nx - 0.5 * ny > 0.45 ? "g3" : nx > 0.5 ? "g1" : "g2"); } else put(x, y, y + 0.5 > bot + 2 ? (nx < -0.3 ? "w2" : "w1") : nx < -0.4 ? "g1" : "g0"); } put(48, 20, "g1"); put(48, 22, "g1"); // holes in the cut face emit(g, "cheese", L.m, "ink"); const Wd = layer(); // the cut wedge, lying in front Wd.stamp(50, 21, ["pq..", "pqqr", "UTTT"]); emit(g, "wedge", Wd.m, "ink"); } else { L.stamp(45, 21, ["..pq", "pqqr", "UUTT"]); // a rind of cheese left emit(g, "cheese", L.m, "ink"); }}function flick(t, s) { const dh = Math.round(0.75 * Math.sin(TAU * (2 * t + s)) + 0.45 * Math.sin(TAU * (3 * t + 2.3 * s))); const lean = 0.7 * Math.sin(TAU * (t + s)) + 0.45 * Math.sin(TAU * (3 * t + 1.7 * s)); return { dh, lean };}function candleFlame(g, fx, fy, h, lean) { for (let i = 0; i < h; i++) { const k = (i + 0.5) / h, y = fy - i; const ax = fx + Math.round(lean * k * k); if (k > 0.12 && k < 0.62 && h > 4) { const edge = k > 0.18 && k < 0.5 ? "fl1" : "fl0"; g.pixel(ax - 1, y, edge); g.pixel(ax + 1, y, edge); } g.pixel(ax, y, k < 0.12 ? "fl1" : k < 0.5 ? "fl3" : k < 0.68 ? "fl2" : k < 0.86 ? "fl1" : "fl0"); }}function candle(g, t, stub) { const { x, base } = CANDLE; const top = stub ? base - 3 : base - 10; const L = layer(), put = L.put; // brass holder: a dish with a ring handle on the shaded side for (let xx = x - 3; xx <= x + 2; xx++) { put(xx, base, xx < x ? "g2" : "g1"); put(xx, base + 1, xx < x - 1 ? "g1" : "g0"); } put(x - 3, base, "g3"); // taper: two columns, lit left for (let y = top; y < base; y++) { put(x - 1, y, "n5"); put(x, y, y > base - 3 ? "w3" : "w4"); } put(x, top, "n4"); if (stub) { put(x - 2, base - 1, "n5"); put(x + 1, base - 1, "w4"); put(x - 2, base - 2, "n5"); put(x + 1, base, "w4"); } // melted wax else { put(x - 2, top + 3, "n5"); put(x - 2, top + 4, "w4"); } emit(g, "candle", L.m, "ink"); const f = flick(t, 0.2); let h = (stub ? 4 : 6) + f.dh, lean = f.lean; if (stub) { // guttering: it dips and leans hard const fr = Math.round(t * N_CLEAR) % N_CLEAR; if (fr === 5 || fr === 6 || fr === 12) h -= 2; if (fr === 6 || fr === 13) lean += 1.2; } g.part("flame", () => { g.pixel(x - 1, top - 1, "ink"); candleFlame(g, x - 1, top - 2, Math.max(2, h), lean); });}// ---- front row ------------------------------------------------------------------------------------------------function bread(g, whole) { const L = layer(), put = L.put; const cx = whole ? 10.5 : 9.5, cy = 26.3, rx = whole ? 5.2 : 2.8, ry = 2.6; for (let y = 22; y <= 29; y++) for (let x = 3; x <= 17; x++) { const i = litIndex(x, y, cx, cy, rx, ry, 4, 0.05); if (i < 0) continue; put(x, y, ["w1", "w2", "w3", "w4"][i]); } if (whole) { for (const sx of [7, 10, 13]) { put(sx, 25, "w1"); put(sx + 1, 24, "w1"); put(sx - 1, 26, "w2"); } // scores put(8, 24, "n5"); put(6, 25, "n5"); // flour } else { for (let y = 25; y <= 28; y++) { put(12, y, y === 25 ? "n5" : "w4"); put(11, y, "n5"); } // the cut face put(11, 27, "w4"); } emit(g, "bread", L.m, "ink");}// pewter goblet: x0 = left column of the bowl, yb = foot rowfunction goblet(g, name, x0, yb, slosh, empty) { const L = layer(); const wine = empty ? ["I", "H", "H"] : slosh === 1 ? ["c", "b", "d"] : slosh === 2 ? ["b", "c", "c"] : ["b", "c", "d"]; L.stamp(x0, yb - 7, [ "MNMLJ", "M" + wine.join("") + "J", "NMLJI", "MMLJI", ".MLI.", "..L..", "..J..", ".MLJ.", ]); emit(g, name, L.m, "ink");}function tippedGoblet(g) { const L = layer(); L.stamp(16, 25, [ "M.....", "MLLMNM", "LJLMLJ", "J.IJJI", ]); emit(g, "gobletTipped", L.m, "ink"); g.part("winePuddle", () => { g.line([23, 29], [25, 29], "r0"); g.pixel(24, 28, "r1"); g.pixel(26, 29, "r0"); });}function spill(g, full) { const F = layer(); if (full) { F.stamp(24, 23, ["..1..", ".uvvw", "uvwvw", ".wvwx", "..wx."]); // grapes F.stamp(29, 25, [".T..", "Nbbc", "bbcd", ".cd."]); // apple F.stamp(33, 25, ["1......", ".pq2qr.", "qq2qq2r", ".rs2rs."]); // striped gourd } else { F.stamp(33, 25, ["1......", ".pq2qr.", "qq2qq2r", ".rs2rs."]); // the gourd was only decoration F.stamp(29, 26, [".T", "Nq", "q.", "Nq"]); // an apple core } emit(g, "spill", F.m, "ink");}function pie(g, full) { const L = layer(), put = L.put; const cx = 51.5, cy = 25.6, rx = 5.7, ry = 2.6; for (let y = 22; y <= 30; y++) for (let x = 44; x <= 58; x++) { const nx = (x + 0.5 - cx) / rx, ny = (y + 0.5 - cy) / ry; const r = nx * nx + ny * ny; if (r <= 1) { if (r > 0.58) { // crimped crust rim / tin rim const lit = -0.6 * nx - 0.6 * ny > 0.2; put(x, y, full ? (lit ? "w4" : (x & 1) ? "w3" : "w2") : lit ? "n4" : "n3"); } else if (full) { // lattice over apple filling const lat = (x + y) % 3 === 0 || (x - y + 60) % 3 === 0; put(x, y, lat ? (nx < 0.2 ? "w4" : "w3") : nx < -0.1 && ny < 0.2 ? "g1" : "g0"); } else put(x, y, ny < -0.2 ? "n1" : "n2"); // the empty tin continue; } // the dish's side below the rim const s = Math.sqrt(Math.max(0, 1 - nx * nx)); if (Math.abs(nx) < 0.97 && y + 0.5 > cy + ry * s && y + 0.5 <= cy + ry * s + 1.6) put(x, y, nx < -0.3 ? "n3" : "n2"); } if (!full) { for (const [x, y] of [[49, 25], [52, 26], [54, 25]]) put(x, y, "w3"); put(50, 26, "w4"); put(51, 26, "w3"); } // crumbs, a bit of crust emit(g, "pie", L.m, "ink");}function steam(g, t) { g.detached("steam", () => { for (let i = 0; i < 3; i++) { const p = (t + i / 3) % 1; const bx = [48, 51, 53][i], y0 = 22 - Math.round(p * 12); const x = bx + Math.round(Math.sin(TAU * (p * 1.2 + i * 0.37)) * 2); // a puff that thins out as it rises: 2x2, then a curl of two, then one faint pixel if (p < 0.35) { g.rect({ x: [x, x + 1], y: [y0 - 1, y0] }, "n5", { fill: "n5" }); g.pixel(x + 1, y0, "n4"); } else if (p < 0.7) { g.pixel(x, y0, "n4"); g.pixel(x + 1, y0, "n5"); } else if (p < 0.9) g.pixel(x, y0, "n3"); } });}function crumbs(g) { g.part("crumbs", () => { for (const [x, y, k] of [[17, 24, "w4"], [19, 25, "w3"], [21, 27, "w3"], [27, 24, "w4"], [38, 26, "w3"], [40, 24, "w4"], [43, 28, "w3"], [14, 29, "w3"], [5, 23, "w4"], [36, 29, "w4"], [45, 21, "w3"]]) g.pixel(x, y, k); g.pixel(30, 23, "r2"); g.pixel(41, 27, "v1"); // a stray leaf, a fallen grape });}// ---- toast: goblets lift, meet over the fruit, clink -------------------------------------------------------const CUP_L = { x0: 17, yb: 29 }, CUP_R = { x0: 41, yb: 29 };const LIFT = [[0, 0], [1, -2], [3, -5], [6, -8], [9, -10], [8, -11], [7, -10], [5, -8], [3, -5], [1, -2], [0, 0], [0, 0]];function cupShadow(g, c, dx, dy) { if (dy === 0) return; g.part("cupShadow", () => { for (let x = c.x0 + 1; x <= c.x0 + 3; x++) { g.pixel(x, c.yb, "w1"); if (dy > -6) g.pixel(x, c.yb - 1, chk(x, c.yb) ? "w1" : "w2"); } });}function clink(g, f) { g.detached("clink", () => { const cx = 31, cy = 10; if (f === 4) for (const [x, y, k] of [[cx, cy - 1, "g3"], [cx, cy - 2, "n5"], [cx - 3, cy, "g2"], [cx + 3, cy, "g2"], [cx - 2, cy - 2, "g3"], [cx + 2, cy - 2, "g3"]]) g.pixel(x, y, k); const drops = { 4: [[0, -1], [-1, -2], [1, -3]], 5: [[-2, -4], [2, -5], [0, -3]], 6: [[-3, -2], [3, -3]], 7: [[-4, 1], [4, 0]] }[f]; if (drops) drops.forEach(([dx, dy], i) => { g.pixel(cx + dx, cy + dy, i % 2 ? "r1" : "r2"); if (f <= 5) g.pixel(cx + dx, cy + dy + 1, "r0"); }); });}function bee(g, t) { const x = Math.round(30 + 8 * Math.sin(TAU * t)), y = Math.round(9 + 2.5 * Math.sin(2 * TAU * t)); const dir = Math.cos(TAU * t) >= 0 ? 1 : -1; // faces where it is flying const up = Math.round(t * N_TOAST * 2) % 2 === 0; g.detached("bee", () => { // striped body (tail, band, band, head), dark underside, flickering wings g.pixel(x - 2 * dir, y, "ink"); g.pixel(x - dir, y, "g2"); g.pixel(x, y, "ink"); g.pixel(x + dir, y, "g3"); g.pixel(x + 2 * dir, y, "ink"); g.pixel(x - dir, y + 1, "g1"); g.pixel(x, y + 1, "ink"); g.pixel(x + dir, y + 1, "g1"); if (up) { g.pixel(x, y - 1, "n5"); g.pixel(x - dir, y - 2, "b3"); g.pixel(x, y - 2, "n5"); } else { g.pixel(x - dir, y - 1, "n5"); g.pixel(x - 2 * dir, y - 1, "b3"); } });}const FeastTable = sprite(pal, (g, t, mode) => { t = ((t % 1) + 1) % 1; const cleared = mode === "cleared", toast = mode === "toast"; contactShadow(g, CX, GROUND, 27, 2); g.outlined("ink", () => trestles(g)); const f = flick(t, 0.2); g.outlined("ink", () => tableTop(g, (cleared ? 5 : 7) + 0.9 * f.dh)); runner(g); g.outlined("ink", () => garland(g)); if (cleared) crumbs(g); // back row let ft = 0, slosh = 0; if (toast) { ft = Math.round(t * N_TOAST) % N_TOAST; slosh = ft >= 4 && ft <= 7 ? (ft % 2 ? 1 : 2) : 0; } jug(g, slosh); horn(g, !cleared); cheese(g, !cleared); candle(g, t, cleared); // front row bread(g, !cleared); spill(g, !cleared); pie(g, !cleared); if (cleared) { tippedGoblet(g); goblet(g, "gobletR", CUP_R.x0, CUP_R.yb, 0, true); return; } if (!toast) { goblet(g, "gobletL", CUP_L.x0, CUP_L.yb, 0, false); goblet(g, "gobletR", CUP_R.x0, CUP_R.yb, 0, false); steam(g, t); return; } const [dx, dy] = LIFT[ft]; cupShadow(g, CUP_L, dx, dy); cupShadow(g, CUP_R, -dx, dy); steam(g, t); const cs = ft >= 3 && ft <= 8 ? 1 + (ft % 2) : 0; goblet(g, "gobletL", CUP_L.x0 + dx, CUP_L.yb + dy, cs, false); goblet(g, "gobletR", CUP_R.x0 - dx, CUP_R.yb + dy, cs ? 3 - cs : 0, false); clink(g, ft); bee(g, t);});submit(FeastTable, { size: [W, H], origin: "top-left", kind: "icon", scale: 1, anchor: { x: CX, y: GROUND }, variants: [ { name: "idle", args: ["idle"], animate: { fps: 10, duration: N_IDLE / 10, loop: true } }, { name: "toast", args: ["toast"], animate: { fps: 10, duration: N_TOAST / 10, loop: true } }, { name: "cleared", args: ["cleared"], animate: { fps: 10, duration: N_CLEAR / 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.