Tessarune
Witch's Hut · witch_broom.js

Witch's broom

32×48 · 190 lines of JavaScript

A 32×48 floor prop: a besom with a crooked ash handle (a knob, a knot, a snapped-off twig), a violet cord wrapped where the straw is bound on, a second tie lower down and a fanned bundle of straw. The whole broom is one model in its own coordinates, ray-cast per pixel, so every angle of the takeoff is lit the same way. “lean” (12 frames) stands it on its bristles with a glint sliding down the handle; “hover” (16 frames) floats it level above its shadow, bobbing, with violet sparkles falling; “takeoff” (10 frames, once) crouches back, lifts and swings level.

Witch's broom — Rendered output6×
Rendered by this program, variant “takeoff” · 32×48 · 10 frames
Witch's HutGet the animated sprites: Witch's Hut on itch.ioVol.5 · 12 Halloween props · 42 animations

The header. The comment at the top of the file is the brief: size, view, light direction and every variant.

witch_broom.js · 1–8
// Witch's broom — 32x48 floor prop, top-down 3/4, key light top-left.// A besom: a crooked ash handle with a knob, a knot and a snapped-off twig, a violet cord wrapped where the straw is// bound on, a second tie lower down, and a fanned bundle of straw. The whole broom is one model in broom space// (s along the axis from the knob, d across it) ray-cast per pixel, so every angle of the takeoff is lit the same way.// Variants://   lean     12f loop: stands on its bristles; a glint slides down the handle and twinkles on the cord//   hover    16f loop: floats horizontally above its shadow, bobbing; the straw flutters, violet sparkles fall off//   takeoff  10f once: lean -> crouch back -> lifts and swings level with an overshoot; ends on hover frame 0

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

