Ale barrel
A 32×32 prop in top-down 3/4: an ale cask lying on a stillage, turned 25 degrees so its right head, which carries a brass tap, faces the viewer a little. The cask is ray-marched per pixel from a small solid model: 16 bulged staves, a recessed head of four boards, four iron hoops, and two rails on four legs. “idle” (12 frames, loop) swells a drop at the spout that falls and splashes on the floor; “pour” (8 frames, once) tips the red-knobbed lever, arcs an amber stream down to the floor and returns the lever.
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.
// Ale barrel on a stillage — 32x32, top-down 3/4, key light top-left.// The cask lies on its side, turned 25 degrees so its right head faces the viewer a little and carries the tap.// It is ray-marched per pixel from a small solid model (camera elevation: ellipse ratio 0.35, as in Vol.1/2):// a bulged cask of 16 staves, a recessed head of four boards inside the chime, four iron hoops, and a stillage of two// rails on four legs with a low stretcher. The brass tap is drawn on top in side profile: a flange on the head, a// short pipe, a valve with a lever and a red-lacquered knob (upright = shut, tipped forward = open), a spout pointing down.// Ale uses the brass ramp (g1 body, g2 core, g3 glint, g0 shade); the splash has one n5 fleck of foam.// Variants:// idle loop 12f — a drop swells at the spout, lets go, falls and splashes on the floor// pour one-shot 8f — the lever tips, an amber stream arcs out and down to the floor, the lever returns;// ends on idle f0The last lines. submit declares the canvas and lists the variants the renderer exports.
submit(Barrel, { size: [W, H], origin: "top-left", kind: "icon", scale: 1, anchor: { x: 16, y: GROUND }, variants: [ { name: "idle", args: ["idle"], animate: { fps: 10, duration: N_IDLE / 10, loop: true } }, { name: "pour", args: ["pour"], animate: { fps: 10, duration: N_POUR / 10, loop: false } }, ],});Show the whole program (259 lines)
// Ale barrel on a stillage — 32x32, top-down 3/4, key light top-left.// The cask lies on its side, turned 25 degrees so its right head faces the viewer a little and carries the tap.// It is ray-marched per pixel from a small solid model (camera elevation: ellipse ratio 0.35, as in Vol.1/2):// a bulged cask of 16 staves, a recessed head of four boards inside the chime, four iron hoops, and a stillage of two// rails on four legs with a low stretcher. The brass tap is drawn on top in side profile: a flange on the head, a// short pipe, a valve with a lever and a red-lacquered knob (upright = shut, tipped forward = open), a spout pointing down.// Ale uses the brass ramp (g1 body, g2 core, g3 glint, g0 shade); the splash has one n5 fleck of foam.// Variants:// idle loop 12f — a drop swells at the spout, lets go, falls and splashes on the floor// pour one-shot 8f — the lever tips, an amber stream arcs out and down to the floor, the lever returns;// ends on idle f0const W = 32, H = 32, CX = 15, GROUND = 29, REFY = 25.6;const SE = 0.35, CE = Math.sqrt(1 - SE * SE);const LIGHT = (() => { const v = [-0.55, 0.5, 0.68], m = Math.hypot(...v); return v.map((c) => c / m); })();const toView = (n) => [n[0], n[1] * CE - n[2] * SE, n[1] * SE + n[2] * CE];const dotL = (n) => { const m = Math.hypot(...n) || 1, v = toView(n.map((c) => c / m)); return v[0] * LIGHT[0] + v[1] * LIGHT[1] + v[2] * LIGHT[2]; };const WOOD = ["w0", "w1", "w2", "w3", "w4"], IRON = ["n0", "n1", "n2", "n3", "n4"];const N_IDLE = 12, N_POUR = 8;// ---- model (barrel frame: a = along the axis toward the tapped head, c = across toward the camera, h = up) -------const YAW = (25 * Math.PI) / 180, CA = Math.cos(YAW), SA = Math.sin(YAW);const toLocal = (x, z) => [x * CA + z * SA, -x * SA + z * CA];const toWorld = (a, c) => [a * CA - c * SA, a * SA + c * CA];const HC = 14.2, HALF = 9.4, RB = 8.6, RE = 7.3, HEADA = 8.7, CHW = 0.9, HOOPT = 0.35, RI = RE - CHW;const HOOPS = [[6.9, 8.6], [2.5, 4.1]];const STAVES = 16;const R = (a) => RE + (RB - RE) * (1 - (a / HALF) ** 2);const dR = (a) => (-(RB - RE) * 2 * a) / (HALF * HALF);// stillageconst RAIL_C = 5.0, RAIL_W = 0.85, RAIL_H = [5.0, 7.3], RAIL_A = 9.9;const LEG_A = 8.3, LEG_W = 0.85;const STR_H = [1.6, 2.7];function box(a, c, h, a0, a1, c0, c1, h0, h1) { if (a < a0 || a > a1 || c < c0 || c > c1 || h < h0 || h > h1) return null; // nearest face -> normal in barrel frame const faces = [[a - a0, [-1, 0, 0]], [a1 - a, [1, 0, 0]], [c - c0, [0, 0, -1]], [c1 - c, [0, 0, 1]], [h1 - h, [0, 1, 0]], [h - h0, [0, -1, 0]]]; let best = faces[0]; for (const f of faces) if (f[0] < best[0]) best = f; return best[1];}function solid(x, h, z) { const [a, c] = toLocal(x, z), aa = Math.abs(a); // barrel if (aa <= HALF) { const rho = Math.hypot(c, h - HC), r = R(a); const hoop = HOOPS.findIndex(([p, q]) => aa >= p && aa <= q); const ro = hoop >= 0 ? r + HOOPT : r; if (rho <= ro && !(aa > HEADA && rho < RI)) { let s = hoop >= 0 ? "hoop" : "stave", best = ro - rho; if (rho >= RI && HALF - aa < best) { s = "chime"; best = HALF - aa; } if (aa <= HEADA && rho < RI && HEADA - aa < best) { s = "head"; best = HEADA - aa; } if (aa > HEADA && rho - RI < best) { s = "chimein"; best = rho - RI; } const nl = s === "stave" || s === "hoop" ? [-dR(a), (h - HC) / rho, c / rho] : s === "chimein" ? [0, -(h - HC) / rho, -c / rho] : [Math.sign(a), 0, 0]; const [nx, nz] = toWorld(nl[0], nl[2]); return { o: "barrel", s, a, c, h, rho, hoop, n: [nx, nl[1], nz] }; } } // stillage: rails, legs, front stretcher const parts = []; for (const sc of [1, -1]) { parts.push(["rail", sc, box(a, c, h, -RAIL_A, RAIL_A, sc * RAIL_C - RAIL_W, sc * RAIL_C + RAIL_W, RAIL_H[0], RAIL_H[1])]); for (const sa of [1, -1]) parts.push(["leg", sc, box(a, c, h, sa * LEG_A - LEG_W, sa * LEG_A + LEG_W, sc * RAIL_C - LEG_W, sc * RAIL_C + LEG_W, 0, RAIL_H[0])]); } parts.push(["stretcher", 1, box(a, c, h, -LEG_A, LEG_A, RAIL_C - 0.6, RAIL_C + 0.4, STR_H[0], STR_H[1])]); for (const [kind, side, nl] of parts) { if (!nl) continue; const [nx, nz] = toWorld(nl[0], nl[2]); return { o: "stand", s: kind, side, a, c, h, face: nl, n: [nx, nl[1], nz] }; } return null;}function cast() { const buf = new Array(W * H).fill(null); for (let py = 0; py < H; py++) for (let px = 0; px < W; px++) { const X = px + 0.5 - CX, Y = py + 0.5 - REFY; for (let d = 22; d >= -22; d -= 0.12) { const h = -Y * CE + d * SE, z = Y * SE + d * CE; const hit = solid(X, h, z); if (hit) { hit.d = dotL(hit.n); buf[py * W + px] = hit; break; } } } return tone(buf);}const lvWood = (d) => (d > 0.9 ? 4 : d > 0.56 ? 3 : d > 0.2 ? 2 : d > -0.15 ? 1 : 0);const staveOf = (b) => (b && b.o === "barrel" && (b.s === "stave" || b.s === "hoop") ? Math.floor(((Math.atan2(b.h - HC, b.c) + Math.PI) / TAU) * STAVES + 0.5) % STAVES : -1);const boardOf = (b) => (b && b.o === "barrel" && b.s === "head" ? Math.min(3, Math.max(0, Math.floor((b.h - HC + RI) / (RI / 2)))) : -1);function tone(buf) { const out = new Array(W * H).fill(null); for (let i = 0; i < W * H; i++) { const b = buf[i]; if (!b) continue; const px = i % W, py = (i - px) / W; const up = py > 0 ? buf[i - W] : null, dn = py < H - 1 ? buf[i + W] : null; const lf = px > 0 ? buf[i - 1] : null, rt = px < W - 1 ? buf[i + 1] : null; if (b.o === "stand") { let lv = b.face[1] > 0 ? 3 : b.face[2] > 0 ? 2 : b.face[0] > 0 ? 1 : 1; // top / front / end if (b.side < 0) lv = Math.max(0, lv - 1); // back rail & legs in shadow if (b.s === "leg" && b.face[2] > 0 && (!lf || lf.o !== "stand")) lv = Math.min(3, lv + 1); // lit left edge if (up && up.o === "barrel" && b.face[1] <= 0) lv = Math.max(0, Math.min(lv, 1)); // under the cask out[i] = { key: WOOD[lv], part: "stillage" }; continue; } if (b.s === "hoop") { let lv = b.d > 0.8 ? 3 : b.d > 0.4 ? 2 : b.d > 0.0 ? 1 : 0; const same = (o) => o && o.o === "barrel" && o.s === "hoop" && o.hoop === b.hoop && Math.sign(o.a) === Math.sign(b.a); if (!same(lf) && lv > 0) lv = Math.min(4, lv + 1); // the hoop's left edge catches the light else if (!same(rt) && lv > 0) lv -= 1; const rivet = Math.abs(Math.abs(b.a) - (HOOPS[b.hoop][0] + HOOPS[b.hoop][1]) / 2) < 0.5 && Math.abs(((Math.atan2(b.h - HC, b.c) / TAU) * 5 + 20.35) % 1 - 0.5) < 0.06; out[i] = { key: rivet ? "n4" : IRON[lv], part: "hoops" }; continue; } if (b.s === "stave") { let lv = lvWood(b.d); const k = staveOf(b), ku = staveOf(up); // stave seams: a darker line where the next stave begins (above, on screen) // (only on the side facing the camera: on the foreshortened top the seams would crowd into stripes) if (ku >= 0 && ku !== k && Math.abs(b.h - HC) < 5.2) lv = Math.max(0, Math.min(lv, 3) - 1); out[i] = { key: WOOD[lv], part: "staves" }; continue; } if (b.s === "chime" || b.s === "chimein") { // end grain of the staves: pale, lit on the upper side const lv = b.h - HC > 3 ? 4 : b.h - HC > -3 ? 3 : 2; out[i] = { key: WOOD[lv], part: "staves" }; continue; } // head: faces right-front, away from the key light. Only ~4px wide on screen, so: flat boards, the chime's // shadow down its left rim, one seam across the middle let lv = b.h - HC > 3.5 ? 3 : 2; const rim = (o) => o && o.o === "barrel" && o.s !== "head"; if (rim(lf)) lv = 1; // the chime shades the head's left rim else if (boardOf(up) >= 0 && boardOf(up) !== boardOf(b) && boardOf(b) === 2) lv = 1; // one board seam out[i] = { key: WOOD[lv], part: "head" }; } return out;}// ---- compositing ---------------------------------------------------------------------------------------------------function drawBody(g, buf) { const parts = []; for (const b of buf) if (b && !parts.includes(b.part)) parts.push(b.part); g.outlined("ink", () => { for (const p of parts) g.part(p, () => g.field({ x: [0, W - 1], y: [0, H - 1] }, (x, y) => { const b = buf[y * W + x]; return b && b.part === p ? b.key : null; })); });}// tap: attached to the head a little below its centreconst HEAD_PT = (() => { const [x, z] = toWorld(HEADA, 0); return [x, HC - 2.2, z]; })();const TX = Math.round(CX + HEAD_PT[0]) - 1, TY = Math.round(REFY - HEAD_PT[1] * CE + HEAD_PT[2] * SE);const SX = TX + 3; // left column of the 2px spoutconst SPOUT_Y = TY + 3; // nozzle rowconst FLOOR_Y = Math.round(REFY + (HEAD_PT[2] + 1.5) * SE) + 1; // floor under the spoutfunction tap(g, lever) { // lever: 0 upright (shut), 1 tipped 45 degrees, 2 tipped forward level (open). // Built as a pixel map and ringed with ink by hand: outlined() only inks empty pixels, and most of the tap sits // over the cask. const m = new Map(); const P = (x, y, k) => m.set((TY + y) * W + TX + x, k); P(0, -1, "g1"); P(0, 0, "g1"); P(0, 1, "g0"); P(0, 2, "g0"); // flange on the head P(1, 0, "g2"); P(2, 0, "g2"); P(1, 1, "g1"); P(2, 1, "g1"); // pipe P(3, -1, "g3"); P(4, -1, "g2"); P(3, 0, "g2"); P(4, 0, "g1"); P(3, 1, "g1"); P(4, 1, "g0"); // valve P(3, 2, "g2"); P(4, 2, "g0"); P(3, 3, "g1"); P(4, 3, "g0"); // spout const knob = (x, y) => { P(x, y, "r3"); P(x + 1, y, "r2"); P(x, y + 1, "r2"); P(x + 1, y + 1, "r1"); }; // red-lacquered knob if (lever === 0) { P(3, -2, "g2"); P(3, -3, "g1"); knob(3, -5); } else if (lever === 1) { P(4, -2, "g2"); knob(5, -4); } else { P(5, -1, "g2"); knob(6, -2); } 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) && !(nx < TX)) ink.add(k); // no ink between the flange and the head } } g.outlined("ink", () => g.part("tap", () => g.field({ x: [TX - 1, W - 1], y: [TY - 7, TY + 5] }, (x, y) => { const i = y * W + x; return m.has(i) ? m.get(i) : ink.has(i) ? "ink" : null; })));}function splash(g, k) { // k: 0 hit (a crown with a fleck of foam), 1 droplets thrown wide, 2 last fleck g.detached("splash", () => { const x = SX, y = FLOOR_Y; if (k === 0) { g.pixel(x - 1, y, "g2"); g.pixel(x + 2, y, "g2"); g.pixel(x, y, "n5"); g.pixel(x + 1, y, "g3"); g.pixel(x - 1, y - 1, "g1"); g.pixel(x + 2, y - 1, "g1"); } else if (k === 1) { g.pixel(x - 2, y - 1, "g2"); g.pixel(x + 3, y - 1, "g2"); g.pixel(x, y, "g1"); g.pixel(x + 1, y, "g2"); } else { g.pixel(x - 2, y, "g1"); g.pixel(x + 3, y, "g1"); } });}function drop(g, x, y, size) { // size 0: bead at the nozzle, 1: swollen bead, 2: falling drop (teardrop) g.detached("drip", () => { if (size === 0) { g.pixel(x, y, "g2"); } else if (size === 1) { g.pixel(x, y, "g1"); g.pixel(x, y + 1, "g3"); g.pixel(x + 1, y + 1, "g1"); } else { g.pixel(x, y, "g1"); g.pixel(x, y + 1, "g3"); g.pixel(x, y + 2, "g2"); } });}function stream(g, y0, y1, phase, arc) { // a 2px amber rope under the spout; the first rows bow out forward (to the right) when arc is set; highlights // drift down with phase so the flow reads g.detached("ale", () => { for (let y = y0; y <= y1; y++) { const k = y - y0, dx = arc && k >= 1 && k <= 3 ? 1 : 0; const band = ((y - phase) % 3 + 3) % 3; g.pixel(SX + dx, y, band === 0 ? "g3" : "g2"); g.pixel(SX + 1 + dx, y, band === 1 ? "g2" : "g1"); } });}const CACHE = {};const body = () => CACHE.body || (CACHE.body = cast());const Barrel = sprite(pal, (g, t, mode) => { contactShadow(g, CX + 1, GROUND - 2, 12, 2); drawBody(g, body()); if (mode === "idle") { const f = Math.round(t * N_IDLE) % N_IDLE; tap(g, 0); const ny = SPOUT_Y + 1; if (f >= 3 && f <= 4) drop(g, SX, ny, 0); else if (f >= 5 && f <= 6) drop(g, SX, ny, 1); else if (f === 7) drop(g, SX, ny + 2, 2); else if (f === 8) drop(g, SX, ny + 5, 2); else if (f === 9) splash(g, 0); else if (f === 10) splash(g, 2); return; } // pour const f = Math.min(N_POUR - 1, Math.round(t * N_POUR)); tap(g, [0, 1, 2, 2, 2, 1, 0, 0][f]); const y0 = SPOUT_Y + 1; if (f === 2) stream(g, y0, y0 + 3, 0, true); else if (f === 3) { stream(g, y0, FLOOR_Y - 1, 1, true); splash(g, 0); } else if (f === 4) { stream(g, y0, FLOOR_Y - 1, 2, true); splash(g, 1); } else if (f === 5) { stream(g, y0 + 3, FLOOR_Y - 1, 0, false); splash(g, 0); drop(g, SX, y0, 0); } else if (f === 6) { splash(g, 2); }});submit(Barrel, { size: [W, H], origin: "top-left", kind: "icon", scale: 1, anchor: { x: 16, y: GROUND }, variants: [ { name: "idle", args: ["idle"], animate: { fps: 10, duration: N_IDLE / 10, loop: true } }, { name: "pour", args: ["pour"], animate: { fps: 10, duration: N_POUR / 10, loop: false } }, ],});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.