Tessarune
Graveyard & Chapel · raven_tree.js

鴉の木

64×64 · JavaScript 269 行

64×64 の床置きの小物で、らせんの溝とうろのあるねじれた幹、むき出しの根、曲がりくねった枝を持つ葉のない枯れ木です。地面には枯れ葉が少し落ち、右の大枝に黒い鴉がとまっています。idle(16 コマ)は枝がきしみ、鴉が身じろぎして首をかしげ、枯れ葉が一枚くるくると落ちます。caw(10 コマの一回再生)は鴉が身をかがめ、くちばしを大きく開け、翼を半分広げてから落ち着きます。empty(16 コマ)は鴉が飛び去ったあとの木だけで、地面に羽根が一枚落ち、葉が舞い落ちます。

鴉の木 — 描画結果6×
このプログラムで描画(変種「caw」) · 64×64 · 10 コマ
Graveyard & Chapel動くスプライト素材: Graveyard & Chapel(itch.io)Vol.4・小物 12 点・33 アニメ

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

raven_tree.js · 1–7
// Raven tree — 64x64 floor prop, top-down 3/4, key light top-left.// A gnarled, leafless dead tree: a twisted trunk with spiral grooves and a hollow knot, exposed roots, crooked// branches, a few dead leaves on the ground. A black raven perches on the right bough.// Variants://   idle   16f loop  branches creak, the raven shifts and cocks its head, one dead leaf spins down//   caw    10f once  the raven hunches, gapes, half-spreads its wings and settles; ends on idle frame 0//   empty  16f loop  the tree alone (the raven flew off; a lost feather lies on the ground), leaf falls

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

