Tessarune
Graveyard & Chapel · tombstone.js

Tombstone

48×48 · 269 lines of JavaScript

A 48×48 floor prop: a weathered round-topped headstone with an engraved ring-cross rune and an epitaph, moss on the shoulder and the foot, over an old grassy grave mound. “idle” (16 frames) sways the grass tufts while a faint blue soul-wisp circles the stone, passing behind it; “cracked” (16 frames) splits and leans the stone, with violet light seeping from the crack and the rune; “rise” (10 frames, once) makes the mound shiver, bulge and burst as the wisp climbs out of the earth, ending on the first frame of “idle”.

Tombstone — Rendered output6×
Rendered by this program, variant “rise” · 48×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.

tombstone.js · 1–8
// Tombstone — 48x48 floor prop, top-down 3/4, key light top-left.// A weathered round-topped headstone (you see its face, its lit top edge and its shaded right side) with an// engraved ring-cross rune and an epitaph, moss on the shoulder and the foot, over an old grassy grave mound.// Variants://   idle     16f loop  grass tufts sway, a faint soul-wisp (blue, unoutlined) circles the stone, passing behind it//   cracked  16f loop  the stone is split and leaning; violet light seeps from the crack and the rune, pulsing//   rise     10f once  the mound shivers, bulges, bursts (clods fly), settles; the wisp climbs out of the earth//                      and the last frame is idle frame 0

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

