Tessarune
Graveyard & Chapel · cemetery_gate.js

Cemetery gate

64×48 · 262 lines of JavaScript

A 64×48 floor prop facing the camera: two ashlar piers with moulded caps and stone urns, and between them a pair of wrought-iron gate leaves with spear-tipped bars rising to a scroll arch. The leaves are thin iron, so instead of an ink outline, which would fill the gaps between bars, every bar is a lit and shadow pair that reads on light and dark floors. “closed” (12 frames) sways a rusty chain and padlock across the bars while dead leaves skitter; “open” (10 frames, once) drops the padlock, slides the chain off and swings the leaves inward; “open-idle” (10 frames) leaves the chain on the ground.

Cemetery gate — Rendered output6×
Rendered by this program, variant “open” · 64×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.

cemetery_gate.js · 1–10
// Cemetery gate — 64x48 floor prop, top-down 3/4, key light top-left. Camera-facing.// Two ashlar piers with moulded caps carry stone urns; between them hangs a pair of wrought-iron gate leaves:// spear-tipped bars rising to a scroll arch, three rails, C-scrolls in the lunette. The leaves are thin iron, so// they are NOT ink-outlined (an outline would fill the gaps between bars): every bar is a lit/shadow pair instead// (n3 left, n0 right), which reads on light and dark floors alike.// Variants://   closed     12f loop  a rusty chain and padlock across the meeting bars sway; dead leaves skitter along the base//   open       10f once  the padlock drops, the chain slithers off, the leaves swing inward (overshoot, settle);//                        ends on open-idle frame 0//   open-idle  10f loop  gate open, the chain and padlock lie on the ground, dead leaves skitter through

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

