Graveyard & Chapel · stained_window.js
ステンドグラス
32×64 の壁用の小物で、接地影はありません。隅石を積んだ脇柱と張り出した窓台の上に、尖った石のアーチを架け、鉛で区切った小さなガラスを抽象的な模様(色の縁取り、四方に光る星を入れたバラ窓、菱形の飾り 2 つ)にはめています。ガラスは一枚ずつ左上の画素を一段明るく、右下を一段暗くして面取りしています。変種はどれも 16 コマのループで、day は光の帯がガラスを横切り、窓台の下にほこりの舞う光の筋が落ちます。night は暗いガラスの中で月に照らされた数枚だけが淡い青に息づきます。broken は割れたガラスの向こうが闇になり、破片が光ります。
6×
動くスプライト素材: Graveyard & Chapel(itch.io)Vol.4・小物 12 点・33 アニメ
冒頭のコメント。これが指示書です。サイズ、視点、光の向き、すべての変種が書かれています。
stained_window.js · 1–8
// Stained-glass lancet window — 32x64 WALL prop (no contact shadow), 3/4 view, key light top-left.// A pointed (two-centre) arch of dressed stone voussoirs over quoined jambs and a projecting sill. The glass is// leaded into many small panes: a coloured border band, a rose with a four-point star in the arch head, and two// diamond medallions over quarries below an iron saddle bar. Pattern is abstract (no figures).// Every pane is bevelled: its top-left pixel one step lighter, its bottom-right one step darker.// Variants: day (16f loop: a shimmer band sweeps the glass; a dithered light shaft with drifting dust motes spills// below the sill), night (16f loop: dark muted glass, a few moonlit panes breathe a faint blue glow),// broken (16f loop: shattered panes open onto darkness, cracks, a shard glints on the glass and on the sill).最後の数行。submit でキャンバスの大きさと、描画エンジンが書き出す変種の一覧を指定します。
stained_window.js · 261–271
submit(StainedWindow, { size: [W, H], origin: "top-left", kind: "icon", scale: 1, animate: { fps: 10, duration: 1.6, loop: true }, anchor: { x: MX, y: BOT + 4 }, args: ["day"], variants: [ { name: "day" }, { name: "night", args: ["night"] }, { name: "broken", args: ["broken"] }, ],});プログラム全体を表示 (271 行)
stained_window.js · 271 行
// Stained-glass lancet window — 32x64 WALL prop (no contact shadow), 3/4 view, key light top-left.// A pointed (two-centre) arch of dressed stone voussoirs over quoined jambs and a projecting sill. The glass is// leaded into many small panes: a coloured border band, a rose with a four-point star in the arch head, and two// diamond medallions over quarries below an iron saddle bar. Pattern is abstract (no figures).// Every pane is bevelled: its top-left pixel one step lighter, its bottom-right one step darker.// Variants: day (16f loop: a shimmer band sweeps the glass; a dithered light shaft with drifting dust motes spills// below the sill), night (16f loop: dark muted glass, a few moonlit panes breathe a faint blue glow),// broken (16f loop: shattered panes open onto darkness, cracks, a shard glints on the glass and on the sill).const W = 32, H = 64, MX = 16; // MX: the window's vertical axis (between x 15 and 16)const S = 21; // springing line of the archconst GL = 7, GR = 25, R = 17; // glass spans [GL, GR); arch radius (lancet: R well above half-width)const FR = 4; // stone frame thicknessconst BOT = 40; // glass/frame bottom (first sill row)const C_L = GL + R, C_R = GR - R; // arc centres (the left arc is centred on the right, and vice versa)function inArch(x, y, grow) { const px = x + 0.5, py = y + 0.5, r = R + grow; if (px < GL - grow || px > GR + grow || y >= BOT) return false; if (py >= S) return true; return (px - C_L) ** 2 + (py - S) ** 2 <= r * r && (px - C_R) ** 2 + (py - S) ** 2 <= r * r;}const inGlass = (x, y) => inArch(x, y, 0);function ringOf(x, y) { // 0 = touches the stone, 1, 2 ... = distance inward (4-neighbour, in whole px) for (let d = 1; d <= 3; d++) { if (!inGlass(x - d, y) || !inGlass(x + d, y) || !inGlass(x, y - d)) return d - 1; } return 3;}// ---- glass pattern -----------------------------------------------------------------------------------// returns "lead", "bar" or { ramp, pane } where pane is a stable id (for per-pane tweaks)const ROSE = { cx: 16, cy: 16.4, r: 5.2 };const HEAD_Y = 22, BAR_Y = 31;function paneAt(x, y) { if (!inGlass(x, y)) return null; const ring = ringOf(x, y); if (ring === 0) return "lead"; if (ring === 1) { // border band: short alternating red / blue pieces with lead joints if (y >= S) { if ((y - S) % 4 === 3) return "lead"; const s = Math.floor((y - S) / 4) + (x < MX ? 0 : 1); return { ramp: s % 2 ? "r" : "b", pane: 100 + s * 2 + (x < MX ? 0 : 1) }; } const s = Math.floor((y + 40) / 3) + (x < MX ? 0 : 1); if ((y + 40) % 3 === 2) return "lead"; return { ramp: s % 2 ? "r" : "b", pane: 200 + s * 2 + (x < MX ? 0 : 1) }; } if (ring === 2) return "lead"; if (y <= HEAD_Y) { if (y === HEAD_Y) return "lead"; const dx = x + 0.5 - ROSE.cx, dy = y + 0.5 - ROSE.cy, r = Math.hypot(dx, dy); if (r > ROSE.r) return { ramp: "v", pane: dx < 0 ? 1 : 2 }; // spandrels if (r > ROSE.r - 1) return "lead"; if (r < 1.3) return { ramp: "g", pane: 3 }; // boss const star = Math.abs(dx) + Math.abs(dy); if (Math.abs(star - 3.1) < 0.5) return "lead"; if (star < 3.1) return { ramp: "r", pane: 4 }; // four-point star if (Math.abs(Math.abs(dx) - Math.abs(dy)) < 0.6) return "lead"; // petal joints on the diagonals return { ramp: "b", pane: dx < 0 ? (dy < 0 ? 5 : 6) : (dy < 0 ? 7 : 8) }; } if (y === BAR_Y) return "bar"; // iron saddle bar // two diamond medallions, the corners filled with quarries const top = y < BAR_Y, cy = top ? 27 : 36, dx = x + 0.5 - MX, dy = y + 0.5 - cy; const dia = Math.abs(dx) / 1.15 + Math.abs(dy); if (Math.abs(dia - 4.1) < 0.5) return "lead"; if (dia < 4.1) { if (dia < 1.5) return { ramp: "g", pane: top ? 10 : 11 }; // jewel return { ramp: top ? "e" : "r", pane: top ? 12 : 13 }; } if (Math.abs(dy) < 0.6) return "lead"; const q = (dx < 0 ? 0 : 1) + (dy < 0 ? 0 : 2); return { ramp: (q === 0 || q === 3) === top ? "b" : "v", pane: 30 + (top ? 0 : 4) + q };}// precomputed per pixel (the pattern never changes): pane lookup + bevel roleconst GLASS = [];for (let y = 0; y < BOT; y++) { GLASS.push([]); for (let x = 0; x < W; x++) GLASS[y].push(paneAt(x, y));}const cell = (x, y) => (y >= 0 && y < BOT && x >= 0 && x < W ? GLASS[y][x] : null);const isLead = (c) => c === null || typeof c === "string";function bevel(x, y) { // +1 top-left edge of a pane (the lead above or left of it), -1 bottom-right edge const up = isLead(cell(x, y - 1)), lf = isLead(cell(x - 1, y)); const dn = isLead(cell(x, y + 1)), rt = isLead(cell(x + 1, y)); if (up || lf) return (dn || rt) && !(up && lf) ? 0 : 1; if (dn || rt) return -1; return 0;}// ---- broken variant geometry -------------------------------------------------------------------------const HOLES = [ [[9, 33], [12, 32], [13, 34], [16, 33], [15, 36], [18, 38], [14, 39], [12, 37], [9, 39]], [[18, 24], [21, 23], [22, 26], [21, 29], [20, 27], [17, 28]], [[17, 13], [20, 13], [20, 17], [18, 15]],];function inPoly(px, py, P) { let inside = false; for (let i = 0, j = P.length - 1; i < P.length; j = i++) { const [xi, yi] = P[i], [xj, yj] = P[j]; if ((yi > py) !== (yj > py) && px < ((xj - xi) * (py - yi)) / (yj - yi) + xi) inside = !inside; } return inside;}const inHole = (x, y) => HOLES.some((P) => inPoly(x + 0.5, y + 0.5, P));const CRACKS = [[[14, 32], [19, 29]], [[16, 36], [21, 39]], [[18, 24], [14, 23]], [[10, 33], [10, 28]], [[17, 13], [14, 11]]];function onCrack(x, y) { for (const [[x0, y0], [x1, y1]] of CRACKS) { const n = Math.max(Math.abs(x1 - x0), Math.abs(y1 - y0)); for (let i = 0; i <= n; i++) if (Math.round(x0 + ((x1 - x0) * i) / n) === x && Math.round(y0 + ((y1 - y0) * i) / n) === y) return true; } return false;}// ---- glass colour ------------------------------------------------------------------------------------const clampI = (i) => Math.max(0, Math.min(3, i));function moonlit(x, y) { // a few panes at the upper right catch the moon return x - MX - (y - 14) * 0.5 > 1.5 && y < 31;}function glassKey(x, y, mode, t) { const c = cell(x, y); if (c === null) return null; if (mode === "broken" && inHole(x, y)) { // darkness through the hole, with a faint grey interior edge top-left (the reveal behind) return !inHole(x - 1, y) || !inHole(x, y - 1) ? "n0" : "ink"; } if (c === "lead") return "ink"; if (c === "bar") return mode === "night" ? "n1" : x < MX ? "n3" : "n2"; const bv = bevel(x, y); if (mode === "day") { // shimmer band sweeping diagonally (top-left -> bottom-right), off-glass at t=0 and t=1 const p = -16 + t * 70, d = x + y * 0.5 - p; const band = Math.abs(d) < 4 ? (Math.abs(d) < 1.5 ? 2 : 1) : 0; if (band === 2 && bv >= 0) return c.ramp === "g" ? "g3" : "n5"; return `${c.ramp}${clampI(2 + bv + band)}`; } if (mode === "night") { if (moonlit(x, y)) { const pulse = 0.5 + 0.5 * Math.cos(TAU * t + (x + y) * 0.35); return `b${clampI((pulse > 0.55 ? 2 : 1) + bv)}`; } return `${c.ramp}${bv > 0 ? 1 : 0}`; } // broken: overcast daylight; fresh edges and cracks catch the light if (HOLES.length && (inHole(x - 1, y) || inHole(x + 1, y) || inHole(x, y - 1) || inHole(x, y + 1))) return `${c.ramp}3`; if (onCrack(x, y)) return "n4"; return `${c.ramp}${clampI(1 + bv)}`;}// ---- stone frame -------------------------------------------------------------------------------------function stoneKey(x, y) { if (!inArch(x, y, FR) || inGlass(x, y)) return null; const px = x + 0.5, py = y + 0.5, left = px < MX; // reveal ring next to the glass: the left/top reveal is in shade, the right reveal catches the light const nearGlass = inGlass(x + 1, y) || inGlass(x - 1, y) || inGlass(x, y + 1); if (nearGlass) return left || py < S - 6 ? (left ? "n0" : "n1") : "n4"; // outer arris: lit on the left/top, shaded on the right const outer = !inArch(x - 1, y, FR) || !inArch(x + 1, y, FR) || !inArch(x, y - 1, FR); if (outer) return left || py < 5 ? "n4" : "n1"; let id, joint, topEdge; if (py < S) { // voussoirs: radial joints about the arch axis const a = Math.atan2(py - S, px - MX); // -PI .. 0 const u = (a + Math.PI) / (Math.PI / 7); id = Math.floor(u); const f = u - id; joint = f < 0.09 || f > 0.97; topEdge = f < 0.2 ? !left : f > 0.85 ? left : false; if (id === 3 && py < 4) return "n4"; // keystone crown } else { // jamb courses with alternating long/short blocks const row = y - S, course = Math.floor(row / 5); joint = row % 5 === 4 || (course % 2 === 1 && (x === 5 || x === 26)); topEdge = row % 5 === 0; id = 10 + course * 2 + (left ? 0 : 1); } if (joint) return "n1"; if (topEdge) return left ? "n4" : "n3"; const dark = id % 3 === 2; // one stone in three is a shade darker return left ? (dark ? "n2" : "n3") : (dark ? "n1" : "n2");}function sill(g) { g.part("sill", () => { g.rect({ x: [2, 29], y: [BOT, BOT + 4] }, "n2", { fill: "n2" }); g.line([2, BOT], [29, BOT], "n4"); // top face, lit g.line([3, BOT + 1], [29, BOT + 1], "n3"); g.line([2, BOT + 1], [2, BOT + 4], "n3"); // lit left end g.line([29, BOT + 1], [29, BOT + 4], "n1"); g.line([3, BOT + 4], [29, BOT + 4], "n1"); // drip edge g.line([GL, BOT + 1], [GR - 1, BOT + 1], "n2"); // the glass's shadow on the sill g.pixel(7, BOT + 2, "n1"); g.pixel(20, BOT + 3, "n3"); g.pixel(24, BOT + 2, "n1"); // chips });}// ---- effects -----------------------------------------------------------------------------------------const BAYER = [[0, 8, 2, 10], [12, 4, 14, 6], [3, 11, 1, 9], [15, 7, 13, 5]];const SHAFT_Y0 = BOT + 5, SHAFT_Y1 = H - 1;const shaftL = (y) => GL + (y - SHAFT_Y0) * 0.3, shaftR = (y) => GR + (y - SHAFT_Y0) * 0.35;function lightShaft(g, t) { const swell = 0.5 + 0.5 * Math.cos(TAU * t); g.detached("lightShaft", () => { for (let y = SHAFT_Y0; y <= SHAFT_Y1; y++) { const k = (y - SHAFT_Y0) / (SHAFT_Y1 - SHAFT_Y0); const xl = shaftL(y), xr = shaftR(y); for (let x = Math.ceil(xl); x <= Math.floor(xr); x++) { // translucent checker at the window, fading (ordered dither) toward the floor const edge = Math.min(x - xl, xr - x); if (!chk(x, y)) continue; const fade = BAYER[(y >> 1) & 3][(x >> 1) & 3] / 16; if (edge < 1.2 && (x & 2)) continue; if (fade > 1.15 - k * 1.1 + 0.08 * swell) continue; let key = k < 0.35 ? "g3" : "n5"; if (k > 0.6) key = ["b3", "v3", "g3", "r3"][Math.min(3, Math.floor(((x - xl) / (xr - xl)) * 4))]; g.pixel(x, y, key); } } }); g.detached("dust", () => { for (let i = 0; i < 6; i++) { const p = (t + i / 6) % 1; const y = Math.round(SHAFT_Y0 + 1 + p * 16); const x = Math.round((shaftL(y) + shaftR(y)) / 2 + [-5, 2, -2, 5, 0, -4][i] + Math.sin(TAU * (t + i * 0.37)) * 1.5); if (p > 0.85 && !chk(x, i)) continue; g.pixel(x, y, "g3"); g.pixel(x + 1, y, "n5"); } });}function glint(g, x, y, f) { // 4-frame star: dot, cross, long cross, dot if (f < 0 || f > 3) return; g.pixel(x, y, "n5"); if (f === 1 || f === 2) for (const [dx, dy] of [[1, 0], [-1, 0], [0, 1], [0, -1]]) g.pixel(x + dx, y + dy, f === 2 ? "g3" : "n5"); if (f === 2) for (const [dx, dy] of [[2, 0], [-2, 0], [0, 2], [0, -2]]) g.pixel(x + dx, y + dy, "g3");}function brokenFx(g, t) { const f = Math.round(t * 16) % 16; g.part("shardOnSill", () => { g.pixel(21, BOT, "b3"); g.pixel(22, BOT, "b2"); g.pixel(22, BOT + 1, "b1"); g.pixel(23, BOT + 1, "b2"); g.pixel(10, BOT + 1, "r2"); g.pixel(11, BOT + 1, "r1"); }); g.detached("glint", () => { glint(g, 13, 34, f - 2); // on the big hole's jagged edge glint(g, 22, BOT, f - 10); // on the fallen shard });}// ---- sprite ------------------------------------------------------------------------------------------const StainedWindow = sprite(pal, (g, t, mode) => { g.outlined("ink", () => { g.part("frame", () => g.field({ x: [0, W - 1], y: [0, BOT - 1] }, (x, y) => stoneKey(x, y))); g.part("glass", () => g.field({ x: [GL, GR - 1], y: [0, BOT - 1] }, (x, y) => glassKey(x, y, mode, t))); sill(g); }); if (mode === "day") lightShaft(g, t); if (mode === "broken") brokenFx(g, t);});submit(StainedWindow, { size: [W, H], origin: "top-left", kind: "icon", scale: 1, animate: { fps: 10, duration: 1.6, loop: true }, anchor: { x: MX, y: BOT + 4 }, args: ["day"], variants: [ { name: "day" }, { name: "night", args: ["night"] }, { name: "broken", args: ["broken"] }, ],});掲載されたコードでは、ビルドスクリプトが自動付与する共通部分を省略しています。具体的には 1 行のランタイムヘッダー、共有パレット、および汎用ヘルパー関数(contactShadow、litIndex、flame、hash、chk、TAU)です。表示されている行番号は実際のファイルと一致しています。これらのプログラムは AIエージェントが生成し、描画されたコマの仕上がりを確認しながら修正を加えたものです。
エージェントに、こうしたプログラムを書かせる。
Tessarune は描画 API のリファレンス、25 冊の制作マニュアル、結果を確かめるための描画エンジンをエージェントに渡します。