Roast spit
A 48×48 floor prop in top-down 3/4: an open cookfire in a ring of stones, two forked iron stands and an iron spit with a crank, with a twine-trussed ham on it. The ham turns: its scored crackling, twine knots and fat beads travel round the body while the lighting stays fixed, so the beads flash as they pass the highlight. “idle” (16 frames, loop) is one full turn, with the crank handle circling and fat drips falling into the fire to sizzle; “empty” (12 frames, loop) is the bare spit with the fire only.
6×
Get the animated sprites: Tavern & Market on itch.ioVol.3 · 12 tavern and market props · 32 animations
The header. The comment at the top of the file is the brief: size, view, light direction and every variant.
// Roast spit — 48x48 floor prop, top-down 3/4, key light top-left.// An open cookfire in a ring of stones, two forked iron stands, and an iron spit with a crank; a twine-trussed ham// roasts on it. The ham TURNS: its scored crackling, twine knots and fat beads travel round the body (lighting stays// fixed, so the beads flash as they pass through the highlight), the crank handle circles, and fat drips fall into// the fire and sizzle. Variants: idle (16f loop, one full turn), empty (12f loop: bare spit, fire only).The last lines. submit declares the canvas and lists the variants the renderer exports.
submit(Roast, { size: [W, H], origin: "top-left", kind: "icon", scale: 1, animate: { fps: 10, duration: 1.6, loop: true }, anchor: { x: CX, y: GROUND }, args: ["idle"], variants: [ { name: "idle" }, { name: "empty", args: ["empty"], animate: { fps: 10, duration: 1.2, loop: true } }, ],});Show the whole program (215 lines)
// Roast spit — 48x48 floor prop, top-down 3/4, key light top-left.// An open cookfire in a ring of stones, two forked iron stands, and an iron spit with a crank; a twine-trussed ham// roasts on it. The ham TURNS: its scored crackling, twine knots and fat beads travel round the body (lighting stays// fixed, so the beads flash as they pass through the highlight), the crank handle circles, and fat drips fall into// the fire and sizzle. Variants: idle (16f loop, one full turn), empty (12f loop: bare spit, fire only).const W = 48, H = 48, CX = 24, GROUND = 44;const AXIS = 20.5; // spit axis (screen y)const HAM = { x0: 10, x1: 33, r: 7.2, shank: 2.6 };const PIT = { cx: 24, cy: 40, rx: 13, ry: 4 };const MEAT = ["w0", "w1", "w2", "w3", "w4"];// ham half-height along the spit: a round rump on the left tapering to the shank on the rightfunction hamR(x) { const u = (x + 0.5 - HAM.x0) / (HAM.x1 - HAM.x0); if (u < 0 || u > 1) return -1; if (u < 0.42) return HAM.r * Math.sqrt(Math.max(0, 1 - ((0.42 - u) / 0.42) ** 2)); return HAM.shank + (HAM.r - HAM.shank) * (1 - Math.pow((u - 0.42) / 0.58, 1.7));}// ---- fire pit -------------------------------------------------------------------------------------------const STONES = [];for (let i = 0; i < 11; i++) { const a = (i / 11) * TAU + 0.2; STONES.push({ x: Math.round(PIT.cx + Math.cos(a) * PIT.rx), y: Math.round(PIT.cy + Math.sin(a) * PIT.ry), front: Math.sin(a) > 0.05, id: i });}function stone(g, s) { const w = s.front ? 3 : 2, hgt = s.front ? 2 : 1.5; g.field({ x: [s.x - w, s.x + w], y: [Math.floor(s.y - hgt - 1), Math.ceil(s.y + hgt)] }, (x, y) => { const i = litIndex(x, y, s.x + 0.5, s.y, w + 0.5, hgt + 0.6, 4, 0.1 + (hash(s.id, 2) - 0.5) * 0.3); return i < 0 ? null : ["n1", "n2", "n2", "n3"][i]; });}function pitBed(g, lit, t) { g.part("ashBed", () => { g.ellipse({ x: [PIT.cx - 11, PIT.cx + 11], y: [PIT.cy - 3, PIT.cy + 3] }, "n1", { fill: "n1" }); g.ellipse({ x: [PIT.cx - 8, PIT.cx + 8], y: [PIT.cy - 2, PIT.cy + 2] }, lit ? "r0" : "n0", { fill: lit ? "r0" : "n0" }); if (lit) for (let i = 0; i < 7; i++) { const x = PIT.cx - 6 + i * 2, y = PIT.cy - 1 + (i % 3 === 1 ? 1 : 0); const p = 0.5 + 0.5 * Math.sin(TAU * (t + hash(i, 5))); g.pixel(x, y, p > 0.65 ? "r2" : "r1"); } });}function logs(g, t) { const crack = (id) => (0.5 + 0.5 * Math.sin(TAU * (t + hash(id, 3))) > 0.5 ? "fl2" : "fl1"); g.part("logs", () => { g.sweep([[14, 42], [31, 37]], { w0: 1.6, w1: 1.3 }, (u, v) => (v < -0.3 ? "w2" : v > 0.45 ? "w0" : "w1")); g.sweep([[34, 42], [17, 37]], { w0: 1.6, w1: 1.3 }, (u, v) => (v < -0.3 ? "w2" : v > 0.45 ? "w0" : "w1")); for (const [x, y] of [[14, 42], [34, 42]]) { g.ellipse({ x: [x - 1, x + 1], y: [y - 1, y + 1] }, "w3", { fill: "w3" }); g.pixel(x, y, "w1"); } g.pixel(19, 40, crack(1)); g.pixel(20, 40, crack(1)); g.pixel(29, 40, crack(2)); g.pixel(24, 38, "n0"); g.pixel(26, 39, "n0"); });}// ---- flames (layered teardrops) ----------------------------------------------------------------------const LAYERS = [["fl0", 1, 1], ["fl1", 0.86, 0.72], ["fl2", 0.6, 0.48], ["fl3", 0.34, 0.26]];const profile = (k) => (k < 0.22 ? 0.72 + 0.28 * Math.sin((k / 0.22) * Math.PI / 2) : Math.pow((1 - k) / 0.78, 0.9));const FLAMES = [ { name: "flameL", x: 18.5, base: 40, h: 9, hw: 3.4, lean: -1.2, seed: 0.37 }, { name: "flameR", x: 29.5, base: 40, h: 10, hw: 3.4, lean: 1.2, seed: 0.68 }, { name: "flameC", x: 24, base: 41, h: 12, hw: 5, lean: 0, seed: 0.05 },];function drawFlame(g, f, t, boost) { const hh = f.h * (1 + 0.1 * Math.sin(TAU * (2 * t + f.seed)) + 0.06 * Math.sin(TAU * (3 * t + 2 * f.seed)) + boost); g.part(f.name, () => g.field({ x: [6, 42], y: [Math.max(0, f.base - 22), f.base] }, (x, y) => { const k = (f.base + 1 - (y + 0.5)) / hh; if (k < 0 || k > 1) return null; const ax = f.x + 0.5 + (f.lean + 1.1 * Math.sin(TAU * (t + f.seed))) * k * k + 0.9 * Math.sin(TAU * (1.2 * k - 2 * t + f.seed)) * k; const lick = 1 + 0.2 * Math.sin(TAU * (2.3 * k - 2 * t + 1.7 * f.seed)); const dx = Math.abs(x + 0.5 - ax); let key = null; for (const [c, hf, wf] of LAYERS) { const kj = k / hf; if (kj > 1) break; if (dx <= f.hw * wf * profile(kj) * lick) key = c; else break; } return key; }));}// ---- stands, spit, crank -------------------------------------------------------------------------------function stand(g, x, name) { // x = left column of the 2px pole g.part(name, () => { g.line([x, 22], [x, 38], "n3"); g.line([x + 1, 22], [x + 1, 38], "n1"); g.line([x, 21], [x - 2, 17], "n2"); g.line([x + 1, 21], [x + 3, 17], "n1"); // fork g.pixel(x - 2, 17, "n3"); g.pixel(x + 3, 17, "n2"); g.line([x, 36], [x - 3, 43], "n2"); g.line([x + 1, 36], [x + 4, 43], "n1"); // splayed legs g.pixel(x - 3, 43, "n3"); g.pixel(x + 4, 43, "n2"); g.pixel(x, 30, "n4"); // a glint on the lit side });}function spit(g) { g.part("spit", () => { g.line([3, 20], [42, 20], "n3"); g.line([3, 21], [42, 21], "n1"); g.pixel(2, 20, "n2"); g.pixel(2, 21, "n1"); g.pixel(3, 20, "n4"); });}function crank(g, th) { const ps = th + Math.PI / 2; // the arm's angle round the axis const ey = Math.round(AXIS + 4 * Math.sin(ps)), near = Math.cos(ps) > -0.2; g.part("crank", () => { g.line([43, 20], [43, 21], "n2"); const y0 = Math.min(ey, 21), y1 = Math.max(ey, 20); g.line([43, y0], [43, y1], near ? "n2" : "n1"); g.line([44, ey], [46, ey], near ? "w2" : "w1"); // wooden grip g.pixel(44, ey, near ? "w3" : "w2"); });}// ---- the ham ---------------------------------------------------------------------------------------------// every visible pixel maps to a point on the turning surface: a = its screen angle round the axis, phi = a - thconst TWINE = [15, 21, 27];function ham(g, th) { g.part("ham", () => g.field({ x: [HAM.x0, HAM.x1], y: [12, 29] }, (x, y) => { const r = hamR(x); if (r < 0) return null; const v = (y + 0.5 - AXIS) / r; if (Math.abs(v) > 1) return null; const a = Math.asin(v), phi = a - th; // fixed key light from the top-left; the rump end is rounded so it turns toward the light const u = (x + 0.5 - HAM.x0) / (HAM.x1 - HAM.x0); const nx = u < 0.42 ? -Math.sqrt(Math.max(0, 1 - (r / HAM.r) ** 2)) : 0.18; const ny = v * Math.sqrt(1 - nx * nx), nz = Math.sqrt(Math.max(0, 1 - nx * nx - ny * ny)); const d = -0.5 * nx - 0.55 * ny + 0.67 * nz; let lv = d > 0.86 ? 4 : d > 0.58 ? 3 : d > 0.18 ? 2 : d > -0.25 ? 1 : 0; if (v > 0.8 && Math.abs(x + 0.5 - CX) < 8) return v > 0.93 && Math.abs(x + 0.5 - CX) < 4 ? "r2" : "r1"; // firelight on the underside if (TWINE.includes(x)) return v < -0.5 ? "n4" : v < 0.4 ? "n3" : "n2"; // scored crackling: a diamond lattice that travels round the ham as it turns const s = (phi * 4) / TAU; const l1 = (((x / 7 + s) % 1) + 1) % 1, l2 = (((x / 7 - s) % 1) + 1) % 1; if ((l1 < 0.12 || l2 < 0.12) && lv >= 2) lv -= 1; return MEAT[Math.max(0, Math.min(4, lv))]; })); // twine knots and fat beads riding the surface: drawn only on the half facing us g.part("glaze", () => { const onSurface = (x, ph, cb) => { const psi = ph + th, c = Math.cos(psi); const wrapped = Math.atan2(Math.sin(psi), c); if (c < 0.15) return; const r = hamR(x); cb(Math.round(AXIS - 0.5 + Math.sin(wrapped) * r), wrapped); }; for (const [i, x] of TWINE.entries()) onSurface(x, i * 2.1, (y) => { g.pixel(x, y, "n5"); g.pixel(x + 1, y, "n4"); }); const BEADS = [[13, 0.3], [18, 2.2], [24, 4.1], [29, 1.2], [16, 5.2], [23, 3.0]]; for (const [x, ph] of BEADS) onSurface(x, ph, (y, a) => { const inHi = a < -0.25 && a > -1.1; // passing through the highlight: it flashes g.pixel(x, y, inHi ? "g3" : "g2"); if (inHi) g.pixel(x - 1, y, "g2"); }); });}function bone(g) { g.part("bone", () => { g.rect({ x: [33, 36], y: [19, 21] }, "n4", { fill: "n4" }); g.line([33, 19], [36, 19], "n5"); g.line([33, 21], [36, 21], "n3"); g.ellipse({ x: [36, 39], y: [17, 22] }, "n4", { fill: "n4" }); // the knuckle g.pixel(37, 18, "n5"); g.pixel(38, 21, "n3"); g.pixel(39, 20, "n3"); });}// ---- drips, sparks --------------------------------------------------------------------------------------const DRIPS = [[14, 0.0], [21, 0.5], [27, 0.25], [18, 0.75]];function drips(g, t) { g.detached("drips", () => { for (const [x, ph] of DRIPS) { const p = (t * 2 + ph) % 1; const y0 = Math.round(AXIS + hamR(x)); if (p < 0.3) g.pixel(x, y0 + 1, p < 0.15 ? "g1" : "g2"); // a bead swells on the underside else if (p < 0.6) { const y = y0 + 2 + Math.round(((p - 0.3) / 0.3) ** 2 * 12); g.pixel(x, y, "g2"); g.pixel(x, y - 1, "g1"); } else if (p < 0.72) { // it hits the fire: a sizzle pop const y = 35; g.pixel(x, y, "fl3"); g.pixel(x - 1, y - 1, "fl2"); g.pixel(x + 1, y - 1, "fl2"); g.pixel(x, y - 2, "fl1"); } } });}function embers(g, t, full) { g.detached("embers", () => { for (let i = 0; i < 4; i++) { const p = (t * 2 + i / 4) % 1; const x = CX + Math.round((i - 1.5) * 5 + Math.sin(TAU * (p + hash(i, 1))) * 2); const y = 30 - Math.round(p * 18); if (p < 0.8 && (!full || y < 12 || y > 29 || x < HAM.x0 - 1 || x > 40)) g.pixel(x, y, p < 0.3 ? "fl3" : p < 0.55 ? "fl2" : "fl1"); } });}// ---- sprite ---------------------------------------------------------------------------------------------const Roast = sprite(pal, (g, t, state) => { const full = state !== "empty"; const th = full ? TAU * t : 0; contactShadow(g, CX, GROUND - 1, 21, 3); g.outlined("ink", () => { pitBed(g, true, t); for (const s of STONES) if (!s.front) g.part("stones", () => stone(g, s)); }); g.outlined("ink", () => logs(g, t)); g.outlined("ink", () => { stand(g, 6, "standL"); stand(g, 40, "standR"); }); let boost = 0; if (full) for (const [, ph] of DRIPS) { const p = (t * 2 + ph) % 1; if (p >= 0.6 && p < 0.8) boost = 0.12; } for (const f of FLAMES) drawFlame(g, f, t, f.name === "flameC" ? boost : 0); g.outlined("ink", () => { spit(g); crank(g, th); }); if (full) g.outlined("ink", () => { ham(g, th); bone(g); }); g.outlined("ink", () => { for (const s of STONES) if (s.front) g.part("stones", () => stone(g, s)); }); if (full) drips(g, t); embers(g, t, full);});submit(Roast, { size: [W, H], origin: "top-left", kind: "icon", scale: 1, animate: { fps: 10, duration: 1.6, loop: true }, anchor: { x: CX, y: GROUND }, args: ["idle"], variants: [ { name: "idle" }, { name: "empty", args: ["empty"], animate: { fps: 10, duration: 1.2, 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.