Open grave
A 64×48 floor prop: a freshly dug rectangular pit with a trampled earth rim, a far wall of cut soil strata with roots and black depth, the spoil heap beside it with a spade stuck in its crown, and a small iron lantern hung on the spade’s grip. “idle” (12 frames) trickles loose soil off the heap into the pit while the lantern flame flickers warm; “dig” (10 frames, once) has the spade pull itself out, bite into the pit, fling a clod onto the heap and plant itself again, 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.
// Open grave — 64x48 floor prop, top-down 3/4, key light top-left.// A freshly dug rectangular pit (trampled earth rim, a far wall of cut soil strata with roots, black depth),// the spoil heap beside it with a spade stuck in its crown, and a small iron lantern hung on the spade's grip.// Variants:// idle 12f loop loose soil trickles off the heap into the pit, the lantern flame flickers warm// dig 10f once the spade pulls itself out, bites into the pit, flings a clod onto the heap and plants// itself again; ends on idle frame 0The last lines. submit declares the canvas and lists the variants the renderer exports.
submit(OpenGrave, { size: [W, H], origin: "top-left", kind: "icon", scale: 1, animate: { fps: 10, duration: 1.2, loop: true }, anchor: { x: CX, y: GROUND }, args: ["idle"], variants: [ { name: "idle" }, { name: "dig", args: ["dig"], animate: { fps: 10, duration: 1.0, loop: false } }, ],});Show the whole program (244 lines)
// Open grave — 64x48 floor prop, top-down 3/4, key light top-left.// A freshly dug rectangular pit (trampled earth rim, a far wall of cut soil strata with roots, black depth),// the spoil heap beside it with a spade stuck in its crown, and a small iron lantern hung on the spade's grip.// Variants:// idle 12f loop loose soil trickles off the heap into the pit, the lantern flame flickers warm// dig 10f once the spade pulls itself out, bites into the pit, flings a clod onto the heap and plants// itself again; ends on idle frame 0const W = 64, H = 48, CX = 32, GROUND = 45;const PIT = { x0: 5, x1: 31, y0: 20, y1: 36 }; // the openingconst WALL = 11; // far wall height seen in the openingconst RIM = 2; // trampled earth band around the opening// spoil heap: overlapping lumps [cx, cy, rx, ry, light bias]const LUMPS = [[47, 35, 12, 7, 0.12], [42, 30, 6, 4, 0.1], [49, 28, 6, 5, 0.1], [55, 32, 5, 4, 0], [39, 36, 5, 4, 0.05], [53, 38, 7, 4, 0]];const REST = { x: 48, y: 29, a: -23 }; // spade: blade tip position and handle angle (deg) when plantedconst DIRT = ["w0", "w1", "w2", "w3"];const inOpen = (x, y) => x >= PIT.x0 && x <= PIT.x1 && y >= PIT.y0 && y <= PIT.y1;const inRimBand = (x, y) => { if (inOpen(x, y)) return false; const dx = Math.max(PIT.x0 - RIM - x, x - PIT.x1 - RIM, 0), dy = Math.max(PIT.y0 - RIM - y, y - PIT.y1 - RIM - 1, 0); if (dx > 0 || dy > 0) return false; // clip the corners so the band reads as dug earth, not a picture frame const cx = x < PIT.x0 ? PIT.x0 - x : x > PIT.x1 ? x - PIT.x1 : 0, cy = y < PIT.y0 ? PIT.y0 - y : y > PIT.y1 ? y - PIT.y1 : 0; if (cx + cy > RIM + 1) return false; // a ragged outer edge: some columns / rows lose their outermost pixel if ((y === PIT.y0 - RIM || y === PIT.y1 + RIM + 1) && Math.sin(x * 1.7) > 0.55) return false; if ((x === PIT.x0 - RIM || x === PIT.x1 + RIM) && Math.sin(y * 2.1) > 0.6) return false; return true;};// ---- pit ------------------------------------------------------------------------------------------------------function pitTone(x, y) { const d = y - PIT.y0; // depth down the far wall const sh = x - PIT.x0 < 2 + d * 0.45 ? 1 : 0; // the left rim throws a shadow down and to the right if (d < WALL) { // cut soil strata with wavy seams: dark topsoil, a paler clay band, then earth fading into the dark const b = d + Math.round(Math.sin(x * 0.7) * 0.6 + Math.sin(x * 0.23 + 1) * 0.8); let i = b < 2 ? 1 : b < 4 ? 2 : b < 6 ? 1 : b < 8 ? 0 : -1; i -= sh; if (i >= 0) return DIRT[i]; return d >= WALL - 2 ? "ink" : "n0"; } return "ink"; // the bottom of the grave}function rimTone(x, y) { const inner = inOpen(x - 1, y) || inOpen(x + 1, y) || inOpen(x, y - 1) || inOpen(x, y + 1); const near = y > PIT.y1, far = y < PIT.y0, left = x < PIT.x0; if (inner && far) return "w3"; // the far lip catches the light if (inner && left) return "w3"; if (inner && near) return "w1"; // near lip: we look over its dark edge if (far || left) return "w2"; if (near && y === PIT.y1 + RIM + 1) return "w0"; return "w1";}function pit(g) { g.part("rim", () => g.field({ x: [PIT.x0 - RIM - 1, PIT.x1 + RIM + 1], y: [PIT.y0 - RIM - 1, PIT.y1 + RIM + 1] }, (x, y) => inRimBand(x, y) ? rimTone(x, y) : null)); g.part("pit", () => g.field({ x: [PIT.x0, PIT.x1], y: [PIT.y0, PIT.y1] }, (x, y) => pitTone(x, y)));}function pitDetail(g) { g.part("roots", () => { // pale roots hanging out of the far wall, stones in the strata, clods on the bottom for (const [x, y, n] of [[12, 21, 3], [21, 21, 2], [27, 22, 3]]) for (let i = 0; i < n; i++) g.pixel(x + (i === n - 1 ? 1 : 0), y + i, i === 0 ? "w4" : "w3"); for (const [x, y] of [[16, 23], [24, 24], [29, 26]]) { g.pixel(x, y, "n3"); g.pixel(x + 1, y, "n1"); } for (const [x, y] of [[14, 34], [22, 33], [27, 35]]) g.pixel(x, y, "n0"); // vertical bites of the spade in the far wall: a shadowed stroke with a lit edge beside it for (const x of [10, 18, 25]) { g.line([x, PIT.y0 + 3], [x, PIT.y0 + 6], "w0"); g.pixel(x + 1, PIT.y0 + 3, "w3"); } }); g.part("turf", () => { // torn grass on the outer edge of the rim for (const [x, y, k] of [[4, 18, "e1"], [5, 17, "e2"], [9, 17, "e1"], [10, 17, "e2"], [18, 17, "e1"], [25, 17, "e1"], [26, 17, "e2"], [2, 25, "e1"], [2, 26, "e0"], [2, 32, "e1"], [8, 39, "e0"], [9, 39, "e1"], [20, 39, "e0"], [29, 39, "e1"]]) g.pixel(x, y, k); });}// ---- spoil heap -----------------------------------------------------------------------------------------------function heap(g) { g.part("heap", () => g.field({ x: [26, 62], y: [20, 43] }, (x, y) => { // a spoil heap built from overlapping lumps, each lit as an ellipsoid; clods and stones added as marks let i = -1; for (const [cx, cy, rx, ry, b] of LUMPS) i = Math.max(i, litIndex(x, y, cx, cy, rx, ry, 4, b)); if (i < 0) return null; return DIRT[i]; })); g.part("clods", () => { for (const [x, y] of [[41, 29], [53, 27], [44, 37], [56, 34], [49, 40], [37, 36]]) { g.pixel(x, y, "w3"); g.pixel(x + 1, y, "w1"); g.pixel(x, y + 1, "w1"); } for (const [x, y] of [[46, 33], [58, 31], [41, 40]]) { g.pixel(x, y, "n3"); g.pixel(x + 1, y, "n1"); } g.pixel(51, 38, "w0"); g.pixel(52, 38, "w0"); });}function hole(g, open) { // the slot the spade leaves in the crown when it is pulled out if (!open) return; g.part("slot", () => { g.line([REST.x - 2, REST.y - 2], [REST.x + 2, REST.y - 2], "w0"); g.line([REST.x - 1, REST.y - 1], [REST.x + 1, REST.y - 1], "ink"); g.pixel(REST.x - 2, REST.y - 1, "w3"); });}function trickle(g, t) { // three grains roll down the heap's left flank and drop into the pit const P = [[39, 30], [37, 31], [36, 32], [35, 33], [33, 33], [32, 34], [31, 35]]; g.detached("trickle", () => { for (let k = 0; k < 3; k++) { const p = (t + k / 3) % 1; if (p > 0.85) continue; const s = p / 0.85 * (P.length - 1), i = Math.floor(s); const [x, y] = P[i]; g.pixel(x, y, i < 3 ? "w3" : "w2"); if (i < 2 && k === 0) g.pixel(x + 1, y, "w1"); } });}// ---- spade and lantern ----------------------------------------------------------------------------------------------const rot = (px, py, a) => { const c = Math.cos(a * Math.PI / 180), s = Math.sin(a * Math.PI / 180); return [px * c - py * s, px * s + py * c]; };function spadeLocal(g, loaded) { // local frame: blade tip at the origin, handle up the -y axis g.part("blade", () => { g.fillPoly([[-2, -7], [2, -7], [2, -2], [1, 0], [-1, 0], [-2, -2]], "n2"); g.line([-2, -7], [-2, -2], "n4"); // lit left edge g.line([2, -7], [2, -2], "n1"); g.pixel(-1, -6, "n3"); g.pixel(1, -1, "n1"); g.line([-1, -8], [1, -8], "n1"); g.pixel(-1, -8, "n3"); // socket if (loaded) { g.rect({ x: [-2, 2], y: [-5, -3] }, "w2", { fill: "w2" }); g.pixel(-1, -5, "w3"); g.pixel(2, -3, "w0"); g.pixel(1, -6, "w1"); } }); g.part("shaft", () => { g.line([0, -9], [0, -21], "w3"); g.line([1, -9], [1, -21], "w1"); g.pixel(0, -14, "w4"); g.pixel(1, -17, "w0"); // T-grip: a short crossbar, lit on its left end g.line([0, -22], [1, -22], "w2"); g.rect({ x: [-2, 3], y: [-24, -23] }, "w2", { fill: "w2" }); g.line([-2, -24], [1, -24], "w3"); g.pixel(-2, -23, "w3"); g.line([2, -23], [3, -23], "w1"); });}function lanternAt(g, lx, ly, sw, t) { // hook, cap, glass with flame, base; sw = swing offset of the body (px) const flick = [0, 1, 0, 2, 1, 0, 1, 2, 0, 1, 0, 1][Math.floor(((t % 1) + 1) % 1 * 12) % 12]; const bx = lx + sw; g.outlined("ink", () => g.part("lantern", () => { g.line([lx, ly], [lx, ly + 1], "n2"); g.pixel(bx, ly + 2, "n3"); g.line([bx - 1, ly + 3], [bx + 1, ly + 3], "n3"); g.pixel(bx + 1, ly + 3, "n1"); g.line([bx - 2, ly + 4], [bx + 2, ly + 4], "n2"); g.pixel(bx - 2, ly + 4, "n4"); g.rect({ x: [bx - 1, bx + 1], y: [ly + 5, ly + 8] }, "r3", { fill: "r3" }); g.line([bx - 2, ly + 5], [bx - 2, ly + 8], "n2"); g.line([bx + 2, ly + 5], [bx + 2, ly + 8], "n1"); g.pixel(bx, ly + 6, "g3"); g.pixel(bx, ly + 7, flick === 2 ? "g3" : "g2"); if (flick === 1) g.pixel(bx, ly + 5, "g2"); if (flick === 2) g.pixel(bx - 1, ly + 8, "r2"); g.pixel(bx - 1, ly + 5, "g3"); // glint on the glass g.line([bx - 2, ly + 9], [bx + 2, ly + 9], "n1"); g.pixel(bx - 2, ly + 9, "n3"); })); g.detached("glow", () => { // a few warm sparks of halo beside the glass, breathing with the flicker const k = flick === 0 ? "r1" : "r2"; g.pixel(bx - 4, ly + 6, k); g.pixel(bx + 4, ly + 7, k); if (flick === 2) { g.pixel(bx - 4, ly + 8, "r1"); g.pixel(bx + 4, ly + 5, "r1"); } });}function spade(g, S, t) { const { x, y, a } = S; g.outlined("ink", () => g.at(x, y, () => g.rotate(a, () => spadeLocal(g, S.loaded)))); const [hx, hy] = rot(-2, -22, a); lanternAt(g, Math.round(x + hx), Math.round(y + hy) + 1, S.sw, t);}function collar(g) { // a little cone of soil heaped round the planted blade (covers its lower half) g.part("collar", () => g.field({ x: [REST.x - 5, REST.x + 5], y: [REST.y - 4, REST.y + 2] }, (x, y) => { const i = litIndex(x, y, REST.x + 0.5, REST.y - 0.5, 4.5, 3, 4, 0.15); return i < 0 ? null : DIRT[i]; }));}// ---- dig -------------------------------------------------------------------------------------------------------------const DIG = 10;// per frame: blade tip x, y, handle angle, loaded, lantern swingconst KEYS = [ null, [47, 25, -25, 0, 1], // pulls itself out [40, 29, -45, 0, 2], // tips over the pit [31, 32, -58, 0, 1], // bites into the pit [33, 27, -50, 1, -1], // lifts a load [42, 23, -32, 1, -2], // swings back towards the heap [51, 22, -42, 0, -1], // flings (the clod leaves the blade) [50, 26, -30, 0, 1], // comes down [48, 28, -24, 0, 0], // plants itself null,];function stateAt(state, t) { t = ((t % 1) + 1) % 1; if (state === "dig") { const f = Math.min(DIG - 1, Math.round(t * DIG)); if (f === 0 || f === DIG - 1) return stateAt("idle", 0); const [x, y, a, loaded, sw] = KEYS[f]; return { x, y, a, loaded: !!loaded, sw, f, t: f / DIG, planted: f === 8 }; } return { x: REST.x, y: REST.y, a: REST.a, loaded: false, sw: 0, f: -1, t, planted: true };}function flung(g, f) { // the clod arcs onto the heap's crown and breaks const path = { 6: [[54, 17, 3]], 7: [[56, 21, 3]], 8: [[55, 27, 2], [58, 29, 1], [52, 28, 1]] }[f]; if (!path) return; g.outlined("ink", () => g.part("clod", () => { for (const [x, y, s] of path) { if (s >= 2) { g.rect({ x: [x, x + s - 1], y: [y, y + 1] }, "w1", { fill: "w1" }); g.pixel(x, y, "w3"); g.pixel(x + s - 1, y + 1, "w0"); } else g.pixel(x, y, "w2"); } })); if (f === 8) g.detached("puff", () => { for (const [x, y] of [[53, 25], [58, 26], [60, 28], [51, 28]]) g.pixel(x, y, "n3"); });}function pitSpray(g, f) { // the blade's bite throws a few crumbs inside the pit if (f !== 3 && f !== 4) return; g.detached("spray", () => { const pts = f === 3 ? [[27, 28], [24, 27], [29, 26]] : [[26, 25], [23, 23], [30, 22]]; for (const [x, y] of pts) g.pixel(x, y, "w2"); });}const OpenGrave = sprite(pal, (g, t, state) => { const S = stateAt(state, t); const f = S.f; contactShadow(g, CX, GROUND - 1, 30, 2); g.outlined("ink", () => { pit(g); heap(g); }); pitDetail(g); hole(g, !S.planted); spade(g, S, S.t); if (S.planted) collar(g); // soil over the buried half of the blade trickle(g, f < 0 ? S.t : 0); if (f > 0) { flung(g, f); pitSpray(g, f); } if (S.planted) g.part("lampLight", () => { // the lantern's warm light on the heap's shoulder below it const on = Math.floor(S.t * 12) % 3 !== 1; for (const [x, y] of [[40, 27], [42, 26], [38, 29]]) g.pixel(x, y, on ? "w4" : "w3"); });});submit(OpenGrave, { size: [W, H], origin: "top-left", kind: "icon", scale: 1, animate: { fps: 10, duration: 1.2, loop: true }, anchor: { x: CX, y: GROUND }, args: ["idle"], variants: [ { name: "idle" }, { name: "dig", args: ["dig"], 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.