Holy font
A 48×48 floor prop: a carved stone basin with a broad lip, a moulded bowl and a sun boss on the front, on a fluted pedestal and a stepped plinth, filled with water. All three variants are 12-frame loops at 10 fps: “idle” has clear blue water with one slow ripple ring spreading per loop and glints twinkling; “glow” makes the water shine gold and cream, lighting the lip as motes of light rise; “cursed” turns it murky green, with bubbles that swell and pop and green slime running down the bowl.
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.
// Holy font — 48x48 floor prop, top-down 3/4, key light top-left.// A carved stone basin (broad lip, moulded bowl, a sun boss on the front) on a fluted pedestal and a stepped// plinth, filled with water.// Variants (all 12f loops @ 10 fps):// idle — clear blue water, one slow ripple ring spreads per loop, glints twinkle// glow — the water shines gold/cream and lights the lip, motes of light rise (healing / save point)// cursed — murky green water, bubbles swell and pop, green slime runs down the bowl, the boss is deadThe last lines. submit declares the canvas and lists the variants the renderer exports.
submit(Font, { size: [W, H], origin: "top-left", kind: "icon", scale: 1, animate: { fps: 10, duration: N / 10, loop: true }, anchor: { x: CX, y: GROUND }, variants: [ { name: "idle", args: ["idle"] }, { name: "glow", args: ["glow"] }, { name: "cursed", args: ["cursed"] }, ],});Show the whole program (206 lines)
// Holy font — 48x48 floor prop, top-down 3/4, key light top-left.// A carved stone basin (broad lip, moulded bowl, a sun boss on the front) on a fluted pedestal and a stepped// plinth, filled with water.// Variants (all 12f loops @ 10 fps):// idle — clear blue water, one slow ripple ring spreads per loop, glints twinkle// glow — the water shines gold/cream and lights the lip, motes of light rise (healing / save point)// cursed — murky green water, bubbles swell and pop, green slime runs down the bowl, the boss is deadconst W = 48, H = 48, CX = 24, GROUND = 45;const N = 12;const LIP = { cy: 15.5, rx: 16, ry: 6 };const HOLE = { cy: 15, rx: 13, ry: 4 };const WATER = { cy: 15.6, rx: 12, ry: 3.2 };const WR = { idle: ["b0", "b1", "b2", "b3"], glow: ["g1", "g2", "g3", "n5"], cursed: ["e0", "e1", "e2", "e3"] };const stone = (u) => (u < -0.6 ? "n4" : u < -0.1 ? "n3" : u < 0.5 ? "n2" : "n1");// ---- plinth & pedestal ------------------------------------------------------------------------------------function plinth(g) { g.part("plinth", () => { g.field({ x: [10, 37], y: [38, 44] }, (x, y) => { if ((x === 10 || x === 37) && y === 38) return null; // clipped corners of the octagon if (y === 38) return x < 30 ? "n4" : "n3"; if (y === 39) return x >= 26 ? (x > 33 ? "n1" : "n2") : "n3"; // the pedestal's shadow falls down-right if (y === 40) return x < 28 ? "n4" : "n3"; if (y === 44) return "n1"; if (x === 10) return "n3"; if (y === 42 && (x - 12) % 4 === 0 && x < 36) return "n1"; // carved notches return x > 31 ? "n1" : "n2"; }); g.pixel(35, 41, "n1"); g.pixel(34, 42, "n1"); // a chip g.pixel(11, 43, "e1"); g.pixel(12, 43, "e2"); g.pixel(11, 42, "e1"); // moss in the corner });}function pedestal(g) { const R = (y) => (y === 29 ? 5.5 : y >= 36 ? 6.5 : y === 35 ? 5.5 : 4.5); g.part("pedestal", () => g.field({ x: [16, 32], y: [28, 38] }, (x, y) => { const r = R(y), u = (x + 0.5 - CX) / r; if (Math.abs(u) > 1 || y === 28) return null; if (y === 38) return u < 0.3 ? "n1" : "n0"; if (y === 30) return u < 0.2 ? "n1" : "n0"; // shadow under the collar if (y === 29 || y >= 35) return stone(u - 0.1); if ((x === 22 || x === 26) ) return x === 22 ? "n2" : "n0"; // flutes return stone(u); }));}// ---- basin ------------------------------------------------------------------------------------------------const bowlR = (y) => { const k = (y - 16) / 13.5; return k < 0 ? 16 : Math.max(5.5, 16 * Math.sqrt(Math.max(0, 1 - k * k))); };function bowl(g, s) { g.part("bowl", () => g.field({ x: [7, 41], y: [16, 29] }, (x, y) => { const r = bowlR(y), u = (x + 0.5 - CX) / r; if (Math.abs(u) > 1) return null; const nx = (x + 0.5 - CX) / LIP.rx, ny = (y + 0.5 - LIP.cy) / LIP.ry; if (nx * nx + ny * ny <= 1) return null; // lip top drawn separately if (s.slime && slimeAt(x, y)) return slimeAt(x, y); const ey = y - (16 + 6 * Math.sqrt(Math.max(0, 1 - nx * nx))); // rows below the lip's front edge if (ey < 1.2) return stone(u - 0.25); // the lip's thick edge, lit if (ey < 2.2) return u < 0.3 ? "n1" : "n0"; // undercut beneath the lip const li = litIndex(x, y, CX, 13, 16.5, 16, 4, 0.12 - Math.max(0, y - 25) * 0.07); const base = ["n1", "n2", "n3", "n4"][Math.max(0, li)]; const dn = { n4: "n3", n3: "n2", n2: "n1", n1: "n0", n0: "n0" }; // a carved moulding round the belly: a groove with a lit bead beneath it if (y === 24 && Math.abs(u) < 0.97) return dn[base]; if (y === 25 && Math.abs(u) < 0.97) return u < 0.35 ? { n1: "n2", n2: "n3", n3: "n4", n4: "n4" }[base] : base; return base; }));}function slimeAt(x, y) { // green runs down the bowl from the lip (cursed) const runs = [[14, 24], [19, 27], [31, 22]]; for (const [rx, end] of runs) { if (x === rx && y <= end) return y === end ? "e3" : y === end - 1 ? "e2" : "e1"; if (x === rx + 1 && y >= end - 1 && y <= end) return "e1"; // the drop swells at the tip } return null;}function lip(g, s) { const lit = s.state === "glow"; g.part("lip", () => g.field({ x: [CX - LIP.rx, CX + LIP.rx], y: [9, 22] }, (x, y) => { const nx = (x + 0.5 - CX) / LIP.rx, ny = (y + 0.5 - LIP.cy) / LIP.ry; if (nx * nx + ny * ny > 1) return null; const hx = (x + 0.5 - CX) / HOLE.rx, hy = (y + 0.5 - HOLE.cy) / HOLE.ry; if (hx * hx + hy * hy <= 1) return null; // the hollow, drawn by basin() if (lit && hy > 0.2 && hx * hx + hy * hy < 1.25) return "n5"; // the water's light on the near inner rim if (s.slime && hy < 0 && hx * hx + hy * hy < 1.3 && hash(x, y) > 0.55) return "e1"; const a = -0.7 * nx - 0.3 * ny; return a > 0.45 ? "n4" : a > 0.0 ? "n3" : a > -0.45 ? "n2" : "n1"; }));}function basin(g, s, t) { const R = WR[s.state]; g.part("water", () => g.field({ x: [CX - HOLE.rx, CX + HOLE.rx], y: [10, 20] }, (x, y) => { const hx = (x + 0.5 - CX) / HOLE.rx, hy = (y + 0.5 - HOLE.cy) / HOLE.ry; if (hx * hx + hy * hy > 1) return null; const wx = (x + 0.5 - CX) / WATER.rx, wy = (y + 0.5 - WATER.cy) / WATER.ry, wr = Math.sqrt(wx * wx + wy * wy); if (wr > 1) { // inner wall above the water; right side lit if (s.state === "glow") return hx > 0.3 ? "n4" : "n3"; return hx > 0.35 ? "n2" : hx > -0.3 ? "n1" : "n0"; } if (s.state === "cursed") { // murk: slow-turning blotches of scum on dark water const a = Math.atan2(wy, wx) - TAU * t; const m = Math.sin(2 * a + wr * 5) + (wy > 0.55 ? 0.6 : 0); if (wy < -0.5) return "e0"; return m > 1.0 ? "e2" : m > 0.1 ? "e1" : "e0"; } // ripple ring spreading from the centre, one per loop const p = 0.1 + 0.9 * t; if (Math.abs(wr - p) < 0.12 && p < 0.95 && (p < 0.6 || wy > 0)) return s.state === "glow" ? "n5" : p < 0.35 ? R[3] : R[2]; if (s.state === "glow") { if (wy < -0.45) return R[0]; return wr < 0.25 ? R[3] : wr < 0.6 ? R[2] : R[1]; } if (wy < -0.45) return R[0]; // far edge in the lip's shadow if (wy > 0.6 && Math.abs(wx) < 0.8) return R[2]; // near meniscus return R[1]; })); // glints of the key light, twinkling in turn if (s.state !== "cursed") g.detached("glints", () => { const f = Math.round(t * N) % N; const G = [[17, 14, 0], [29, 16, 4], [21, 17, 8]]; for (const [x, y, ph] of G) { const k = (f - ph + N) % N; if (k === 0 || k === 2) g.pixel(x, y, R[3] === "n5" ? "n5" : R[3]); if (k === 1) { g.pixel(x, y, "n5"); g.pixel(x - 1, y, R[3]); g.pixel(x + 1, y, R[3]); } } });}function boss(g, s, t) { // a small sun boss on the front of the bowl const cx = CX, cy = 21; const hot = s.state === "glow" ? (Math.cos(TAU * t) > 0 ? "g3" : "g2") : null; g.part("boss", () => { const dead = s.state === "cursed"; for (const [dx, dy] of [[0, -2], [0, 2], [-2, 0], [2, 0]]) g.pixel(cx + dx, cy + dy, dead ? "n1" : hot ? "g1" : "n1"); g.rect({ x: [cx - 1, cx], y: [cy - 1, cy] }, "n3", { fill: "n3" }); g.pixel(cx - 1, cy - 1, "n4"); g.pixel(cx, cy, "n1"); if (hot) { g.pixel(cx - 1, cy - 1, "g3"); g.pixel(cx, cy - 1, hot); g.pixel(cx - 1, cy, hot); g.pixel(cx, cy, "g1"); } if (dead) { g.pixel(cx, cy - 1, "e1"); g.pixel(cx, cy, "e0"); } });}// ---- effects ----------------------------------------------------------------------------------------------function motes(g, t) { g.detached("motes", () => { for (let i = 0; i < 5; i++) { const p = (t + i / 5) % 1; const x = CX + [-8, -3, 2, 6, 9][i] + Math.round(Math.sin(TAU * (p * 0.9 + i * 0.37)) * 1.5); const y = 15 - Math.round(p * 15); if (p < 0.3) { g.pixel(x, y, "n5"); g.pixel(x, y + 1, "g3"); g.pixel(x, y + 2, "g2"); } else if (p < 0.6) { g.pixel(x, y, "g3"); g.pixel(x, y + 1, "g2"); } else if (p < 0.85) g.pixel(x, y, "g2"); else if (chk(x, y)) g.pixel(x, y, "g1"); } // a sparkle blinking over the water const f = Math.round(t * N) % N; const [sx, sy] = [[20, 11], [27, 9], [24, 12], [30, 11]][Math.floor(f / 3)]; if (f % 3 === 1) { g.pixel(sx, sy, "n5"); for (const [dx, dy] of [[0, -1], [0, 1], [-1, 0], [1, 0]]) g.pixel(sx + dx, sy + dy, "g3"); } else if (f % 3 === 0) g.pixel(sx, sy, "g3"); });}function bubbles(g, t) { const spots = [[-6, 0, 0.0], [3, 1, 0.33], [7, -1, 0.66], [-1, -1, 0.5]]; g.detached("bubbles", () => { for (const [dx, dy, ph] of spots) { const p = (t + ph) % 1, x = CX + dx, y = Math.round(WATER.cy) + dy; if (p < 0.2) g.pixel(x, y, "e2"); else if (p < 0.45) { g.rect({ x: [x - 1, x], y: [y - 1, y] }, "e2", { fill: "e2" }); g.pixel(x - 1, y - 1, "e3"); } else if (p < 0.6) { g.ellipse({ x: [x - 2, x + 1], y: [y - 2, y] }, "e1", { fill: "e2" }); g.pixel(x - 1, y - 2, "e3"); } else if (p < 0.72) for (const [ox, oy] of [[-2, 0], [2, 0], [0, -2], [-1, 1]]) g.pixel(x + ox, y + oy, "e3"); } // a sickly fume rising off the water for (let i = 0; i < 3; i++) { const p = (t + i / 3) % 1; const x = CX + [-4, 1, 6][i] + Math.round(Math.sin(TAU * (p + i * 0.4)) * 2), y = 12 - Math.round(p * 11); if (p < 0.4) g.pixel(x, y, "e2"); else if (p < 0.75 && chk(x, y)) g.pixel(x, y, "e1"); } });}// ---- sprite -----------------------------------------------------------------------------------------------const Font = sprite(pal, (g, t, state) => { const s = { state, slime: state === "cursed" }; contactShadow(g, CX, GROUND, 15, 2); g.outlined("ink", () => { plinth(g); pedestal(g); bowl(g, s); lip(g, s); }); basin(g, s, t); boss(g, s, t); if (state === "glow") motes(g, t); if (state === "cursed") bubbles(g, t);});submit(Font, { size: [W, H], origin: "top-left", kind: "icon", scale: 1, animate: { fps: 10, duration: N / 10, loop: true }, anchor: { x: CX, y: GROUND }, variants: [ { name: "idle", args: ["idle"] }, { name: "glow", args: ["glow"] }, { name: "cursed", args: ["cursed"] }, ],});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.