Bell frame
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”.
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.
// 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.
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)
// 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.