Tessarune
Harvest Festival · autumn_tree.js

秋の木

64×64 · JavaScript 280 行

木の幹から 3 本の枝が分かれ、赤や橙色の葉の塊が丸く茂る見下ろし 3/4 視点の 64×64 の大型の小物です。根元の草には落ち葉の輪が広がっています。16 コマのループである idle(待機)では樹冠が揺れて 3 枚の葉が回りながら落ち、12 コマの gust(突風)では強い風で木が右にしなります。16 コマの apples(リンゴ)では赤い実が落ちて草の上を転がります。

秋の木 — 描画結果6×
このプログラムで描画(変種「gust」) · 64×64 · 12 コマ
Harvest Festival動くスプライト素材: Harvest Festival(itch.io)Vol.6・収穫祭の小物 12 点・37 アニメ

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

autumn_tree.js · 1–10
// Autumn tree — 64x64 floor prop (large), top-down 3/4, key light top-left.// A full, round-crowned autumn tree: a sturdy trunk with furrowed bark and a root flare, three limbs forking into a// crown built from clustered leaf masses (orange, red and gold, with a dry brown clump and a last green one on the shaded side). Every// clump is lit from the top-left on its own; clumps behind are darkened where the clumps in front overlap them, and// the whole crown darkens toward its underside. A ring of fallen leaves lies in the grass around the roots.// Variants://   idle    16f loop  the crown sways a little (top more than bottom), three leaves fall, spin and land in the ring//   gust    12f loop  strong wind from the left: the crown leans right and shudders, a flurry of leaves blows across//   apples  16f loop  apple-tree version (green-gold crown, red apples); one apple drops, bounces, rolls into the//                     grass tuft, and a new one lowers into view from under the leaves so the loop closes

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

