Harvest Festival · autumn_tree.js
秋の木
木の幹から 3 本の枝が分かれ、赤や橙色の葉の塊が丸く茂る見下ろし 3/4 視点の 64×64 の大型の小物です。根元の草には落ち葉の輪が広がっています。16 コマのループである idle(待機)では樹冠が揺れて 3 枚の葉が回りながら落ち、12 コマの gust(突風)では強い風で木が右にしなります。16 コマの apples(リンゴ)では赤い実が落ちて草の上を転がります。
6×
動くスプライト素材: 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 冊の制作マニュアル、結果を確かめるための描画エンジンをエージェントに渡します。