Tessarune
Witch's Hut · bat_roost.js

Bat roost

48×32 · 258 lines of JavaScript

A 48×32 ceiling or wall prop with no contact shadow: a rough-hewn, mossy oak beam with three little bats hanging upside down, wings wrapped and red eyes glinting, a cobweb under the left end and a dangling spider. “idle” (16 frames) sways the bats out of step while the right one stretches a wing; “flutter” (12 frames, once) has them let go one after another and flap out of frame; “empty” (16 frames) is the bare beam with the web swaying; “return” (12 frames, once) flies them back in to hang, ending on the first frame of “idle”.

Bat roost — Rendered output6×
Rendered by this program, variant “return” · 48×32 · 12 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.

bat_roost.js · 1–10
// Bat roost — 48x32 ceiling/wall prop (no contact shadow), key light top-left.// A rough-hewn, mossy oak beam with three little bats hanging upside down from its underside, wings wrapped,// red eyes glinting. A cobweb fills the corner under the left end, where a moss strand hangs; a spider dangles.// Variants://   idle     16f loop  the bats sway on their toes out of step, the right one stretches a wing and wraps it//                      again, ears twitch; the web stirs and the spider bobs//   flutter  12f once  one after another the bats tense, let go, open their wings and flap off out of frame;//                      ends on empty frame 0//   empty    16f loop  the bare beam; the cobweb sways, the spider bobs//   return   12f once  the bats flap in from the edges, swing up and hang; ends on idle frame 0

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

