Witch's Hut · owl_perch.js
ふくろうの止まり木
32×48 の縦長の床置きの小物で、灰色の顔盤と琥珀色の目をした魔女のモリフクロウが、根のような三本の足で立つ節くれだった木の T 字の止まり木にとまっています。柱には鉄の腕木で小さな真鍮の皿が掛かり、縁からネズミの尻尾がのぞきます。idle(16 コマ)は呼吸してゆっくりまばたきし、一度羽をふくらませます。look(10 コマの一回再生)は首を右へ回して戻します。hoot(8 コマの一回再生)は体をふくらませてくちばしを開き、音の弧が 2 つ広がります。night(16 コマ)は羽が闇に沈み、目が緑がかった金色に光ります。
6×
動くスプライト素材: Witch's Hut(itch.io)Vol.5・ハロウィンの小物 12 点・42 アニメ
冒頭のコメント。これが指示書です。サイズ、視点、光の向き、すべての変種が書かれています。
owl_perch.js · 1–8
// 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 blinks最後の数行。submit でキャンバスの大きさと、描画エンジンが書き出す変種の一覧を指定します。
owl_perch.js · 262–273
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"] }, ],});プログラム全体を表示 (273 行)
owl_perch.js · 273 行
// 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"] }, ],});掲載されたコードでは、ビルドスクリプトが自動付与する共通部分を省略しています。具体的には 1 行のランタイムヘッダー、共有パレット、および汎用ヘルパー関数(contactShadow、litIndex、flame、hash、chk、TAU)です。表示されている行番号は実際のファイルと一致しています。これらのプログラムは AIエージェントが生成し、描画されたコマの仕上がりを確認しながら修正を加えたものです。
エージェントに、こうしたプログラムを書かせる。
Tessarune は描画 API のリファレンス、25 冊の制作マニュアル、結果を確かめるための描画エンジンをエージェントに渡します。