Tessarune
Witch's Hut · toadstool_ring.js

Toadstool ring

48×32 · 224 lines of JavaScript

A 48×32 floor prop: a fairy ring on a mossy mound, red caps with white spots and pale violet glowing caps of varied heights standing in a circle, with darker grass along the ring and a lush clearing inside. “idle” (12 frames) pulses the glowing caps out of phase so their light breathes on the moss; “spores” (12 frames) has them puff spores that drift up and fade; “awaken” (10 frames, once) sends a spark round the ring, lighting each toadstool as it passes. The toadstools are drawn nearest-first inside g.depth, so each keeps its own ink outline.

Toadstool ring — Rendered output6×
Rendered by this program, variant “awaken” · 48×32 · 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.

toadstool_ring.js · 1–9
// Toadstool ring — 48x32 floor prop, top-down 3/4, key light top-left.// A fairy/witch ring on a mossy mound: red caps with white spots and pale violet glowing caps of varied heights stand// in a circle; the grass along the ring is darker (the "fairy ring"), the clearing inside is lush.// Variants://   idle    12f loop: the glowing caps pulse softly, each out of phase; their light breathes on the moss//   spores  12f loop: the glowing caps puff spores that drift up, sway and fade//   awaken  10f once: a spark of light runs around the ring, lighting each toadstool as it passes, then every glow//           flares and settles; ends on spores f0// Toadstools are drawn nearest-first inside g.depth so each keeps its own ink outline against the ones behind it.

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

