Graveyard & Chapel · cemetery_gate.js
墓地の門
64×48 の床置きの小物で、正面を向いています。縁取りのある笠石と石の壺を載せた切石の門柱 2 本の間に、槍の穂先の鉄棒が渦巻きのアーチまで伸びる鍛鉄の両開きの扉があります。扉は細い鉄なので、すき間を埋めてしまう輪郭線は使わず、鉄棒を一本ずつ明暗の対で描き、明るい床でも暗い床でも見えるようにしています。closed(12 コマ)は錆びた鎖と南京錠が揺れ、枯れ葉が転がります。open(10 コマの一回再生)は錠が落ち、鎖が滑り落ちて、扉が内側へ開きます。open-idle(10 コマ)は鎖が地面に残ったままです。
6×
動くスプライト素材: Graveyard & Chapel(itch.io)Vol.4・小物 12 点・33 アニメ
冒頭のコメント。これが指示書です。サイズ、視点、光の向き、すべての変種が書かれています。
cemetery_gate.js · 1–10
// Cemetery gate — 64x48 floor prop, top-down 3/4, key light top-left. Camera-facing.// Two ashlar piers with moulded caps carry stone urns; between them hangs a pair of wrought-iron gate leaves:// spear-tipped bars rising to a scroll arch, three rails, C-scrolls in the lunette. The leaves are thin iron, so// they are NOT ink-outlined (an outline would fill the gaps between bars): every bar is a lit/shadow pair instead// (n3 left, n0 right), which reads on light and dark floors alike.// Variants:// closed 12f loop a rusty chain and padlock across the meeting bars sway; dead leaves skitter along the base// open 10f once the padlock drops, the chain slithers off, the leaves swing inward (overshoot, settle);// ends on open-idle frame 0// open-idle 10f loop gate open, the chain and padlock lie on the ground, dead leaves skitter through最後の数行。submit でキャンバスの大きさと、描画エンジンが書き出す変種の一覧を指定します。
cemetery_gate.js · 252–262
submit(Gate, { size: [W, H], origin: "top-left", kind: "icon", scale: 1, animate: { fps: 10, duration: N_IDLE / 10, loop: true }, anchor: { x: CX, y: GROUND }, args: ["closed"], variants: [ { name: "closed", args: ["closed"] }, { name: "open", args: ["open"], animate: { fps: 10, duration: N_OPEN / 10, loop: false } }, { name: "open-idle", args: ["open-idle"], animate: { fps: 10, duration: N_OI / 10, loop: true } }, ],});プログラム全体を表示 (262 行)
cemetery_gate.js · 262 行
// Cemetery gate — 64x48 floor prop, top-down 3/4, key light top-left. Camera-facing.// Two ashlar piers with moulded caps carry stone urns; between them hangs a pair of wrought-iron gate leaves:// spear-tipped bars rising to a scroll arch, three rails, C-scrolls in the lunette. The leaves are thin iron, so// they are NOT ink-outlined (an outline would fill the gaps between bars): every bar is a lit/shadow pair instead// (n3 left, n0 right), which reads on light and dark floors alike.// Variants:// closed 12f loop a rusty chain and padlock across the meeting bars sway; dead leaves skitter along the base// open 10f once the padlock drops, the chain slithers off, the leaves swing inward (overshoot, settle);// ends on open-idle frame 0// open-idle 10f loop gate open, the chain and padlock lie on the ground, dead leaves skitter throughconst W = 64, H = 48, CX = 32, GROUND = 45;const PIERS = [3, 52]; // left x of each 9px pier faceconst LL = 12, LR = 51, LEAF = 20; // gate span: left leaf LL..LL+19, right leaf LR-19..LRconst RAILS = [22, 31, 40]; // rail rows (2px each: lit top, dark bottom)const BARS = [0, 4, 8, 12, 16]; // bar offsets from the hinge (2px each; 0 = hinge stile, 16 = meeting bar)const K = 0.3; // screen rise per px of depthconst N_IDLE = 12, N_OPEN = 10, N_OI = 10, REST = 58;const OPEN_TH = [0, 0, 3, 11, 24, 38, 50, 59, 63, REST];const STONE = ["n0", "n1", "n2", "n3", "n4"];// ---- piers and urns -------------------------------------------------------------------------------------------function pierTone(px, x, y) { const u = x - px; // 0..8 on the face if (y >= 12 && y <= 15) { // cap: wider by one px each side if (u < -1 || u > 9) return null; if (y === 12) return u === -1 ? "n4" : u === 9 ? "n3" : "n4"; // top face if (y === 13) return u === 9 ? "n2" : "n3"; if (y === 14) return u === -1 ? "n3" : u === 9 ? "n1" : "n2"; return u < 0 || u > 8 ? null : "n0"; // shadow under the cap } if (y >= 41 && y <= 44) { // plinth if (u < -1 || u > 9) return null; if (y === 41) return u === 9 ? "n2" : "n4"; if (y === 44) return u === 9 ? "n0" : "n1"; return u === -1 ? "n3" : u === 9 ? "n1" : "n2"; } if (y < 16 || y > 40 || u < 0 || u > 8) return null; if (y === 16) return "n1"; const c = Math.floor((y - 17) / 5), k = (y - 17) % 5; if (k === 4) return "n1"; // mortar if ((c & 1) && u === 4) return "n1"; // staggered joint let i = u === 0 ? 4 : u === 8 ? 2 : 3; if (k === 0 && u < 8) i = Math.min(4, i + 1); if (hash(c, px) < 0.35 && u > 0 && u < 8 && k > 0) i -= 1; // a darker block if (u === 7 && k > 0) i = 2; return STONE[i];}function urnTone(px, x, y) { const cx = px + 4.5; if (y === 11) return x >= px + 1 && x <= px + 7 ? (x === px + 1 ? "n4" : x === px + 7 ? "n1" : "n2") : null; // foot slab if (y === 10) return x >= px + 3 && x <= px + 5 ? (x === px + 3 ? "n3" : "n1") : null; // stem if (y === 9) return x >= px + 2 && x <= px + 6 ? (x === px + 2 ? "n3" : x === px + 6 ? "n1" : "n2") : null; if (y >= 5 && y <= 8) { // bowl const i = litIndex(x, y, cx, 6.6, 4.7, 2.9, 4, 0.1); if (i < 0) return null; if (y === 7 && x > px && x < px + 8) return i >= 2 ? "n4" : i === 1 ? "n2" : "n1"; // carved gadroon band return STONE[Math.min(4, i + 1)]; } if (y === 4) return x >= px + 2 && x <= px + 6 ? (x === px + 2 ? "n2" : "n0") : null; // shadow under the lip if (y === 3) return x >= px && x <= px + 8 ? (x <= px + 1 ? "n4" : x >= px + 7 ? "n2" : "n3") : null; // flared lip if (y === 2) return x >= px + 2 && x <= px + 6 ? (x <= px + 3 ? "n4" : x === px + 6 ? "n2" : "n3") : null; // lid if (y === 1) return x >= px + 4 && x <= px + 4 ? "n4" : null; // knob return null;}function piers(g) { for (const px of PIERS) { g.part(px < CX ? "pierL" : "pierR", () => g.field({ x: [px - 1, px + 9], y: [12, 44] }, (x, y) => pierTone(px, x, y))); g.part(px < CX ? "urnL" : "urnR", () => g.field({ x: [px, px + 9], y: [1, 11] }, (x, y) => urnTone(px, x, y))); }}function pierDetail(g) { g.part("moss", () => { for (const [x, y, k] of [[3, 40, "e1"], [4, 40, "e2"], [3, 39, "e1"], [5, 40, "e1"], [2, 43, "e1"], [3, 44, "e2"], [60, 40, "e1"], [59, 40, "e1"], [60, 39, "e2"], [61, 44, "e1"], [4, 12, "e2"], [5, 12, "e1"], [54, 12, "e1"], [3, 7, "e1"], [4, 8, "e2"], [59, 5, "e1"]]) g.pixel(x, y, k); g.pixel(56, 25, "n1"); g.pixel(57, 26, "n1"); g.pixel(57, 27, "n0"); g.pixel(56, 28, "n1"); // crack g.pixel(8, 33, "n2"); g.pixel(6, 20, "n4"); // chips }); g.part("hinges", () => { for (const y of [23, 38]) { g.pixel(12, y, "n2"); g.pixel(51, y, "n1"); } });}// ---- gate ---------------------------------------------------------------------------------------------------------const topY = (x) => Math.round(18 - 8 * (1 - ((x + 0.5 - CX) / 20.5) ** 2)); // bar tops follow the arch// leaf-local pixel list: [u, y, key] with u = distance from the hinge (0..19); built oncefunction leafPixels(left) { const P = []; const sx = (u) => (left ? LL + u : LR - u); const add = (u, y, k) => P.push([u, y, k]); // bars: lit pixel on the screen-left, dark on the screen-right for (const b of BARS) for (const d of [0, 1]) { const u = left ? b + d : b + 1 - d; // keep the lit pixel on the screen-left const x = sx(u), t = topY(x), heavy = b === 0 || b === 16; for (let y = t; y <= 41; y++) add(u, y, d === 0 ? (heavy ? "n3" : "n2") : (heavy ? "n1" : "n0")); if (d === 0) add(u, t - 1, "n3"); } // spear tips for (const b of BARS) { const uL = left ? b : b + 1, uR = left ? b + 1 : b; // screen-left / screen-right pixel of the bar const t = topY(sx(uL)); add(uL, t - 2, "n4"); add(uR, t - 2, "n2"); add(uL, t - 3, "n4"); const out = left ? uL - 1 : uL + 1, outR = left ? uR + 1 : uR - 1; // barbs either side if (out >= 0 && out < LEAF) add(out, t - 1, "n2"); if (outR >= 0 && outR < LEAF) add(outR, t - 1, "n0"); } // rails and the arched top rail for (const r of RAILS) for (let u = 0; u <= 17; u++) { add(u, r, u % 4 === 1 ? "n2" : "n3"); add(u, r + 1, "n0"); } for (let u = 0; u <= 17; u++) { const t = topY(sx(u)) + 2; add(u, t, "n3"); add(u, t + 1, "n1"); } // C-scrolls in the lunette between the arch rail and the top rail (two per leaf), and a band of rings low down const scroll = (uc, yc, r) => { for (let a = 0; a < 16; a++) { const ang = a / 16 * TAU; if (a >= 10 && a <= 13) continue; // open side of the C const u = Math.round(uc + Math.cos(ang) * r), y = Math.round(yc + Math.sin(ang) * r); add(left ? u : u, y, Math.sin(ang) < -0.3 || Math.cos(ang) < -0.5 ? "n3" : "n1"); } }; scroll(14.5, 17.5, 1.5); for (const [uc, ry] of [[2.5, 36], [6.5, 36], [10.5, 36], [14.5, 36], [2.5, 27], [6.5, 27], [10.5, 27], [14.5, 27]]) for (const [du, dy, k] of [[-1, 0, "n2"], [0, -1, "n3"], [1, 0, "n0"], [0, 1, "n0"]]) add(Math.round(uc + du + 0.01), ry + dy, k); return P;}const LEAF_PX = { L: leafPixels(true), R: leafPixels(false) };function gateMap(thL, thR) { // project both leaves: a leaf turned by th keeps its hinge; each u moves to hinge + u cos th and rises u sin th K const M = new Map(); for (const [left, th] of [[true, thL], [false, thR]]) { const a = th * Math.PI / 180, c = Math.cos(a), s = Math.sin(a); const dark = th > 70 ? 1 : 0; for (const [u, y, k] of LEAF_PX[left ? "L" : "R"]) { const x = left ? LL + Math.round(u * c) : LR - Math.round(u * c); const yy = y - Math.round(u * s * K); if (x < 0 || x >= W || yy < 0) continue; const key = dark && k !== "n0" ? STONE[Math.max(0, STONE.indexOf(k) - 1)] : k; const id = x + "," + yy, prev = M.get(id); // where turned bars bunch up, keep the lighter pixel (the nearer, lit edge wins) if (!prev || STONE.indexOf(key) > STONE.indexOf(prev)) M.set(id, key); } } return M;}function gate(g, thL, thR) { const M = gateMap(thL, thR); g.part("gate", () => g.field({ x: [LL - 1, LR + 1], y: [0, 44] }, (x, y) => M.get(x + "," + y) ?? null));}// ---- chain, padlock, leaves --------------------------------------------------------------------------------------function chain(g, sway) { // links wrapped round both meeting bars, a drooping loop and a padlock hanging from it const dx = Math.round(sway * 1.4); g.part("chain", () => { const links = [[28, 33], [29, 33], [30, 34], [31, 34], [32, 34], [33, 34], [34, 33], [35, 33]]; links.forEach(([x, y], i) => g.pixel(x, y, i & 1 ? "w1" : "w3")); const drop = [[29, 34], [30, 35], [31 + Math.round(sway * 0.6), 36], [32 + Math.round(sway * 0.6), 36], [33, 35], [34, 34]]; drop.forEach(([x, y], i) => g.pixel(x, y, i & 1 ? "w3" : "w1")); }); g.outlined("ink", () => g.part("padlock", () => padlock(g, 31 + dx, 37)));}function padlock(g, x, y) { // iron shackle over a squat iron body scabbed with rust, keyhole g.pixel(x, y, "n4"); g.pixel(x + 1, y, "n3"); g.pixel(x - 1, y + 1, "n4"); g.pixel(x + 2, y + 1, "n2"); g.rect({ x: [x - 1, x + 2], y: [y + 2, y + 5] }, "n2", { fill: "n2" }); g.line([x - 1, y + 2], [x + 1, y + 2], "n4"); g.pixel(x + 2, y + 2, "n3"); g.pixel(x - 1, y + 3, "n3"); g.line([x - 1, y + 5], [x + 2, y + 5], "n1"); g.pixel(x + 2, y + 3, "w1"); g.pixel(x - 1, y + 4, "w2"); g.pixel(x + 2, y + 4, "w0"); // rust g.pixel(x, y + 3, "ink"); g.pixel(x, y + 4, "ink"); // keyhole}function lockOnGround(g, x) { // padlock on its side in the dirt, chain coiled beside it g.outlined("ink", () => g.part("padlock", () => { g.rect({ x: [x, x + 3], y: [42, 44] }, "n2", { fill: "n2" }); g.line([x, 42], [x + 3, 42], "n4"); g.pixel(x + 3, 44, "n0"); g.pixel(x, 43, "w2"); g.pixel(x + 3, 43, "w1"); g.pixel(x + 4, 43, "n3"); g.pixel(x + 5, 43, "n2"); g.pixel(x + 4, 42, "n4"); g.pixel(x + 2, 43, "ink"); })); g.part("chain", () => { for (const [dx, dy, k] of [[-1, 44, "w2"], [-2, 43, "w1"], [-3, 44, "w2"], [-4, 44, "n2"], [-5, 43, "w2"], [-6, 44, "w1"], [-7, 44, "w2"], [6, 44, "w2"], [7, 44, "w1"]]) g.pixel(x + dx, dy, k); });}function chainFalling(g, f) { // f1: lock drops, chain slides down the bars · f2..: the lock lies on the ground if (f === 1) { g.part("chain", () => { for (const [x, y, k] of [[29, 36], [30, 37], [31, 38], [32, 38], [33, 37], [34, 36], [28, 35], [35, 35]].map(([x, y], i) => [x, y, i & 1 ? "w1" : "w2"])) g.pixel(x, y, k); }); g.outlined("ink", () => g.part("padlock", () => padlock(g, 31, 39 - 1))); } else lockOnGround(g, 30);}function deadLeaves(g, t, closedGate) { // two leaves blow across the base once per loop (hidden behind the pier plinths at each end), one flips in place g.detached("leaves", () => { for (const [ph, y0, dir, k1, k2] of [[0, 43, 1, "w3", "w2"], [0.5, 44, 1, "g1", "w2"]]) { const p = (t + ph) % 1; const x = Math.round(dir > 0 ? 8 + p * 48 : 56 - p * 48); const hop = Math.round(Math.abs(Math.sin(p * Math.PI * 3)) * 2); if (x < 12 || x > 49) continue; // behind the plinths const flip = Math.floor(p * 12) & 1; const y = y0 - hop; if (flip) { g.pixel(x, y, k1); g.pixel(x + 1, y, k1); g.pixel(x + 2, y, k2); g.pixel(x + 1, y - 1, k2); } else { g.pixel(x, y, k2); g.pixel(x + 1, y - 1, k1); g.pixel(x, y - 1, k1); g.pixel(x + 1, y - 2, k2); } } // a leaf caught against the right plinth, lifting and settling const q = Math.sin(TAU * t); g.pixel(49, 44, "r1"); g.pixel(50, 44, "r0"); g.pixel(48, 44, q > 0.3 ? "r0" : "r1"); if (q > 0.3) { g.pixel(49, 43, "r1"); g.pixel(50, 42, "r2"); } else g.pixel(50, 43, "r1"); g.pixel(17, 44, "w2"); g.pixel(18, 44, "w1"); g.pixel(18, 43, "w3"); // a settled one g.pixel(40, 44, "g1"); g.pixel(41, 44, "g0"); });}// ---- states -------------------------------------------------------------------------------------------------------function stateAt(state, t) { t = ((t % 1) + 1) % 1; if (state === "closed") { const f = Math.round(t * N_IDLE) % N_IDLE, tt = f / N_IDLE; return { th: 0, lock: "hang", sway: Math.sin(TAU * tt), lt: tt }; } if (state === "open-idle") { const f = Math.round(t * N_OI) % N_OI, tt = f / N_OI; return { th: REST, lock: "ground", lt: tt }; } const f = Math.min(N_OPEN - 1, Math.round(t * N_OPEN)); if (f === 0) return stateAt("closed", 0); if (f === N_OPEN - 1) return stateAt("open-idle", 0); return { th: OPEN_TH[f], thR: OPEN_TH[Math.max(0, f - 1)] * 0.4 + OPEN_TH[f] * 0.6, lock: "fall", f, lt: f / N_OI };}const Gate = sprite(pal, (g, t, state) => { const S = stateAt(state, t); contactShadow(g, CX, GROUND, 30, 2); // gate shadow on the ground: a dithered band under the closed leaves, shrinking to the hinges as they open const a = S.th * Math.PI / 180, reach = Math.round(LEAF * Math.cos(a)); g.part("gateShadow", () => { for (let x = LL; x <= LR; x++) { const inL = x - LL < reach, inR = LR - x < reach; if ((inL || inR) && chk(x, 42)) g.pixel(x, 43, "ink"); } }); gate(g, S.th, S.thR ?? S.th); g.outlined("ink", () => piers(g)); pierDetail(g); if (S.lock === "hang") chain(g, S.sway); else if (S.lock === "fall") chainFalling(g, S.f); else lockOnGround(g, 30); deadLeaves(g, S.lt, S.th === 0);});submit(Gate, { size: [W, H], origin: "top-left", kind: "icon", scale: 1, animate: { fps: 10, duration: N_IDLE / 10, loop: true }, anchor: { x: CX, y: GROUND }, args: ["closed"], variants: [ { name: "closed", args: ["closed"] }, { name: "open", args: ["open"], animate: { fps: 10, duration: N_OPEN / 10, loop: false } }, { name: "open-idle", args: ["open-idle"], animate: { fps: 10, duration: N_OI / 10, loop: true } }, ],});掲載されたコードでは、ビルドスクリプトが自動付与する共通部分を省略しています。具体的には 1 行のランタイムヘッダー、共有パレット、および汎用ヘルパー関数(contactShadow、litIndex、flame、hash、chk、TAU)です。表示されている行番号は実際のファイルと一致しています。これらのプログラムは AIエージェントが生成し、描画されたコマの仕上がりを確認しながら修正を加えたものです。
エージェントに、こうしたプログラムを書かせる。
Tessarune は描画 API のリファレンス、25 冊の制作マニュアル、結果を確かめるための描画エンジンをエージェントに渡します。