Graveyard & Chapel · raven_tree.js
鴉の木
64×64 の床置きの小物で、らせんの溝とうろのあるねじれた幹、むき出しの根、曲がりくねった枝を持つ葉のない枯れ木です。地面には枯れ葉が少し落ち、右の大枝に黒い鴉がとまっています。idle(16 コマ)は枝がきしみ、鴉が身じろぎして首をかしげ、枯れ葉が一枚くるくると落ちます。caw(10 コマの一回再生)は鴉が身をかがめ、くちばしを大きく開け、翼を半分広げてから落ち着きます。empty(16 コマ)は鴉が飛び去ったあとの木だけで、地面に羽根が一枚落ち、葉が舞い落ちます。
6×
動くスプライト素材: 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 冊の制作マニュアル、結果を確かめるための描画エンジンをエージェントに渡します。