Tessarune
Tavern & Market · street_lantern.js

Street lantern

32×64 · 242 lines of JavaScript

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”.

Street lantern — Rendered output6×
Rendered by this program, variant “lit” · 32×64 · 12 frames
Tavern & MarketGet 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.js · 1–8
// 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 flame

The last lines. submit declares the canvas and lists the variants the renderer exports.

street_lantern.js · 230–242
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.js · 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.