Owl perch
A 32×48 tall floor prop: a witch’s tawny owl with a grey facial disc and amber eyes on a gnarled wooden T-perch standing on three root-like feet, with a little brass dish on an iron bracket and a mouse tail curling over its rim. “idle” (16 frames) breathes, blinks slowly and ruffles its feathers once; “look” (10 frames, once) swivels the head right and back; “hoot” (8 frames, once) puffs up and opens the beak as two sound arcs ripple out; “night” (16 frames) drops the plumage into shadow while the eyes glow green-gold.
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.
// Owl perch — 32x48 floor prop (tall), top-down 3/4, key light top-left.// A witch's tawny owl with a grey facial disc and amber eyes sits on a gnarled wooden T-perch that stands on// three root-like feet. A little brass dish hangs off the post on an iron bracket (a mouse tail curls over its rim).// Variants:// idle 16f loop breathes, blinks slowly, ruffles its feathers once// look 10f once the head swivels to the right and back; ends on idle frame 0// hoot 8f once puffs up, opens the beak, two sound arcs ripple out; ends on idle frame 0// night 16f loop in the dark: the plumage falls into shadow, the eyes glow green-gold, it blinksThe last lines. submit declares the canvas and lists the variants the renderer exports.
submit(OwlPerch, { size: [W, H], origin: "top-left", kind: "icon", scale: 1, animate: { fps: 10, duration: 1.6, loop: true }, anchor: { x: 16, y: GROUND }, args: ["idle"], variants: [ { name: "idle", args: ["idle"] }, { name: "look", args: ["look"], animate: { fps: 10, duration: 1.0, loop: false } }, { name: "hoot", args: ["hoot"], animate: { fps: 10, duration: 0.8, loop: false } }, { name: "night", args: ["night"] }, ],});Show the whole program (273 lines)
// Owl perch — 32x48 floor prop (tall), top-down 3/4, key light top-left.// A witch's tawny owl with a grey facial disc and amber eyes sits on a gnarled wooden T-perch that stands on// three root-like feet. A little brass dish hangs off the post on an iron bracket (a mouse tail curls over its rim).// Variants:// idle 16f loop breathes, blinks slowly, ruffles its feathers once// look 10f once the head swivels to the right and back; ends on idle frame 0// hoot 8f once puffs up, opens the beak, two sound arcs ripple out; ends on idle frame 0// night 16f loop in the dark: the plumage falls into shadow, the eyes glow green-gold, it blinksconst W = 32, H = 48, GROUND = 45;const OX = 15; // owl centre columnconst L = [-0.74, -0.67];const WOOD = ["w0", "w1", "w2", "w3", "w4"];const BAYER = [0, 8, 2, 10, 12, 4, 14, 6, 3, 11, 1, 9, 15, 7, 13, 5];const dith = (x, y, d) => BAYER[(y & 3) * 4 + (x & 3)] / 16 < d;// night: every plumage/wood/brass key falls two steps into shadow (the eyes are exempt)const NIGHT = { w4: "w2", w3: "w1", w2: "w1", w1: "w0", w0: "n0", n5: "n3", n4: "n2", n3: "n2", n2: "n1", n1: "n0", n0: "n0", g3: "g1", g2: "g0", g1: "g0", g0: "w0" };let NIGHT_ON = false;const K = (k) => (NIGHT_ON && NIGHT[k]) || k;function limb(g, path, w0, w1, opt = {}) { const a = path[0], b = path[path.length - 1]; const dx = b[0] - a[0], dy = b[1] - a[1], len = Math.hypot(dx, dy) || 1; const nl = (-dy / len) * L[0] + (dx / len) * L[1]; g.sweep(path, { w0, w1, wobble: opt.wobble ?? 0.2, wobblePhase: opt.ph ?? 0.5 }, (u, v) => { let l = v * nl * 1.3 + 0.1 - (opt.dark || 0); if (opt.grain) { const s = u * opt.grain + v * 0.4; if (s - Math.floor(s) < 0.18) l -= 0.5; } return K(WOOD[l > 0.85 ? 4 : l > 0.15 ? 3 : l > -0.4 ? 2 : l > -0.85 ? 1 : 0]); });}// ---- the perch -----------------------------------------------------------------------------------------------function perch(g) { g.part("feet", () => { limb(g, [[16, 41], [12, 43], [9, 44], [6, 45]], 1.7, 0.6, { ph: 0.3 }); limb(g, [[16, 41], [20, 43], [23, 44], [26, 45]], 1.7, 0.6, { ph: 1.7, dark: 0.15 }); limb(g, [[16, 41], [17, 43], [18, 44]], 1.3, 0.7, { ph: 2.4, dark: 0.3 }); }); g.part("post", () => { limb(g, [[16, 42], [15, 38], [17, 34], [16, 30]], 2.0, 1.5, { grain: 3, ph: 1.1 }); });}function crossbar(g) { g.part("crossbar", () => { limb(g, [[2, 30], [7, 29], [14, 29], [20, 28], [25, 29], [29, 27]], 1.6, 1.1, { ph: 0.9, grain: 4 }); limb(g, [[4, 29], [2, 26], [2, 24]], 0.8, 0.4, { ph: 2.0 }); // a twig left on the end limb(g, [[26, 28], [28, 25]], 0.7, 0.4, { ph: 0.4 }); g.pixel(22, 28, K("w0")); g.pixel(23, 28, K("w1")); // a knot });}function dish(g) { g.part("bracket", () => { g.line([17, 35], [21, 35], K("n2")); g.line([17, 36], [20, 36], K("n0")); g.pixel(17, 35, K("n3")); }); g.part("dish", () => { // a shallow brass bowl: lit rim, dark inside, rounded belly g.ellipse({ x: [20, 28], y: [32, 35] }, K("g1"), { fill: K("g1") }); g.line([21, 36], [27, 36], K("g1")); g.line([22, 37], [26, 37], K("g0")); g.line([20, 34], [22, 36], K("g2")); g.line([27, 35], [26, 36], K("g0")); g.ellipse({ x: [21, 27], y: [32, 34] }, K("g0"), { fill: K("g0") }); g.line([21, 32], [24, 32], K("g3")); g.pixel(20, 33, K("g2")); // the lit rim edge g.line([25, 32], [27, 32], K("g2")); g.pixel(28, 33, K("g1")); g.pixel(23, 33, K("w0")); g.pixel(24, 33, K("n1")); g.pixel(25, 34, K("w0")); // something in it }); g.part("mouseTail", () => { g.pixel(26, 33, K("n3")); g.pixel(27, 33, K("n3")); g.pixel(28, 34, K("n3")); g.pixel(29, 35, K("n3")); g.pixel(29, 36, K("n2")); g.pixel(28, 37, K("n2")); });}// ---- the owl -------------------------------------------------------------------------------------------------// pose: { hy head offset, chest 0/1 breath, blink 0..1, turn 0..1, puff 0..1, beak bool, ruffle 0..1 }function tail(g) { g.part("tail", () => { g.fillPoly([[12, 27], [19, 27], [18, 32], [13, 32]], K("w1")); g.line([13, 28], [13, 31], K("w2")); g.line([14, 30], [17, 30], K("w0")); g.line([13, 32], [17, 32], K("w2")); });}// breast streaks: short dark shaft marks, placed by hand so they read as feathers, not stripesconst STREAKS = [[-2, -2], [1, -1], [-1, 2], [2, 3], [0, 5], [-2, 6]];function body(g, p) { // a pear: narrow under the head, full at the belly; it swells when the owl breathes or puffs up const cx = OX + 0.5, rx = 6.6 + p.puff * 1.3 + p.ruffle * 0.4, cy = 20.5 - p.chest * 0.5, ry = 7 + p.chest * 0.5 + p.puff * 0.3; const streak = (x, y) => STREAKS.some(([dx, dy]) => x === OX + dx && (y === Math.round(cy) + dy || y === Math.round(cy) + dy + 1)); g.part("body", () => g.field({ x: [OX - 10, OX + 11], y: [11, 28] }, (x, y) => { const ny = (y + 0.5 - cy) / ry; const nx = (x + 0.5 - cx) / (rx * (0.86 + 0.14 * Math.min(1, ny + 0.4))); let r2 = nx * nx + ny * ny; // ruffled: the outline breaks into feather tips if (p.ruffle > 0 && r2 > 0.85 && r2 < 1.3 && Math.abs(nx) > 0.5 && (y + (x > OX ? 1 : 0)) % 2 === 0) r2 = 0.9; if (r2 > 1 || y > 27) return null; const i = litIndex(x, y, cx, cy, rx, ry, 4, 0.05); if (y <= 15 + p.hy) return K(i >= 2 ? "w1" : "w0"); // shadow under the head if (p.puff > 0.4 && ny < -0.2 && Math.abs(nx) < 0.35) return K("n5"); // white throat puffed out if (streak(x, y)) return K(i >= 3 ? "w2" : "w1"); return K(["w1", "w2", "w3", "w4"][i]); }));}// folded wings lie over the flanks: a tapering shape from the shoulder to a tip that crosses the tailfunction wings(g, p) { const out = Math.round(p.puff * 1.2 + p.ruffle * 0.6); for (const s of [-1, 1]) { const lit = s < 0; const path = [[OX + 0.5 + s * (5.4 + out), 15], [OX + 0.5 + s * (6.3 + out), 20], [OX + 0.5 + s * (5.3 + out), 25], [OX + 0.5 + s * 2.4, 29]]; g.part(lit ? "wingL" : "wingR", () => g.sweep(path, { w0: 2.3, w1: 0.7, wobble: p.ruffle * 0.25, wobblePhase: s }, (u, v) => { const front = v * -s; // +1 at the edge facing the breast if (front > 0.55 && u < 0.75) return K(lit ? "w0" : "w0"); // the wing's front edge if (front > -0.1 && front < 0.5 && u > 0.1 && u < 0.5 && Math.abs(((u * 7.5) % 1) - 0.5) < 0.13) return K("n4"); // pale scapular spots const bar = (u * 9 + v * 0.6) % 1 < 0.3; const l = (lit ? 0.1 : -0.45) - front * 0.2 + (bar ? -0.6 : 0); return K(l > 0.3 ? "w3" : l > -0.25 ? "w2" : l > -0.75 ? "w1" : "w0"); })); }}function head(g, p) { const cx = OX + 0.5, cy = 9.5 + p.hy, rx = 8.1 + p.puff * 0.3, ry = 6.1; const fx = Math.round(p.turn * 3); // the face swings this far to the right const fcx = cx + fx, fcy = 10 + p.hy; const E = [[OX - 4 + fx, 8 + p.hy], [OX + 3 + fx, 8 + p.hy]]; // eye top-left corners (3x3) const inHead = (x, y) => { const nx = (x + 0.5 - cx) / (rx - 0.7), ny = (y + 0.5 - cy) / (ry - 0.7); return nx * nx + ny * ny <= 1; }; g.part("head", () => g.field({ x: [OX - 9, OX + 10], y: [2 + p.hy, 17 + p.hy] }, (x, y) => { const nx = (x + 0.5 - cx) / rx, ny = (y + 0.5 - cy) / ry; if (nx * nx + ny * ny > 1) return null; const i = litIndex(x, y, cx, cy, rx, ry, 4, 0.1); // facial disc: one grey disc, notched at the top by the brows, rimmed in dark brown const dx = (x + 0.5 - fcx) / 5.6, dy = (y + 0.5 - fcy) / 4.4; const dr = Math.sqrt(dx * dx + dy * dy) + (dy < -0.4 && Math.abs(dx) < 0.25 ? 0.25 : 0); if (dr < 1.2 && inHead(x, y)) { if (dr < 1) { const d0 = Math.min(Math.hypot(x - E[0][0] - 1, y - E[0][1] - 1), Math.hypot(x - E[1][0] - 1, y - E[1][1] - 1)); return K(d0 < 2.6 ? (i >= 2 ? "n4" : "n3") : (i >= 2 ? "n3" : "n2")); } if (dy > -0.5) return K("w0"); } // crown: tawny, flecked if ((x * 3 + y * 2) % 7 === 0 && ny < 0.4) return K(i >= 2 ? "w1" : "w0"); if ((x * 3 + y * 2) % 7 === 3 && ny < 0 && i >= 2) return K("w4"); return K(["w0", "w1", "w2", "w3"][i]); })); // pale brows over the eyes meeting above the beak (the stern look of a witch owl) g.part("brows", () => { const by = 6 + p.hy; for (const [px, py] of [[OX + fx, by + 1], [OX - 1 + fx, by], [OX - 2 + fx, by], [OX - 3 + fx, by], [OX + 1 + fx, by + 1], [OX + 2 + fx, by], [OX + 3 + fx, by], [OX + 4 + fx, by]]) if (inHead(px, py)) g.pixel(px, py, K(px <= OX + fx ? "n5" : "n4")); }); g.part("eyes", () => { E.forEach(([ex, ey], k) => { for (let dy = 0; dy < 3; dy++) for (let dx = 0; dx < 3; dx++) { const x = ex + dx, y = ey + dy; if (!inHead(x, y)) continue; if (k === 1 && p.turn > 0.6 && dx === 2) continue; // the far eye foreshortens as the head turns const lid = p.blink >= 1 ? 3 : p.blink > 0 ? 1 : 0; let c; if (dy < lid) c = dy === 2 || (lid === 1 && dy === 0) ? "w0" : "w2"; else if (p.night) c = dx === 1 && dy >= 1 ? "g3" : dy === 0 ? "e2" : "e3"; else if (dx === 1 && dy >= 1) c = "ink"; else if (dx === 0 && dy === lid) c = "g3"; else c = dy === 2 ? "g1" : "g2"; g.pixel(x, y, p.night && dy >= lid ? c : K(c)); } }); }); g.part("beak", () => { const bx = OX + fx, by = 10 + p.hy; if (p.beak) { g.pixel(bx, by, K("n4")); g.pixel(bx + 1, by, K("n3")); g.pixel(bx, by + 1, "ink"); g.pixel(bx + 1, by + 1, "r0"); g.pixel(bx, by + 2, K("n3")); g.pixel(bx + 1, by + 2, K("n2")); } else { g.pixel(bx, by, K("n4")); g.pixel(bx + 1, by, K("n3")); g.pixel(bx, by + 1, K("n3")); g.pixel(bx + 1, by + 1, K("n2")); g.pixel(bx, by + 2, K("n2")); } });}function feet(g) { g.part("talons", () => { for (const x of [OX - 3, OX + 2]) { g.pixel(x, 27, K("n4")); g.pixel(x + 1, 27, K("n3")); g.pixel(x + 2, 27, K("n3")); g.pixel(x, 28, K("n1")); g.pixel(x + 2, 28, K("n1")); } });}function sound(g, f) { // two concentric arcs ripple out from the beak to the right g.detached("sound", () => { for (const k of [0, 1]) { const age = f - 2 - k; if (age < 0 || age > 2) continue; const r = 2 + age + k * 2.5; const key = age === 0 ? "n5" : age === 1 ? "n4" : "n3"; for (let a = -0.9; a <= 0.9; a += 0.1) { const x = Math.round(OX + 7 + r * Math.cos(a)), y = Math.round(10 + r * Math.sin(a) * 1.25); if (x < W) g.pixel(x, y, key); } } });}function idlePose(t) { const i = Math.round(t * 16) % 16; const s = Math.sin(TAU * t); return { hy: s > 0.35 ? -1 : 0, chest: s > 0.35 ? 1 : 0, blink: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0.5, 1, 0.5, 0, 0, 0, 0][i], ruffle: [0, 0, 0, 0.5, 1, 1, 0.5, 0, 0, 0, 0, 0, 0, 0, 0, 0][i], turn: 0, puff: 0, beak: false, };}const OwlPerch = sprite(pal, (g, t, mode) => { t = ((t % 1) + 1) % 1; NIGHT_ON = mode === "night"; let p = idlePose(mode === "look" || mode === "hoot" ? 0 : t); let f = 0; if (mode === "look") { f = Math.min(9, Math.round(t * 10)); p = { ...p, turn: [0, 0.35, 0.7, 1, 1, 1, 1, 0.65, 0.3, 0][f], blink: f === 5 ? 1 : f === 4 || f === 6 ? 0.5 : 0 }; } if (mode === "hoot") { f = Math.min(7, Math.round(t * 8)); const puff = [0, 0.5, 1, 1, 1, 0.6, 0.2, 0][f]; p = { ...p, puff, beak: f >= 2 && f <= 4, hy: f >= 2 && f <= 4 ? 1 : 0, chest: f === 1 || f === 5 ? 1 : 0 }; } if (mode === "night") { const i = Math.round(t * 16) % 16; p = { ...p, blink: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.5, 1, 0.5, 0][i], ruffle: 0 }; } p.night = NIGHT_ON; contactShadow(g, 16, GROUND, 11, 1); g.outlined("ink", () => { perch(g); dish(g); tail(g); crossbar(g); body(g, p); wings(g, p); head(g, p); feet(g); }); if (mode === "hoot") sound(g, f);});submit(OwlPerch, { size: [W, H], origin: "top-left", kind: "icon", scale: 1, animate: { fps: 10, duration: 1.6, loop: true }, anchor: { x: 16, y: GROUND }, args: ["idle"], variants: [ { name: "idle", args: ["idle"] }, { name: "look", args: ["look"], animate: { fps: 10, duration: 1.0, loop: false } }, { name: "hoot", args: ["hoot"], animate: { fps: 10, duration: 0.8, loop: false } }, { name: "night", args: ["night"] }, ],});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.