Tessarune
Graveyard & Chapel · chapel_altar.js

Chapel altar

64×48 · 364 lines of JavaScript

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.

Chapel altar — Rendered output6×
Rendered by this program, variant “blessing” · 64×48 · 10 frames
Graveyard & ChapelGet 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.js · 1–9
// 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 motes

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

chapel_altar.js · 355–364
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.js · 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.