Chapel altar
A 64×48 floor prop: a stone altar on a wide step, dressed with a deep-red frontal cloth with gold trim and a sun sigil, two tall brass candlesticks, an open tome on a wooden rest and a gold chalice; the candles light the stone around their feet. “idle” (12 frames) flickers the flames while the warm light pool on the slab breathes; “blessing” (10 frames, once) flares the pages gold as rays rise and a ring of light motes lifts and fades; “desecrated” (12 frames) shows snuffed, melted candles, a torn frontal, a cracked slab, a spilled chalice and a tome shedding violet motes.
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.
// Chapel altar — 64x48 floor prop, top-down 3/4, key light top-left.// A stone altar (overhanging mensa slab on a recessed block, on a wide step) dressed with a deep-red frontal cloth// with gold trim, fringe and a sun sigil; two tall brass candlesticks, an open tome on a wooden rest in the centre,// a gold chalice. The candles light the stone around their feet.// Variants:// idle loop, 12f — flames flicker and sway, the warm light pool on the slab breathes// blessing one-shot, 10f — pages flare gold, rays rise, a ring of light motes lifts and fades; ends == idle f0// desecrated loop, 12f — candles snuffed and melted with thin smoke, frontal torn, slab cracked, chalice knocked// onto the step and spilled, the tome glows violet and sheds violet motesThe last lines. submit declares the canvas and lists the variants the renderer exports.
submit(Altar, { size: [W, H], origin: "top-left", kind: "icon", scale: 1, animate: { fps: 10, duration: N_IDLE / 10, loop: true }, anchor: { x: CX, y: GROUND }, variants: [ { name: "idle", args: ["idle"] }, { name: "blessing", args: ["blessing"], animate: { fps: 10, duration: N_BLESS / 10, loop: false } }, { name: "desecrated", args: ["desecrated"], animate: { fps: 10, duration: N_DES / 10, loop: true } }, ],});Show the whole program (364 lines)
// Chapel altar — 64x48 floor prop, top-down 3/4, key light top-left.// A stone altar (overhanging mensa slab on a recessed block, on a wide step) dressed with a deep-red frontal cloth// with gold trim, fringe and a sun sigil; two tall brass candlesticks, an open tome on a wooden rest in the centre,// a gold chalice. The candles light the stone around their feet.// Variants:// idle loop, 12f — flames flicker and sway, the warm light pool on the slab breathes// blessing one-shot, 10f — pages flare gold, rays rise, a ring of light motes lifts and fades; ends == idle f0// desecrated loop, 12f — candles snuffed and melted with thin smoke, frontal torn, slab cracked, chalice knocked// onto the step and spilled, the tome glows violet and sheds violet motesconst W = 64, H = 48, CX = 32, GROUND = 45;const N_IDLE = 12, N_BLESS = 10, N_DES = 12;const CL = ["w0", "r0", "r1", "r2"]; // cloth ramp: deep fold -> lit ridgeconst CANDLES = [13, 51];const CHAL = 44;const gs = (u) => (u < -0.6 ? "g3" : u < -0.1 ? "g2" : u < 0.5 ? "g1" : "g0"); // brass across a round form// ---- light & marks on the altar top -----------------------------------------------------------------------function castShadow(x, y, s) { // countable shadows thrown down-right by what stands on the slab (light is top-left) for (const cx of CANDLES) if (y >= 24 && y <= 25 && x >= cx + 3 && x <= cx + 4 + (y - 24)) return true; if (!s.fallen && ((x === CHAL + 3 && y >= 22 && y <= 25) || (y === 25 && x >= CHAL + 1 && x <= CHAL + 3))) return true; if (y === 26 && x >= 27 && x <= 41) return true; if (x === 41 && y >= 18 && y <= 25) return true; return false;}function warmAt(x, y, s) { // the candles' light pool on the stone: 2 = inside, 1 = dithered rim, 0 = none if (!s.lit) return 0; let best = 0; for (const [i, cx] of CANDLES.entries()) { const r = 6.5 + s.pool * 1.2 + (i ? -0.4 : 0); const dx = (x + 0.5 - cx - 0.5) / r, dy = (y + 0.5 - 23) / (r * 0.42), d = dx * dx + dy * dy; if (d < 0.55) best = 2; } return best;}function tomeLight(x, y, s) { // magic light around the tome (gold when blessing, violet when desecrated): 0..2 const L = s.goldLight || s.violet; if (!L) return 0; const r = 7 + L * 2.2; const dx = (x + 0.5 - CX) / r, dy = (y + 0.5 - 22) / (r * 0.45), d = dx * dx + dy * dy; return d < 0.35 ? 2 : d < 1 ? 1 : 0;}const CRACK = new Set(["20,19", "21,20", "21,21", "22,22", "23,22", "24,23", "50,30", "51,31", "51,32", "52,33", "52,34", "10,33", "11,34"]);function stoneTop(x, y, s, base) { if (castShadow(x, y, s)) return "n2"; const tl = tomeLight(x, y, s); if (tl === 2 && s.violet) return s.violet > 1 && chk(x, y) ? "v2" : "v1"; if (tl && s.goldLight) { if (tl === 2) return chk(x, y) || s.goldLight > 2 ? "n5" : "n4"; if (chk(x, y)) return "n4"; } const w = warmAt(x, y, s); if (w === 2) return base === "n4" ? "n5" : "n4"; return base;}// ---- the altar block --------------------------------------------------------------------------------------function plinth(g) { g.part("step", () => g.field({ x: [6, 57], y: [39, 44] }, (x, y) => { if (y === 39) return x >= 11 && x <= 56 ? (x > 46 ? "n1" : "n2") : "n3"; // the altar's shadow on the step if (y === 40) return x === 6 ? "n4" : x < 42 ? "n4" : "n3"; if (y === 44) return "n1"; if (x === 6) return "n3"; if (y >= 42 && (x - 8) % 5 === 0 && x < 56) return "n1"; // dentils if (y >= 42 && (x - 9) % 5 === 0 && x < 56) return x < 44 ? "n3" : "n2"; return x > 46 ? "n1" : "n2"; }));}function altarBlock(g, s) { g.part("altar", () => { // recessed body under the slab g.field({ x: [10, 53], y: [30, 38] }, (x, y) => { if (s.crack && CRACK.has(x + "," + y)) return "n0"; if (y === 30) return "n0"; // shadow under the overhang const right = x >= 47; const joint = y === 34 || ((x === 14 || x === 49) && y < 34) || ((x === 12 || x === 51) && y > 34); if (joint) return right ? "n0" : "n1"; if (x === 10) return "n3"; return right ? "n1" : "n2"; }); // mensa slab: lit top face, then its front edge g.field({ x: [8, 55], y: [19, 29] }, (x, y) => { if (s.crack && CRACK.has(x + "," + y)) return y < 28 ? "n1" : "n0"; if (y === 28) return x === 55 ? "n2" : x <= 33 ? "n4" : "n3"; if (y === 29) return x === 55 ? "n1" : x <= 47 ? "n2" : "n1"; let base = "n3"; if ((y === 19 && x < 52) || x === 8) base = "n4"; else if (x === 55) base = "n2"; else if (y === 27 && x < 40) base = "n4"; // bevel along the near edge return stoneTop(x, y, s, base); }); // chips on the slab corners (countable) g.pixel(55, 19, "n2"); g.pixel(9, 28, "n3"); });}// ---- the frontal cloth ------------------------------------------------------------------------------------const clothBottom = (x, s) => (s.torn ? 34 + Math.floor(hash(x, 7) * 3.2) : 37);const ripped = (x, y, s) => s.torn && y >= 30 && Math.abs(x + 0.5 - 37 + (y - 30) * 0.3) <= (y - 29) * 0.75;function cloth(g, s) { const dark = s.torn ? 1 : 0; const tone = (k) => CL[Math.max(0, k - dark)]; const trim = (x) => (s.torn ? (x < 32 ? "g1" : "g0") : x < 36 ? "g2" : "g1"); g.part("cloth", () => { // the part lying on the slab g.field({ x: [19, 44], y: [25, 27] }, (x, y) => { if (y === 25) return trim(x); if (castShadow(x, y, s)) return tone(1); const tl = tomeLight(x, y, s); if (tl === 2 && chk(x, y)) return s.violet ? "v1" : "r3"; if (y === 26) return tone(2); return x < 38 ? tone(3) : tone(2); // the fold over the edge catches the light }); // the hanging part: vertical folds lit from the left g.field({ x: [19, 44], y: [28, 38] }, (x, y) => { const bot = clothBottom(x, s); if (y > bot + 1 || ripped(x, y, s)) return null; if (y === bot + 1) return s.torn ? (hash(x, 3) > 0.55 ? tone(1) : null) : (x % 2 === 0 ? (x < 36 ? "g2" : "g1") : null); // fringe / threads if (x === 19 || x === 44) return x === 19 ? trim(x) : s.torn ? "g0" : "g1"; if (y === bot - 1) return s.torn ? tone(1) : trim(x); if (y === bot) return tone(1); if (s.torn && Math.abs(x + 0.5 - 37 + (y - 30) * 0.3) <= (y - 29) * 0.75 + 1) return tone(0); // frayed lip of the rip const u = (x - 19) / 25; const v = 0.62 * Math.sin(u * TAU * 3 + 0.5) + 0.28 * Math.sin(u * TAU * 1.3 + 1.1) - (u - 0.45) * 0.9 - (y - 28) * 0.03; return tone(v > 0.62 ? 3 : v > -0.05 ? 2 : v > -0.7 ? 1 : 0); }); });}function sigil(g, s) { // a sun disc with eight rays, embroidered in gold on the frontal const cx = 31, cy = 32, lvl = s.sigil || 0; const disc = s.torn ? (s.violet > 1 ? ["v3", "v2", "v1"] : ["v2", "v1", "v0"]) : lvl >= 2 ? ["g3", "g3", "g2"] : lvl ? ["g3", "g2", "g2"] : ["g3", "g2", "g1"]; const ray = s.torn ? "v1" : lvl >= 2 ? "g3" : "g1"; g.part("sigil", () => { for (const [dx, dy] of [[0, -3], [0, 3], [-3, 0], [3, 0], [-2, -2], [2, -2], [-2, 2], [2, 2]]) { if (s.torn && (dx > 0 || dy > 1)) continue; // half the rays unpicked g.pixel(cx + dx, cy + dy, ray); } g.rect({ x: [cx - 1, cx + 1], y: [cy - 1, cy + 1] }, disc[1], { fill: disc[1] }); g.pixel(cx - 1, cy - 1, disc[0]); g.pixel(cx + 1, cy + 1, disc[2]); g.pixel(cx, cy + 1, disc[2]); g.pixel(cx + 1, cy, disc[2]); if (lvl >= 2) for (const [dx, dy] of [[0, -4], [0, 4], [-4, 0], [4, 0]]) g.pixel(cx + dx, cy + dy, "g2"); });}// ---- candlesticks -----------------------------------------------------------------------------------------function candlestick(g, cx, s) { const top = s.lit ? 7 : 10; // snuffed candles have burned down g.part(cx < CX ? "candleL" : "candleR", () => { // foot: a stepped dome for (const [y, hw] of [[22, 1], [23, 2], [24, 3]]) for (let x = cx - hw; x <= cx + hw; x++) g.pixel(x, y, gs((x - cx) / (hw + 0.5))); g.pixel(cx - 1, 22, "g3"); // stem with a knop for (let y = 16; y <= 21; y++) { g.pixel(cx - 1, y, "g2"); g.pixel(cx, y, "g1"); g.pixel(cx + 1, y, "g0"); } for (let x = cx - 2; x <= cx + 2; x++) g.pixel(x, 19, gs((x - cx) / 2.5)); g.pixel(cx - 2, 19, "g3"); // drip pan for (let x = cx - 3; x <= cx + 3; x++) g.pixel(x, 15, gs((x - cx) / 3.5)); for (let x = cx - 2; x <= cx + 2; x++) g.pixel(x, 16, x < cx ? "g1" : "g0"); g.pixel(cx - 2, 14, "g2"); g.pixel(cx + 2, 14, "g1"); // the candle: cream wax, lit on the left, a drip on the right for (let y = top; y <= 14; y++) { g.pixel(cx - 1, y, "n5"); g.pixel(cx, y, "n4"); g.pixel(cx + 1, y, "n3"); } g.pixel(cx, top, "n5"); g.pixel(cx + 1, top + 1, "n4"); g.pixel(cx + 1, top + 2, "n4"); if (!s.lit) { g.pixel(cx + 2, 14, "n4"); g.pixel(cx - 2, 13, "n5"); } // melted wax pooled on the pan if (s.lit) { g.pixel(cx - 1, top, "g3"); g.pixel(cx, top, "g3"); } // the flame lights the rim of the wax });}function candleFlame(g, x, t, ph, extra) { // a teardrop that flickers (height) and sways (lean), periodic in t const s = Math.sin(TAU * (t * 2 + ph)), c = Math.sin(TAU * (t * 3 + ph * 1.7)); const h = Math.min(7, 5 + (c > 0.2 ? 1 : 0) + extra); const yb = 6; for (let i = 0; i < h; i++) { const k = i / (h - 1), y = yb - i, ox = x + Math.round(s * k * 1.4); if (i <= 2 + (extra > 1 ? 1 : 0)) { g.pixel(ox - 1, y, i === 0 ? "r1" : "r2"); g.pixel(ox + 1, y, i === 0 ? "r1" : "r2"); g.pixel(ox, y, i === 0 ? "r3" : i <= 2 ? "g3" : "r3"); } else g.pixel(ox, y, i === h - 1 ? "r1" : i === h - 2 ? "r2" : "r3"); }}function smoke(g, x, t, ph) { // a thin wisp curling up from the snuffed wick; an ember blinks on the wick now and then const wick = 9; g.pixel(x, wick, (t * 12 + ph * 12) % 12 < 3 ? "r1" : "ink"); for (let j = 0; j < 6; j++) { const p = (t + j / 6 + ph) % 1; const y = wick - 1 - Math.round(p * 11); const xx = x + Math.round(Math.sin(TAU * (p * 1.3 + ph)) * (0.5 + p * 1.8)); if (p < 0.4) g.pixel(xx, y, "n3"); else if (p < 0.8) g.pixel(xx, y, "n2"); }}// ---- the tome and the chalice ---------------------------------------------------------------------------function tome(g, s) { g.part("bookrest", () => { g.rect({ x: [26, 38], y: [24, 25] }, "w2", { fill: "w2" }); g.line([26, 24], [34, 24], "w3"); g.rect({ x: [36, 38], y: [24, 25] }, "w1", { fill: "w1" }); g.pixel(26, 25, "w3"); }); const P = s.pages; // page palette: base, shaded edge, gutter, text, initial const K = P === "violet" ? ["n4", "v3", "v1", "v2", "v3"] : P === 3 ? ["g3", "g3", "g2", "g2", "g2"] : P === 2 ? ["g3", "g2", "g1", "g1", "g1"] : P === 1 ? ["n5", "g3", "g1", "g1", "r1"] : ["n5", "n4", "n2", "n3", "r1"]; g.part("tome", () => { g.rect({ x: [24, 40], y: [17, 23] }, "r0", { fill: "r0" }); // leather boards g.line([24, 23], [40, 23], "w0"); g.pixel(24, 17, "g2"); g.pixel(40, 17, "g1"); g.pixel(24, 23, "g1"); g.pixel(40, 23, "g0"); // brass corners g.line([25, 22], [31, 22], "n3"); g.line([33, 22], [39, 22], "n3"); // page stacks g.pixel(25, 22, "n4"); g.pixel(33, 22, "n4"); g.field({ x: [25, 39], y: [16, 21] }, (x, y) => { if (x === 32) return y >= 17 ? K[2] : null; // gutter if (y === 16 && (x === 25 || x === 31 || x === 33 || x === 39)) return null; // rounded page heads const line = x < 32 ? (y === 17 || y === 19 || y === 21) : (y === 19 || y === 21 || (y === 17 && x > 36)); if (line && ((x >= 26 && x <= 30) || (x >= 34 && x <= 38)) && hash(x, y * 3) > 0.28) return K[3]; if (x >= 34 && x <= 35 && y >= 17 && y <= 18) return K[4]; // illuminated initial if (x === 31 || x === 33 || x === 39) return K[1]; return K[0]; }); g.line([32, 24], [32, 25], "r1"); // ribbon marker });}function chalice(g) { const R = { 15: [43, ["g2", "g2", "g1"]], 16: [42, ["g3", "r0", "r0", "r1", "g1"]], 17: [42, ["g3", "g2", "g2", "g1", "g0"]], 18: [42, ["g2", "g2", "g1", "g1", "g0"]], 19: [43, ["g2", "g1", "g0"]], 20: [44, ["g1"]], 21: [43, ["g3", "g1", "g0"]], 22: [44, ["g1"]], 23: [43, ["g2", "g1", "g0"]], 24: [42, ["g3", "g2", "g1", "g1", "g0"]], }; g.part("chalice", () => { for (const [y, [x0, ks]] of Object.entries(R)) ks.forEach((k, i) => g.pixel(x0 + i, +y, k)); });}function fallenChalice(g) { // knocked off the altar: lying on the step, mouth to the right g.part("chalice", () => { [["g3", 37], ["g2", 38], ["g1", 39], ["g0", 40]].forEach(([k, y]) => g.pixel(45, y, k)); g.pixel(46, 38, "g2"); g.pixel(46, 39, "g1"); g.pixel(47, 38, "g3"); g.pixel(47, 39, "g0"); const bowl = [["g3", "g3", "g2", "g2"], ["g2", "g2", "g2", "g1"], ["g1", "g1", "g1", "g0"], ["g0", "g0", "g0", "g0"]]; bowl.forEach((row, j) => row.forEach((k, i) => g.pixel(48 + i, 37 + j, k))); ["g3", "r0", "r0", "r1", "r0", "g0"].forEach((k, j) => g.pixel(52, 36 + j, k)); });}function spill(g) { g.part("spill", () => { g.line([53, 40], [57, 40], "r0"); g.line([54, 39], [55, 39], "r0"); g.pixel(54, 39, "r1"); g.pixel(57, 41, "r0"); g.pixel(57, 42, "r0"); });}// ---- effects ----------------------------------------------------------------------------------------------function rays(g, len) { g.detached("rays", () => { for (const [x, m] of [[27, 0.7], [29, 1], [32, 1.2], [35, 1], [37, 0.7]]) { const n = Math.round(len * m); for (let i = 0; i < n; i++) g.pixel(x, 15 - i, i < n * 0.5 ? "g3" : "g2"); } });}function moteRing(g, p) { // ten motes on a level ring that lifts off the tome, widening and fading g.detached("ring", () => { const rx = 6 + p * 12, ry = rx * 0.38, cy = 20 - p * 15; for (let i = 0; i < 10; i++) { const a = TAU * i / 10 + p * 1.6; const x = Math.round(CX + Math.cos(a) * rx), y = Math.round(cy + Math.sin(a) * ry); const near = Math.sin(a) > 0; const k = p > 0.8 ? "g1" : p > 0.55 ? "g2" : near ? "g3" : "g2"; g.pixel(x, y, k); if (p < 0.55 && near) g.pixel(x, y + 1, "g2"); if (p < 0.3) { g.pixel(x - 1, y, "g2"); g.pixel(x + 1, y, "g2"); } } });}function violetAura(g, lvl, t) { // a flickering corona hugging the tome: a dotted crown over the pages and short flames at the sides g.detached("aura", () => { const sh = Math.round(t * N_DES) % 2; for (let x = 25; x <= 39; x++) if (x !== 32 && (x + sh) % 2 === 0) g.pixel(x, 15, lvl > 1 ? "v2" : "v1"); if (lvl > 1) for (const x of [27, 30, 34, 37]) g.pixel(x + sh, 14, "v1"); for (let y = 17; y <= 22; y++) if ((y + sh) % 2 === 0) { g.pixel(23, y, "v1"); g.pixel(41, y, lvl > 1 ? "v2" : "v1"); } });}function violetMotes(g, t) { g.detached("motes", () => { for (let i = 0; i < 4; i++) { const p = (t + i / 4) % 1; const x = CX + [-5, 2, 6, -1][i] + Math.round(Math.sin(TAU * (p + i * 0.3)) * 1.5); const y = 16 - Math.round(p * 15); if (p < 0.35) { g.pixel(x, y, "v3"); g.pixel(x, y + 1, "v2"); } else if (p < 0.7) g.pixel(x, y, "v2"); else if (chk(x, y)) g.pixel(x, y, "v1"); } });}// ---- scene ------------------------------------------------------------------------------------------------function scene(g, s) { contactShadow(g, CX, GROUND, 29, 2); g.outlined("ink", () => { plinth(g); altarBlock(g, s); cloth(g, s); }); sigil(g, s); for (const cx of CANDLES) g.outlined("ink", () => candlestick(g, cx, s)); g.outlined("ink", () => tome(g, s)); if (s.fallen) { spill(g); g.outlined("ink", () => fallenChalice(g)); } else g.outlined("ink", () => chalice(g)); if (s.lit) g.detached("flames", () => CANDLES.forEach((cx, i) => candleFlame(g, cx, s.t, i * 0.37, s.flameExtra))); else g.detached("smoke", () => CANDLES.forEach((cx, i) => smoke(g, cx, s.t, i * 0.5))); if (s.rays) rays(g, s.rays); if (s.ring !== undefined) moteRing(g, s.ring); if (s.violet) { violetAura(g, s.violet, s.t); violetMotes(g, s.t); }}const base = (t) => ({ t, lit: true, pool: 0.5 + 0.5 * Math.sin(TAU * t * 2), flameExtra: 0, pages: 0, sigil: 0 });const Altar = sprite(pal, (g, t, state) => { if (state === "idle") { scene(g, base(t)); return; } if (state === "blessing") { const f = Math.min(N_BLESS - 1, Math.round(t * N_BLESS)); if (f === N_BLESS - 1) { scene(g, base(0)); return; } // back to idle frame 0 const s = base(f / N_BLESS); s.pages = [1, 2, 3, 3, 3, 2, 2, 1, 1][f]; s.goldLight = [0, 1, 2, 3, 3, 2, 1, 1, 0][f]; s.sigil = [0, 1, 2, 2, 2, 2, 1, 1, 0][f]; s.flameExtra = [1, 1, 2, 2, 2, 1, 1, 0, 0][f]; s.rays = [0, 3, 7, 8, 5, 2, 0, 0, 0][f]; if (f >= 2) s.ring = (f - 2) / 6.5; scene(g, s); return; } // desecrated const pulse = 0.5 + 0.5 * Math.cos(TAU * t); scene(g, { t, lit: false, pool: 0, pages: "violet", torn: true, fallen: true, crack: true, violet: pulse > 0.5 ? 2 : 1 });});submit(Altar, { size: [W, H], origin: "top-left", kind: "icon", scale: 1, animate: { fps: 10, duration: N_IDLE / 10, loop: true }, anchor: { x: CX, y: GROUND }, variants: [ { name: "idle", args: ["idle"] }, { name: "blessing", args: ["blessing"], animate: { fps: 10, duration: N_BLESS / 10, loop: false } }, { name: "desecrated", args: ["desecrated"], animate: { fps: 10, duration: N_DES / 10, 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.