Tessarune
Graveyard & Chapel · bell_frame.js

Bell frame

48×64 · 248 lines of JavaScript

A 48×64 floor prop: two timber posts on stone footings with knee braces and a crossbeam under a small slate-shingled cap, a bronze bell on an iron hanger and a hemp rope hanging from the clapper. The bell is drawn by one g.field that rotates each pixel back into bell space, so it can swing by any angle while the light stays top-left. “idle” (12 frames) keeps the bell still while the rope sways; “toll” (12 frames, once) swings it twice, the clapper striking and sound rings spreading at the peaks, and ends on the first frame of “idle”.

Bell frame — Rendered output6×
Rendered by this program, variant “toll” · 48×64 · 12 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.

bell_frame.js · 1–7
// Graveyard bell frame — 48x64 floor prop, top-down 3/4, key light top-left.// Two timber posts on stone footings, knee braces, a crossbeam under a small slate-shingled cap. A bronze bell hangs// from an iron hanger; a hemp rope hangs from the clapper.// The bell is drawn by ONE g.field that inverse-rotates each pixel into bell space, so it can swing by any angle// while the light stays top-left (the highlight stripe does not rotate with it).// Variants: idle (12f loop: bell still, rope sways), toll (12f one-shot: two swings with the clapper striking and// sound rings at the peaks; ends on idle frame 0).

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

