Soul lantern
A 32×64 tall floor prop: a cemetery path lamp with a wrought-iron post on a mossy stone footing, a twisted section, a spear finial and a shepherd’s-crook arm. A caged lantern hangs from the hook on two chain links with a candle stub burning inside; the panes glow and the cage throws bars of light on the ground in a dithered pool. All three variants are 12-frame loops, and the flame is recoloured by swapping one colour ramp, with no new colours: “soul” (blue soulfire, motes drifting up), “fire” (an ordinary warm flame, an ember now and then) and “witch” (violet witchfire).
6×
Get the animated sprites: Graveyard & Chapel on itch.ioVol.4 · 12 props · 33 animations
The header. The comment at the top of the file is the brief: size, view, light direction and every variant.
// Soul lantern — 32x64 floor prop (tall), top-down 3/4, key light top-left.// A cemetery path lamp: a wrought-iron post on a mossy stone footing, a twisted section, a spear finial, and a// shepherd's-crook arm with a scroll brace. A caged lantern hangs from the hook on two chain links; a candle stub// burns inside. The glass panes glow and catch a glint of the key light; the cage throws bars of light on the// ground (a detached, dithered pool with the bar shadows radiating from under the lantern).// Variants (12f loops; the flame is the fl0..3 family, recoloured by ramp swap — no new colours):// soul blue soulfire, motes drift up and away// fire ordinary warm candle flame, one ember now and then// witch violet witchfire, motes drift up and awayThe last lines. submit declares the canvas and lists the variants the renderer exports.
submit(SoulLantern, { size: [W, H], origin: "top-left", kind: "icon", scale: 1, animate: { fps: 10, duration: 1.2, loop: true }, anchor: { x: PX, y: GROUND }, args: ["soul"], variants: [ { name: "soul", repalette: swap("fl", "soul") }, { name: "fire", args: ["fire"] }, { name: "witch", args: ["witch"], repalette: swap("fl", "witch") }, ],});Show the whole program (225 lines)
// Soul lantern — 32x64 floor prop (tall), top-down 3/4, key light top-left.// A cemetery path lamp: a wrought-iron post on a mossy stone footing, a twisted section, a spear finial, and a// shepherd's-crook arm with a scroll brace. A caged lantern hangs from the hook on two chain links; a candle stub// burns inside. The glass panes glow and catch a glint of the key light; the cage throws bars of light on the// ground (a detached, dithered pool with the bar shadows radiating from under the lantern).// Variants (12f loops; the flame is the fl0..3 family, recoloured by ramp swap — no new colours):// soul blue soulfire, motes drift up and away// fire ordinary warm candle flame, one ember now and then// witch violet witchfire, motes drift up and awayconst W = 32, H = 64, GROUND = 61;const PX = 10; // post centre column (the post is 3px: PX-1 .. PX+1)const LX = 22; // lantern centre columnconst FC = { x: LX, y: 25 }; // light centre (flame)const IRON = ["n0", "n1", "n2", "n3"];const L = [-0.74, -0.67];// iron bar swept along a path, shaded as a round bar lit from the top-leftfunction bar(g, path, w0, w1) { const a = path[0], b = path[path.length - 1]; const dx = b[0] - a[0], dy = b[1] - a[1], len = Math.hypot(dx, dy) || 1; const nl = (-dy / len) * L[0] + (dx / len) * L[1]; g.sweep(path, { w0, w1 }, (u, v) => { const l = v * nl * 1.4 + 0.15; return IRON[l > 0.5 ? 3 : l > -0.1 ? 2 : l > -0.7 ? 1 : 0]; });}// ---- stone footing, post, crook -----------------------------------------------------------------------------function footing(g) { g.part("footing", () => { // top face (lit, left brighter), then the front face g.rect({ x: [PX - 5, PX + 5], y: [54, 55] }, "n3", { fill: "n3" }); g.line([PX - 5, 54], [PX - 1, 54], "n4"); g.rect({ x: [PX - 5, PX + 5], y: [56, 60] }, "n2", { fill: "n2" }); g.line([PX - 5, 56], [PX + 5, 56], "n1"); // shadow under the top lip g.line([PX + 4, 57], [PX + 4, 60], "n1"); g.line([PX + 5, 56], [PX + 5, 60], "n1"); // shaded right edge g.line([PX - 5, 57], [PX - 5, 60], "n3"); // cracks and a chip, a tuft of moss in the corner g.pixel(PX - 2, 58, "n1"); g.pixel(PX - 1, 59, "n1"); g.pixel(PX - 1, 57, "n1"); g.pixel(PX + 2, 55, "n2"); g.pixel(PX - 5, 60, "e1"); g.pixel(PX - 4, 60, "e0"); g.pixel(PX - 5, 59, "e2"); g.pixel(PX + 5, 54, "n2"); });}function post(g) { g.part("post", () => { for (let y = 8; y <= 53; y++) { g.pixel(PX - 1, y, "n2"); g.pixel(PX, y, "n1"); g.pixel(PX + 1, y, "n0"); } for (let y = 30; y <= 44; y++) { // twisted section: a spiral of lit ridges const k = (y - 30) % 3; g.pixel(PX - 1 + k, y, k === 0 ? "n3" : k === 1 ? "n2" : "n1"); } for (const y of [12, 13, 20, 21, 49, 50]) g.pixel(PX - 1, y, "n3"); // specular streaks }); g.part("collars", () => { for (const y0 of [28, 45, 52]) { for (let x = PX - 2; x <= PX + 2; x++) { const u = (x - PX) / 2; g.pixel(x, y0, u < -0.4 ? "n4" : u < 0.4 ? "n3" : "n2"); g.pixel(x, y0 + 1, u < -0.4 ? "n2" : u < 0.4 ? "n1" : "n0"); } } }); g.part("finial", () => { // spear point with a pair of barbs g.pixel(PX, 1, "n3"); g.pixel(PX - 1, 2, "n3"); g.pixel(PX, 2, "n2"); g.pixel(PX - 1, 3, "n3"); g.pixel(PX, 3, "n2"); g.pixel(PX + 1, 3, "n1"); g.pixel(PX - 2, 4, "n3"); g.pixel(PX - 1, 4, "n2"); g.pixel(PX, 4, "n1"); g.pixel(PX + 1, 4, "n1"); g.pixel(PX + 2, 4, "n0"); g.pixel(PX, 5, "n1"); g.pixel(PX - 1, 6, "n3"); g.pixel(PX, 6, "n2"); g.pixel(PX + 1, 6, "n1"); g.pixel(PX, 7, "n1"); });}function crook(g) { g.part("crook", () => { // the arm leaves the post, runs out to the right and curls down into a hook bar(g, [[PX + 1, 11], [14, 8], [20, 7], [24, 8], [26, 11], [25, 13], [23, 13]], 1.0, 0.6); // scroll brace under the arm, rolling into a small curl against the post bar(g, [[17, 8], [14, 11], [12, 15], [PX + 1, 19]], 0.6, 0.6); g.pixel(13, 17, "n2"); g.pixel(14, 18, "n1"); g.pixel(14, 17, "n0"); // chain: two links from the arm down to the lantern ring g.pixel(LX, 9, "n3"); g.pixel(LX, 10, "n1"); g.pixel(LX - 1, 11, "n2"); g.pixel(LX + 1, 11, "n0"); g.pixel(LX, 12, "n3"); });}// ---- the lantern ------------------------------------------------------------------------------------------const GT = 19, GB = 29; // glass rowsfunction lantern(g, t) { g.part("lanternCap", () => { for (let y = 13; y <= 18; y++) { const hw = y === 18 ? 6 : y - 13; for (let x = LX - hw; x <= LX + hw; x++) { const u = (x - LX) / (hw + 0.5); let k = u < -0.35 ? "n3" : u <= 0.35 ? "n2" : "n1"; if (y === 18) k = u < -0.5 ? "n3" : u < 0.5 ? "n2" : "n1"; if (y === 13) k = "n3"; g.pixel(x, y, k); } } g.pixel(LX - 5, 18, "n4"); g.pixel(LX - 2, 15, "n4"); g.pixel(LX, 16, "n0"); // a vent }); const flick = 0.5 + 0.5 * Math.sin(TAU * (2 * t + 0.1)) * 0.7 + 0.15 * Math.sin(TAU * (3 * t + 0.6)); g.part("lanternCage", () => g.field({ x: [LX - 6, LX + 6], y: [GT, GB + 1] }, (x, y) => { const hw = y === GB + 1 ? 5 : 5 - (y - GT >= 8 ? 0 : 0); const dx = x - LX, ax = Math.abs(dx); if (ax > hw) return null; if (y === GB + 1) return dx < -2 ? "n3" : dx < 3 ? "n2" : "n1"; // bottom band if (ax === 5) return dx < 0 ? "n2" : "n0"; // corner posts if (ax === 2) return dx < 0 ? "n1" : "n0"; // cage bars (silhouetted by the glow) if (y === 24) return dx < 0 ? "n1" : "n0"; // cross bar // glass: front pane brightest near the flame; side panes dimmer const front = ax < 2, left = dx < 0; const d = Math.hypot(x - FC.x, (y - FC.y) * 0.9); // glint of the key light on the left pane (fixed, the glass is hard and cold) if (left && !front && (x - (LX - 4)) + (y - GT) === 1 && y < GT + 3) return "n5"; if (front) return d < 3.2 + flick ? "fl2" : "fl1"; return d < 3.4 + flick ? "fl1" : "fl0"; })); g.part("lanternBase", () => { g.line([LX - 3, GB + 2], [LX + 3, GB + 2], "n1"); g.pixel(LX - 3, GB + 2, "n3"); g.pixel(LX - 2, GB + 2, "n2"); g.line([LX - 1, GB + 3], [LX + 1, GB + 3], "n1"); g.pixel(LX - 1, GB + 3, "n2"); g.pixel(LX, GB + 4, "n1"); }); g.part("candle", () => { g.rect({ x: [LX - 1, LX + 1], y: [27, 29] }, "n4", { fill: "n4" }); g.line([LX + 1, 27], [LX + 1, 29], "n3"); g.pixel(LX - 1, 27, "n5"); g.pixel(LX, 27, "n5"); g.pixel(LX + 1, 28, "n4"); // a wax drip g.pixel(LX, 26, "n0"); // wick });}function flameShape(g, t, mode) { const tall = mode === "fire" ? 4.2 : 5.2; const hgt = tall + 0.9 * Math.sin(TAU * (2 * t + 0.1)) + 0.5 * Math.sin(TAU * (3 * t + 0.6)); const sway = mode === "fire" ? 0.6 : 1.1; g.part("flame", () => g.field({ x: [LX - 3, LX + 3], y: [18, 26] }, (x, y) => { const k = (25.5 - y) / hgt; if (k < 0 || k > 1) return null; const ax = LX + (sway * Math.sin(TAU * (t + 0.2)) + 0.4 * Math.sin(TAU * 2 * t)) * k * k; const hw = k < 0.3 ? 1.1 + k : 1.4 * Math.pow((1 - k) / 0.7, 0.8); const d = Math.abs(x - ax); if (d > hw) return null; if (k > 0.8) return "fl2"; return d < hw * 0.55 && k < 0.65 ? "fl3" : "fl2"; }));}const BAYER = [0, 8, 2, 10, 12, 4, 14, 6, 3, 11, 1, 9, 15, 7, 13, 5];const dith = (x, y, dens) => BAYER[(y & 3) * 4 + (x & 3)] / 16 < dens;function halo(g, t) { const R = 11 + 0.7 * Math.sin(TAU * 2 * t) + 0.4 * Math.sin(TAU * (3 * t + 0.4)); g.detached("halo", () => g.field({ x: [LX - 12, W - 1], y: [12, 38] }, (x, y) => { const r = Math.hypot(x - FC.x, (y - FC.y) * 0.85); if (r > R) return null; if (!dith(x, y, 0.55 * Math.pow(1 - r / R, 1.1))) return null; return r < R * 0.7 ? "fl1" : "fl0"; }));}// light through the cage on the ground: an elliptical pool with radial bar shadows and a dark spot under the basefunction groundLight(g, t) { const C = { x: LX - 1, y: 57 }, RX = 9.5, RY = 4.4; const breathe = 1 + 0.06 * Math.sin(TAU * 2 * t); g.detached("groundLight", () => g.field({ x: [LX - 10, W - 1], y: [51, 63] }, (x, y) => { const nx = (x + 0.5 - C.x) / (RX * breathe), ny = (y + 0.5 - C.y) / (RY * breathe); const r = Math.sqrt(nx * nx + ny * ny); if (r > 1 || r < 0.2) return null; // dark spot under the lantern base const ang = Math.atan2(ny, nx); if (r > 0.3 && Math.abs(Math.sin(3 * ang + 0.5)) < 0.26) return null; // bar shadows radiate out const dens = r < 0.5 ? 0.7 : r < 0.75 ? 0.45 : 0.22; if (!dith(x, y, dens)) return null; return r < 0.4 ? "fl2" : r < 0.7 ? "fl1" : "fl0"; }));}function motes(g, t, mode) { g.detached("motes", () => { if (mode === "fire") { // one ember lifts off the candle const p = (t * 2) % 1; if (p < 0.55) g.pixel(LX + Math.round(Math.sin(TAU * p) * 2), 20 - Math.round(p * 16), p < 0.3 ? "fl3" : "fl2"); return; } for (let i = 0; i < 6; i++) { const p = (t + i / 6) % 1; const x = Math.round(LX + (i % 2 ? 1 : -1) * (2 + p * 5) * Math.sin(TAU * (p * 0.9 + i * 0.23)) + (i - 2.5) * 1.1); const y = Math.round(31 - p * 30); if (y < 0) continue; if (p < 0.45) { g.pixel(x, y, "fl3"); g.pixel(x, y + 1, "fl2"); } else if (p < 0.75) { g.pixel(x, y, "fl2"); g.pixel(x, y + 1, "fl0"); } else if (chk(x, Math.floor(t * 12))) g.pixel(x, y, "fl1"); } });}const SoulLantern = sprite(pal, (g, t, mode) => { t = ((t % 1) + 1) % 1; contactShadow(g, PX, GROUND, 7, 1); g.depth(2, () => groundLight(g, t)); g.outlined("ink", () => { post(g); crook(g); footing(g); lantern(g, t); }); flameShape(g, t, mode); g.depth(2, () => halo(g, t)); motes(g, t, mode);});submit(SoulLantern, { size: [W, H], origin: "top-left", kind: "icon", scale: 1, animate: { fps: 10, duration: 1.2, loop: true }, anchor: { x: PX, y: GROUND }, args: ["soul"], variants: [ { name: "soul", repalette: swap("fl", "soul") }, { name: "fire", args: ["fire"] }, { name: "witch", args: ["witch"], repalette: swap("fl", "witch") }, ],});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.