Tessarune
Witch's Hut · owl_perch.js

ふくろうの止まり木

32×48 · JavaScript 273 行

32×48 の縦長の床置きの小物で、灰色の顔盤と琥珀色の目をした魔女のモリフクロウが、根のような三本の足で立つ節くれだった木の T 字の止まり木にとまっています。柱には鉄の腕木で小さな真鍮の皿が掛かり、縁からネズミの尻尾がのぞきます。idle(16 コマ)は呼吸してゆっくりまばたきし、一度羽をふくらませます。look(10 コマの一回再生)は首を右へ回して戻します。hoot(8 コマの一回再生)は体をふくらませてくちばしを開き、音の弧が 2 つ広がります。night(16 コマ)は羽が闇に沈み、目が緑がかった金色に光ります。

ふくろうの止まり木 — 描画結果6×
このプログラムで描画(変種「hoot」) · 32×48 · 8 コマ
Witch's Hut動くスプライト素材: 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 冊の制作マニュアル、結果を確かめるための描画エンジンをエージェントに渡します。