bell_frame.js · 239–248
submit(BellFrame, {  size: [W, H], origin: "top-left", kind: "icon", scale: 1,  animate: { fps: 10, duration: IDLE_N / 10, loop: true },  anchor: { x: CX, y: GROUND },  args: ["idle"],  variants: [    { name: "idle" },    { name: "toll", args: ["toll"], animate: { fps: 10, duration: TOLL.length / 10, loop: false } },  ],});
Show the whole program (248 lines)
bell_frame.js · 248 lines
// Graveyard bell frame — 48x64 floor prop, top-down 3/4, key light top-left.// Two timber posts on stone footings, knee braces, a crossbeam under a small slate-shingled cap. A bronze bell hangs// from an iron hanger; a hemp rope hangs from the clapper.// The bell is drawn by ONE g.field that inverse-rotates each pixel into bell space, so it can swing by any angle// while the light stays top-left (the highlight stripe does not rotate with it).// Variants: idle (12f loop: bell still, rope sways), toll (12f one-shot: two swings with the clapper striking and// sound rings at the peaks; ends on idle frame 0).const W = 48, H = 64, CX = 24, GROUND = 61;const PIV = { x: 24, y: 18 };                 // hanger pin (bell crown loop turns on it)const HB = 17;                                // bell height below the crown (local v 0..HB)// half-width of the bell per local row v (crown -> waist -> flare -> lip)const HW = [3.2, 4.4, 5.0, 5.3, 5.4, 5.5, 5.6, 5.7, 5.9, 6.1, 6.4, 6.8, 7.2, 7.6, 8.0, 8.3, 8.5, 8.5];const hwAt = (v) => { const i = Math.max(0, Math.min(HB, v)); const a = Math.floor(i), b = Math.min(HB, a + 1); return HW[a] + (HW[b] - HW[a]) * (i - a); };const RAD = Math.PI / 180;// ---- frame -------------------------------------------------------------------------------------------function footing(g, x0, lit) {  g.part(lit ? "footingL" : "footingR", () => {    g.rect({ x: [x0, x0 + 8], y: [55, 60] }, "n2", { fill: "n2" });    g.line([x0, 55], [x0 + 8, 55], "n4");                                   // top face edge    g.line([x0, 56], [x0 + 8, 56], "n3");    g.line([x0, 57], [x0, 60], lit ? "n3" : "n2");    g.line([x0 + 8, 57], [x0 + 8, 60], "n1");    g.line([x0 + 1, 60], [x0 + 8, 60], "n1");    g.pixel(x0 + 3, 58, "n1"); g.pixel(x0 + 6, 57, "n3"); g.pixel(x0 + 5, 59, "n1");   // chips  });}function post(g, x0, lit) {  g.part(lit ? "postL" : "postR", () => {    g.field({ x: [x0, x0 + 3], y: [16, 55] }, (x, y) => {      const u = x - x0;      if (u === 0) return lit ? "w3" : "w2";      if (u === 3) return lit ? "w1" : "w0";      // grain: a few countable streaks      if ((u === 1 && (y === 24 || y === 25 || y === 41)) || (u === 2 && (y === 33 || y === 34 || y === 48))) return "w1";      return u === 1 && lit ? "w3" : "w2";    });    g.pixel(x0 + 1, 29, "w0"); g.pixel(x0 + 2, 29, "w1");                     // a knot  });}function sill(g) {  g.part("sill", () => {    g.rect({ x: [9, 38], y: [52, 54] }, "w1", { fill: "w1" });    g.line([9, 52], [38, 52], "w3");    g.line([9, 54], [38, 54], "w0");    g.pixel(15, 53, "w0"); g.pixel(16, 53, "w0"); g.pixel(30, 53, "w2");  });}function braces(g) {  g.part("braces", () => {    for (let i = 0; i <= 5; i++) {      g.pixel(7 + i, 23 - i, "w3"); g.pixel(7 + i, 24 - i, "w1");          // left brace, lit top edge      g.pixel(40 - i, 23 - i, "w2"); g.pixel(40 - i, 24 - i, "w0");        // right brace, in shade    }  });}function beam(g) {  g.part("crossbeam", () => {    g.rect({ x: [1, 46], y: [13, 16] }, "w2", { fill: "w2" });    g.line([2, 13], [45, 13], "w1");                                        // under the eave shadow    g.line([2, 14], [20, 14], "w3");    g.line([1, 16], [46, 16], "w0");    // end grain: lit on the left, dark on the right    g.rect({ x: [1, 2], y: [13, 16] }, "w3", { fill: "w3" }); g.pixel(1, 14, "w4");    g.rect({ x: [45, 46], y: [13, 16] }, "w1", { fill: "w1" });    g.line([27, 15], [31, 15], "w1"); g.line([10, 15], [12, 15], "w1");     // grain    for (const x of [5, 42]) { g.pixel(x, 14, "n4"); g.pixel(x, 15, "n1"); }   // peg heads over the posts  });}function cap(g) {  // front slope of a gable cap seen from above: trapezoid, ridge at the top, eave at the bottom  const top = 4, eave = 11, l0 = 10, r0 = 37;  g.part("cap", () => {    g.field({ x: [0, 47], y: [top, eave + 1] }, (x, y) => {      const k = (y - top) / (eave - top);      const xl = l0 - k * l0, xr = r0 + k * (47 - r0);      if (x < Math.round(xl) || x > Math.round(xr)) return null;      if (y === eave + 1) return x < 2 || x > 45 ? null : "w1";              // fascia board      if (y === top) return x < 22 ? "n4" : "n3";                            // ridge tiles      const row = Math.floor((y - top - 1) / 2), sub = (y - top - 1) % 2;      if (x - xl < 1.2) return "n4";                                         // lit left hip      if (xr - x < 1.2) return "n1";                                         // right hip in shade      const joint = (x + row * 2) % 5 === 0;      if (sub === 1) return joint ? "n0" : x < 18 ? "n2" : "n1";             // slate butt edge      if (joint) return "n1";      return x < 16 ? "n3" : "n2";    });    g.pixel(24, 3, "n3"); g.pixel(24, 2, "n4"); g.pixel(25, 3, "n2");        // finial knob    g.pixel(13, 7, "n1"); g.pixel(33, 9, "e1");                              // a cracked slate, a tuft of moss  });}function hanger(g) {  g.part("hanger", () => {    g.rect({ x: [22, 26], y: [17, 17] }, "n1", { fill: "n1" });    g.pixel(22, 17, "n3"); g.pixel(23, 17, "n2");    g.pixel(24, 18, "n2");  });}// ---- bell --------------------------------------------------------------------------------------------function toLocal(x, y, th) {  const dx = x + 0.5 - PIV.x, dy = y + 0.5 - PIV.y, c = Math.cos(th), s = Math.sin(th);  return [dx * c - dy * s, dx * s + dy * c];}function toWorld(u, v, th) {  const c = Math.cos(th), s = Math.sin(th);  return [PIV.x + u * c + v * s, PIV.y - u * s + v * c];}// patina flecks in bell space (u, v) — sparse, in the crevices under the bandsconst PATINA = [[3.5, 5.5, "e1"], [3.5, 6.5, "e1"], [3.5, 7.5, "e0"], [4.5, 5.5, "e0"], [5.5, 13.5, "e0"], [5.5, 12.5, "e1"]];function bellKey(x, y, th) {  const [u, v0] = toLocal(x, y, th);  const v = v0 - 1;                                                          // v 0 = top of the crown  if (v < -2.2 || v > HB + 0.5) return null;  if (v < 0) {                                                               // crown loop (canon) on the pin    const r = Math.hypot(u, v + 1.1);    if (r > 1.9 || r < 0.7) return null;    return u < 0 ? "g2" : "g0";  }  const hw = hwAt(v);  if (Math.abs(u) > hw) return null;  const nu = u / hw;  // surface slope: the shoulder faces up, the flare faces down/out  const nyl = v < 2.5 ? -0.7 : v > 11 ? 0.35 : 0;  const nz = Math.sqrt(Math.max(0, 1 - nu * nu));  const c = Math.cos(th), s = Math.sin(th);  const nxw = nu * c + nyl * s, nyw = -nu * s + nyl * c;  const edge = hw - Math.abs(u);  for (const [pu, pv, k] of PATINA) if (Math.abs(u - pu) < 0.5 && Math.abs(v - pv) < 0.5) return k;  // lip band: a raised roll along the mouth  if (v > HB - 0.5) return nxw < -0.2 ? "g1" : "g0";                          // underside of the roll  if (v > HB - 1.5) return nxw < -0.35 ? "g3" : nxw < 0.3 ? "g2" : "g1";      // lit top of the roll  if (v > HB - 2.5 && v <= HB - 1.5) return nxw < 0 ? "g1" : "g0";            // groove above the roll  // shoulder band (two thin rings)  if (v > 3 && v <= 4) return "g0";  if (v > 4 && v <= 5) return nxw < -0.2 ? "g3" : nxw < 0.35 ? "g2" : "g1";  // sharp lit rim along the top-left silhouette, bounce light on the right  if (edge < 1 && nxw < -0.3) return "g3";  if (edge < 1 && nxw > 0.6) return "g1";  // vertical specular stripe (world-lit, not bell-lit)  // cylindrical bands keyed to the WORLD-space normal, so the light stays top-left as the bell swings  const b = nxw - (v < 2.5 ? 0.25 : 0) + (v > 12 ? 0.1 : 0);  if (b > -0.52 && b < -0.36 && v > 5 && v < HB - 2.5) return "g3";          // specular stripe  if (b < 0.02) return "g2";  if (b < 0.42) return "g1";  if (b < 0.66 || v < 2.5) return "g0";  return "w1";                                                               // core shadow: dark bronze}function bell(g, th) {  g.part("bell", () => g.field({ x: [10, 38], y: [14, 40] }, (x, y) => bellKey(x, y, th)));}// clapper: bell-space angle psi (relative to the bell axis); the ball hangs just below the lipfunction clapperPos(th, psi) {  const L = HB + 2.2;  return toWorld(Math.sin(psi) * L, 1 + Math.cos(psi) * L, th);}function clapper(g, th, psi) {  const [bx, by] = clapperPos(th, psi);  const x = Math.round(bx), y = Math.round(by);  g.part("clapper", () => {    g.rect({ x: [x - 1, x + 1], y: [y - 1, y + 1] }, "n1", { fill: "n1" });    g.pixel(x - 1, y - 1, "n3"); g.pixel(x, y - 1, "n2"); g.pixel(x - 1, y, "n2");    g.pixel(x + 1, y + 1, "n0");  });  return [x, y + 2];}// ---- rope --------------------------------------------------------------------------------------------const ROPE_LEN = 15;function rope(g, top, sway) {  const [tx, ty] = top;  g.part("rope", () => {    let px = tx;    for (let i = 0; i <= ROPE_LEN; i++) {      const k = i / ROPE_LEN;      const x = Math.round(tx + sway * Math.pow(k, 1.4)), y = ty + i;      if (Math.abs(x - px) > 1) g.pixel((x + px) >> 1, y, "w3");            // keep the strand connected      g.pixel(x, y, (i + Math.floor(y / 1)) % 3 === 0 ? "w2" : "w4");       // twisted hemp      px = x;    }    // knot and frayed tassel    const ex = Math.round(tx + sway), ey = ty + ROPE_LEN;    g.rect({ x: [ex - 1, ex + 1], y: [ey + 1, ey + 2] }, "w3", { fill: "w3" });    g.pixel(ex - 1, ey + 1, "w4"); g.pixel(ex + 1, ey + 2, "w1");    g.pixel(ex - 1, ey + 3, "w2"); g.pixel(ex + 1, ey + 3, "w2"); g.pixel(ex, ey + 4, "w3");  });}// ---- sound rings (toll peaks) ------------------------------------------------------------------------function rings(g, th, age) {  // two arc pairs around the bell's belly; age 0 = strike (tight, bright), 1 = spreading (dithered)  const [cx, cy] = toWorld(0, 10, th);  g.detached("sound", () => {    for (let n = 0; n < 2; n++) {      const r = 11 + n * 3 + age * 3;      for (let a = -34; a <= 34; a += 4) {        for (const side of [-1, 1]) {          const x = Math.round(cx + side * Math.cos(a * RAD) * r), y = Math.round(cy + Math.sin(a * RAD) * r * 0.9);          if (age > 0 && !chk(x, y)) continue;          g.pixel(x, y, n === 0 && age === 0 ? "n5" : "g3");        }      }    }  });}// ---- motion ------------------------------------------------------------------------------------------const IDLE_N = 12;const TOLL = [0, 11, 21, 12, -3, -15, -20, -10, 2, 7, 3, 0];               // bell angle per frame (deg, + = right)const PSI = [0, -3, 5, 2, 3, 3, -5, -2, -2, 1, 0, 0];                        // clapper vs bell (deg); strikes at peaksconst RING = { 2: 0, 3: 1, 6: 0, 7: 1 };const BellFrame = sprite(pal, (g, t, state) => {  let th = 0, psi = 0, sway, f = -1;  if (state === "toll") {    f = Math.min(TOLL.length - 1, Math.round(t * TOLL.length));    th = TOLL[f] * RAD; psi = PSI[f] * RAD;    const prev = TOLL[Math.max(0, f - 1)];    sway = Math.round(-(TOLL[f] - prev) * 0.18 - TOLL[f] * 0.12);           // rope lags behind the swing    if (f === TOLL.length - 1) sway = 0;  } else sway = Math.round(1.6 * Math.sin(TAU * t));  contactShadow(g, CX, GROUND, 22, 2);  g.outlined("ink", () => {    footing(g, 1, true); footing(g, 38, false);    sill(g);    post(g, 3, true); post(g, 41, false);    braces(g);    beam(g);    cap(g);    hanger(g);  });  let ropeTop;  g.outlined("ink", () => { ropeTop = clapper(g, th, psi); });  g.outlined("ink", () => { bell(g, th); });  g.outlined("ink", () => { rope(g, ropeTop, sway); });  if (f in RING) rings(g, th, RING[f]);});submit(BellFrame, {  size: [W, H], origin: "top-left", kind: "icon", scale: 1,  animate: { fps: 10, duration: IDLE_N / 10, loop: true },  anchor: { x: CX, y: GROUND },  args: ["idle"],  variants: [    { name: "idle" },    { name: "toll", args: ["toll"], animate: { fps: 10, duration: TOLL.length / 10, 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.