witch_broom.js · 180–190
submit(WitchBroom, {  size: [W, H], origin: "top-left", kind: "icon", scale: 1,  animate: { fps: 10, duration: N_LEAN / 10, loop: true },  anchor: { x: 15, y: GROUND },  args: ["lean"],  variants: [    { name: "lean", args: ["lean"] },    { name: "hover", args: ["hover"], animate: { fps: 10, duration: N_HOVER / 10, loop: true } },    { name: "takeoff", args: ["takeoff"], animate: { fps: 10, duration: N_TAKE / 10, loop: false } },  ],});
Show the whole program (190 lines)
witch_broom.js · 190 lines
// Witch's broom — 32x48 floor prop, top-down 3/4, key light top-left.// A besom: a crooked ash handle with a knob, a knot and a snapped-off twig, a violet cord wrapped where the straw is// bound on, a second tie lower down, and a fanned bundle of straw. The whole broom is one model in broom space// (s along the axis from the knob, d across it) ray-cast per pixel, so every angle of the takeoff is lit the same way.// Variants://   lean     12f loop: stands on its bristles; a glint slides down the handle and twinkles on the cord//   hover    16f loop: floats horizontally above its shadow, bobbing; the straw flutters, violet sparkles fall off//   takeoff  10f once: lean -> crouch back -> lifts and swings level with an overshoot; ends on hover frame 0const W = 32, H = 48, GROUND = 45, L = 30;const N_LEAN = 12, N_HOVER = 16, N_TAKE = 10;const BIND0 = 16.5, BIND1 = 19.3, TIE0 = 23.2, TIE1 = 24.3;const WOOD = ["w0", "w1", "w2", "w3"], STRAW = ["w1", "g0", "g1", "g2"], CORD = ["v0", "v1", "v2", "v3"];const LEAN = { th: -6, base: [15, 45.6] };const HOVER = { th: 78, c: [14.5, 26.5] };const rad = (deg) => (deg * Math.PI) / 180;// handle centre line wanders (a crooked branch); the bound part continues straight from where the binding startsconst crook = (s) => 0.7 * Math.sin(Math.min(s, BIND0) * 0.24 + 0.2) + 0.25 * Math.sin(Math.min(s, BIND0) * 0.7);function halfW(s) {  if (s < 1.9) return 1.95;                                  // knob  if (s < BIND0) return 1.45 - s * 0.012;  if (s < BIND1) return 2.5;                               // cord wrap  const u = (s - BIND1) / (L - BIND1);  return 2.3 + 4.1 * Math.pow(u, 0.7);                     // straw fans out}function centreFromBase(th, base, lift) {  const dx = Math.sin(rad(th)), dy = Math.cos(rad(th));  return [base[0] - dx * L / 2, base[1] - lift - dy * L / 2];}// one tone function for the whole broom: pose = {th, c:[cx,cy]}, flutter = phase of the straw tips (or null)function broomTone(pose, flutter, glint) {  const th = rad(pose.th), dx = Math.sin(th), dy = Math.cos(th), px = Math.cos(th), py = -Math.sin(th);  return (x, y) => {    const X = x + 0.5 - pose.c[0], Y = y + 0.5 - pose.c[1];    const s = X * dx + Y * dy + L / 2, d = X * px + Y * py;    if (s < 0 || s > L + 1) return null;    const dd = d - crook(s);    let hw = halfW(s);    // snapped twig on the shadow side of the handle    const twig = s > 9.0 && s < 10.8 && dd > 1.2 && dd < 2.6 + (s - 9.0) * 0.5;    if (s >= BIND1) {      const dn = dd / hw;      const sid = Math.floor((dn + 1) * 5.5);                    // individual strand tips of uneven length      const rag = 0.3 + 2.0 * hash(sid, 3) + (flutter == null ? 0 : 1.1 * (0.5 + 0.5 * Math.sin(TAU * flutter + sid * 1.9)));      if (s > L - rag) return null;    }    if (Math.abs(dd) > hw && !twig) return null;    const dn = twig ? 0.2 : Math.max(-1, Math.min(1, dd / hw));    const nz = Math.sqrt(Math.max(0, 1 - dn * dn));    const lit = -0.72 * (px + py) * dn + 0.55 * nz - 0.25;   // light from the top-left, a little from the front    const lvl = lit > 0.42 ? 3 : lit > 0.05 ? 2 : lit > -0.35 ? 1 : 0;    if (twig) return s < 9.7 ? "w2" : "w1";    if (s < BIND0) {      if (glint != null && Math.abs(s - glint) < 1.1 && lvl >= 2) return "n5";      if (s > 6.0 && s < 7.0 && Math.abs(dd) < 0.7) return "w0";        // knot      if (s > 13.0 && s < 13.9 && dd < -0.3) return "w0";               // a ring of bark      if (s < 1.0) return lvl >= 2 ? "w3" : "w1";                       // cut end of the knob      if (lvl === 3 && ((s > 2 && s < 4.5) || (s > 7.8 && s < 11.5))) return "w4";      return WOOD[lvl];    }    if (s < BIND1) {                                                     // cord: wraps read as ridges      const ridge = Math.floor((s - BIND0) * 1.6) % 2 === 1;      return CORD[Math.max(0, Math.min(3, lvl - (ridge ? 1 : 0) - (lvl === 3 && Math.abs(dn) > 0.6 ? 1 : 0)))];    }    if (s > TIE0 && s < TIE1) return CORD[Math.max(0, lvl - 1)];    // straw: strands converge on the binding (stripes in normalised width), one step darker every other strand    const strand = Math.floor((dn + 1) * 6.5 + 0.5);    const sl = lit > 0.22 ? 3 : lit > -0.22 ? 2 : 1;    let k = sl - (strand % 3 === 0 ? 1 : 0);    const sid = Math.floor((dn + 1) * 5.5);    if (s > L - 0.3 - 2.0 * hash(sid, 3) - 1.2 && lvl >= 2 && strand % 3 !== 0) return "w4";   // sunlit strand tips    if (s < BIND1 + 1.2 || (s > TIE1 && s < TIE1 + 0.9)) k -= 1;        // tucked under the ties    return STRAW[Math.max(0, Math.min(3, k))];  };}function drawBroom(g, pose, flutter, glint) {  g.outlined("ink", () => g.part("broom", () => g.field({ x: [0, W - 1], y: [0, H - 1] }, broomTone(pose, flutter, glint))));}function endPoint(pose) {                                        // centre of the straw end, in screen space  const th = rad(pose.th);  return [pose.c[0] + Math.sin(th) * (L / 2 - 1), pose.c[1] + Math.cos(th) * (L / 2 - 1)];}function star(g, x, y, size) {                                   // 4-point twinkle  x = Math.round(x); y = Math.round(y);  if (size <= 0) return;  g.pixel(x, y, size > 1 ? "n5" : "v3");  if (size >= 2) for (const [a, b] of [[1, 0], [-1, 0], [0, 1], [0, -1]]) g.pixel(x + a, y + b, "v3");  if (size >= 3) for (const [a, b] of [[2, 0], [-2, 0], [0, 2], [0, -2]]) g.pixel(x + a, y + b, "v2");}const bobAt = (t) => Math.round(2 * Math.sin(TAU * t));const hoverPose = (t) => ({ th: HOVER.th, c: [HOVER.c[0], HOVER.c[1] - bobAt(t)] });function trail(g, t, keep) {                                     // sparkles shaken off the straw as it hovers  g.detached("sparkles", () => {    for (let i = 0; i < 9; i++) {      const p = (t + i / 9) % 1;      if (p > keep) continue;      const e = endPoint(hoverPose(t - p));                      // emitted where the straw was at the time      const side = ((i * 4) % 9) / 8 - 0.5;      const x = e[0] - 1 + side * 5 + p * 2 + Math.sin(TAU * (p + i * 0.37)) * 1.2;      const y = e[1] + 2 + side * 3 + p * 14;      if (y > GROUND) continue;      if (p < 0.12) star(g, x, y, 1);      else if (p < 0.3) star(g, x, y, 2);      else if (p < 0.45) { g.pixel(Math.round(x), Math.round(y), "v3"); g.pixel(Math.round(x), Math.round(y) - 1, "v2"); }      else if (p < 0.75) g.pixel(Math.round(x), Math.round(y), "v2");      else if (chk(Math.round(x), Math.round(y))) g.pixel(Math.round(x), Math.round(y), "v1");    }  });}function leanFrame(g, t) {  const pose = { th: LEAN.th, c: centreFromBase(LEAN.th, LEAN.base, 0) };  contactShadow(g, 15, GROUND, 5, 1);  // a glint slides down the handle in the first half, then the cord twinkles, then a mote off the knob  const glint = t < 0.5 ? 1.5 + (t / 0.5) * 13.5 : null;  drawBroom(g, pose, null, glint);  const th = rad(pose.th), at = (s, d) => [pose.c[0] + Math.sin(th) * (s - L / 2) + Math.cos(th) * d, pose.c[1] + Math.cos(th) * (s - L / 2) - Math.sin(th) * d];  g.detached("glints", () => {    const k = Math.round(t * N_LEAN) % N_LEAN;    const cord = [0, 0, 0, 0, 0, 0, 1, 2, 3, 2, 1, 0][k];    const tip = [1, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0][k];    const [cx, cy] = at(17.9, -3.4); star(g, cx, cy, cord);    const [kx, ky] = at(0.2, -2.6); star(g, kx, ky, tip);    // two slow motes rising off the straw    for (let i = 0; i < 2; i++) {      const p = (t + i / 2) % 1;      const [mx, my] = at(26 + i * 2, (i ? 4 : -4) + Math.sin(TAU * (p + i * 0.3)) * 1.5);      const y = Math.round(my - p * 10), x = Math.round(mx);      if (p < 0.5) g.pixel(x, y, "v2"); else if (p < 0.8) g.pixel(x, y, "v1");    }  });}function hoverFrame(g, t, keep) {  const pose = hoverPose(t);  const lift = bobAt(t);  contactShadow(g, 15, GROUND, lift > 0 ? 8 : 9, 1);  drawBroom(g, pose, (2 * t) % 1, null);  trail(g, t, keep);}// takeoff keyframes: tilt (deg), lift off the base (px), blend from base-pivot to the hover centre, extra riseconst TAKE = [[-6, 0, 0, 0], [-13, 0, 0, 0], [2, 2, 0, 0], [26, 5, 0.2, 0], [52, 7, 0.5, 0], [70, 8, 0.8, 0], [84, 0, 1, 2], [80, 0, 1, 1], [77, 0, 1, 0]];function takeoffFrame(g, f) {  if (f >= N_TAKE - 1) return hoverFrame(g, 0, 1);  if (f === 0) return leanFrame(g, 0);  const [th, lift, k, rise] = TAKE[f];  const cb = centreFromBase(th, LEAN.base, lift);  const pose = { th, c: [cb[0] + (HOVER.c[0] - cb[0]) * k, cb[1] + (HOVER.c[1] - cb[1]) * k - rise] };  contactShadow(g, 15, GROUND, Math.round(5 + 4 * Math.min(1, f / 5)), 1);  drawBroom(g, pose, f >= 3 ? (f * 0.2) % 1 : null, null);  // a burst of sparkles where it pushed off, then the trail starts  g.detached("burst", () => {    if (f >= 2 && f <= 5) {      const r = (f - 1) * 3;      for (let i = 0; i < 8; i++) {        const a = (i / 8) * TAU + 0.3;        const x = Math.round(15 + Math.cos(a) * r), y = Math.round(GROUND - 1 + Math.sin(a) * r * 0.35 - (f - 1));        g.pixel(x, y, f < 4 ? "v3" : f < 5 ? "v2" : "v1");      }    }  });  if (f >= 6) trail(g, 0, (f - 5) / 4);}const WitchBroom = sprite(pal, (g, t, mode) => {  if (mode === "takeoff") return takeoffFrame(g, Math.min(N_TAKE - 1, Math.round(t * N_TAKE)));  t = ((t % 1) + 1) % 1;  if (mode === "hover") return hoverFrame(g, t, 1);  leanFrame(g, t);});submit(WitchBroom, {  size: [W, H], origin: "top-left", kind: "icon", scale: 1,  animate: { fps: 10, duration: N_LEAN / 10, loop: true },  anchor: { x: 15, y: GROUND },  args: ["lean"],  variants: [    { name: "lean", args: ["lean"] },    { name: "hover", args: ["hover"], animate: { fps: 10, duration: N_HOVER / 10, loop: true } },    { name: "takeoff", args: ["takeoff"], animate: { fps: 10, duration: N_TAKE / 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.