toadstool_ring.js · 214–224
submit(ToadstoolRing, {  size: [W, H], origin: "top-left", kind: "icon", scale: 1,  animate: { fps: 10, duration: N_LOOP / 10, loop: true },  anchor: { x: CX, y: GROUND },  args: ["idle"],  variants: [    { name: "idle", args: ["idle"] },    { name: "spores", args: ["spores"] },    { name: "awaken", args: ["awaken"], animate: { fps: 10, duration: N_AWAKE / 10, loop: false } },  ],});
Show the whole program (224 lines)
toadstool_ring.js · 224 lines
// Toadstool ring — 48x32 floor prop, top-down 3/4, key light top-left.// A fairy/witch ring on a mossy mound: red caps with white spots and pale violet glowing caps of varied heights stand// in a circle; the grass along the ring is darker (the "fairy ring"), the clearing inside is lush.// Variants://   idle    12f loop: the glowing caps pulse softly, each out of phase; their light breathes on the moss//   spores  12f loop: the glowing caps puff spores that drift up, sway and fade//   awaken  10f once: a spark of light runs around the ring, lighting each toadstool as it passes, then every glow//           flares and settles; ends on spores f0// Toadstools are drawn nearest-first inside g.depth so each keeps its own ink outline against the ones behind it.const W = 48, H = 32, GROUND = 28, CX = 24;const N_LOOP = 12, N_AWAKE = 10;const MOSS = { cx: 24, cy: 20, rx: 22, ry: 7.5 };const RING = { cx: 24, cy: 19.5, rx: 17, ry: 6.5 };const LIGHT = { cx: 24, cy: 19.5, rx: 13, ry: 4.6 };          // the path the spark runs, just inside the ring// [angle deg (0 = right, 90 = front), type, stem height, cap radius, pulse phase]const SHROOMS = [  [196, "glow", 6, 3.0, 0.0],  [236, "red", 8, 3.8, 0],  [274, "glow", 4, 2.4, 0.42],  [312, "red", 6, 3.0, 0],  [350, "glow", 5, 3.0, 0.71],  [33, "red", 3, 2.4, 0],  [82, "glow", 2, 2.4, 0.27],  [122, "red", 3, 2.8, 0],  [158, "glow", 3, 2.2, 0.86],].map(([a, type, h, cr, ph], i) => {  const r = (a * Math.PI) / 180;  return { i, a: r, type, h, cr, ph, bx: Math.round(RING.cx + RING.rx * Math.cos(r)), by: Math.round(RING.cy + RING.ry * Math.sin(r)) };});const BAYER = [0, 8, 2, 10, 12, 4, 14, 6, 3, 11, 1, 9, 15, 7, 13, 5];const dith = (x, y, dens) => BAYER[(y & 3) * 4 + (x & 3)] / 16 < dens;// ---- moss mound -----------------------------------------------------------------------------------------------function moss(g) {  const { cx, cy, rx, ry } = MOSS;  g.part("moss", () => {    // earth lip under the moss, seen on the near side    g.field({ x: [cx - rx, cx + rx], y: [cy - ry, cy + ry + 2] }, (x, y) => {      const nx = (x + 0.5 - cx) / rx;      for (let d = 1; d <= 2; d++) {        const ny = (y + 0.5 - d - cy) / ry;        const ny0 = (y + 0.5 - cy) / ry;        if (nx * nx + ny0 * ny0 <= 1) return null;        if (nx * nx + ny * ny <= 1) return d === 1 ? (nx < -0.5 ? "e1" : "e0") : (nx < -0.5 ? "w1" : "w0");      }      return null;    });    g.field({ x: [cx - rx, cx + rx], y: [cy - ry, cy + ry] }, (x, y) => {      const nx = (x + 0.5 - cx) / rx, ny = (y + 0.5 - cy) / ry;      if (nx * nx + ny * ny > 1) return null;      // distance from the ring line (in ring-normalised units)      const rxn = (x + 0.5 - RING.cx) / RING.rx, ryn = (y + 0.5 - RING.cy) / RING.ry;      const rr = Math.sqrt(rxn * rxn + ryn * ryn);      const lit = -0.6 * nx - 0.5 * ny;      if (Math.abs(rr - 1) < 0.16) return chk(x, y) ? "e0" : "e1";       // the darker fairy-ring band      if (rr < 0.84) {                                                    // lush clearing inside the ring        if (lit > 0.32 && !chk(x, y)) return "e3";        if (lit > 0.45) return "e3";        return lit > -0.35 ? "e2" : "e1";      }      return lit > 0.35 ? "e2" : lit > -0.45 ? "e1" : "e0";             // outer moss    });    // tufts and pebbles: countable marks    for (const [x, y, k] of [[6, 17, "e3"], [7, 16, "e2"], [41, 23, "e2"], [12, 25, "e2"], [35, 26, "n2"], [4, 22, "n3"], [44, 18, "e2"], [20, 20, "e1"], [28, 18, "e1"], [24, 22, "e1"]]) g.pixel(x, y, k);  });}// ---- a toadstool -----------------------------------------------------------------------------------------------function toadstool(g, s, glow) {  const { bx, by, h, cr, type, i } = s;  const capY = by - h;                          // cap rim row  const crY = cr * 0.78;  const sw = cr >= 4 ? 3 : 2;                   // stem width  const x0 = bx - (sw === 3 ? 1 : 1);  g.part("stem" + i, () => {    for (let y = capY + 1; y <= by; y++) {      for (let k = 0; k < sw; k++) {        const x = x0 + k;        const key = k === 0 ? "n5" : k === sw - 1 ? "n3" : "n4";        g.pixel(x, y, y === by ? (k === 0 ? "n3" : "n2") : key);      }    }    if (h >= 5) g.pixel(x0 + sw - 1, capY + 2, "n2");                       // shadow under the cap    if (type === "red" && h >= 5) { g.line([x0 - 1, capY + 3], [x0 + sw, capY + 3], "n4"); g.pixel(x0 + sw, capY + 3, "n3"); } // the ring (annulus)  });  const RED = ["r0", "r1", "r1", "r2"];  const VIO = ["v1", "v2", "v2", "v3"];  g.part("cap" + i, () => g.field({ x: [Math.floor(bx - cr - 1), Math.ceil(bx + cr + 1)], y: [Math.floor(capY - crY - 1), capY + 1] }, (x, y) => {    const nx = (x + 0.5 - (bx + 0.5)) / (cr + 0.5), ny = (y + 0.5 - (capY + 0.5)) / (crY + 0.5);    if (ny > 0.55) return null;    if (nx * nx + ny * ny > 1) return null;    // underside / gills: the bottom row of the cap    if (ny > 0.05) return type === "red" ? (nx < -0.3 ? "n4" : "n3") : (glow > 0.5 ? "v3" : "v2");    if (type === "red") {      let k = litIndex(x, y, bx + 0.5, capY + 0.5, cr + 0.5, crY + 0.5, 4, 0.1);      return RED[Math.max(0, Math.min(3, k))];    }    let k = litIndex(x, y, bx + 0.5, capY + 0.5, cr + 0.5, crY + 0.5, 4, -0.15 + 0.4 * glow);    return VIO[Math.max(0, Math.min(3, k))];  }));  // spots on the red caps (fixed, countable)  if (type === "red") g.part("spots" + i, () => {    const pts = cr >= 4 ? [[-2, -2], [1, -3], [2, -1], [-3, 0], [0, -1]] : cr >= 3 ? [[-1, -2], [1, -1], [-2, 0]] : [[-1, -1], [1, 0]];    for (const [dx, dy] of pts) g.pixel(bx + dx, capY + dy, dx < 1 ? "n5" : "n4");  });  else g.part("glint" + i, () => { g.pixel(bx - 1, capY - Math.round(crY) + 1, "n5"); });}// soft dithered light around a glowing cap and on the moss under itfunction halo(g, s, glow) {  const { bx, by, h, cr } = s;  const cy = by - h - 1;  const R = cr + 1 + 2 * glow;  g.field({ x: [Math.floor(bx - R - 1), Math.ceil(bx + R + 1)], y: [Math.floor(cy - R), Math.ceil(by + 2)] }, (x, y) => {    const d = Math.hypot(x + 0.5 - (bx + 0.5), (y + 0.5 - cy) * 1.1);    const dg = Math.hypot((x + 0.5 - (bx + 0.5)) / (cr + 2 + 2 * glow), (y + 0.5 - by) / (1.6 + glow));    if (dg < 1 && dith(x, y, 0.1 + 0.4 * glow * (1 - dg))) return dg < 0.5 && glow > 0.6 ? "v2" : "v1";   // light pool on the moss    if (d > R || d < cr) return null;    if (!dith(x, y, Math.max(0, 0.3 * glow - 0.05) * (1 - (d - cr) / (R - cr)))) return null;    return d < cr + 1.2 ? "v2" : "v1";  });}function sporesOf(g, s, t) {  const { bx, by, h, cr, ph } = s;  const top = by - h - Math.round(cr * 0.78);  for (let k = 0; k < 3; k++) {    const p = (t + ph + k / 3) % 1;    const x = Math.round(bx + (k - 1) * 1.2 + Math.sin(TAU * (p * 0.9 + ph + k * 0.37)) * (1 + p * 2.5));    const y = Math.round(top - 1 - p * 15);    if (y < 0) continue;    if (p < 0.12) { g.pixel(x, y, "v3"); g.pixel(x + 1, y, "v2"); g.pixel(x, y + 1, "v2"); }   // the puff    else if (p < 0.5) { g.pixel(x, y, "v3"); }    else if (p < 0.78) { g.pixel(x, y, "v2"); }    else if (chk(x, y + Math.floor(p * 12))) g.pixel(x, y, "v1");  }}// ---- scene ----------------------------------------------------------------------------------------------------// glowAt(s) -> 0..1.6 glow of each glowing capfunction scene(g, glowAt, extra) {  contactShadow(g, CX, GROUND, 21, 1);  const order = SHROOMS.slice().sort((a, b) => b.by - a.by);              // nearest first  for (const s of order) {    const gl = s.type === "glow" ? glowAt(s) : 0;    g.depth(-s.by * 2, () => g.outlined("ink", () => toadstool(g, s, Math.min(1, gl))));  }  g.depth(0, () => g.outlined("ink", () => moss(g)));  // light over everything solid but behind nothing important: halos sit in front of the moss and behind the caps  g.depth(-10, () => g.detached("glow", () => {    for (const s of SHROOMS) if (s.type === "glow") halo(g, s, Math.min(1.1, glowAt(s)));  }));  if (extra) g.depth(-10, () => g.detached("fx", extra));}const pulse = (s, t) => 0.5 + 0.5 * Math.sin(TAU * (t + s.ph));const ToadstoolRing = sprite(pal, (g, t, mode) => {  t = ((t % 1) + 1) % 1;  if (mode === "awaken") {    const f = Math.min(N_AWAKE - 1, Math.round(t * N_AWAKE));    if (f === N_AWAKE - 1) {                                              // settle: exactly spores f0      scene(g, (s) => pulse(s, 0), () => { for (const s of SHROOMS) if (s.type === "glow") sporesOf(g, s, 0); });      return;    }    // the spark runs once around the ring in frames 0..5 (from the back-left, clockwise as seen from above)    const run = Math.min(1, f / 5);    const head = (200 * Math.PI) / 180 + run * TAU;    const flare = f >= 6 ? [1.6, 1.25, 0.95][f - 6] : 0;    const lit = (s) => {      const passed = run * TAU >= (((s.a - (200 * Math.PI) / 180) % TAU) + TAU) % TAU;      return passed ? 0.85 : 0.15;    };    scene(g, (s) => (f >= 6 ? flare : lit(s)), () => {      if (f <= 5) {        // trail of light along the ring line        for (let k = 0; k < 14; k++) {          const a = head - k * 0.13;          if (a < (200 * Math.PI) / 180) break;          const x = Math.round(LIGHT.cx + LIGHT.rx * Math.cos(a)), y = Math.round(LIGHT.cy + LIGHT.ry * Math.sin(a));          g.pixel(x, y, k < 3 ? "v3" : k < 8 ? "v2" : "v1");          if (k === 0) { g.pixel(x + 1, y, "v3"); g.pixel(x - 1, y, "v3"); g.pixel(x, y - 1, "v3"); g.pixel(x, y + 1, "v2"); g.pixel(x, y - 2, "v2"); g.pixel(x + 2, y, "v1"); g.pixel(x - 2, y, "v1"); }        }      }      if (f >= 5) {        // the whole ring line lights up, then fades        const key = f === 6 ? "v3" : f === 7 || f === 5 ? "v2" : "v1";        for (let k = 0; k < 140; k++) {          const a = (k / 140) * TAU;          const x = Math.round(LIGHT.cx + LIGHT.rx * Math.cos(a)), y = Math.round(LIGHT.cy + LIGHT.ry * Math.sin(a));          if (f === 8 && !chk(x, y)) continue;          g.pixel(x, y, key);          if (f === 6 && Math.sin(a) > 0) g.pixel(x, y + (Math.sin(a) > 0.5 ? 1 : 0), "v2");        }      }      if (f >= 6) {        // a burst of motes rising from the centre of the ring        for (let k = 0; k < 6; k++) {          const p = (f - 6 + 1) / 3;          const x = Math.round(LIGHT.cx + Math.cos(k * 1.05) * (3 + p * 6)), y = Math.round(LIGHT.cy - 2 - p * (4 + (k % 3) * 2) + Math.sin(k * 1.05) * 2);          g.pixel(x, y, f === 6 ? "v3" : f === 7 ? "v2" : "v1");        }      }    });    return;  }  const f = Math.round(t * N_LOOP) % N_LOOP;  const tt = f / N_LOOP;  scene(g, (s) => pulse(s, tt), mode === "spores" ? () => { for (const s of SHROOMS) if (s.type === "glow") sporesOf(g, s, tt); } : null);});submit(ToadstoolRing, {  size: [W, H], origin: "top-left", kind: "icon", scale: 1,  animate: { fps: 10, duration: N_LOOP / 10, loop: true },  anchor: { x: CX, y: GROUND },  args: ["idle"],  variants: [    { name: "idle", args: ["idle"] },    { name: "spores", args: ["spores"] },    { name: "awaken", args: ["awaken"], animate: { fps: 10, duration: N_AWAKE / 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.