cemetery_gate.js · 252–262
submit(Gate, {  size: [W, H], origin: "top-left", kind: "icon", scale: 1,  animate: { fps: 10, duration: N_IDLE / 10, loop: true },  anchor: { x: CX, 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"], animate: { fps: 10, duration: N_OI / 10, loop: true } },  ],});
Show the whole program (262 lines)
cemetery_gate.js · 262 lines
// Cemetery gate — 64x48 floor prop, top-down 3/4, key light top-left. Camera-facing.// Two ashlar piers with moulded caps carry stone urns; between them hangs a pair of wrought-iron gate leaves:// spear-tipped bars rising to a scroll arch, three rails, C-scrolls in the lunette. The leaves are thin iron, so// they are NOT ink-outlined (an outline would fill the gaps between bars): every bar is a lit/shadow pair instead// (n3 left, n0 right), which reads on light and dark floors alike.// Variants://   closed     12f loop  a rusty chain and padlock across the meeting bars sway; dead leaves skitter along the base//   open       10f once  the padlock drops, the chain slithers off, the leaves swing inward (overshoot, settle);//                        ends on open-idle frame 0//   open-idle  10f loop  gate open, the chain and padlock lie on the ground, dead leaves skitter throughconst W = 64, H = 48, CX = 32, GROUND = 45;const PIERS = [3, 52];                     // left x of each 9px pier faceconst LL = 12, LR = 51, LEAF = 20;         // gate span: left leaf LL..LL+19, right leaf LR-19..LRconst RAILS = [22, 31, 40];                // rail rows (2px each: lit top, dark bottom)const BARS = [0, 4, 8, 12, 16];            // bar offsets from the hinge (2px each; 0 = hinge stile, 16 = meeting bar)const K = 0.3;                             // screen rise per px of depthconst N_IDLE = 12, N_OPEN = 10, N_OI = 10, REST = 58;const OPEN_TH = [0, 0, 3, 11, 24, 38, 50, 59, 63, REST];const STONE = ["n0", "n1", "n2", "n3", "n4"];// ---- piers and urns -------------------------------------------------------------------------------------------function pierTone(px, x, y) {  const u = x - px;                                                     // 0..8 on the face  if (y >= 12 && y <= 15) {                                             // cap: wider by one px each side    if (u < -1 || u > 9) return null;    if (y === 12) return u === -1 ? "n4" : u === 9 ? "n3" : "n4";       // top face    if (y === 13) return u === 9 ? "n2" : "n3";    if (y === 14) return u === -1 ? "n3" : u === 9 ? "n1" : "n2";    return u < 0 || u > 8 ? null : "n0";                                // shadow under the cap  }  if (y >= 41 && y <= 44) {                                             // plinth    if (u < -1 || u > 9) return null;    if (y === 41) return u === 9 ? "n2" : "n4";    if (y === 44) return u === 9 ? "n0" : "n1";    return u === -1 ? "n3" : u === 9 ? "n1" : "n2";  }  if (y < 16 || y > 40 || u < 0 || u > 8) return null;  if (y === 16) return "n1";  const c = Math.floor((y - 17) / 5), k = (y - 17) % 5;  if (k === 4) return "n1";                                             // mortar  if ((c & 1) && u === 4) return "n1";                                  // staggered joint  let i = u === 0 ? 4 : u === 8 ? 2 : 3;  if (k === 0 && u < 8) i = Math.min(4, i + 1);  if (hash(c, px) < 0.35 && u > 0 && u < 8 && k > 0) i -= 1;           // a darker block  if (u === 7 && k > 0) i = 2;  return STONE[i];}function urnTone(px, x, y) {  const cx = px + 4.5;  if (y === 11) return x >= px + 1 && x <= px + 7 ? (x === px + 1 ? "n4" : x === px + 7 ? "n1" : "n2") : null;  // foot slab  if (y === 10) return x >= px + 3 && x <= px + 5 ? (x === px + 3 ? "n3" : "n1") : null;                          // stem  if (y === 9) return x >= px + 2 && x <= px + 6 ? (x === px + 2 ? "n3" : x === px + 6 ? "n1" : "n2") : null;  if (y >= 5 && y <= 8) {                                                // bowl    const i = litIndex(x, y, cx, 6.6, 4.7, 2.9, 4, 0.1);    if (i < 0) return null;    if (y === 7 && x > px && x < px + 8) return i >= 2 ? "n4" : i === 1 ? "n2" : "n1";   // carved gadroon band    return STONE[Math.min(4, i + 1)];  }  if (y === 4) return x >= px + 2 && x <= px + 6 ? (x === px + 2 ? "n2" : "n0") : null;                           // shadow under the lip  if (y === 3) return x >= px && x <= px + 8 ? (x <= px + 1 ? "n4" : x >= px + 7 ? "n2" : "n3") : null;           // flared lip  if (y === 2) return x >= px + 2 && x <= px + 6 ? (x <= px + 3 ? "n4" : x === px + 6 ? "n2" : "n3") : null;       // lid  if (y === 1) return x >= px + 4 && x <= px + 4 ? "n4" : null;                                                    // knob  return null;}function piers(g) {  for (const px of PIERS) {    g.part(px < CX ? "pierL" : "pierR", () => g.field({ x: [px - 1, px + 9], y: [12, 44] }, (x, y) => pierTone(px, x, y)));    g.part(px < CX ? "urnL" : "urnR", () => g.field({ x: [px, px + 9], y: [1, 11] }, (x, y) => urnTone(px, x, y)));  }}function pierDetail(g) {  g.part("moss", () => {    for (const [x, y, k] of [[3, 40, "e1"], [4, 40, "e2"], [3, 39, "e1"], [5, 40, "e1"], [2, 43, "e1"], [3, 44, "e2"],      [60, 40, "e1"], [59, 40, "e1"], [60, 39, "e2"], [61, 44, "e1"], [4, 12, "e2"], [5, 12, "e1"], [54, 12, "e1"],      [3, 7, "e1"], [4, 8, "e2"], [59, 5, "e1"]]) g.pixel(x, y, k);    g.pixel(56, 25, "n1"); g.pixel(57, 26, "n1"); g.pixel(57, 27, "n0"); g.pixel(56, 28, "n1");   // crack    g.pixel(8, 33, "n2"); g.pixel(6, 20, "n4");                                                  // chips  });  g.part("hinges", () => {    for (const y of [23, 38]) { g.pixel(12, y, "n2"); g.pixel(51, y, "n1"); }  });}// ---- gate ---------------------------------------------------------------------------------------------------------const topY = (x) => Math.round(18 - 8 * (1 - ((x + 0.5 - CX) / 20.5) ** 2));   // bar tops follow the arch// leaf-local pixel list: [u, y, key] with u = distance from the hinge (0..19); built oncefunction leafPixels(left) {  const P = [];  const sx = (u) => (left ? LL + u : LR - u);  const add = (u, y, k) => P.push([u, y, k]);  // bars: lit pixel on the screen-left, dark on the screen-right  for (const b of BARS) for (const d of [0, 1]) {    const u = left ? b + d : b + 1 - d;                                  // keep the lit pixel on the screen-left    const x = sx(u), t = topY(x), heavy = b === 0 || b === 16;    for (let y = t; y <= 41; y++) add(u, y, d === 0 ? (heavy ? "n3" : "n2") : (heavy ? "n1" : "n0"));    if (d === 0) add(u, t - 1, "n3");  }  // spear tips  for (const b of BARS) {    const uL = left ? b : b + 1, uR = left ? b + 1 : b;                  // screen-left / screen-right pixel of the bar    const t = topY(sx(uL));    add(uL, t - 2, "n4"); add(uR, t - 2, "n2");    add(uL, t - 3, "n4");    const out = left ? uL - 1 : uL + 1, outR = left ? uR + 1 : uR - 1;   // barbs either side    if (out >= 0 && out < LEAF) add(out, t - 1, "n2");    if (outR >= 0 && outR < LEAF) add(outR, t - 1, "n0");  }  // rails and the arched top rail  for (const r of RAILS) for (let u = 0; u <= 17; u++) { add(u, r, u % 4 === 1 ? "n2" : "n3"); add(u, r + 1, "n0"); }  for (let u = 0; u <= 17; u++) { const t = topY(sx(u)) + 2; add(u, t, "n3"); add(u, t + 1, "n1"); }  // C-scrolls in the lunette between the arch rail and the top rail (two per leaf), and a band of rings low down  const scroll = (uc, yc, r) => {    for (let a = 0; a < 16; a++) {      const ang = a / 16 * TAU;      if (a >= 10 && a <= 13) continue;                                  // open side of the C      const u = Math.round(uc + Math.cos(ang) * r), y = Math.round(yc + Math.sin(ang) * r);      add(left ? u : u, y, Math.sin(ang) < -0.3 || Math.cos(ang) < -0.5 ? "n3" : "n1");    }  };  scroll(14.5, 17.5, 1.5);  for (const [uc, ry] of [[2.5, 36], [6.5, 36], [10.5, 36], [14.5, 36], [2.5, 27], [6.5, 27], [10.5, 27], [14.5, 27]]) for (const [du, dy, k] of [[-1, 0, "n2"], [0, -1, "n3"], [1, 0, "n0"], [0, 1, "n0"]])    add(Math.round(uc + du + 0.01), ry + dy, k);  return P;}const LEAF_PX = { L: leafPixels(true), R: leafPixels(false) };function gateMap(thL, thR) {  // project both leaves: a leaf turned by th keeps its hinge; each u moves to hinge + u cos th and rises u sin th K  const M = new Map();  for (const [left, th] of [[true, thL], [false, thR]]) {    const a = th * Math.PI / 180, c = Math.cos(a), s = Math.sin(a);    const dark = th > 70 ? 1 : 0;    for (const [u, y, k] of LEAF_PX[left ? "L" : "R"]) {      const x = left ? LL + Math.round(u * c) : LR - Math.round(u * c);      const yy = y - Math.round(u * s * K);      if (x < 0 || x >= W || yy < 0) continue;      const key = dark && k !== "n0" ? STONE[Math.max(0, STONE.indexOf(k) - 1)] : k;      const id = x + "," + yy, prev = M.get(id);      // where turned bars bunch up, keep the lighter pixel (the nearer, lit edge wins)      if (!prev || STONE.indexOf(key) > STONE.indexOf(prev)) M.set(id, key);    }  }  return M;}function gate(g, thL, thR) {  const M = gateMap(thL, thR);  g.part("gate", () => g.field({ x: [LL - 1, LR + 1], y: [0, 44] }, (x, y) => M.get(x + "," + y) ?? null));}// ---- chain, padlock, leaves --------------------------------------------------------------------------------------function chain(g, sway) {  // links wrapped round both meeting bars, a drooping loop and a padlock hanging from it  const dx = Math.round(sway * 1.4);  g.part("chain", () => {    const links = [[28, 33], [29, 33], [30, 34], [31, 34], [32, 34], [33, 34], [34, 33], [35, 33]];    links.forEach(([x, y], i) => g.pixel(x, y, i & 1 ? "w1" : "w3"));    const drop = [[29, 34], [30, 35], [31 + Math.round(sway * 0.6), 36], [32 + Math.round(sway * 0.6), 36], [33, 35], [34, 34]];    drop.forEach(([x, y], i) => g.pixel(x, y, i & 1 ? "w3" : "w1"));  });  g.outlined("ink", () => g.part("padlock", () => padlock(g, 31 + dx, 37)));}function padlock(g, x, y) {  // iron shackle over a squat iron body scabbed with rust, keyhole  g.pixel(x, y, "n4"); g.pixel(x + 1, y, "n3");  g.pixel(x - 1, y + 1, "n4"); g.pixel(x + 2, y + 1, "n2");  g.rect({ x: [x - 1, x + 2], y: [y + 2, y + 5] }, "n2", { fill: "n2" });  g.line([x - 1, y + 2], [x + 1, y + 2], "n4"); g.pixel(x + 2, y + 2, "n3");  g.pixel(x - 1, y + 3, "n3"); g.line([x - 1, y + 5], [x + 2, y + 5], "n1");  g.pixel(x + 2, y + 3, "w1"); g.pixel(x - 1, y + 4, "w2"); g.pixel(x + 2, y + 4, "w0");   // rust  g.pixel(x, y + 3, "ink"); g.pixel(x, y + 4, "ink");                                      // keyhole}function lockOnGround(g, x) {  // padlock on its side in the dirt, chain coiled beside it  g.outlined("ink", () => g.part("padlock", () => {    g.rect({ x: [x, x + 3], y: [42, 44] }, "n2", { fill: "n2" });    g.line([x, 42], [x + 3, 42], "n4"); g.pixel(x + 3, 44, "n0"); g.pixel(x, 43, "w2"); g.pixel(x + 3, 43, "w1");    g.pixel(x + 4, 43, "n3"); g.pixel(x + 5, 43, "n2"); g.pixel(x + 4, 42, "n4"); g.pixel(x + 2, 43, "ink");  }));  g.part("chain", () => {    for (const [dx, dy, k] of [[-1, 44, "w2"], [-2, 43, "w1"], [-3, 44, "w2"], [-4, 44, "n2"], [-5, 43, "w2"], [-6, 44, "w1"], [-7, 44, "w2"], [6, 44, "w2"], [7, 44, "w1"]])      g.pixel(x + dx, dy, k);  });}function chainFalling(g, f) {  // f1: lock drops, chain slides down the bars · f2..: the lock lies on the ground  if (f === 1) {    g.part("chain", () => {      for (const [x, y, k] of [[29, 36], [30, 37], [31, 38], [32, 38], [33, 37], [34, 36], [28, 35], [35, 35]].map(([x, y], i) => [x, y, i & 1 ? "w1" : "w2"])) g.pixel(x, y, k);    });    g.outlined("ink", () => g.part("padlock", () => padlock(g, 31, 39 - 1)));  } else lockOnGround(g, 30);}function deadLeaves(g, t, closedGate) {  // two leaves blow across the base once per loop (hidden behind the pier plinths at each end), one flips in place  g.detached("leaves", () => {    for (const [ph, y0, dir, k1, k2] of [[0, 43, 1, "w3", "w2"], [0.5, 44, 1, "g1", "w2"]]) {      const p = (t + ph) % 1;      const x = Math.round(dir > 0 ? 8 + p * 48 : 56 - p * 48);      const hop = Math.round(Math.abs(Math.sin(p * Math.PI * 3)) * 2);      if (x < 12 || x > 49) continue;                                     // behind the plinths      const flip = Math.floor(p * 12) & 1;      const y = y0 - hop;      if (flip) { g.pixel(x, y, k1); g.pixel(x + 1, y, k1); g.pixel(x + 2, y, k2); g.pixel(x + 1, y - 1, k2); }      else { g.pixel(x, y, k2); g.pixel(x + 1, y - 1, k1); g.pixel(x, y - 1, k1); g.pixel(x + 1, y - 2, k2); }    }    // a leaf caught against the right plinth, lifting and settling    const q = Math.sin(TAU * t);    g.pixel(49, 44, "r1"); g.pixel(50, 44, "r0"); g.pixel(48, 44, q > 0.3 ? "r0" : "r1");    if (q > 0.3) { g.pixel(49, 43, "r1"); g.pixel(50, 42, "r2"); } else g.pixel(50, 43, "r1");    g.pixel(17, 44, "w2"); g.pixel(18, 44, "w1"); g.pixel(18, 43, "w3");       // a settled one    g.pixel(40, 44, "g1"); g.pixel(41, 44, "g0");  });}// ---- states -------------------------------------------------------------------------------------------------------function stateAt(state, t) {  t = ((t % 1) + 1) % 1;  if (state === "closed") {    const f = Math.round(t * N_IDLE) % N_IDLE, tt = f / N_IDLE;    return { th: 0, lock: "hang", sway: Math.sin(TAU * tt), lt: tt };  }  if (state === "open-idle") {    const f = Math.round(t * N_OI) % N_OI, tt = f / N_OI;    return { th: REST, lock: "ground", lt: tt };  }  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);  return { th: OPEN_TH[f], thR: OPEN_TH[Math.max(0, f - 1)] * 0.4 + OPEN_TH[f] * 0.6, lock: "fall", f, lt: f / N_OI };}const Gate = sprite(pal, (g, t, state) => {  const S = stateAt(state, t);  contactShadow(g, CX, GROUND, 30, 2);  // gate shadow on the ground: a dithered band under the closed leaves, shrinking to the hinges as they open  const a = S.th * Math.PI / 180, reach = Math.round(LEAF * Math.cos(a));  g.part("gateShadow", () => {    for (let x = LL; x <= LR; x++) {      const inL = x - LL < reach, inR = LR - x < reach;      if ((inL || inR) && chk(x, 42)) g.pixel(x, 43, "ink");    }  });  gate(g, S.th, S.thR ?? S.th);  g.outlined("ink", () => piers(g));  pierDetail(g);  if (S.lock === "hang") chain(g, S.sway);  else if (S.lock === "fall") chainFalling(g, S.f);  else lockOnGround(g, 30);  deadLeaves(g, S.lt, S.th === 0);});submit(Gate, {  size: [W, H], origin: "top-left", kind: "icon", scale: 1,  animate: { fps: 10, duration: N_IDLE / 10, loop: true },  anchor: { x: CX, 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"], animate: { fps: 10, duration: N_OI / 10, loop: true } },  ],});

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.