tombstone.js · 259–269
submit(Tombstone, {  size: [W, H], origin: "top-left", kind: "icon", scale: 1,  animate: { fps: 10, duration: 1.6, loop: true },  anchor: { x: CX, y: GROUND },  args: ["idle"],  variants: [    { name: "idle" },    { name: "cracked", args: ["cracked"] },    { name: "rise", args: ["rise"], animate: { fps: 10, duration: 1.0, loop: false } },  ],});
Show the whole program (269 lines)
tombstone.js · 269 lines
// Tombstone — 48x48 floor prop, top-down 3/4, key light top-left.// A weathered round-topped headstone (you see its face, its lit top edge and its shaded right side) with an// engraved ring-cross rune and an epitaph, moss on the shoulder and the foot, over an old grassy grave mound.// Variants://   idle     16f loop  grass tufts sway, a faint soul-wisp (blue, unoutlined) circles the stone, passing behind it//   cracked  16f loop  the stone is split and leaning; violet light seeps from the crack and the rune, pulsing//   rise     10f once  the mound shivers, bulges, bursts (clods fly), settles; the wisp climbs out of the earth//                      and the last frame is idle frame 0const W = 48, H = 48, CX = 24, GROUND = 45;const FX0 = 15, FX1 = 30, AY = 12.5, AR = 8, BASE = 34;   // stone face: columns, arch centre row, arch radius, footconst ACX = 23;                                           // arch centre x (pixel-centre coords)const DX = 2, DY = -2;                                    // stone thickness, seen up and to the rightconst MND = { cx: 24, cy: 39, rx: 14, ry: 5 };            // grave moundconst inFace = (x, y) => {  if (x < FX0 || x > FX1 || y > BASE) return false;  if (y >= AY) return true;  const dx = x + 0.5 - ACX, dy = y + 0.5 - AY;  return dx * dx + dy * dy <= AR * AR;};const inSlab = (x, y) => inFace(x, y) || inFace(x - 1, y + 1) || inFace(x - DX, y - DY);// ---- stone -------------------------------------------------------------------------------------------------// crack: a zigzag down the face (cracked variant); returns the crack column for a rowconst CRACK = [[27, 3], [28, 7], [27, 10], [28, 14], [27, 18], [25, 22], [26, 26], [24, 30], [25, 35]];function crackX(y) {  for (let i = 0; i + 1 < CRACK.length; i++) {    const [x0, y0] = CRACK[i], [x1, y1] = CRACK[i + 1];    if (y >= y0 && y <= y1) return Math.round(x0 + (x1 - x0) * (y - y0) / (y1 - y0));  }  return -99;}const RUNE = (() => {  // ring-cross carved into the face: a ring of radius 3 round (23,16), a shaft and a crossbar  const s = new Set();  for (let a = 0; a < 64; a++) {    const x = Math.round(22.5 + 3.4 * Math.cos(TAU * a / 64)), y = Math.round(15.5 + 3.4 * Math.sin(TAU * a / 64));    s.add(x + "," + y);  }  for (let y = 10; y <= 23; y++) s.add("23," + y);  for (let x = 18; x <= 27; x++) s.add(x + ",15");  for (const x of [20, 21, 22, 24, 26, 27]) s.add(x + ",26");            // epitaph: two lines of "words"  for (const x of [19, 20, 22, 23, 24, 26]) s.add(x + ",28");  return s;})();const isRune = (x, y) => RUNE.has(x + "," + y);function faceTone(x, y, cracked, pulse) {  const lEdge = !inFace(x - 1, y), tEdge = !inFace(x, y - 1), rEdge = !inFace(x + 1, y);  if (cracked) {    const c = crackX(y);    if (x === c) return pulse > 0.66 ? "v3" : pulse > 0.3 ? "v2" : "v1";   // the glowing split    if (x === c + 1) return "n1";                                         // right lip in the crack's shade    if (x === c - 1 && !lEdge) return pulse > 0.4 ? "v1" : "n2";           // light spilling onto the left lip  }  if (isRune(x, y)) {    if (cracked && y < 26) return pulse > 0.7 ? "v1" : "n0";    return y >= 26 ? "n1" : "n0";  }  if (!cracked && isRune(x - 1, y - 1) && y < 30) return "n3";            // the carving's lit lower-right wall  if (cracked && isRune(x - 1, y - 1) && y < 30) return "n3";  if (lEdge || (tEdge && x < ACX + 2)) return "n4";                       // lit arris on the top-left  if (rEdge) return "n1";  if (!inFace(x + 2, y) && chk(x, y)) return "n1";                        // turning away from the light  if (y >= BASE - 2) return "n1";                                         // damp foot  if (y >= BASE - 5) return (x & 1) === (y & 1) && y === BASE - 3 ? "n1" : "n2";  if (y < 9 || x <= FX0 + 1) return "n3";                                 // upper arch and left strip catch more light  return "n2";}function slabTone(x, y) {  // top edge (faces the sky, lit) and right side (turned away)  const dx = x + 0.5 - ACX - 1, dy = y + 0.5 - AY + 1;  if (dy < 0 && x <= FX1 + 1) return dx < -3 ? "n4" : dx < 3 ? "n3" : "n2";  return y > BASE - 4 ? "n0" : "n1";}function stone(g, cracked, pulse) {  g.part("stone", () => g.field({ x: [FX0 - 1, FX1 + DX + 1], y: [2, BASE] }, (x, y) => {    if (inFace(x, y)) return faceTone(x, y, cracked, pulse);    if (inSlab(x, y)) return slabTone(x, y);    return null;  }));  g.part("chips", () => {    // countable weathering: dents (dark top-left pixel, lit bottom-right pixel)    for (const [x, y] of [[17, 12], [28, 20], [19, 31], [27, 7]]) {      if (cracked && Math.abs(x - crackX(y)) < 2) continue;      g.pixel(x, y, "n1"); g.pixel(x + 1, y + 1, "n4");    }  });  g.part("moss", () => {    // on the lit shoulder, dripping down the left arris, and a band at the damp foot    for (const [x, y, k] of [[18, 5, "e1"], [19, 4, "e2"], [20, 4, "e2"], [21, 3, "e1"], [22, 3, "e1"], [17, 6, "e1"],      [16, 7, "e1"], [16, 8, "e0"], [15, 10, "e1"], [15, 11, "e1"], [15, 12, "e0"], [18, 6, "e0"], [20, 3, "e3"], [17, 5, "e2"]])      g.pixel(x, y, k);    for (let x = FX0; x <= FX1 + 1; x++) {      const h = Math.floor(hash(x, 5) * 4) - (x > 25 ? 1 : 0);      for (let i = 0; i < h; i++) g.pixel(x, BASE - i, i === h - 1 ? (x < 22 ? "e2" : "e1") : "e0");    }  });}// ---- grave mound -------------------------------------------------------------------------------------------------const DIRT = ["w0", "w1", "w2", "w3"];function mound(g, bulge) {  const { cx, cy, rx, ry } = MND;  const B = { cx: 24, cy: 39 - bulge, rx: 5 + bulge * 0.6, ry: 2.5 + bulge * 0.6 };  g.part("mound", () => g.field({ x: [cx - rx, cx + rx], y: [cy - ry - 5, cy + ry] }, (x, y) => {    if (bulge > 0) {      const bi = litIndex(x, y, B.cx, B.cy, B.rx, B.ry, 4, 0.05);      if (bi >= 0) return DIRT[bi];    }    const i = litIndex(x, y, cx, cy, rx, ry, 4, 0.1);    if (i < 0) return null;    // old grave: a grass crown with a wavy edge; bare earth on the flanks and the near face    const gx = (x + 0.5 - cx) / (rx - 2), gy = (y + 0.5 - (cy - 1.2)) / (ry - 1.3 + 0.6 * Math.sin(x * 1.3));    if (gx * gx + gy * gy <= 1) {      const gi = litIndex(x, y, cx - 1, cy - 1, rx - 2, ry - 1, 4, 0.05);      return ["e0", "e1", "e1", "e2"][Math.max(0, gi)];    }    return DIRT[i];  }));  g.part("blades", () => {    // countable marks on the crown: a few blade strokes and lit tips    for (const [x, y] of [[16, 37], [20, 36], [27, 36], [31, 38], [23, 40], [18, 40]]) { g.pixel(x, y, "e1"); g.pixel(x, y - 1, "e2"); }    for (const [x, y] of [[17, 36], [19, 37], [22, 35], [15, 38]]) g.pixel(x, y, "e3");  });  g.part("pebbles", () => {    for (const [x, y] of [[13, 41], [33, 40], [29, 43], [18, 43]]) { g.pixel(x, y, "n3"); g.pixel(x + 1, y, "n1"); }  });}function cracks(g, f) {  // shivering earth before the burst  if (f < 1 || f > 4) return;  g.part("fissures", () => {    const L = f === 1 ? [[[22, 39], [24, 38], [26, 39]]] :      [[[20, 38], [22, 37], [24, 38], [26, 37], [28, 38]], [[24, 38], [23, 40]]];    for (const path of L) for (let i = 0; i + 1 < path.length; i++) g.line(path[i], path[i + 1], "ink");  });}// ---- grass -------------------------------------------------------------------------------------------------------const TUFTS = [[12, 38, 0.0, 1], [14, 34, 0.3, 1], [33, 35, 0.55, 0], [36, 39, 0.8, 0], [20, 44, 0.15, 1], [30, 44, 0.65, 0], [10, 42, 0.4, 1]];function grass(g, t) {  g.part("grass", () => {    for (const [bx, by, ph, lit] of TUFTS) {      const sw = Math.round(Math.sin(TAU * (t + ph)) * 1.2);      for (const [dx, h, lean] of [[-1, 3, -1], [0, 4, 0], [1, 3, 1]]) {        const x0 = bx + dx, tip = [x0 + lean + sw, by - h];        g.line([x0, by], tip, "e1");        g.pixel(x0, by, "e0");        g.pixel(tip[0], tip[1], lit ? "e3" : "e2");      }    }  });}// ---- soul wisp -----------------------------------------------------------------------------------------------------const wispAt = (t) => [23 + 14 * Math.cos(TAU * t + 2.3), 21 + 6 * Math.sin(TAU * t + 2.3) - 2 * Math.sin(2 * TAU * t)];function wispBlob(g, x, y, tail, flick) {  // a soft glowing head (white core, blue body, dithered halo) and a tail of fading dots  x = Math.round(x); y = Math.round(y);  for (const [dx, dy] of [[-2, -1], [-1, -2], [3, 1], [1, 3], [-2, 2], [3, -1], [1, -2], [-1, 3], [0, -2], [-2, 0], [3, 0], [0, 3]])    if (chk(x + dx, y + dy + (flick ? 1 : 0))) g.pixel(x + dx, y + dy, "b1");  for (const [dx, dy] of [[0, -1], [1, -1], [-1, 0], [2, 0], [-1, 1], [2, 1], [0, 2], [1, 2]]) g.pixel(x + dx, y + dy, "b2");  g.pixel(x, y, "n5"); g.pixel(x + 1, y, flick ? "b3" : "n5"); g.pixel(x, y + 1, "b3"); g.pixel(x + 1, y + 1, "b2");  tail.forEach(([tx, ty], i) => g.pixel(Math.round(tx), Math.round(ty), i < 2 ? "b2" : "b1"));}function wisp(g, t) {  const [x, y] = wispAt(t);  const tail = [0.045, 0.07, 0.095, 0.12, 0.145].map((d) => wispAt(t - d)).map(([tx, ty], i) => [tx + 0.5, ty - 0.5 - i * 0.7]);  const behind = Math.sin(TAU * t + 2.3) < -0.15;  g.depth(behind ? 2 : -2, () => g.detached("wisp", () => wispBlob(g, x, y, tail, Math.sin(TAU * t * 4) > 0)));}// ---- rise -----------------------------------------------------------------------------------------------------------const RISE = 10;const BULGE = [0, 0, 1, 2, 3, 1, 1, 0, 0, 0];function clods(g, f) {  // [x0, vx, peak height, landing y]  const C = [[22, -7, 8, 38], [25, 7, 10, 39], [23, -2, 12, 36], [21, -11, 5, 42], [26, 10, 6, 42]];  if (f < 4 || f > 7) return;  const p = (f - 3) / 4;                                        // 0.25 .. 1  g.outlined("ink", () => g.part("clods", () => {    C.forEach(([x0, vx, hp, ly], i) => {      const x = Math.round(x0 + vx * p), y = Math.round(37 + (ly - 37) * p - hp * 4 * p * (1 - p));      if (f === 7) { g.pixel(x, y, "w2"); g.pixel(x + 1, y, "w1"); return; }   // landed crumbs      g.rect({ x: [x, x + 2], y: [y, y + 1] }, "w1", { fill: "w1" });      g.pixel(x, y, "w3"); g.pixel(x + 1, y, "w2"); g.pixel(x + 2, y + 1, "w0");      if (i < 3) { g.pixel(x + 1, y - 1, "w2"); g.pixel(x + 1, y + 2, "w0"); }      if (i === 1) g.pixel(x + 2, y - 1, "e1");                  // a clod still wearing its turf    });  }));  if (f <= 6) g.detached("dust", () => {    const r = [0, 0, 0, 0, 4, 7, 9][f];    for (let a = 0; a < 12; a++) {      const x = Math.round(24 + r * Math.cos(TAU * a / 12)), y = Math.round(38 + r * 0.45 * Math.sin(TAU * a / 12) - (f - 3));      if (chk(x, y) || f === 4) g.pixel(x, y, f === 6 ? "n2" : "n3");    }  });}function stateAt(state, t) {  t = ((t % 1) + 1) % 1;  if (state === "rise") {    const f = Math.min(RISE - 1, Math.round(t * RISE));    if (f === 0 || f === RISE - 1) return { state: "idle", t: 0, f: -1 };    return { state: "rise", t: 0, f };  }  return { state, t, f: -1 };}const Tombstone = sprite(pal, (g, t, state) => {  const S = stateAt(state, t);  const cracked = S.state === "cracked";  const pulse = 0.5 + 0.5 * Math.cos(TAU * S.t);  const f = S.f;  contactShadow(g, CX, GROUND - 1, 17, 2);  const shake = f === 2 || f === 4 ? 1 : f === 3 ? -1 : 0;  g.outlined("ink", () => {    if (cracked) g.at(23, BASE, () => g.rotate(8, () => g.at(-23, -BASE, () => stone(g, true, pulse))));    else g.at(shake, 0, () => stone(g, false, pulse));  });  g.outlined("ink", () => mound(g, f > 0 ? BULGE[f] : 0));  cracks(g, f);  if (f === 4 || f === 5) g.part("burst", () => {    // the earth tears open where the clods came from    g.line([22, 37 - BULGE[f]], [26, 37 - BULGE[f]], "ink"); g.line([23, 38 - BULGE[f]], [25, 38 - BULGE[f]], "w0");  });  grass(g, f > 0 ? 0 : S.t);  if (cracked) {    g.part("fallenChip", () => { g.pixel(33, 33, "n3"); g.pixel(34, 33, "n2"); g.pixel(33, 34, "n2"); g.pixel(34, 34, "n1"); });    g.detached("motes", () => {      for (let k = 0; k < 4; k++) {        const p = (S.t * 2 + k / 4) % 1;        const x = 28 + [-1, 1, 0, 2][k] + Math.round(Math.sin(TAU * (p + k * 0.3)) * 1.5);        const y = 12 + k * 5 - Math.round(p * 8);        if (p < 0.4) g.pixel(x, y, "v3"); else if (p < 0.8) g.pixel(x, y, "v2");      }    });    g.part("seep", () => {      // the glow reaches the ground at the stone's foot      const k = pulse > 0.5 ? "v2" : "v1";      g.pixel(24, 35, k); g.pixel(23, 36, "v1"); g.pixel(25, 36, "v1");      if (pulse > 0.66) g.pixel(24, 36, "v3");    });  } else if (f < 0) {    wisp(g, S.t);  }  if (f > 0) {    clods(g, f);    if (f >= 5) {      // the wisp climbs out of the burst point towards its idle frame-0 spot      const [ex, ey] = wispAt(0), k = (f - 4) / 5;      const x = 24 + (ex - 24) * k, y = 37 + (ey - 37) * k;      g.depth(-2, () => g.detached("wisp", () => wispBlob(g, x, y, [[x + (24 - ex) * 0.12, y + 2], [x + (24 - ex) * 0.2, y + 4]], true)));    }  }});submit(Tombstone, {  size: [W, H], origin: "top-left", kind: "icon", scale: 1,  animate: { fps: 10, duration: 1.6, loop: true },  anchor: { x: CX, y: GROUND },  args: ["idle"],  variants: [    { name: "idle" },    { name: "cracked", args: ["cracked"] },    { name: "rise", args: ["rise"], animate: { fps: 10, duration: 1.0, loop: false } },  ],});

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.