Crypt door
A 64×64 floor prop facing the camera: the stone facade of a small mausoleum with a pediment carrying a carved winged skull, two columns on a stepped plinth, a keystone with a violet sigil, and heavy double doors of iron-banded planks with ring pulls. “closed” (12 frames) creeps pale fog along the doorsill while the sigil pulses; “open” (10 frames, once) flares the seal and swings the doors inward with an overshoot as grit falls from the lintel and a soulfire on an altar fades in; “open-idle” (12 frames) keeps the soulfire breathing while fog spills over the steps.
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.
// Crypt door — 64x64 floor prop, top-down 3/4, key light top-left. Camera-facing.// The stone facade of a small mausoleum: a pediment with a carved winged skull under an orb finial, a dentilled// cornice, two columns on a stepped plinth, an ashlar wall with a moulded door surround, and a keystone carrying a// violet sigil. Heavy double doors of iron-banded planks with ring pulls.// Variants:// closed 12f loop pale fog creeps out along the doorsill, the sigil on the keystone pulses// open 10f once the seal flares, the doors jolt and swing inward (overshoot, settle), grit falls from the// lintel; the dark tomb and a soulfire on an altar fade in; ends on open-idle frame 0// open-idle 12f loop doors open, the soulfire breathes on its altar, fog spills out over the stepsThe last lines. submit declares the canvas and lists the variants the renderer exports.
submit(Crypt, { size: [W, H], origin: "top-left", kind: "icon", scale: 1, animate: { fps: 10, duration: N_IDLE / 10, loop: true }, anchor: { x: 32, y: GROUND }, args: ["closed"], variants: [ { name: "closed", args: ["closed"] }, { name: "open", args: ["open"], animate: { fps: 10, duration: N_OPEN / 10, loop: false } }, { name: "open-idle", args: ["open-idle"] }, ],});Show the whole program (386 lines)
// Crypt door — 64x64 floor prop, top-down 3/4, key light top-left. Camera-facing.// The stone facade of a small mausoleum: a pediment with a carved winged skull under an orb finial, a dentilled// cornice, two columns on a stepped plinth, an ashlar wall with a moulded door surround, and a keystone carrying a// violet sigil. Heavy double doors of iron-banded planks with ring pulls.// Variants:// closed 12f loop pale fog creeps out along the doorsill, the sigil on the keystone pulses// open 10f once the seal flares, the doors jolt and swing inward (overshoot, settle), grit falls from the// lintel; the dark tomb and a soulfire on an altar fade in; ends on open-idle frame 0// open-idle 12f loop doors open, the soulfire breathes on its altar, fog spills out over the stepsconst W = 64, H = 64, CX = 31.5, GROUND = 61;const DX0 = 22, DX1 = 41, DY0 = 31, DY1 = 53; // doorway (inclusive)const LEAF = 10, RISE = 4; // leaf width, how far a fully turned free edge rises (depth)const SLOPE = 2.1, APEX = 4.5; // pediment rakeconst N_IDLE = 12, N_OPEN = 10, REST = 80;const OPEN_TH = [0, 3, 9, 21, 37, 53, 67, 79, 85, REST];const WOOD = ["w0", "w1", "w2", "w3", "w4"], STONE = ["n0", "n1", "n2", "n3", "n4"];const clampI = (i, n) => Math.max(0, Math.min(n - 1, i));function dim(key, k) { // step a key down its own ramp if (!k) return key; const r = key[0] === "w" ? WOOD : key[0] === "n" ? STONE : null; if (!r) return key; const i = r.indexOf(key); return i < 0 ? key : (i - k < 0 ? "ink" : r[i - k]);}const BAYER = [[0, 8, 2, 10], [12, 4, 14, 6], [3, 11, 1, 9], [15, 7, 13, 5]];const thr = (x, y) => (BAYER[y & 3][x & 3] + 0.5) / 16;// ---- facade regions ----------------------------------------------------------------------------------------const pedEdge = (x) => APEX + Math.abs(x + 0.5 - 32) / SLOPE; // top edge of the raking cornicefunction region(x, y) { if (y <= 5 && x >= 29 && x <= 34) return "finial"; if (y >= 5 && y <= 16 && y >= pedEdge(x) - 0.2) return "pediment"; if (y >= 17 && y <= 21 && x >= 5 && x <= 58) return y >= 20 && (x < 7 || x > 56) ? null : "entablature"; if (y >= 54 && y <= 57 && x >= 9 && x <= 54) return "steps"; if (y >= 58 && y <= 60 && x >= 5 && x <= 58) return "steps"; const inCol = (a) => (y >= 22 && y <= 24 && x >= a - 2 && x <= a + 6) || (y >= 50 && y <= 53 && x >= a - 2 && x <= a + 6) || (y >= 25 && y <= 49 && x >= a && x <= a + 4); if (inCol(10) || inCol(49)) return "columns"; if (y >= 22 && y <= 53 && x >= 15 && x <= 48) { if (x >= DX0 && x <= DX1 && y >= DY0) return null; // doorway: interior / leaves return "wall"; } return null;}function finialTone(x, y) { if (y === 5) return x === 30 || x === 33 ? null : x === 31 ? "n3" : "n2"; const i = litIndex(x, y, 32, 2.7, 2.6, 2.6, 4, 0.1); if (i < 0) return null; return x === 31 && y === 1 ? "n5" : STONE[i + 1];}function pedTone(x, y) { const dv = y + 0.5 - pedEdge(x), left = x < 32; if (dv < 1.1) return left ? "n4" : "n3"; // top of the raking cornice if (dv < 2.4) return left ? "n3" : "n2"; // its front fascia if (dv < 3.3) return "n0"; // shadow cast into the recessed tympanum if (y === 16) return "n0"; return (x + y * 3) % 17 === 0 ? "n0" : "n1"; // tympanum, a few pits}function entTone(x, y) { if (y === 17) return x < 8 || x > 55 ? "n4" : x < 32 ? "n4" : "n3"; if (y === 18) return x === 5 ? "n4" : x === 58 ? "n2" : "n3"; if (y === 19) return x < 7 || x > 56 ? "n1" : (x & 1) ? "n1" : "n3"; // dentils if (y === 20) return x === 7 ? "n4" : "n3"; return x === 56 ? "n1" : "n2";}function colTone(x, y) { const a = x < 32 ? 10 : 49, u = x - a; if (y <= 24) { // capital: abacus + echinus if (y === 22) return u === -2 ? "n4" : u === 6 ? "n2" : "n3"; if (y === 23) return u <= 0 ? "n4" : u <= 3 ? "n3" : "n2"; return u < -1 || u > 5 ? null : u <= 1 ? "n2" : "n1"; } if (y >= 50) { // base: torus + plinth if (y === 50) return u < -1 || u > 5 ? null : u <= 1 ? "n4" : u <= 3 ? "n3" : "n2"; if (y === 51) return u <= -1 ? "n3" : u <= 3 ? "n2" : "n1"; if (y === 52) return u === -2 ? "n4" : u === 6 ? "n1" : "n3"; return u === 6 ? "n0" : "n2"; } let k = ["n3", "n4", "n3", "n2", "n1"][u]; if (y === 25) k = dim(k, 1); // shadow under the capital if (u === 2 && (y % 6 === 3)) k = "n2"; // weathered flute marks return k;}function wallTone(x, y) { if (y === 22) return "n0"; // deep shadow under the frieze if (y === 23) return "n1"; if (x === 15 || (x === 16 && y > 24)) return "n1"; // the left column's shadow // door surround: jambs and lintel with a keystone if (y >= 25 && y <= 30 && x >= 29 && x <= 34) { if (y === 25) return x === 29 ? "n4" : x === 34 ? "n2" : "n4"; return x === 29 ? "n4" : x === 34 ? "n1" : "n3"; } if (y >= 27 && y <= 30 && x >= 19 && x <= 44) { if (y === 27) return x === 44 ? "n2" : "n4"; if (y === 30) return x >= DX0 && x <= DX1 ? "n1" : "n2"; return x === 19 ? "n4" : x === 44 ? "n1" : "n3"; } if (y >= 31 && (x === 20 || x === 21 || x === 42 || x === 43)) { if (x === 20 || x === 42) return "n4"; return "n2"; } if (y >= 31 && (x === 19 || x === 44)) return x === 19 ? "n2" : "n0"; // surround's shadowed return // ashlar: 4-row courses, staggered joints const r = Math.floor((y - 24) / 4), k = (y - 24) % 4; if (k === 3) return "n1"; const bx = x + (r & 1) * 3; if (bx % 7 === 0) return "n1"; const dark = hash(Math.floor(bx / 7), r) < 0.3; if (k === 0) return dark ? "n2" : "n3"; return dark ? "n1" : "n2";}function stepTone(x, y) { if (y === 54) return x === 9 ? "n3" : (x >= DX0 && x <= DX1) ? "n3" : "n2"; // back of the upper tread, sill if (y === 55) return x === 54 ? "n3" : "n4"; if (y === 56) return x === 9 ? "n3" : "n2"; if (y === 57) return "n1"; if (y === 58) return x < 9 || x > 54 ? (x === 5 ? "n4" : "n4") : "n3"; if (y === 59) return x === 5 ? "n3" : x === 58 ? "n1" : "n2"; return "n1";}const TONES = { finial: finialTone, pediment: pedTone, entablature: entTone, columns: colTone, wall: wallTone, steps: stepTone };function facade(g) { for (const name of ["finial", "pediment", "entablature", "wall", "columns", "steps"]) g.part(name, () => g.field({ x: [0, W - 1], y: [0, H - 1] }, (x, y) => (region(x, y) === name ? TONES[name](x, y) : null)));}function carvings(g) { // winged skull in the tympanum (relief: lit upper-left edges, shadowed lower-right) g.part("skull", () => { const S = [ "..####..", ".######.", "#XX##XX#", "#XX##XX#", ".##NN##.", "..TTTT..", ]; S.forEach((row, j) => [...row].forEach((c, i) => { const x = 28 + i, y = 10 + j; if (c === ".") return; let k; if (c === "X") k = (j === 2 && (i === 1 || i === 5)) ? "n0" : "ink"; else if (c === "N") k = i === 3 ? "n1" : "n0"; else if (c === "T") k = (i & 1) ? "n2" : "n4"; else k = (i <= 2 && j <= 1) || i === 0 ? "n5" : i >= 6 || j >= 4 ? "n3" : "n4"; g.pixel(x, y, k); })); for (const [x, y] of [[36, 12], [36, 13], [35, 14], [34, 15], [34, 16], [33, 16], [32, 16], [31, 16], [30, 16]]) g.pixel(x, y, "n0"); const wing = (s) => { // a swept wing of three feathers: lit top edge, relief face, hard shadow underneath const sx = (d) => s < 0 ? 27 - d : 36 + d; const rows = [[12, 0, 3], [13, 0, 6], [14, 0, 8], [15, 1, 7]]; for (const [y, d0, d1] of rows) for (let d = d0; d <= d1; d++) { let k = "n3"; if (y === 12 || (y === 13 && d >= 4) || (y === 14 && d >= 7)) k = s < 0 ? "n4" : "n3"; if (y === 15) k = d === 3 || d === 6 ? "n0" : "n1"; if (y === 14 && (d === 3 || d === 6) ) k = "n2"; g.pixel(sx(d), y, k); } g.pixel(sx(8), 15, "n0"); }; wing(-1); wing(1); }); g.part("weathering", () => { // rain streaks from the cornice, a crack, moss at the feet for (const [x, y0, n] of [[17, 24, 5], [46, 24, 3], [24, 24, 2]]) for (let i = 0; i < n; i++) g.pixel(x, y0 + i, i === n - 1 ? "n2" : "n1"); g.pixel(52, 30, "n1"); g.pixel(51, 31, "n1"); g.pixel(51, 32, "n2"); g.pixel(52, 33, "n1"); // crack in the right shaft g.pixel(45, 14, "n2"); // pits in the tympanum for (const [x, y, k] of [[8, 53, "e1"], [9, 53, "e2"], [10, 53, "e1"], [8, 52, "e1"], [47, 53, "e1"], [48, 53, "e2"], [9, 55, "e1"], [10, 54, "e2"], [11, 54, "e1"], [53, 56, "e1"], [54, 56, "e1"], [5, 59, "e1"], [6, 58, "e2"], [7, 58, "e1"], [57, 59, "e1"], [58, 59, "e1"], [16, 51, "e1"], [16, 52, "e2"], [15, 53, "e1"]]) g.pixel(x, y, k); g.pixel(40, 59, "n1"); g.pixel(41, 59, "n0"); g.pixel(19, 56, "n1"); // chips in the steps });}function sigil(g, pulse) { // a diamond rune cut into the keystone; the whole face tints violet at the peak g.part("sigil", () => { if (pulse > 0.75) for (const [x, y] of [[30, 26], [33, 26], [30, 30], [33, 30], [31, 26], [32, 30]]) g.pixel(x, y, "v0"); const rim = pulse > 0.6 ? "v2" : "v1", core = pulse > 0.75 ? "v3" : pulse > 0.35 ? "v2" : "v1"; g.pixel(31, 27, rim); g.pixel(32, 27, rim); g.pixel(30, 28, rim); g.pixel(33, 28, rim); g.pixel(31, 28, core); g.pixel(32, 28, core); g.pixel(31, 29, rim); g.pixel(32, 29, rim); if (pulse < 0.3) { g.pixel(31, 27, "v0"); g.pixel(32, 29, "v0"); } });}// ---- inside ---------------------------------------------------------------------------------------------------function interior(g, glow, ft, R) { // the tomb: black depth, a floor that catches the soulfire, an altar at the back wall const fx = 31.5, fy = 37; g.part("interior", () => g.field({ x: [DX0, DX1], y: [DY0, DY1] }, (x, y) => { const d = Math.hypot(x + 0.5 - fx, (y + 0.5 - fy) * 1.25); const r = R * glow; if (y >= 47) { // floor const fd = Math.hypot(x + 0.5 - fx, (y - 47) * 2.2); if (glow > 0.2 && fd < r * 0.8) return y === 47 ? "b1" : chk(x, y) ? "b0" : "n0"; if (glow > 0.2 && fd < r * 1.2) return chk(x, y) ? "n0" : "ink"; return "ink"; } if (glow > 0.05 && d < r * 0.45) return "b0"; if (glow > 0.05 && d < r * 0.75) return "v0"; if (glow > 0.05 && d < r * 0.95) return chk(x, y) ? "v0" : "ink"; return "ink"; })); if (glow > 0.2) g.part("altar", () => { { const k = glow > 0.6 ? "b1" : "n1"; g.rect({ x: [28, 35], y: [42, 42] }, k, { fill: k }); } g.rect({ x: [28, 35], y: [43, 46] }, "n0", { fill: "n0" }); g.line([28, 43], [35, 43], "b0"); g.pixel(28, 42, "n2"); g.pixel(35, 42, "n1"); }); if (glow > 0.35) g.detached("soulfire", () => { const h = Math.round((9 + 1.5 * Math.sin(TAU * ft)) * Math.min(1, glow * 1.2)); flame(g, 32, 41, 6, h, ft, 0.2, ["b1", "b2", "b3", "n5"]); flame(g, 29, 41, 3, Math.max(2, h - 4), ft, 0.65, ["b1", "b2", "b3", "n5"]); // a wisp drifting up const p = ft % 1; if (p < 0.7) g.pixel(31 + Math.round(Math.sin(TAU * p) * 2), 33 - Math.round(p * 3), p < 0.35 ? "b3" : "b2"); });}function leaves(g, th, lit) { // two leaves hinged at the jambs, swinging inward; the free edge rises as it recedes const a = th * Math.PI / 180; const w = Math.max(1, Math.round(LEAF * Math.cos(a))); const dy = RISE * Math.sin(a); const tex = (sx, ly, left) => { // closed texture at screen column sx, row ly const lx = left ? sx - DX0 : DX1 - sx; // 0 at the hinge if (ly === DY1) return "w0"; // strap hinges: two rows from the hinge, ending in a spear tip near the meeting stile for (const by of [35, 47]) { if (ly === by - 1 && lx === 7) return "n2"; if (ly === by + 2 && lx === 7) return "n0"; if ((ly === by || ly === by + 1) && lx <= 8) { if (lx === 8) return ly === by ? "n2" : "n0"; const top = ly === by; if (top && (lx === 1 || lx === 4)) return "n4"; if (!top && (lx === 1 || lx === 4)) return "n0"; return top ? (left ? "n3" : "n2") : "n1"; } } if (lx === LEAF - 1) return left ? "w1" : "w0"; // meeting stiles // ring pulls on round back-plates const rx = left ? 29 : 34, ry = 42, ddx = sx - rx, ddy = ly - ry; if (ddy === -3 && ddx === 0) return "n3"; if (ddy === -2 && Math.abs(ddx) <= 1) return ddx < 0 ? "n3" : ddx === 0 ? "n4" : "n1"; if (ddy >= -1 && ddy <= 2 && Math.abs(ddx) <= 1) { if (ddx === 0 && (ddy === 0 || ddy === 1)) return "w0"; if (ddx === -1) return ddy === 2 ? "n2" : "n4"; if (ddx === 1) return "n1"; return ddy === -1 ? "n4" : "n1"; } // planks: 3px each from the left edge of the doorway const p = (sx - DX0) % 3, pi = Math.floor((sx - DX0) / 3); let k = p === 0 ? "w1" : p === 1 ? "w3" : "w2"; if (!left && p === 1) k = "w2"; // the right leaf sits a touch darker if (p === 2 && ly % 7 === 3 && hash(pi, ly) < 0.5) k = "w1"; // a few grain marks if (sx === 31 || sx === 32) k = "w1"; if (ly <= DY0 + 2) k = dim(k, ly === DY0 ? 2 : 1); // the lintel's shadow if (ly >= DY1 - 1) k = dim(k, 1); return k; }; const leafDim = (left) => (left ? (th > 55 ? 2 : th > 25 ? 1 : 0) : (th > 45 ? 1 : 0)); g.part("doors", () => g.field({ x: [DX0, DX1], y: [DY0 - 1, DY1] }, (x, y) => { for (const left of [true, false]) { const i = left ? x - DX0 : DX1 - x; if (i < 0 || i >= w) continue; const sh = w > 1 ? Math.round(dy * i / (w - 1)) : 0; const ly = y + sh; if (y < DY0 || ly > DY1 || ly < DY0) continue; const lx = Math.min(LEAF - 1, Math.floor((i + 0.5) * LEAF / w)); const sx = left ? DX0 + lx : DX1 - lx; let k = dim(tex(sx, ly, left), leafDim(left)); if (th > 30 && i === w - 1) k = lit > 0.5 && (ly & 1) ? "b1" : "w0"; // free edge, rimmed by the soulfire if (th > 30 && ly === DY1) k = "ink"; return k; } return null; }));}// ---- fog ----------------------------------------------------------------------------------------------------------function puff(g, cx, cy, rx, ry, dens) { if (dens <= 0.02) return; for (let y = Math.floor(cy - ry); y <= Math.ceil(cy + ry); y++) for (let x = Math.floor(cx - rx); x <= Math.ceil(cx + rx); x++) { const nx = (x + 0.5 - cx) / rx, ny = (y + 0.5 - cy) / ry, r2 = nx * nx + ny * ny; if (r2 > 1) continue; const v = dens * (1 - r2); if (v > 0.5) g.pixel(x, y, "n5"); else if (v > thr(x, y) * 0.55) g.pixel(x, y, "n4"); }}function fogSill(g, t, amt) { // low wisps squeezing out under the doors and creeping along the sill if (amt <= 0) return; g.detached("fog", () => { for (const s of [-1, 1]) for (let k = 0; k < 3; k++) { const p = (t + k / 3 + (s > 0 ? 0.17 : 0)) % 1; const x = CX + s * (3 + p * 16), y = 54 + p * 2.5; puff(g, x, y, 3.2 + p * 3, 1.5 + p * 0.8, amt * Math.min(1, Math.sin(Math.PI * p) * 1.2)); } puff(g, CX, 54, 6 + Math.sin(TAU * t), 1.5, amt * (0.75 + 0.15 * Math.cos(TAU * t))); });}function fogSpill(g, t, amt) { // open: fog pours over the sill and rolls down the steps, spreading outwards if (amt <= 0) return; g.detached("fog", () => { for (let k = 0; k < 5; k++) { const p = (t + k / 5) % 1, s = (k & 1) ? 1 : -1; const x = CX + s * (1 + p * 17) + (k === 2 ? -3 : 0), y = 51.5 + p * 7.5; puff(g, x, y, 3.5 + p * 4, 1.7 + p * 1, amt * Math.min(1, Math.sin(Math.PI * p) * 1.3)); } puff(g, CX, 53, 8 + Math.sin(TAU * t) * 0.8, 2, amt * (0.7 + 0.12 * Math.cos(TAU * t))); });}function grit(g, f) { if (f < 2 || f > 6) return; g.detached("grit", () => { for (const [x, ph] of [[25, 0], [30, 1], [36, 0], [39, 2]]) { const y = DY0 + (f - 2 - ph) * 4; if (y >= DY0 && y <= DY1 - 1) { g.pixel(x, y, "n3"); if (y - 1 >= DY0) g.pixel(x, y - 1, "n1"); } } });}// ---- states -------------------------------------------------------------------------------------------------------function stateAt(state, t) { t = ((t % 1) + 1) % 1; const breathe = (tt) => 0.5 + 0.5 * Math.cos(TAU * tt); if (state === "closed") { const f = Math.round(t * N_IDLE) % N_IDLE, tt = f / N_IDLE; return { th: 0, glow: 0, pulse: breathe(tt), sill: 1, spill: 0, ft: tt, st: tt, R: 8, f: -1 }; } if (state === "open-idle") { const f = Math.round(t * N_IDLE) % N_IDLE, tt = f / N_IDLE; return { th: REST, glow: 1, pulse: breathe(tt), sill: 0, spill: 1, ft: tt, st: tt, R: 8 + 1.2 * Math.cos(TAU * tt), f: -1 }; } const f = Math.min(N_OPEN - 1, Math.round(t * N_OPEN)); if (f === 0) return stateAt("closed", 0); if (f === N_OPEN - 1) return stateAt("open-idle", 0); const tt = (((f - (N_OPEN - 1)) / N_IDLE) % 1 + 1) % 1; // run the idle clock backwards from the end const k = Math.min(1, f / 7); const glow = Math.max(0, Math.min(1, (f - 2) / 6)); return { th: OPEN_TH[f], glow, pulse: 1, sill: 1 - k, spill: k, ft: tt, st: f / N_IDLE, R: 9.2, f, flare: f <= 3 };}const Crypt = sprite(pal, (g, t, state) => { const S = stateAt(state, t); contactShadow(g, 32, GROUND, 28, 2); g.outlined("ink", () => { facade(g); interior(g, S.glow, S.ft, S.R); leaves(g, S.th, S.glow); }); carvings(g); sigil(g, S.pulse); if (S.flare) g.detached("flare", () => { for (const [x, y] of [[28, 27], [35, 27], [31, 24], [32, 32]]) g.pixel(x, y, S.f === 2 ? "v3" : "v2"); }); if (S.glow > 0.5) g.part("spill", () => { // soulfire light on the sill for (let x = 27; x <= 36; x++) if (chk(x, 54) || Math.abs(x - 31.5) < 2) g.pixel(x, 54, "b1"); }); if (S.f > 0) grit(g, S.f); fogSill(g, S.st, S.sill); fogSpill(g, S.ft, S.spill);});submit(Crypt, { size: [W, H], origin: "top-left", kind: "icon", scale: 1, animate: { fps: 10, duration: N_IDLE / 10, loop: true }, anchor: { x: 32, y: GROUND }, args: ["closed"], variants: [ { name: "closed", args: ["closed"] }, { name: "open", args: ["open"], animate: { fps: 10, duration: N_OPEN / 10, loop: false } }, { name: "open-idle", args: ["open-idle"] }, ],});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.