raven_tree.js · 259–269
submit(RavenTree, {  size: [W, H], origin: "top-left", kind: "icon", scale: 1,  animate: { fps: 10, duration: 1.6, loop: true },  anchor: { x: CX, y: GROUND },  args: ["idle"],  variants: [    { name: "idle" },    { name: "caw", args: ["caw"], animate: { fps: 10, duration: 1.0, loop: false } },    { name: "empty", args: ["empty"] },  ],});
プログラム全体を表示 (269 行)
raven_tree.js · 269 行
// Raven tree — 64x64 floor prop, top-down 3/4, key light top-left.// A gnarled, leafless dead tree: a twisted trunk with spiral grooves and a hollow knot, exposed roots, crooked// branches, a few dead leaves on the ground. A black raven perches on the right bough.// Variants://   idle   16f loop  branches creak, the raven shifts and cocks its head, one dead leaf spins down//   caw    10f once  the raven hunches, gapes, half-spreads its wings and settles; ends on idle frame 0//   empty  16f loop  the tree alone (the raven flew off; a lost feather lies on the ground), leaf fallsconst W = 64, H = 64, CX = 32, GROUND = 61;const L = [-0.74, -0.67];                                   // key light direction (top-left)const BARK = ["n0", "w0", "w1", "w2", "n3"];                // cool shadow -> lit bark// ---- tree -------------------------------------------------------------------------------------------------// a limb swept along a path; shaded as a cylinder lit from the top-left (v runs across the limb)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];         // how much the +v side faces the light  const grooves = opt.grooves || 0;  g.sweep(path, { w0, w1, wobble: opt.wobble ?? 0.15, wobblePhase: opt.ph ?? 0.7 }, (u, v) => {    let l = v * nl * 1.3 + 0.12 - (opt.dark || 0);    if (grooves) {                                           // twisted trunk: spiral grooves, one step darker      const s = u * grooves + v * 0.55;      if (Math.abs(v) < 0.7 && s - Math.floor(s) < 0.2) l -= 0.55;    }    return BARK[l > 0.9 ? 4 : l > 0.12 ? 3 : l > -0.38 ? 2 : l > -0.8 ? 1 : 0];  });}// creak: tips drift by up to ~1px, periodic in t (a whole cycle per loop)const creak = (t, [x, y], k, ph) => [x + 0.9 * k * Math.sin(TAU * (t + ph)), y + 0.35 * k * Math.sin(TAU * (t + ph + 0.25))];const PERCH = { x: 46, y: 26 };                             // the raven's feetfunction tree(g, t) {  g.part("roots", () => {    limb(g, [[29, 56], [25, 59], [21, 60], [16, 61]], 2.4, 0.5, { ph: 0.2 });    limb(g, [[35, 56], [39, 59], [43, 60], [48, 61]], 2.4, 0.5, { ph: 1.4, dark: 0.1 });    limb(g, [[33, 57], [34, 60], [36, 61]], 1.8, 0.7, { ph: 2.1 });    limb(g, [[30, 57], [28, 60], [27, 61]], 1.6, 0.6, { ph: 0.5 });  });  g.part("trunk", () => {    limb(g, [[32, 58], [33, 51], [30, 43], [32, 35], [30, 28], [31, 22]], 4.3, 2.0, { grooves: 3.5, wobble: 0.14, ph: 1.9 });  });  g.part("branches", () => {    // left bough, gnarled, with a drooping fork and an upturned twig    limb(g, [[30, 36], [26, 33], [22, 32], [18, 28], [14, 27], creak(t, [9, 22], 1, 0.1)], 2.0, 0.4, { ph: 0.3 });    limb(g, [[18, 29], [13, 32], creak(t, [8, 33], 1, 0.35)], 1.0, 0.4, { ph: 2.4 });    limb(g, [[22, 32], [20, 27], creak(t, [21, 23], 0.7, 0.6)], 0.9, 0.4, { ph: 1.1 });    // top leader, crooked, with side twigs    limb(g, [[31, 24], [33, 17], [29, 12], [31, 7], creak(t, [28, 2], 1, 0.5)], 2.0, 0.4, { ph: 0.9 });    limb(g, [[32, 17], [35, 15], creak(t, [36, 12], 0.6, 0.8)], 0.9, 0.4, { ph: 1.7 });    limb(g, [[30, 11], [26, 9], creak(t, [24, 5], 0.7, 0.2)], 0.8, 0.4, { ph: 0.4 });    // upper-left arm    limb(g, [[30, 25], [26, 21], [24, 16], [19, 13], creak(t, [17, 9], 0.9, 0.9)], 1.4, 0.4, { ph: 2.8 });    // right bough: the raven's perch (stiff where the bird sits), tip creaks    limb(g, [[32, 34], [38, 30], [44, 28], [50, 26], [55, 24], creak(t, [60, 20], 1, 0.3)], 2.0, 0.4, { ph: 1.2, dark: 0.05 });    limb(g, [[50, 26], [54, 29], creak(t, [58, 31], 0.8, 0.7)], 0.9, 0.4, { ph: 0.6 });    limb(g, [[38, 30], [41, 33], creak(t, [40, 37], 0.6, 0.4)], 0.8, 0.4, { ph: 2.2 });  });}function barkMarks(g) {  g.part("knot", () => {    // a hollow knot on the trunk: dark hole, lit lower lip, shaded upper rim    g.ellipse({ x: [30, 33], y: [42, 46] }, "ink", { fill: "ink" });    g.pixel(31, 43, "v0");    g.line([30, 47], [32, 47], "w2"); g.pixel(29, 46, "n3");    g.line([30, 41], [32, 41], "w0");  });  g.part("barkMarks", () => {    g.pixel(29, 52, "w3"); g.pixel(28, 55, "w3");          // lit bark ridges near the flare    g.pixel(35, 38, "w0"); g.pixel(35, 53, "w0");  });}// ---- ground litter ----------------------------------------------------------------------------------------function litter(g, feather) {  g.part("litter", () => {    for (const [x, y, k] of [[13, 60, "w3"], [15, 61, "w2"], [21, 62, "r1"], [49, 61, "w3"], [52, 60, "w2"], [41, 62, "w2"]]) {      g.pixel(x, y, k); g.pixel(x + 1, y, k === "w3" ? "w2" : "w1");    }    g.pixel(24, 61, "n2"); g.pixel(24, 60, "n3"); g.pixel(25, 59, "n2");       // dry grass tufts    g.pixel(45, 60, "n2"); g.pixel(46, 59, "n3"); g.pixel(40, 60, "n2");    if (feather) {                                                             // a lost raven feather      g.line([51, 62], [55, 60], "v0"); g.pixel(52, 61, "b0"); g.pixel(56, 60, "n3");    }  });}// ---- falling leaf -----------------------------------------------------------------------------------------function leaf(g, t) {  const p = ((t % 1) + 1) % 1;  const fall = 0.82;  if (p > fall + 0.1) return;                                // it has landed in the litter (13,60)  const q = Math.min(1, p / fall);  const x = Math.round(13 + 4 * Math.sin(TAU * q * 2.2) * Math.sin(Math.PI * q) - q * 0.5);  const y = Math.round(21 + q * q * 0.35 * 39 + q * 0.65 * 39);  const spin = Math.floor(p * 32) % 4;  g.detached("leaf", () => {    if (q >= 1) { g.pixel(13, 60, "w3"); g.pixel(14, 60, "w2"); return; }    if (spin === 0) { g.pixel(x, y, "w3"); g.pixel(x + 1, y, "r1"); }    else if (spin === 1) { g.pixel(x, y, "w3"); g.pixel(x + 1, y + 1, "w1"); }    else if (spin === 2) { g.pixel(x, y, "w2"); g.pixel(x, y + 1, "w1"); }    else { g.pixel(x, y, "w1"); g.pixel(x + 1, y, "w2"); }  });}// ---- raven ------------------------------------------------------------------------------------------------// Drawn in bird-local coords (x < 0 = forward, toward the head) and mirrored into the world so it faces right,// out over open sky. Lighting is computed in world space, so the top-left of each mass catches the key light.// pose: { by body drop, hx/hy head offset, look "fwd"|"down"|"up"|"back", gape 0..2, wing 0..1, tail }const WING_TUCK = [[-1, -7], [1, -8], [5, -8], [7, -6], [6, -5], [7, -4], [5, -4], [5, -3], [3, -3]];const WING_OPEN = [[-1, -7], [-1, -12], [3, -14], [7, -12], [7, -10], [8, -8], [7, -6], [6, -4], [3, -3]];// primary feathers: [base (tucked position), spread tip]const FINGERS = [[[2, -9], [-1, -18]], [[3, -8], [3, -19]], [[4, -7], [7, -18]], [[5, -6], [11, -15]]];function raven(g, P) {  const X = PERCH.x, Y = PERCH.y;  const at = (x, y) => [X - Math.round(x), Y + Math.round(y)];  const by = P.by || 0, s = P.wing || 0;  g.part("ravenLegs", () => {    g.line(at(-1, -2), at(-1, 0), "n2"); g.line(at(1, -2), at(1, 0), "n1");    g.pixel(...at(-2, 0), "n3"); g.pixel(...at(2, 0), "n2");  });  if (s > 0) {                                               // far wing: its tip shows above the near wing    g.part("ravenWingFar", () => {      const pts = [[-2, -8], [-2, -8 - 5 * s], [3, -9 - 7 * s], [3, -8]].map(([x, y]) => at(x, y + by));      g.fillPoly(pts, "ink");      g.line(pts[1], pts[2], "v0");    });  }  g.part("ravenBody", () => {    g.field({ x: [X - 10, X + 6], y: [Y - 10 + by, Y + 3 + by] }, (x, y) => {      const lx = X - x, ly = y - Y - by;      const dx = lx - 0.5, dy = ly + 5.2;      const pu = (dx * -0.8 + dy * -0.6) / 5.4, pv = (dx * 0.6 + dy * -0.8) / 3.4;   // body leans chest-up      const inBody = pu * pu + pv * pv <= 1.02;      const nx = dx / 4.6, ny = dy / 4.2;      // tail: a wedge hanging down and back behind the perch      const ax = 3, ay = -3.5, bx = 8 + (P.tail || 0), bY = 1.5;      const tx = bx - ax, ty = bY - ay, tl2 = tx * tx + ty * ty;      const k = ((lx - ax) * tx + (ly - ay) * ty) / tl2;      const dperp = Math.abs((lx - ax) * ty - (ly - ay) * tx) / Math.sqrt(tl2);      const inTail = k >= 0 && k <= 1 && dperp < 1.9 - 0.5 * k;      if (!inBody && !inTail) return null;      if (!inBody) {        const side = (lx - ax) * ty - (ly - ay) * tx;          // > 0 = upper (lit) side of the tail        return side > 0.4 ? "v0" : "ink";      }      const d = 0.6 * nx - 0.8 * ny;                           // world light: top-left (local +x is world left)      if (d > 0.72) return "b0";      if (d > 0.0) return "v0";      return "ink";    });  });  g.part("ravenWing", () => {    const L2 = (A, B) => at(A[0] + (B[0] - A[0]) * s, A[1] + (B[1] - A[1]) * s + by);    const pts = WING_TUCK.map((p, i) => L2(p, WING_OPEN[i]));    g.fillPoly(pts, "v0");    g.line(pts[0], pts[1], "b0");                               // lit leading edge (arm)    if (s > 0.25) {      // primaries fan out as separate fingers past the wing's edge      FINGERS.forEach(([b0, tip], i) => {        const base = at(b0[0], b0[1] + by), end = L2(b0, tip);        g.line(base, end, "v0");        g.pixel(...end, i === 0 ? "b1" : "b0");      });      g.line(pts[1], pts[2], "b0");      g.pixel(...pts[1], "b1");    } else {      g.line(at(2, -4 + by), at(6, -4 + by), "ink");           // folded primaries edge      g.pixel(...at(4, -6 + by), "ink");      g.line(pts[1], pts[2], "v0");    }  });  const hx = -4 + (P.hx || 0), hy = -10 + by + (P.hy || 0);  g.part("ravenHead", () => {    const [cx, cy] = at(hx, hy);    g.ellipse({ x: [cx - 2, cx + 2], y: [cy - 2, cy + 1] }, "v0", { fill: "v0" });    g.line([cx - 1, cy - 2], [cx, cy - 2], "b0");    g.pixel(cx + 2, cy + 1, "ink"); g.pixel(cx + 1, cy + 1, "ink"); g.pixel(cx + 2, cy, "ink");    g.pixel(cx, cy + 2, "ink"); g.pixel(cx + 1, cy + 2, "ink");              // throat hackles  });  g.part("ravenBeak", () => {    const look = P.look || "fwd", o = P.gape || 0;    const B = (x, y) => at(hx + x, hy + y);    if (look === "back") {      g.fillPoly([B(1, -1), B(5, -1), B(1, 0)], "n1"); g.line(B(1, -1), B(4, -1), "n2");    } else if (o > 0) {      g.fillPoly([B(-2, -1), B(-6, -1 - o), B(-2, 0)], "n1");          // upper mandible      g.line(B(-2, -1), B(-5, -1 - o), "n2");      g.fillPoly([B(-2, 1), B(-5, 1 + o), B(-2, 1)], "n0");            // lower mandible      g.line(B(-2, 1), B(-5, 1 + o), "n1");      g.pixel(...B(-3, 0), "r0"); if (o > 1) { g.pixel(...B(-3, 1), "r1"); g.pixel(...B(-4, 0), "r0"); g.pixel(...B(-4, 1), "r0"); }    } else {      const dy = look === "down" ? 1 : look === "up" ? -1 : 0;      g.fillPoly([B(-2, -1), B(-6, dy), B(-2, 1)], "n1");      g.line(B(-2, -1), B(-5, dy), "n2");                              // lit ridge (culmen)    }  });  g.part("ravenEye", () => g.pixel(...at(P.look === "back" ? hx + 1 : hx - 1, hy - 1), "g3"));  g.part("ravenSheen", () => g.line(at(1, -8 + by), at(3, -8 + by), "b1"));   // one lit sheen on the back}// idle choreography over 16 frames: hold, cock head down, hold, look back, hold, shift weightfunction idlePose(t) {  const f = Math.round(((t % 1) + 1) % 1 * 16) % 16;  if (f >= 3 && f <= 5) return { look: "down", hx: -1, hy: 1 };  if (f === 6) return { look: "up", hy: -1 };  if (f >= 9 && f <= 11) return { look: "back", hx: 1 };  if (f === 13) return { by: 1, tail: 1 };  if (f === 14) return { by: 0, tail: 1, hy: -1 };  return {};}// caw over 10 frames; frame 9 is idle frame 0const CAW = [  {},  { by: 1, hx: -1, hy: 1, look: "down" },  { by: 1, hx: -1, hy: 1, gape: 1, wing: 0.4 },  { by: 0, hx: -1, hy: -1, gape: 2, wing: 0.85, tail: 1 },  { by: 0, hx: -1, hy: -1, gape: 2, wing: 1, tail: 1, sound: 1 },  { by: 0, hx: -1, hy: -1, gape: 2, wing: 0.9, tail: 1, sound: 2 },  { by: 0, hx: -1, gape: 1, wing: 0.5 },  { by: 1, hx: -1, hy: 1, wing: 0.2, look: "down" },  { by: 1 },  {},];function sound(g, s) {  const X = PERCH.x + 13, Y = PERCH.y - 13;  g.detached("caw", () => {    // two sound arcs opening away from the beak; the first one travels out and fades    const o = s === 1 ? 0 : 3, k = s === 1 ? "n4" : "n3";    g.pixel(X + o, Y - 2, k); g.pixel(X + o + 1, Y - 1, k); g.pixel(X + o + 1, Y, k); g.pixel(X + o, Y + 1, k);    if (s === 2) { g.pixel(X, Y - 1, "n4"); g.pixel(X + 1, Y, "n4"); g.pixel(X, Y + 1, "n4"); }  });}const RavenTree = sprite(pal, (g, t, state) => {  t = ((t % 1) + 1) % 1;  let ft = t, P = null;  if (state === "idle") P = idlePose(t);  else if (state === "caw") {    const f = Math.min(9, Math.round(t * 10));    ft = ((f - 9) / 16 + 1) % 1;                             // tree and leaf on the idle clock; f9 = idle t 0    P = CAW[f];  }  contactShadow(g, CX, GROUND, 13, 2);  litter(g, state === "empty");  g.outlined("ink", () => {    tree(g, ft);    if (P) raven(g, P);  });  barkMarks(g);  leaf(g, ft);  if (P && P.sound) sound(g, P.sound);});submit(RavenTree, {  size: [W, H], origin: "top-left", kind: "icon", scale: 1,  animate: { fps: 10, duration: 1.6, loop: true },  anchor: { x: CX, y: GROUND },  args: ["idle"],  variants: [    { name: "idle" },    { name: "caw", args: ["caw"], animate: { fps: 10, duration: 1.0, loop: false } },    { name: "empty", args: ["empty"] },  ],});

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

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

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