Tombstone
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”.
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.
// 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 0The last lines. submit declares the canvas and lists the variants the renderer exports.
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 — 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.