bat_roost.js · 247–258
submit(Roost, {  size: [W, H], origin: "top-left", kind: "icon", scale: 1,  animate: { fps: 10, duration: N_IDLE / 10, loop: true },  anchor: { x: 24, y: TOP },  args: ["idle"],  variants: [    { name: "idle", args: ["idle"] },    { name: "flutter", args: ["flutter"], animate: { fps: 10, duration: N_ONCE / 10, loop: false } },    { name: "empty", args: ["empty"] },    { name: "return", args: ["return"], animate: { fps: 10, duration: N_ONCE / 10, loop: false } },  ],});
Show the whole program (258 lines)
bat_roost.js · 258 lines
// Bat roost — 48x32 ceiling/wall prop (no contact shadow), key light top-left.// A rough-hewn, mossy oak beam with three little bats hanging upside down from its underside, wings wrapped,// red eyes glinting. A cobweb fills the corner under the left end, where a moss strand hangs; a spider dangles.// Variants://   idle     16f loop  the bats sway on their toes out of step, the right one stretches a wing and wraps it//                      again, ears twitch; the web stirs and the spider bobs//   flutter  12f once  one after another the bats tense, let go, open their wings and flap off out of frame;//                      ends on empty frame 0//   empty    16f loop  the bare beam; the cobweb sways, the spider bobs//   return   12f once  the bats flap in from the edges, swing up and hang; ends on idle frame 0const W = 48, H = 32;const BX0 = 2, BX1 = 45;                 // beam endsconst TOP = 3, FRONT = 6, BOT = 10;      // top face rows 3..5, front face rows 6..10const FOOT = 11;                         // bats grip the underside hereconst N_IDLE = 16, N_ONCE = 12;const BATS = [{ x: 17, ph: 0.0 }, { x: 27, ph: 0.37 }, { x: 37, ph: 0.68 }];// ---- beam ---------------------------------------------------------------------------------------------------function beam(g) {  g.part("beam", () => g.field({ x: [BX0, BX1], y: [TOP, BOT] }, (x, y) => {    // hewn ends: the top-left corner is chamfered, the right end has a split    if (x === BX0 && y === TOP) return null;    if (x === BX1 && (y === TOP || y === BOT)) return null;    if (y < FRONT) {                                           // top face, lit; brightest at the left      if (y === TOP) return x < 20 ? "w4" : "w3";      if (y === FRONT - 1) return "w2";                         // the arris rounds over      return (x + 2 * y) % 13 === 0 ? "w2" : "w3";             // adze marks    }    if (y === FRONT) return "w3";                               // lit upper edge of the front face    if (y === BOT) return "w0";                                 // underside in shadow    // front face: long grain lines, a knot, darker toward the right    if (y === 8 && ((x > 6 && x < 15) || (x > 27 && x < 38))) return "w1";    if (y === 9 && x > 17 && x < 25) return "w1";    if (x === BX1 || x === BX1 - 1 && y > 7) return "w1";    if ((x === 30 || x === 31) && y === 7) return "w1";        // knot    return x > 34 ? "w1" : "w2";  }));  g.part("beamEnd", () => {    g.line([BX0, TOP + 1], [BX0, BOT], "w3");                 // the cut end catches the light    g.pixel(BX0 + 1, FRONT + 2, "w4");    g.pixel(30, 8, "w0"); g.pixel(31, 8, "w3");               // knot heart and its rim    g.line([BX1 - 1, 7], [BX1 - 3, 8], "w0");                 // split at the right end    g.pixel(BX1, 6, "w0");  });  g.part("nails", () => {                                    // iron spikes holding the beam    for (const x of [8, 40]) { g.pixel(x, 7, "n4"); g.pixel(x, 8, "n1"); }  });  g.part("moss", () => {    // cushions on the top face, tufts lapping over the front edge, a strand hanging off the left end    const pads = [[3, 9], [16, 21], [33, 37], [42, 44]];    for (const [a, b] of pads) for (let x = a; x <= b; x++) {      const u = (x - a) / Math.max(1, b - a);      g.pixel(x, TOP, u < 0.4 ? "e3" : "e2");      g.pixel(x, TOP + 1, u < 0.5 ? "e2" : "e1");      if (u > 0.15 && u < 0.85) g.pixel(x, TOP - 1, u < 0.5 ? "e2" : "e1");    }    for (const [x, n] of [[4, 2], [6, 3], [8, 1], [17, 2], [19, 3], [34, 2], [36, 1], [43, 2]])      for (let i = 0; i < n; i++) g.pixel(x, FRONT + i, i === 0 ? "e2" : "e1");    for (const x of [5, 7, 18, 35]) g.pixel(x, FRONT, "e1");    for (let y = FOOT; y <= 21; y++) g.pixel(3, y, y < 16 ? "e1" : "e0");      // hanging strand    g.pixel(2, 12, "e2"); g.pixel(4, 16, "e1"); g.pixel(2, 19, "e0"); g.pixel(3, 22, "e0");  });}// ---- cobweb + spider (not outlined: they are threads) ----------------------------------------------------------function web(g, wt) {  const s = Math.sin(TAU * wt), c = Math.cos(TAU * wt);  g.detached("cobweb", () => {    const C = [3, FOOT];                                       // corner under the beam by the strand    const ends = [[13, FOOT], [11 + 0.6 * s, 17 + 0.5 * c], [3, 21]];    const P = (e, k) => [C[0] + (e[0] - C[0]) * k, C[1] + (e[1] - C[1]) * k];    const R = (p) => [Math.round(p[0]), Math.round(p[1])];    for (const e of ends) g.line(R(C), R(e), "n3");    for (const [k, key] of [[0.4, "n3"], [0.8, "n3"]]) {      for (let j = 0; j < ends.length - 1; j++) {        const a = P(ends[j], k), b = P(ends[j + 1], k);        const m = [(a[0] + b[0]) / 2 - 0.4, (a[1] + b[1]) / 2 + 0.6 + 0.4 * s];   // the thread sags        g.line(R(a), R(m), key); g.line(R(m), R(b), key);      }    }    g.pixel(Math.round(P(ends[1], 0.8)[0]), Math.round(P(ends[1], 0.8)[1]), "n5"); // a dew drop  });  g.detached("spider", () => {    const sx = 8, top = 17, y = 23 + Math.round(1.5 + 1.5 * Math.sin(TAU * wt + 1.2));    g.line([sx, top], [sx, y - 1], "n3");    g.pixel(sx, y, "n0"); g.pixel(sx, y + 1, "ink"); g.pixel(sx + 1, y, "ink");    g.pixel(sx - 1, y - 1, "ink"); g.pixel(sx + 2, y - 1, "ink"); g.pixel(sx - 1, y + 2, "ink"); g.pixel(sx + 2, y + 2, "ink");    g.pixel(sx, y, "r1");                                         // red hourglass glint on the back  });}// ---- bats ----------------------------------------------------------------------------------------------------// hanging, wings wrapped. col 4 = centre, row 0 = toes on the beam. Upside down: chin up, ears at the bottom.const HANG = [  "...k.k...",  "...l.l...",  "..AMMMB..",  ".AMMFMMB.",  ".AMMFMMB.",  ".AMMFMMB.",  ".AAMFMBB.",  "..cAFBc..",  "..GHnHH..",  "..GeHeH..",  "..GHHHH..",  "..EE.EE..",  "...E.E...",];const HKEY = { k: "n4", l: "n1", A: "n3", M: "n2", B: "n1", F: "n0", c: "n4", H: "w0", G: "w1", n: "ink", e: "r2", E: "n1" };// flying, front view, three flap poses. col 6 = centre, row 3 = centre row.const FLY = {  up: [    "M...........M",    "MM..E...E..MM",    "MMM.GHHHH.MMM",    ".MMMGeHeHMMM.",    "..MMGHHHHMM..",    "...M.HHH.M...",    "......H......",  ],  mid: [    ".............",    "....E...E....",    "....GHHHH....",    "MMMMGeHeHMMMM",    ".MMMGHHHHMMM.",    "..M.MHHHM.M..",    "......H......",  ],  down: [    ".............",    "....E...E....",    "....GHHHH....",    "...MGeHeHM...",    "..MMGHHHHMM..",    ".MMM.HHH.MMM.",    "MM....H....MM",  ],};function grid(g, rows, ox, oy, keyOf) {  rows.forEach((row, j) => [...row].forEach((ch, i) => {    if (ch === ".") return;    const k = keyOf(ch, i, j);    if (k) g.pixel(ox + i, oy + j, k);  }));}// a hanging bat. sway shears the body about the toes; twitch flicks an ear; wing = 0..1 stretch of one wingfunction hangingBat(g, bx, oy, sway, opt = {}) {  const dxAt = (j) => Math.round(sway * j / 12);  HANG.forEach((row, j) => [...row].forEach((ch, i) => {    if (ch === ".") return;    let x = bx - 4 + i + dxAt(j), y = oy + j;    if (ch === "E" && opt.twitch && j === 12) { x += i < 4 ? -1 : 1; y -= 1; }     // ear tips flick outward    if (ch === "e" && opt.blink) { g.pixel(x, y, "w0"); return; }    if (ch === "k" && opt.release) return;                        // toes let go    g.pixel(x, y, HKEY[ch]);  }));  for (const [side, s] of [[1, opt.wingR || 0], [-1, opt.wingL || 0]]) {    if (s <= 0) continue;    // the wing unfurls from the shoulder: forearm out to the wrist, two fingers fanning down, scalloped edge    const x0 = bx + side * 3 + dxAt(3), y0 = oy + 2;    const X = (dx) => x0 + side * Math.round(dx * s);    const lit = side < 0, Y = (dy) => y0 + Math.round(dy * (0.45 + 0.55 * s));    // forearm to the wrist, three finger tips with deep scallops between them, back to the body    const P = [[0, 0], [5, -1], [8, 2], [6, 3], [6, 6], [4, 4], [3, 7], [1, 5], [0, 7]].map(([dx, dy]) => [X(dx), Y(dy)]);    g.fillPoly(P, lit ? "n2" : "n1");    g.line(P[0], P[1], lit ? "n4" : "n3");    for (const tip of [P[2], P[4], P[6]]) g.line(P[1], tip, lit ? "n1" : "n0");    if (s > 0.6) g.pixel(P[1][0], P[1][1] - 1, "n4");                       // thumb claw  }}function flyingBat(g, cx, cy, pose) {  grid(g, FLY[pose], cx - 6, cy - 3, (ch, i, j) => {    if (ch === "M") {      // leading edge (topmost membrane pixel in its column) lighter; the left wing faces the light      const above = j > 0 && FLY[pose][j - 1][i] === "M";      if (i < 6) return above ? "n2" : "n3";      return above ? "n1" : "n2";    }    return HKEY[ch];  });}// per-bat flight: k = 0 hanging, 1 tense, 2 drop, 3..6 flying along the path, >= 7 goneconst PATHS = [  [[15, 22], [9, 24], [2, 23], [-6, 21]],  [[27, 24], [30, 27], [33, 32], [36, 39]],  [[40, 22], [45, 23], [51, 21], [58, 19]],];const POSES = ["up", "mid", "down"];function bat(g, i, k, sway, f, opt) {  const B = BATS[i];  if (k <= 0) return hangingBat(g, B.x, FOOT, sway, opt);  if (k === 1) return hangingBat(g, B.x, FOOT, 0, { twitch: true });  if (k === 2) return hangingBat(g, B.x, FOOT + 3, 0, { twitch: true, release: true, wingL: 0.55, wingR: 0.55 });  if (k >= 7) return;  const [x, y] = PATHS[i][k - 3];  flyingBat(g, x, y, POSES[(f + i) % 3]);}// ---- states ----------------------------------------------------------------------------------------------------const tri = (u) => Math.max(0, 1 - Math.abs(u));function idleBats(g, tt) {  BATS.forEach((B, i) => {    const sway = 1.6 * Math.sin(TAU * (tt + B.ph));    const opt = {};    if (i === 0) opt.twitch = tt >= 0.25 && tt < 0.32;    if (i === 1) { opt.twitch = (tt >= 0.75 && tt < 0.82); opt.blink = tt >= 0.5 && tt < 0.57; }    if (i === 2) {      // the right bat stretches its right wing out, holds, and wraps it again      const u = (tt - 0.3) / 0.45;      opt.wingR = u > 0 && u < 1 ? [0.4, 0.7, 0.9, 0.9, 0.9, 0.7, 0.4][Math.min(6, Math.floor(u * 7))] : 0;      opt.twitch = tt >= 0.06 && tt < 0.13;    }    bat(g, i, 0, opt.wingR ? sway * 0.4 : sway, 0, opt);  });}const START = [2, 1, 4];      // flutter: frame each bat tensesconst LAND = [10, 11, 8];     // return: frame each bat is hanging againconst Roost = sprite(pal, (g, t, state) => {  t = ((t % 1) + 1) % 1;  let wt, draw;  if (state === "idle" || state === "empty") {    const f = Math.round(t * N_IDLE) % N_IDLE, tt = f / N_IDLE;    wt = tt;    draw = state === "idle" ? () => idleBats(g, tt) : () => {};  } else {    const f = Math.min(N_ONCE - 1, Math.round(t * N_ONCE));    wt = f / (N_ONCE - 1);                    // the web shivers once through the whole action and comes to rest    const ks = state === "flutter" ? START.map((s) => Math.max(0, f - s + 1)) : LAND.map((e) => Math.max(0, e - f));    draw = () => ks.forEach((k, i) => bat(g, i, k, k === 0 ? 1.6 * Math.sin(TAU * BATS[i].ph) : 0, f, {}));  }  web(g, wt);  g.outlined("ink", () => {    beam(g);    g.detached("bats", draw);  });});submit(Roost, {  size: [W, H], origin: "top-left", kind: "icon", scale: 1,  animate: { fps: 10, duration: N_IDLE / 10, loop: true },  anchor: { x: 24, y: TOP },  args: ["idle"],  variants: [    { name: "idle", args: ["idle"] },    { name: "flutter", args: ["flutter"], animate: { fps: 10, duration: N_ONCE / 10, loop: false } },    { name: "empty", args: ["empty"] },    { name: "return", args: ["return"], animate: { fps: 10, duration: N_ONCE / 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.