Street lantern
A tall 32×64 floor prop in top-down 3/4: a cast-iron lamp post with a stepped plinth, a brass collar and scroll brackets, carrying a six-sided glass lantern under a pyramid cap with a brass finial, a gas flame on a brass burner inside. “lit” (12 frames, loop) flickers the flame, breathes the panes with it, pulses a dithered halo and circles two moths; “unlit” (16 frames, loop) slides a faint glint across the dark glass once per loop; “light” (8 frames, once) sparks at the burner and the flame catches and flares. The flame colours swap as one family for the blue “lit-soul” and “light-soul”.
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.
// Street lantern — 32x64 floor prop (tall), top-down 3/4, key light top-left.// A cast-iron lamp post: stepped plinth, turned foot, brass collar, scroll brackets, and a six-sided glass lantern// (three panes face the viewer) under a pyramid cap with a brass finial. A gas flame burns on a brass burner.// Variants (flame colours in fl0..3, so swap("fl","soul") makes a blue magical lamp):// lit 12f loop the flame flickers, the panes breathe with it, a dithered halo pulses, two moths circle// unlit 16f loop dark glass; once per loop a faint glint slides across the panes// light 8f once a spark at the burner, the flame catches and flares, the glass fills; ends on lit frame 0// lit-soul / light-soul the same with a blue soul flameThe last lines. submit declares the canvas and lists the variants the renderer exports.
submit(Lantern, { size: [W, H], origin: "top-left", kind: "icon", scale: 1, animate: { fps: 10, duration: 1.2, loop: true }, anchor: { x: CX, y: GROUND }, args: ["lit"], variants: [ { name: "lit" }, { name: "unlit", args: ["unlit"], animate: { fps: 10, duration: 1.6, loop: true } }, { name: "light", args: ["light"], animate: { fps: 10, duration: 0.8, loop: false } }, { name: "lit-soul", repalette: swap("fl", "soul") }, { name: "light-soul", args: ["light"], animate: { fps: 10, duration: 0.8, loop: false }, repalette: swap("fl", "soul") }, ],});Show the whole program (242 lines)
// Street lantern — 32x64 floor prop (tall), top-down 3/4, key light top-left.// A cast-iron lamp post: stepped plinth, turned foot, brass collar, scroll brackets, and a six-sided glass lantern// (three panes face the viewer) under a pyramid cap with a brass finial. A gas flame burns on a brass burner.// Variants (flame colours in fl0..3, so swap("fl","soul") makes a blue magical lamp):// lit 12f loop the flame flickers, the panes breathe with it, a dithered halo pulses, two moths circle// unlit 16f loop dark glass; once per loop a faint glint slides across the panes// light 8f once a spark at the burner, the flame catches and flares, the glass fills; ends on lit frame 0// lit-soul / light-soul the same with a blue soul flameconst W = 32, H = 64, CX = 16, GROUND = 61;const GT = 15, GB = 24; // glass rowsconst glassHW = (y) => 5.6 - (y - GT) * (1.4 / (GB - GT)); // panes flare wider at the topconst FC = { x: 16, y: 20 }; // light centre (flame)// ---- iron ------------------------------------------------------------------------------------------------// cylinder/prism shading across a half-width: left lit, right shadedconst ironAt = (u) => (u < -0.45 ? "n3" : u < 0.3 ? "n2" : "n1");function taperRows(g, rows) { // rows: [y, hw, style] — turned iron parts drawn row by row, shaded across for (const [y, hw, top] of rows) { for (let x = CX - hw; x <= CX + hw; x++) { const u = hw ? (x - CX) / hw : 0; let k = ironAt(u); if (top) k = u < -0.3 ? "n4" : u < 0.5 ? "n3" : "n2"; g.pixel(x, y, k); } }}function plinth(g) { g.part("plinth", () => { taperRows(g, [[58, 6, true], [59, 6], [60, 6], [61, 6], [55, 4, true], [56, 4], [57, 4]]); g.line([CX - 6, 61], [CX + 6, 61], "n1"); g.pixel(CX - 6, 61, "n2"); g.line([CX - 4, 57], [CX + 4, 57], "n1"); g.pixel(CX - 5, 60, "n4"); g.pixel(CX + 3, 59, "n0"); // a lit bolt, a pit g.pixel(CX - 5, 59, "n2"); }); g.part("foot", () => { taperRows(g, [[50, 1], [51, 2], [52, 3], [53, 3], [54, 2]]); g.pixel(CX - 2, 52, "n4"); g.pixel(CX - 3, 52, "n3"); });}function post(g) { g.part("post", () => { for (let y = 29; y <= 49; y++) { g.pixel(CX - 1, y, "n3"); g.pixel(CX, y, "n2"); g.pixel(CX + 1, y, "n1"); } for (const y of [31, 32, 44, 45, 46]) g.pixel(CX - 1, y, "n4"); // specular streaks }); g.part("collar", () => { for (const [y, hw] of [[39, 2], [40, 2], [41, 1]]) for (let x = CX - hw; x <= CX + hw; x++) { const u = (x - CX) / hw; g.pixel(x, y, y === 39 ? (u < -0.4 ? "g3" : u < 0.4 ? "g2" : "g1") : u < -0.4 ? "g2" : u < 0.4 ? "g1" : "g0"); } });}function brackets(g) { g.part("brackets", () => { // a cradle cone under the lantern, then two scrolls curling down to the post taperRows(g, [[26, 4], [27, 3], [28, 2]]); for (const s of [-1, 1]) { const px = (dx, dy, k) => g.pixel(CX + s * dx, dy, k); const hi = s < 0 ? "n3" : "n2", lo = s < 0 ? "n2" : "n1"; px(4, 27, hi); px(5, 28, hi); px(5, 29, lo); px(4, 30, lo); px(3, 30, lo); px(3, 29, s < 0 ? "n4" : "n2"); px(2, 31, lo); px(2, 32, lo); } });}function cap(g, litState) { g.part("cap", () => { // pyramid roof: three faces (left lit, front, right shaded) over a wide brim for (let y = 6; y <= 12; y++) { const hw = y === 12 ? 7 : y - 5; for (let x = CX - hw; x <= CX + hw; x++) { const u = (x - CX) / (hw + 0.5); let k = u < -0.38 ? "n4" : u <= 0.38 ? "n2" : "n1"; if (y === 12) k = u < -0.5 ? "n3" : u < 0.5 ? "n2" : "n1"; // brim edge if (y === 6) k = "n3"; g.pixel(x, y, k); } } g.pixel(CX - 6, 12, "n4"); g.line([CX - 6, 13], [CX + 6, 13], "n0"); // brim underside shadow // vent holes on the front face g.pixel(CX, 10, "n0"); g.pixel(CX, 8, "n1"); }); g.part("finial", () => { g.pixel(CX, 1, "g1"); g.pixel(CX - 1, 2, "g2"); g.pixel(CX, 2, "g3"); g.pixel(CX + 1, 2, "g1"); g.pixel(CX - 1, 3, "g1"); g.pixel(CX, 3, "g2"); g.pixel(CX + 1, 3, "g0"); g.pixel(CX, 4, "g1"); g.pixel(CX, 5, "g0"); });}// ---- glass -----------------------------------------------------------------------------------------------// light: { lvl 0..1 (glass fill), gr glow radius, glint (unlit sweep position or null) }function frameAndGlass(g, L) { g.part("lantern", () => g.field({ x: [CX - 7, CX + 7], y: [14, 25] }, (x, y) => { if (y === 14 || y === 25) { // top and bottom frame bands const hw = y === 14 ? 6 : 5, u = (x - CX) / hw; if (Math.abs(x - CX) > hw) return null; return y === 14 ? (u < -0.5 ? "n3" : u < 0.5 ? "n2" : "n1") : (u < -0.5 ? "n2" : "n1"); } const hw = glassHW(y), u = (x - CX) / hw; if (Math.abs(u) > 1.02) return null; const au = Math.abs(u); if (au > 0.84) return u < 0 ? "n3" : "n1"; // outer frame posts if (au > 0.33 && au < 0.52) return L.lvl > 0.5 ? "n0" : u < 0 ? "n2" : "n1"; // mullions const front = au <= 0.33, left = u < 0; if (L.lvl <= 0) { // dark glass: the left pane catches sky, the right pane is in shade; a faint fixed reflection if (L.glint != null) { const s = x + (y - GT) * 0.6 - L.glint; if (Math.abs(s) < 0.8) return front ? "n4" : "n3"; if (Math.abs(s - 2) < 0.6) return front ? "n3" : "n2"; } if (front) return Math.round(x - CX + (y - GT) * 0.5) === -1 && y < GB - 3 ? "n2" : y < GT + 2 ? "n1" : "b0"; return left ? "n2" : "n0"; } const d = Math.hypot(x - FC.x, (y - FC.y) * 0.8); if (L.lvl < 1) { // glass filling while it catches if (d < L.gr) return front ? "fl1" : "fl0"; if (d < L.gr + 2) return front ? "fl0" : (left ? "n2" : "n0"); return front ? "n1" : left ? "n2" : "n0"; } if (front) return d < L.gr ? "fl2" : "fl1"; return au < 0.66 && d < L.gr + 2 ? "fl1" : "fl0"; })); g.part("burner", () => { g.line([CX - 1, 23], [CX + 1, 23], L.lvl >= 1 ? "g0" : "g1"); g.line([CX - 2, 24], [CX + 2, 24], L.lvl >= 1 ? "g0" : "g2"); g.pixel(CX - 2, 24, L.lvl >= 1 ? "g1" : "g3"); g.pixel(CX, 22, "n0"); // wick });}function flameShape(g, t, s) { // a small gas flame over the burner: a teardrop that sways and stretches (periodic in t) if (s <= 0) return; const hgt = s * (5 + 0.9 * Math.sin(TAU * (2 * t + 0.1)) + 0.5 * Math.sin(TAU * (3 * t + 0.6))); g.part("flame", () => g.field({ x: [CX - 3, CX + 3], y: [12, 22] }, (x, y) => { const k = (21.5 - y) / hgt; // 0 at the base, 1 at the tip if (k < 0 || k > 1) return null; const ax = CX + (0.9 * Math.sin(TAU * (t + 0.2)) + 0.5 * Math.sin(TAU * (2 * t))) * k * k; const hw = Math.min(1, s) * (k < 0.3 ? 1.2 + k : 1.5 * Math.pow((1 - k) / 0.7, 0.8)); const dx = Math.abs(x - ax); if (dx > hw) return null; if (k < 0.18) return "fl1"; // blue-ish root in real gas; warm here return dx < hw * 0.5 && k < 0.7 ? "fl3" : "fl2"; }));}const BAYER = [0, 8, 2, 10, 12, 4, 14, 6, 3, 11, 1, 9, 15, 7, 13, 5];function halo(g, t, amt) { if (amt <= 0) return; const R = (12.5 + 0.8 * Math.sin(TAU * 2 * t) + 0.4 * Math.sin(TAU * (3 * t + 0.4))) * amt; g.detached("halo", () => g.field({ x: [0, W - 1], y: [4, 36] }, (x, y) => { const r = Math.hypot(x - FC.x, (y - FC.y) * 0.85); if (r > R) return null; const dens = 0.68 * Math.pow(1 - r / R, 0.85); if (BAYER[(y & 3) * 4 + (x & 3)] / 16 >= dens) return null; return r < R * 0.62 ? "fl2" : "fl1"; }));}function moths(g, t) { const M = [ { rx: 10, ry: 5, cy: 17, sp: 1, ph: 0.1 }, { rx: 9, ry: 9, cy: 21, sp: -2, ph: 0.55 }, ]; M.forEach((m, i) => { const a = TAU * (m.sp * t + m.ph); const x = Math.round(FC.x + Math.cos(a) * m.rx + Math.sin(TAU * (3 * t + i * 0.4)) * 0.8); const y = Math.round(m.cy + Math.sin(a) * m.ry * 0.6 + Math.sin(TAU * (4 * t + i * 0.3)) * 1.2); const near = Math.sin(a) > 0; const flap = (Math.round(t * 12) + i) % 2 === 0; g.depth(near ? -3 : 3, () => g.detached(`moth${i}`, () => { g.pixel(x, y, "n4"); if (flap) g.pixel(x, y - 1, "n5"); else g.pixel(x + (m.sp > 0 ? -1 : 1), y, "n3"); })); });}function spark(g, f) { g.detached("spark", () => { if (f === 1) { g.pixel(CX, 21, "fl3"); for (const [ox, oy] of [[1, 0], [-1, 0], [0, -1]]) g.pixel(CX + ox, 21 + oy, "fl2"); g.pixel(CX + 2, 19, "fl1"); g.pixel(CX - 2, 18, "fl1"); } else if (f === 2) { g.pixel(CX + 2, 17, "fl2"); g.pixel(CX - 3, 16, "fl1"); } });}// ---- states ------------------------------------------------------------------------------------------------const LIGHT = 8;// per light frame: flame scale, glass level, glow radius, halo amountconst LS = [0, 0, 0.35, 0.6, 1.0, 1.25, 1.08, 1];const LL = [0, 0, 0.5, 0.7, 1, 1, 1, 1];const LG = [0, 0, 1.5, 2.5, 3.2, 4, 3.6, 0];const LH = [0, 0, 0, 0, 0.55, 1.1, 1.02, 1];const glowR = (t) => 3 + 0.5 * Math.sin(TAU * (2 * t + 0.1)) + 0.3 * Math.sin(TAU * (3 * t + 0.6));const Lantern = sprite(pal, (g, t, state) => { t = ((t % 1) + 1) % 1; // hosts probe t = 1 and t < 0 too let L, fs = 0, ft = t, hal = 0, f = -1, withMoths = false; if (state === "lit") { L = { lvl: 1, gr: glowR(t) }; fs = 1; hal = 1; withMoths = true; } else if (state === "unlit") { const p = (t - 0.4) / 0.35; // the glint crosses mid-loop L = { lvl: 0, glint: p >= 0 && p <= 1 ? CX - 9 + p * 18 : null }; } else { f = Math.min(LIGHT - 1, Math.round(t * LIGHT)); ft = ((f - (LIGHT - 1)) / 12 + 1) % 1; // on the lit loop's 10 fps clock fs = LS[f]; hal = LH[f]; L = f >= LIGHT - 1 ? { lvl: 1, gr: glowR(ft) } : f >= 4 ? { lvl: 1, gr: LG[f] } : { lvl: LL[f], gr: LG[f], glint: null }; if (f === 0) L = { lvl: 0, glint: null }; withMoths = f >= LIGHT - 1; } contactShadow(g, CX, GROUND, 7, 2); g.outlined("ink", () => { cap(g); frameAndGlass(g, L); brackets(g); post(g); plinth(g); }); flameShape(g, ft, fs); if (f >= 1 && f <= 2) spark(g, f); g.depth(2, () => halo(g, ft, hal)); if (withMoths) moths(g, ft);});submit(Lantern, { size: [W, H], origin: "top-left", kind: "icon", scale: 1, animate: { fps: 10, duration: 1.2, loop: true }, anchor: { x: CX, y: GROUND }, args: ["lit"], variants: [ { name: "lit" }, { name: "unlit", args: ["unlit"], animate: { fps: 10, duration: 1.6, loop: true } }, { name: "light", args: ["light"], animate: { fps: 10, duration: 0.8, loop: false } }, { name: "lit-soul", repalette: swap("fl", "soul") }, { name: "light-soul", args: ["light"], animate: { fps: 10, duration: 0.8, loop: false }, repalette: swap("fl", "soul") }, ],});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.