autumn_tree.js · 272–280
submit(AutumnTree, {  size: [W, H], origin: "top-left", kind: "icon", scale: 1,  anchor: { x: CX, y: GROUND },  variants: [    { name: "idle", args: ["idle"], animate: { fps: 10, duration: N_IDLE / 10, loop: true } },    { name: "gust", args: ["gust"], animate: { fps: 10, duration: N_GUST / 10, loop: true } },    { name: "apples", args: ["apples"], animate: { fps: 10, duration: N_APPLE / 10, loop: true } },  ],});
プログラム全体を表示 (280 行)
autumn_tree.js · 280 行
// Autumn tree — 64x64 floor prop (large), top-down 3/4, key light top-left.// A full, round-crowned autumn tree: a sturdy trunk with furrowed bark and a root flare, three limbs forking into a// crown built from clustered leaf masses (orange, red and gold, with a dry brown clump and a last green one on the shaded side). Every// clump is lit from the top-left on its own; clumps behind are darkened where the clumps in front overlap them, and// the whole crown darkens toward its underside. A ring of fallen leaves lies in the grass around the roots.// Variants://   idle    16f loop  the crown sways a little (top more than bottom), three leaves fall, spin and land in the ring//   gust    12f loop  strong wind from the left: the crown leans right and shudders, a flurry of leaves blows across//   apples  16f loop  apple-tree version (green-gold crown, red apples); one apple drops, bounces, rolls into the//                     grass tuft, and a new one lowers into view from under the leaves so the loop closesconst W = 64, H = 64, CX = 32, GROUND = 61;const N_IDLE = 16, N_GUST = 12, N_APPLE = 16;const L = [-0.74, -0.67];                                         // key light direction (top-left)// leaf ramps: index 0 = occluded shadow, 4 = lit edgeconst RAMP = {  r: ["w0", "r0", "r1", "r2", "r3"],                               // red  o: ["r0", "r1", "r2", "r3", "g2"],                               // orange  y: ["w1", "g0", "g1", "g2", "g3"],                               // gold  w: ["w0", "w1", "w2", "w3", "w4"],                               // dry brown  e: ["e0", "e1", "e2", "e3", "g2"],                               // green  d: ["e0", "e0", "e1", "e2", "e3"],                               // deep green (apple tree)  a: ["e0", "e0", "e1", "e2", "g2"],                               // apple-tree green};// clumps back -> front: [cx, cy, r, autumn ramp, apple ramp]const CLUMPS = [  [27, 10, 7.5, "o", "a"], [39, 11, 7.0, "y", "y"], [18, 16, 6.8, "r", "d"], [47, 17, 6.6, "d", "a"],  [32, 15, 7.6, "y", "a"], [11, 25, 6.0, "o", "a"], [53, 25, 6.0, "w", "d"], [22, 22, 7.8, "r", "y"],  [42, 22, 7.6, "o", "d"], [33, 26, 4.8, "w", "a"], [16, 31, 6.6, "y", "a"], [48, 31, 6.4, "r", "y"],  [29, 31, 7.6, "o", "d"], [40, 34, 6.0, "y", "a"], [22, 36, 4.8, "r", "d"],];const CROWN = { cx: 32, cy: 22, rx: 24, ry: 19 };// clump membership with a scalloped (leafy) rimfunction inClump(c, ox, x, y) {  const [cx, cy, r] = c;  const dx = x + 0.5 - (cx + ox[0]), dy = y + 0.5 - (cy + ox[1]);  const ry = r * 0.9;  const d = Math.sqrt((dx * dx) / (r * r) + (dy * dy) / (ry * ry));  const a = Math.atan2(dy, dx);  return d < 1 + 0.13 * Math.sin(a * 7 + cx * 1.3) + 0.06 * Math.sin(a * 11 + cy);}// countable leaf marks per clump (relative to the clump centre): lit leaf tips upper-left, one dark notch lower-rightfunction mark(c, rx, ry) {  const r = c[2];  const a = Math.round(-0.45 * r), b = Math.round(-0.35 * r);  if ((rx === a && ry === b) || (rx === a + 1 && ry === b) || (rx === a && ry === b + 1)) return 2;  const p = Math.round(0.1 * r), q = Math.round(-0.6 * r);  if ((rx === p && ry === q) || (rx === p + 1 && ry === q - 1)) return 2;  const s = Math.round(-0.75 * r), u = Math.round(0.05 * r);  if (rx === s && ry === u) return 1;  const m = Math.round(0.35 * r), n = Math.round(0.4 * r);  if ((rx === m && ry === n) || (rx === m + 1 && ry === n)) return -1;  return 0;}function crown(g, offs, rampIdx) {  const n = CLUMPS.length;  g.part("crown", () => g.field({ x: [0, W - 1], y: [0, 46] }, (x, y) => {    let top = -1;    for (let i = n - 1; i >= 0; i--) if (inClump(CLUMPS[i], offs[i], x, y)) { top = i; break; }    if (top < 0) return null;    const c = CLUMPS[top], o = offs[top];    const cx = c[0] + o[0], cy = c[1] + o[1], r = c[2];    // each clump: a lit crescent on its upper-left, a body tone, a shaded lower-right    const nx = (x + 0.5 - cx) / (r * 1.05), ny = (y + 0.5 - cy) / (r * 0.95);    const nz = Math.sqrt(Math.max(0, 1 - nx * nx - ny * ny));    const d = -0.62 * nx - 0.62 * ny + 0.3 * nz - 0.05;    // whole-crown light: the underside and the far right sit in shade    const gx = (x + 0.5 - CROWN.cx) / CROWN.rx, gy = (y + 0.5 - CROWN.cy) / CROWN.ry;    const G = -0.45 * gx - 0.6 * gy;    const li = d > 0.6 && G > -0.15 ? 3 : d > 0.12 ? 2 : d > -0.35 ? 1 : 0;    let idx = 1 + li;    if (G < -0.2) idx -= 1;    if (G < -0.7) idx -= 1;    // contact shade on a back clump around the clumps in front of it (+ their cast shadow to the bottom-right)    for (let j = top + 1; j < n; j++) {      const D = CLUMPS[j], q = offs[j];      if (inClump(D, q, x, y + 1.6) || inClump(D, q, x + 1.4, y + 0.6) || inClump(D, q, x - 1.4, y + 0.6) || inClump(D, q, x - 2, y - 2)) { idx -= 1; break; }    }    const mk = mark(c, x - Math.round(cx), y - Math.round(cy));    if (mk > 0 && li >= 1) idx = Math.max(idx, Math.min(4, li + mk));    if (mk < 0) idx -= 1;    const ramp = RAMP[c[rampIdx]];    return ramp[Math.max(0, Math.min(4, idx))];  }));}// ---- trunk & limbs ----------------------------------------------------------------------------------------const BARK = ["n0", "w0", "w1", "w2", "w3"];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.12, wobblePhase: opt.ph ?? 0.7 }, (u, v) => {    let l = v * nl * 1.25 + 0.15 - (opt.dark || 0);    if (opt.furrows) {                                              // vertical bark furrows, one step darker      const s = v * 2.3 + u * 0.4 + 0.2;      if (Math.abs(v) < 0.8 && s - Math.floor(s) < 0.2) l -= 0.5;    }    return BARK[l > 0.95 ? 4 : l > 0.2 ? 3 : l > -0.35 ? 2 : l > -0.8 ? 1 : 0];  });}function tree(g) {  g.part("roots", () => {    limb(g, [[29, 56], [26, 59], [22, 60], [19, 61]], 2.6, 0.6, { ph: 0.2 });    limb(g, [[35, 56], [38, 59], [42, 60], [45, 61]], 2.6, 0.6, { ph: 1.4, dark: 0.1 });    limb(g, [[31, 58], [30, 61]], 2.0, 1.0, { ph: 2.1 });    limb(g, [[34, 58], [36, 61]], 1.8, 0.9, { ph: 0.5, dark: 0.1 });  });  g.part("limbs", () => {    limb(g, [[31, 43], [27, 37], [22, 32], [18, 28]], 2.4, 1.0, { ph: 0.3 });    limb(g, [[33, 43], [37, 37], [42, 32], [46, 28]], 2.4, 1.0, { ph: 1.1, dark: 0.05 });    limb(g, [[32, 42], [33, 35], [31, 28]], 2.2, 1.0, { ph: 2.4 });  });  g.part("trunk", () => limb(g, [[32, 57], [32, 53], [31, 48], [32, 41]], 4.8, 3.2, { furrows: true, wobble: 0.1, ph: 1.9 }));}function barkMarks(g) {  g.part("barkMarks", () => {    g.line([30, 49], [30, 51], "ink"); g.pixel(31, 52, "w0"); g.pixel(29, 48, "w3");   // a knot with a lit lip    g.pixel(28, 55, "w3"); g.pixel(27, 57, "w3");                                       // lit ridges on the flare    g.pixel(35, 46, "w0"); g.pixel(36, 53, "w0");  });}// ---- ground: fallen-leaf ring and grass ---------------------------------------------------------------------const LEAF_KEYS = [["r2", "r1"], ["r3", "r2"], ["g2", "g1"], ["r1", "r0"], ["w3", "w2"], ["g1", "g0"]];const LITTER = [];for (let i = 0; i < 34; i++) {  const a = (i / 34) * TAU + hash(i, 3) * 0.25;  const rr = 0.72 + hash(i, 7) * 0.38;  LITTER.push([Math.round(CX + Math.cos(a) * 22 * rr), Math.round(59 + Math.sin(a) * 3.3 * rr), LEAF_KEYS[i % 6]]);}function litter(g, apple) {  g.part("litter", () => {    for (const [x, y] of [[13, 60], [19, 58], [47, 57], [52, 60], [26, 62], [41, 62]]) {    // grass blades      g.pixel(x, y, "e1"); g.pixel(x, y - 1, "e2"); g.pixel(x + 1, y, "e1");    }    for (const [x, y, k] of LITTER) {      if (apple && (x + y) % 3 === 0) continue;                      // fewer leaves under the apple tree      g.pixel(x, y, k[0]); g.pixel(x + 1, y, k[1]);      if ((x * 7 + y) % 4 === 0) g.pixel(x, y + 1, k[1]);    }  });}// ---- falling leaves -----------------------------------------------------------------------------------------// a spinning 2px leaf: flat, tipped, edge-on, tipped the other wayfunction leafShape(g, x, y, k, spin, big) {  if (spin === 0) { g.pixel(x, y, k[0]); g.pixel(x + 1, y, k[1]); if (big) g.pixel(x - 1, y - 1, k[0]); }  else if (spin === 1) { g.pixel(x, y, k[0]); g.pixel(x + 1, y + 1, k[1]); if (big) g.pixel(x - 1, y, k[0]); }  else if (spin === 2) { g.pixel(x, y, k[0]); g.pixel(x, y + 1, k[1]); }  else { g.pixel(x, y + 1, k[0]); g.pixel(x + 1, y, k[1]); }}// falls from inside the crown (hidden behind it) and lands exactly on a litter leaf, so it vanishes into the ringfunction fallingLeaf(g, t, x0, y0, li, ph, sway) {  const [lx, ly, k] = LITTER[li];  const q = (((t + ph) % 1) + 1) % 1, F = 0.72;  if (q >= F) return;  const s = q / F;  const x = Math.round(x0 + (lx - x0) * s + sway * Math.sin(TAU * s * 1.5) * Math.sin(Math.PI * s));  const y = Math.round(y0 + (ly - y0) * (0.35 * s + 0.65 * s * s));  leafShape(g, x, y, k, s > 0.88 ? 0 : Math.floor(q * 40) % 4, s <= 0.88);}// ---- apples ---------------------------------------------------------------------------------------------------const APPLE = [  "..s...",  ".KsKK.",  "K5221K",  "K2211K",  "K1110K",  ".KKKK.",];const AK = { s: "w1", K: "ink", "5": "n5", "2": "r2", "1": "r1", "0": "r0" };function apple(g, x, y, squash) {  APPLE.forEach((row, j) => {    if (squash && j === 0) return;    [...row].forEach((ch, i) => { if (ch !== ".") g.pixel(x + i, y + j + (squash && j < 2 ? 1 : 0), AK[ch]); });  });}// apples among the leaves: [clump index, dx, dy] (top-left of the sprite, relative to the clump centre)const HANGING = [[0, -4, 0], [4, 2, -2], [7, -3, 1], [8, 1, -1], [11, -1, 0], [10, -5, -2], [3, -2, 1]];const DROP = { clump: 13, x: 37, y: 38 };                             // the apple that falls (sits under clump 13)const TUFT = { x: 46, y: 54 };function tuft(g) {  g.part("tuft", () => {    const blades = [[0, 3, "e1"], [1, 6, "e2"], [2, 4, "e1"], [3, 7, "e3"], [4, 5, "e2"], [5, 7, "e2"], [6, 4, "e1"], [7, 5, "e2"], [8, 2, "e1"]];    for (const [dx, h, k] of blades) {      for (let j = 0; j < h; j++) g.pixel(TUFT.x + dx, TUFT.y + 7 - j, j < 2 ? "e0" : k);    }    g.pixel(TUFT.x + 3, TUFT.y + 1, "e3"); g.pixel(TUFT.x + 1, TUFT.y + 2, "e3");  });}// 16f drop choreography: [dx, dy, squash] from the hanging spot; null = hidden (it has rolled behind the tuft)const DROP_PATH = [  [0, 0], [0, 0], [0, 0], [1, 0], [0, 1], [0, 4], [0, 9], [0, 14], [0, 17, 1], [1, 14], [2, 13], [3, 15], [4, 17], [6, 17], [8, 17], [10, 17],];const REGROW = { 14: -4, 15: -2 };                                    // a new apple lowering into view from the leaves// ---- crown motion -------------------------------------------------------------------------------------------const hF = (cy) => Math.max(0, Math.min(1, (40 - cy) / 32));function swayIdle(t) {  return CLUMPS.map((c) => [Math.round(1.25 * hF(c[1]) * Math.sin(TAU * (t + c[1] * 0.012))), 0]);}function swayGust(t) {  return CLUMPS.map((c) => {    const h = hF(c[1]);    const lean = h * (3.6 + 1.3 * Math.sin(TAU * (2 * t + c[1] * 0.02))) + 0.8;    return [Math.round(lean), h > 0.5 ? 1 : 0];  });}// gust flurry: [speed (crossings per loop), phase, base y, wave amplitude, colour]const FLURRY = [  [1, 0.0, 12, 3, 0], [1, 0.33, 24, 4, 1], [2, 0.1, 18, 2, 2], [1, 0.66, 36, 3, 3],  [2, 0.55, 30, 3, 1], [1, 0.18, 46, 2, 2], [2, 0.8, 8, 2, 0], [1, 0.5, 52, 2, 4],  [1, 0.85, 20, 3, 2], [2, 0.3, 42, 3, 0], [1, 0.12, 30, 2, 5], [2, 0.67, 14, 2, 1],];function flurry(g, t) {  g.detached("flurry", () => {    FLURRY.forEach(([sp, ph, by, amp, ki], i) => {      const p = (t * sp + ph) % 1;      const x = Math.round(-6 + p * 76);      const y = Math.round(by + amp * Math.sin(TAU * (t * 2 + ph * 3)) + (p - 0.5) * 6);      leafShape(g, x, y, LEAF_KEYS[ki], Math.floor((t * N_GUST * 2 + i)) % 4, i % 2 === 0);    });  });}const AutumnTree = sprite(pal, (g, t, mode) => {  t = ((t % 1) + 1) % 1;  const isApple = mode === "apples";  const offs = mode === "gust" ? swayGust(t) : swayIdle(t);  contactShadow(g, CX, GROUND, 18, 2);  litter(g, isApple);  g.outlined("ink", () => tree(g));  barkMarks(g);  let f = 0;  if (isApple) {    f = Math.round(t * N_APPLE) % N_APPLE;    // the dropping apple is drawn behind the crown so it hangs under (and lowers out of) the leaves    const o = offs[DROP.clump];    g.detached("dropApple", () => {      const p = DROP_PATH[f];      const attached = f < 4;      apple(g, DROP.x + p[0] + (attached ? o[0] : 0), DROP.y + p[1], p[2]);      if (REGROW[f] !== undefined) apple(g, DROP.x + o[0], DROP.y + REGROW[f]);    });  }  g.detached("leaves", () => {    if (mode === "gust") return;    fallingLeaf(g, t, 20, 30, isApple ? 1 : 3, 0.0, 3);    fallingLeaf(g, t, 44, 29, 5, 0.38, -3);    if (!isApple) fallingLeaf(g, t, 30, 33, 7, 0.7, 2);  });  g.outlined("ink", () => crown(g, offs, isApple ? 4 : 3));  if (isApple) {    g.detached("apples", () => {      for (const [ci, dx, dy] of HANGING) {        const c = CLUMPS[ci], o = offs[ci];        apple(g, Math.round(c[0] + o[0] + dx), Math.round(c[1] + o[1] + dy));      }    });    tuft(g);  }  if (mode === "gust") flurry(g, t);});submit(AutumnTree, {  size: [W, H], origin: "top-left", kind: "icon", scale: 1,  anchor: { x: CX, y: GROUND },  variants: [    { name: "idle", args: ["idle"], animate: { fps: 10, duration: N_IDLE / 10, loop: true } },    { name: "gust", args: ["gust"], animate: { fps: 10, duration: N_GUST / 10, loop: true } },    { name: "apples", args: ["apples"], animate: { fps: 10, duration: N_APPLE / 10, loop: true } },  ],});

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

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

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