Graveyard & Chapel · open_grave.js
掘りかけの墓穴
64×48 の床置きの小物で、踏み固めた土の縁、根の見える土の層の奥の壁、真っ暗な底を持つ掘りたての四角い穴と、その横の土の山があります。山のてっぺんには鋤が刺さり、柄に小さな鉄のランタンが下がっています。idle(12 コマ)は山から穴へ土がこぼれ落ち、ランタンの炎が暖かく揺れます。dig(10 コマの一回再生)は鋤がひとりでに抜けて穴を掘り、土の塊を山へ放り、また刺さって、idle の最初のコマで終わります。
6×
動くスプライト素材: Graveyard & Chapel(itch.io)Vol.4・小物 12 点・33 アニメ
冒頭のコメント。これが指示書です。サイズ、視点、光の向き、すべての変種が書かれています。
open_grave.js · 1–7
// Open grave — 64x48 floor prop, top-down 3/4, key light top-left.// A freshly dug rectangular pit (trampled earth rim, a far wall of cut soil strata with roots, black depth),// the spoil heap beside it with a spade stuck in its crown, and a small iron lantern hung on the spade's grip.// Variants:// idle 12f loop loose soil trickles off the heap into the pit, the lantern flame flickers warm// dig 10f once the spade pulls itself out, bites into the pit, flings a clod onto the heap and plants// itself again; ends on idle frame 0最後の数行。submit でキャンバスの大きさと、描画エンジンが書き出す変種の一覧を指定します。
open_grave.js · 235–244
submit(OpenGrave, { size: [W, H], origin: "top-left", kind: "icon", scale: 1, animate: { fps: 10, duration: 1.2, loop: true }, anchor: { x: CX, y: GROUND }, args: ["idle"], variants: [ { name: "idle" }, { name: "dig", args: ["dig"], animate: { fps: 10, duration: 1.0, loop: false } }, ],});プログラム全体を表示 (244 行)
open_grave.js · 244 行
// Open grave — 64x48 floor prop, top-down 3/4, key light top-left.// A freshly dug rectangular pit (trampled earth rim, a far wall of cut soil strata with roots, black depth),// the spoil heap beside it with a spade stuck in its crown, and a small iron lantern hung on the spade's grip.// Variants:// idle 12f loop loose soil trickles off the heap into the pit, the lantern flame flickers warm// dig 10f once the spade pulls itself out, bites into the pit, flings a clod onto the heap and plants// itself again; ends on idle frame 0const W = 64, H = 48, CX = 32, GROUND = 45;const PIT = { x0: 5, x1: 31, y0: 20, y1: 36 }; // the openingconst WALL = 11; // far wall height seen in the openingconst RIM = 2; // trampled earth band around the opening// spoil heap: overlapping lumps [cx, cy, rx, ry, light bias]const LUMPS = [[47, 35, 12, 7, 0.12], [42, 30, 6, 4, 0.1], [49, 28, 6, 5, 0.1], [55, 32, 5, 4, 0], [39, 36, 5, 4, 0.05], [53, 38, 7, 4, 0]];const REST = { x: 48, y: 29, a: -23 }; // spade: blade tip position and handle angle (deg) when plantedconst DIRT = ["w0", "w1", "w2", "w3"];const inOpen = (x, y) => x >= PIT.x0 && x <= PIT.x1 && y >= PIT.y0 && y <= PIT.y1;const inRimBand = (x, y) => { if (inOpen(x, y)) return false; const dx = Math.max(PIT.x0 - RIM - x, x - PIT.x1 - RIM, 0), dy = Math.max(PIT.y0 - RIM - y, y - PIT.y1 - RIM - 1, 0); if (dx > 0 || dy > 0) return false; // clip the corners so the band reads as dug earth, not a picture frame const cx = x < PIT.x0 ? PIT.x0 - x : x > PIT.x1 ? x - PIT.x1 : 0, cy = y < PIT.y0 ? PIT.y0 - y : y > PIT.y1 ? y - PIT.y1 : 0; if (cx + cy > RIM + 1) return false; // a ragged outer edge: some columns / rows lose their outermost pixel if ((y === PIT.y0 - RIM || y === PIT.y1 + RIM + 1) && Math.sin(x * 1.7) > 0.55) return false; if ((x === PIT.x0 - RIM || x === PIT.x1 + RIM) && Math.sin(y * 2.1) > 0.6) return false; return true;};// ---- pit ------------------------------------------------------------------------------------------------------function pitTone(x, y) { const d = y - PIT.y0; // depth down the far wall const sh = x - PIT.x0 < 2 + d * 0.45 ? 1 : 0; // the left rim throws a shadow down and to the right if (d < WALL) { // cut soil strata with wavy seams: dark topsoil, a paler clay band, then earth fading into the dark const b = d + Math.round(Math.sin(x * 0.7) * 0.6 + Math.sin(x * 0.23 + 1) * 0.8); let i = b < 2 ? 1 : b < 4 ? 2 : b < 6 ? 1 : b < 8 ? 0 : -1; i -= sh; if (i >= 0) return DIRT[i]; return d >= WALL - 2 ? "ink" : "n0"; } return "ink"; // the bottom of the grave}function rimTone(x, y) { const inner = inOpen(x - 1, y) || inOpen(x + 1, y) || inOpen(x, y - 1) || inOpen(x, y + 1); const near = y > PIT.y1, far = y < PIT.y0, left = x < PIT.x0; if (inner && far) return "w3"; // the far lip catches the light if (inner && left) return "w3"; if (inner && near) return "w1"; // near lip: we look over its dark edge if (far || left) return "w2"; if (near && y === PIT.y1 + RIM + 1) return "w0"; return "w1";}function pit(g) { g.part("rim", () => g.field({ x: [PIT.x0 - RIM - 1, PIT.x1 + RIM + 1], y: [PIT.y0 - RIM - 1, PIT.y1 + RIM + 1] }, (x, y) => inRimBand(x, y) ? rimTone(x, y) : null)); g.part("pit", () => g.field({ x: [PIT.x0, PIT.x1], y: [PIT.y0, PIT.y1] }, (x, y) => pitTone(x, y)));}function pitDetail(g) { g.part("roots", () => { // pale roots hanging out of the far wall, stones in the strata, clods on the bottom for (const [x, y, n] of [[12, 21, 3], [21, 21, 2], [27, 22, 3]]) for (let i = 0; i < n; i++) g.pixel(x + (i === n - 1 ? 1 : 0), y + i, i === 0 ? "w4" : "w3"); for (const [x, y] of [[16, 23], [24, 24], [29, 26]]) { g.pixel(x, y, "n3"); g.pixel(x + 1, y, "n1"); } for (const [x, y] of [[14, 34], [22, 33], [27, 35]]) g.pixel(x, y, "n0"); // vertical bites of the spade in the far wall: a shadowed stroke with a lit edge beside it for (const x of [10, 18, 25]) { g.line([x, PIT.y0 + 3], [x, PIT.y0 + 6], "w0"); g.pixel(x + 1, PIT.y0 + 3, "w3"); } }); g.part("turf", () => { // torn grass on the outer edge of the rim for (const [x, y, k] of [[4, 18, "e1"], [5, 17, "e2"], [9, 17, "e1"], [10, 17, "e2"], [18, 17, "e1"], [25, 17, "e1"], [26, 17, "e2"], [2, 25, "e1"], [2, 26, "e0"], [2, 32, "e1"], [8, 39, "e0"], [9, 39, "e1"], [20, 39, "e0"], [29, 39, "e1"]]) g.pixel(x, y, k); });}// ---- spoil heap -----------------------------------------------------------------------------------------------function heap(g) { g.part("heap", () => g.field({ x: [26, 62], y: [20, 43] }, (x, y) => { // a spoil heap built from overlapping lumps, each lit as an ellipsoid; clods and stones added as marks let i = -1; for (const [cx, cy, rx, ry, b] of LUMPS) i = Math.max(i, litIndex(x, y, cx, cy, rx, ry, 4, b)); if (i < 0) return null; return DIRT[i]; })); g.part("clods", () => { for (const [x, y] of [[41, 29], [53, 27], [44, 37], [56, 34], [49, 40], [37, 36]]) { g.pixel(x, y, "w3"); g.pixel(x + 1, y, "w1"); g.pixel(x, y + 1, "w1"); } for (const [x, y] of [[46, 33], [58, 31], [41, 40]]) { g.pixel(x, y, "n3"); g.pixel(x + 1, y, "n1"); } g.pixel(51, 38, "w0"); g.pixel(52, 38, "w0"); });}function hole(g, open) { // the slot the spade leaves in the crown when it is pulled out if (!open) return; g.part("slot", () => { g.line([REST.x - 2, REST.y - 2], [REST.x + 2, REST.y - 2], "w0"); g.line([REST.x - 1, REST.y - 1], [REST.x + 1, REST.y - 1], "ink"); g.pixel(REST.x - 2, REST.y - 1, "w3"); });}function trickle(g, t) { // three grains roll down the heap's left flank and drop into the pit const P = [[39, 30], [37, 31], [36, 32], [35, 33], [33, 33], [32, 34], [31, 35]]; g.detached("trickle", () => { for (let k = 0; k < 3; k++) { const p = (t + k / 3) % 1; if (p > 0.85) continue; const s = p / 0.85 * (P.length - 1), i = Math.floor(s); const [x, y] = P[i]; g.pixel(x, y, i < 3 ? "w3" : "w2"); if (i < 2 && k === 0) g.pixel(x + 1, y, "w1"); } });}// ---- spade and lantern ----------------------------------------------------------------------------------------------const rot = (px, py, a) => { const c = Math.cos(a * Math.PI / 180), s = Math.sin(a * Math.PI / 180); return [px * c - py * s, px * s + py * c]; };function spadeLocal(g, loaded) { // local frame: blade tip at the origin, handle up the -y axis g.part("blade", () => { g.fillPoly([[-2, -7], [2, -7], [2, -2], [1, 0], [-1, 0], [-2, -2]], "n2"); g.line([-2, -7], [-2, -2], "n4"); // lit left edge g.line([2, -7], [2, -2], "n1"); g.pixel(-1, -6, "n3"); g.pixel(1, -1, "n1"); g.line([-1, -8], [1, -8], "n1"); g.pixel(-1, -8, "n3"); // socket if (loaded) { g.rect({ x: [-2, 2], y: [-5, -3] }, "w2", { fill: "w2" }); g.pixel(-1, -5, "w3"); g.pixel(2, -3, "w0"); g.pixel(1, -6, "w1"); } }); g.part("shaft", () => { g.line([0, -9], [0, -21], "w3"); g.line([1, -9], [1, -21], "w1"); g.pixel(0, -14, "w4"); g.pixel(1, -17, "w0"); // T-grip: a short crossbar, lit on its left end g.line([0, -22], [1, -22], "w2"); g.rect({ x: [-2, 3], y: [-24, -23] }, "w2", { fill: "w2" }); g.line([-2, -24], [1, -24], "w3"); g.pixel(-2, -23, "w3"); g.line([2, -23], [3, -23], "w1"); });}function lanternAt(g, lx, ly, sw, t) { // hook, cap, glass with flame, base; sw = swing offset of the body (px) const flick = [0, 1, 0, 2, 1, 0, 1, 2, 0, 1, 0, 1][Math.floor(((t % 1) + 1) % 1 * 12) % 12]; const bx = lx + sw; g.outlined("ink", () => g.part("lantern", () => { g.line([lx, ly], [lx, ly + 1], "n2"); g.pixel(bx, ly + 2, "n3"); g.line([bx - 1, ly + 3], [bx + 1, ly + 3], "n3"); g.pixel(bx + 1, ly + 3, "n1"); g.line([bx - 2, ly + 4], [bx + 2, ly + 4], "n2"); g.pixel(bx - 2, ly + 4, "n4"); g.rect({ x: [bx - 1, bx + 1], y: [ly + 5, ly + 8] }, "r3", { fill: "r3" }); g.line([bx - 2, ly + 5], [bx - 2, ly + 8], "n2"); g.line([bx + 2, ly + 5], [bx + 2, ly + 8], "n1"); g.pixel(bx, ly + 6, "g3"); g.pixel(bx, ly + 7, flick === 2 ? "g3" : "g2"); if (flick === 1) g.pixel(bx, ly + 5, "g2"); if (flick === 2) g.pixel(bx - 1, ly + 8, "r2"); g.pixel(bx - 1, ly + 5, "g3"); // glint on the glass g.line([bx - 2, ly + 9], [bx + 2, ly + 9], "n1"); g.pixel(bx - 2, ly + 9, "n3"); })); g.detached("glow", () => { // a few warm sparks of halo beside the glass, breathing with the flicker const k = flick === 0 ? "r1" : "r2"; g.pixel(bx - 4, ly + 6, k); g.pixel(bx + 4, ly + 7, k); if (flick === 2) { g.pixel(bx - 4, ly + 8, "r1"); g.pixel(bx + 4, ly + 5, "r1"); } });}function spade(g, S, t) { const { x, y, a } = S; g.outlined("ink", () => g.at(x, y, () => g.rotate(a, () => spadeLocal(g, S.loaded)))); const [hx, hy] = rot(-2, -22, a); lanternAt(g, Math.round(x + hx), Math.round(y + hy) + 1, S.sw, t);}function collar(g) { // a little cone of soil heaped round the planted blade (covers its lower half) g.part("collar", () => g.field({ x: [REST.x - 5, REST.x + 5], y: [REST.y - 4, REST.y + 2] }, (x, y) => { const i = litIndex(x, y, REST.x + 0.5, REST.y - 0.5, 4.5, 3, 4, 0.15); return i < 0 ? null : DIRT[i]; }));}// ---- dig -------------------------------------------------------------------------------------------------------------const DIG = 10;// per frame: blade tip x, y, handle angle, loaded, lantern swingconst KEYS = [ null, [47, 25, -25, 0, 1], // pulls itself out [40, 29, -45, 0, 2], // tips over the pit [31, 32, -58, 0, 1], // bites into the pit [33, 27, -50, 1, -1], // lifts a load [42, 23, -32, 1, -2], // swings back towards the heap [51, 22, -42, 0, -1], // flings (the clod leaves the blade) [50, 26, -30, 0, 1], // comes down [48, 28, -24, 0, 0], // plants itself null,];function stateAt(state, t) { t = ((t % 1) + 1) % 1; if (state === "dig") { const f = Math.min(DIG - 1, Math.round(t * DIG)); if (f === 0 || f === DIG - 1) return stateAt("idle", 0); const [x, y, a, loaded, sw] = KEYS[f]; return { x, y, a, loaded: !!loaded, sw, f, t: f / DIG, planted: f === 8 }; } return { x: REST.x, y: REST.y, a: REST.a, loaded: false, sw: 0, f: -1, t, planted: true };}function flung(g, f) { // the clod arcs onto the heap's crown and breaks const path = { 6: [[54, 17, 3]], 7: [[56, 21, 3]], 8: [[55, 27, 2], [58, 29, 1], [52, 28, 1]] }[f]; if (!path) return; g.outlined("ink", () => g.part("clod", () => { for (const [x, y, s] of path) { if (s >= 2) { g.rect({ x: [x, x + s - 1], y: [y, y + 1] }, "w1", { fill: "w1" }); g.pixel(x, y, "w3"); g.pixel(x + s - 1, y + 1, "w0"); } else g.pixel(x, y, "w2"); } })); if (f === 8) g.detached("puff", () => { for (const [x, y] of [[53, 25], [58, 26], [60, 28], [51, 28]]) g.pixel(x, y, "n3"); });}function pitSpray(g, f) { // the blade's bite throws a few crumbs inside the pit if (f !== 3 && f !== 4) return; g.detached("spray", () => { const pts = f === 3 ? [[27, 28], [24, 27], [29, 26]] : [[26, 25], [23, 23], [30, 22]]; for (const [x, y] of pts) g.pixel(x, y, "w2"); });}const OpenGrave = sprite(pal, (g, t, state) => { const S = stateAt(state, t); const f = S.f; contactShadow(g, CX, GROUND - 1, 30, 2); g.outlined("ink", () => { pit(g); heap(g); }); pitDetail(g); hole(g, !S.planted); spade(g, S, S.t); if (S.planted) collar(g); // soil over the buried half of the blade trickle(g, f < 0 ? S.t : 0); if (f > 0) { flung(g, f); pitSpray(g, f); } if (S.planted) g.part("lampLight", () => { // the lantern's warm light on the heap's shoulder below it const on = Math.floor(S.t * 12) % 3 !== 1; for (const [x, y] of [[40, 27], [42, 26], [38, 29]]) g.pixel(x, y, on ? "w4" : "w3"); });});submit(OpenGrave, { size: [W, H], origin: "top-left", kind: "icon", scale: 1, animate: { fps: 10, duration: 1.2, loop: true }, anchor: { x: CX, y: GROUND }, args: ["idle"], variants: [ { name: "idle" }, { name: "dig", args: ["dig"], animate: { fps: 10, duration: 1.0, loop: false } }, ],});掲載されたコードでは、ビルドスクリプトが自動付与する共通部分を省略しています。具体的には 1 行のランタイムヘッダー、共有パレット、および汎用ヘルパー関数(contactShadow、litIndex、flame、hash、chk、TAU)です。表示されている行番号は実際のファイルと一致しています。これらのプログラムは AIエージェントが生成し、描画されたコマの仕上がりを確認しながら修正を加えたものです。
エージェントに、こうしたプログラムを書かせる。
Tessarune は描画 API のリファレンス、25 冊の制作マニュアル、結果を確かめるための描画エンジンをエージェントに渡します。