Tessarune
Witch's Hut · bat_roost.js

蝙蝠のねぐら

48×32 · JavaScript 258 行

48×32 の天井・壁用の小物で、接地影はありません。苔むした荒削りの樫の梁に、翼をたたみ赤い目を光らせた小さな蝙蝠が 3 匹逆さにぶら下がり、左端の下には蜘蛛の巣と吊り下がった蜘蛛がいます。idle(16 コマ)は蝙蝠がずれた拍子で揺れ、右の 1 匹が翼を伸ばします。flutter(12 コマの一回再生)は 1 匹ずつ足を離して画面の外へ飛び去ります。empty(16 コマ)は何もいない梁で、巣が揺れます。return(12 コマの一回再生)は蝙蝠が戻ってぶら下がり、idle の最初のコマで終わります。

蝙蝠のねぐら — 描画結果6×
このプログラムで描画(変種「return」) · 48×32 · 12 コマ
Witch's Hut動くスプライト素材: Witch's Hut(itch.io)Vol.5・ハロウィンの小物 12 点・42 アニメ

冒頭のコメント。これが指示書です。サイズ、視点、光の向き、すべての変種が書かれています。

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

最後の数行。submit でキャンバスの大きさと、描画エンジンが書き出す変種の一覧を指定します。

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 } },  ],});
プログラム全体を表示 (258 行)
bat_roost.js · 258 行
// 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 } },  ],});

掲載されたコードでは、ビルドスクリプトが自動付与する共通部分を省略しています。具体的には 1 行のランタイムヘッダー、共有パレット、および汎用ヘルパー関数(contactShadow、litIndex、flame、hash、chk、TAU)です。表示されている行番号は実際のファイルと一致しています。これらのプログラムは AIエージェントが生成し、描画されたコマの仕上がりを確認しながら修正を加えたものです。

エージェントに、こうしたプログラムを書かせる。

Tessarune は描画 API のリファレンス、25 冊の制作マニュアル、結果を確かめるための描画エンジンをエージェントに渡します。