Bat roost
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”.
6×
Get 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 — 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 0The last lines. submit declares the canvas and lists the variants the renderer exports.
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 — 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.