Dungeon Mechanisms · orb_pedestal.js
宝珠の台座
32×48 の床置きの小物で、段のある台座、紋を彫って真鍮の帯を巻いた丸い柱、浅い受け口のある広がった皿から成る石の柱の上に、ガラスと光の宝珠が浮かんでいます。宝珠は向かい合う面を照らし、受け口の光だまり、皿の奥の壁、柱の紋の溝が光ります。idle(12 コマ)は宝珠が 1px 上下し、渦が回り、光の粒が昇ります。take(8 コマの一回再生)は宝珠が光って浮き上がり、一筋に縮んで光の粒になって弾けます。empty(8 コマ)は空の受け口です。idle-violet と idle-green は光の色を差し替えたものです。
6×
動くスプライト素材: Dungeon Mechanisms(itch.io)Vol.2・罠と扉と壊れる物 12 点
冒頭のコメント。これが指示書です。サイズ、視点、光の向き、すべての変種が書かれています。
orb_pedestal.js · 1–9
// Orb pedestal — 32x48 floor prop, top-down 3/4, key light top-left.// A carved stone column (stepped plinth, round rune-cut shaft with a brass band, a flared dish with a shallow socket)// under a floating glass-and-light orb. The orb lights what faces it: the socket pool, the far wall of the dish and// the rune channel in the shaft all take the glow ramp, so the colour variants re-light the stone too.// Variants:// idle loop, 12f — orb bobs 1px, two-arm swirl turns half a turn, socket pool breathes, motes rise// take one-shot, 8f — orb flares, lifts, collapses to a streak and bursts into sparkles; ends == empty f0// empty loop, 8f — bare socket, dead rune, a faint residual glint in the socket// idle-violet / idle-green — glow ramp swapped (no new colours)最後の数行。submit でキャンバスの大きさと、描画エンジンが書き出す変種の一覧を指定します。
orb_pedestal.js · 230–241
submit(OrbPedestal, { size: [32, 48], origin: "top-left", kind: "icon", scale: 1, animate: { fps: 10, duration: N_IDLE / 10, loop: true }, anchor: { x: 16, y: GROUND }, variants: [ { name: "idle", args: ["idle"] }, { name: "take", args: ["take"], animate: { fps: 10, duration: N_TAKE / 10, loop: false } }, { name: "empty", args: ["empty"], animate: { fps: 10, duration: N_EMPTY / 10, loop: true } }, { name: "idle-violet", args: ["idle"], repalette: swap("glow", "v") }, { name: "idle-green", args: ["idle"], repalette: swap("glow", "e") }, ],});プログラム全体を表示 (241 行)
orb_pedestal.js · 241 行
// Orb pedestal — 32x48 floor prop, top-down 3/4, key light top-left.// A carved stone column (stepped plinth, round rune-cut shaft with a brass band, a flared dish with a shallow socket)// under a floating glass-and-light orb. The orb lights what faces it: the socket pool, the far wall of the dish and// the rune channel in the shaft all take the glow ramp, so the colour variants re-light the stone too.// Variants:// idle loop, 12f — orb bobs 1px, two-arm swirl turns half a turn, socket pool breathes, motes rise// take one-shot, 8f — orb flares, lifts, collapses to a streak and bursts into sparkles; ends == empty f0// empty loop, 8f — bare socket, dead rune, a faint residual glint in the socket// idle-violet / idle-green — glow ramp swapped (no new colours)const CX = 16, GROUND = 45;const N_IDLE = 12, N_TAKE = 8, N_EMPTY = 8;const ORB_R = 6.5, ORB_Y = 8; // orb centre (continuous coords) at bob 0const BOWL = { cx: 16, cy: 18, rx: 5, ry: 2 };const GL = ["glow0", "glow1", "glow2", "glow3"];const up = (k, n) => (k + n > 3 ? "n5" : GL[Math.max(0, k + n)]);// ---- stone -----------------------------------------------------------------------------------------------// a block seen top-down 3/4: lit top face, a lit front edge on the left, front face darkening to the rightfunction block(g, x0, x1, yTop, yFront, yBot, o = {}) { const w = x1 - x0; g.rect({ x: [x0, x1], y: [yTop, yFront - 1] }, "n3", { fill: "n3" }); if (o.farEdge !== false) g.line([x0 + 1, yTop], [x1 - 2, yTop], "n4"); g.line([x0, yFront - 1], [x0 + Math.round(w * 0.6), yFront - 1], "n4"); g.rect({ x: [x0, x1], y: [yFront, yBot] }, "n2", { fill: "n2" }); g.rect({ x: [x1 - Math.round(w * 0.22), x1], y: [yFront, yBot] }, "n1", { fill: "n1" }); g.line([x0, yFront], [x0, yBot], "n3"); if (yBot - yFront >= 2) g.line([x0 + 1, yBot], [x1, yBot], "n1");}function plinth(g) { g.part("plinth", () => { block(g, 5, 26, 40, 42, 44); // dentil carving along the front: countable notches for (let x = 8; x <= 23; x += 3) { g.pixel(x, 43, "n1"); g.pixel(x + 1, 43, x < 20 ? "n3" : "n2"); } g.line([24, 40], [25, 41], "n2"); // chipped corner g.pixel(10, 41, "n2"); g.pixel(11, 41, "n2"); // crack on the step g.line([6, 41], [8, 41], "e1"); g.pixel(7, 40, "e2"); g.pixel(6, 42, "e1"); g.pixel(5, 43, "e0"); // moss in the corner block(g, 8, 23, 36, 38, 39); });}function shaft(g) { // round column: cylinder shading across its width, a collar under the dish and a torus at the foot const R = (y) => (y <= 26 ? 3.5 : y <= 32 ? 3.5 : y === 33 ? 4.5 : 5.5); g.part("shaft", () => { g.field({ x: [9, 22], y: [24, 35] }, (x, y) => { const r = R(y), u = (x + 0.5 - CX) / r; if (Math.abs(u) > 1) return null; if (y === 33 || y === 34) return u < -0.5 ? "n4" : u < 0.1 ? "n3" : u < 0.6 ? "n2" : "n1"; // torus if (y === 35) return u < 0.4 ? "n1" : "n0"; if (y === 26) return u < -0.6 ? "g3" : u < 0 ? "g2" : u < 0.6 ? "g1" : "g0"; // brass band if (y === 27) return u < 0.3 ? "n1" : "n0"; // its shadow return u < -0.6 ? "n3" : u < 0.05 ? "n2" : u < 0.6 ? "n1" : "n0"; }); });}function rune(g, lvl) { // a carved channel down the column's face; its light is the orb's (lvl -1 = dead) g.part("rune", () => { const c = lvl < 0 ? "n0" : GL[Math.min(3, lvl)], d = lvl < 0 ? "n1" : GL[Math.max(0, lvl - 1)]; g.line([15, 28], [15, 32], c); g.pixel(14, 29, d); g.pixel(16, 29, d); g.pixel(14, 31, d); g.pixel(16, 31, d); if (lvl >= 2) g.pixel(15, 30, up(lvl, 1)); if (lvl === 0) g.pixel(15, 30, "glow1"); // a last ember of light in the cut });}// dish: a flared stone cup, top ellipse centred at DISH.cyconst DISH = { cy: 19.5, rx: 9, ry: 3, irx: 7, iry: 2 };const dishR = (y) => (y < 22 ? 9 : y === 22 ? 8 : y === 23 ? 6.5 : y === 24 ? 5 : 4.5);function dish(g) { g.part("dish", () => { g.field({ x: [6, 25], y: [16, 25] }, (x, y) => { const nx = (x + 0.5 - CX) / DISH.rx, ny = (y + 0.5 - DISH.cy) / DISH.ry; if (nx * nx + ny * ny <= 1) return null; // top face drawn below if (y + 0.5 < DISH.cy) return null; const r = dishR(y), u = (x + 0.5 - CX) / r; if (Math.abs(u) > 1) return null; if (y === 22 && Math.abs(u) < 0.95) return u < -0.3 ? "n2" : "n1"; // bead under the lip if (y >= 23 && (x === 13 || x === 18)) return x === 13 ? "n1" : "n0"; // flutes return u < -0.55 ? "n3" : u < 0.1 ? "n2" : u < 0.6 ? "n1" : "n0"; }); g.field({ x: [6, 25], y: [16, 23] }, (x, y) => { const nx = (x + 0.5 - CX) / DISH.rx, ny = (y + 0.5 - DISH.cy) / DISH.ry; if (nx * nx + ny * ny > 1) return null; const ix = (x + 0.5 - CX) / DISH.irx, iy = (y + 0.5 - (DISH.cy - 0.3)) / DISH.iry; if (ix * ix + iy * iy <= 1) return null; // the hollow, drawn by socket() const a = -0.7 * nx - 0.35 * ny; return a > 0.4 ? "n4" : a > -0.1 ? "n3" : a > -0.55 ? "n2" : "n1"; }); });}function socket(g, pool) { // the hollow of the dish; pool = light level of the orb reflected in it (-1 = dark) const cy = DISH.cy - 0.3, rx = DISH.irx, ry = DISH.iry; g.part("socket", () => g.field({ x: [CX - rx, CX + rx - 1], y: [Math.floor(cy - ry), Math.ceil(cy + ry)] }, (x, y) => { const nx = (x + 0.5 - CX) / rx, ny = (y + 0.5 - cy) / ry; const r = nx * nx + ny * ny; if (r > 1) return null; if (ny < -0.3 || r > 0.75) { // inner wall; right side lit if (pool >= 2 && ny < -0.3 && Math.abs(nx) < 0.4) return "glow1"; // orb light on the far wall return nx > 0.45 ? "n2" : nx > -0.2 ? "n1" : "n0"; } if (pool < 0) return "n0"; const k = pool - (Math.abs(nx) > 0.45 ? 1 : 0); return k < 0 ? "n0" : GL[Math.min(3, k)]; }));}// ---- orb ---------------------------------------------------------------------------------------------------function orbTone(nx, ny, t, lvl) { const r = Math.hypot(nx, ny); if (r > 1) return null; const sx = nx + 0.45, sy = ny + 0.45; if (sx * sx * 1.4 + sy * sy < 0.03) return "n5"; // window specular, upper left if (r > 0.86) return up(0, lvl); // dark rim of the glass const d = 0.5 * nx + 0.86 * ny; // light leaving the far (lower) side if (r > 0.64 && d > 0.62) return up(3, lvl); const cx = nx - 0.05, cy = ny - 0.05; if (cx * cx + cy * cy < 0.06) return up(3, lvl); // hot core const th = Math.atan2(ny, nx); const m = Math.cos(2 * th + 4.6 * r - TAU * t); // two arms, half a turn per cycle if (r > 0.22 && m > 0.5) return up(2, lvl); if (d < -0.55) return up(0, lvl); // upper glass reflects the dark room return up(1, lvl);}function orb(g, cx, cy, R, t, lvl) { g.detached("orb", () => g.outlined("ink", () => g.field({ x: [Math.floor(cx - R), Math.ceil(cx + R)], y: [Math.floor(cy - R), Math.ceil(cy + R)] }, (x, y) => orbTone((x + 0.5 - cx) / R, (y + 0.5 - cy) / R, t, lvl))));}function motes(g, t, n) { // sparks of light leave the dish rim and drift up beside the orb (head + fading tail) g.detached("motes", () => { for (let i = 0; i < n; i++) { const p = (t + i / n) % 1; const side = i % 2 ? 1 : -1; const x = Math.round(CX - 0.5 + side * (8.5 - p * 2.5)); const y = 17 - Math.round(p * 14); if (p > 0.8) continue; g.pixel(x, y, p < 0.45 ? "glow3" : "glow2"); if (p < 0.6) g.pixel(x, y + 1, p < 0.3 ? "glow2" : "glow1"); } });}function sparkle(g, x, y, s, c) { // s = arm length 0..2 ; a 4-point star g.pixel(x, y, s > 0 ? "n5" : c); for (let i = 1; i <= s; i++) { const k = i === s ? c : "n5"; g.pixel(x - i, y, k); g.pixel(x + i, y, k); g.pixel(x, y - i, k); g.pixel(x, y + i, k); }}// ---- scene ------------------------------------------------------------------------------------------------function pedestal(g, s) { contactShadow(g, CX, GROUND, 12, 2); g.outlined("ink", () => { plinth(g); shaft(g); dish(g); }); rune(g, s.rune); socket(g, s.pool);}const OrbPedestal = sprite(pal, (g, t, state) => { if (state === "idle") { const bob = Math.round(0.5 - 0.5 * Math.cos(TAU * t)); const breathe = bob ? 3 : 2; pedestal(g, { rune: 2 + (t > 0.3 && t < 0.7 ? 1 : 0) - (t < 0.1 || t > 0.9 ? 1 : 0), pool: breathe }); orb(g, CX, ORB_Y + bob, ORB_R, t, 0); motes(g, t, 3); return; } if (state === "take") { const f = Math.min(N_TAKE - 1, Math.round(t * N_TAKE)); // f: 0 idle | 1 flare | 2 lift+brighter | 3 high, shrinking | 4 streak | 5 burst | 6 fading | 7 == empty f0 const KEYS = [ { rune: 1, pool: 2 }, { rune: 3, pool: 3 }, { rune: 3, pool: 3 }, { rune: 2, pool: 2 }, { rune: 1, pool: 1 }, { rune: 0, pool: 0 }, { rune: 0, pool: -1 }, ]; if (f === N_TAKE - 1) { drawEmpty(g, 0); return; } pedestal(g, KEYS[f]); if (f === 0) { orb(g, CX, ORB_Y, ORB_R, 0, 0); motes(g, 0, 3); return; } g.detached("flare", () => { if (f === 1) { orb(g, CX, ORB_Y, ORB_R, 0.1, 1); for (const [x, y] of [[8, 3], [24, 5], [10, 15], [23, 13]]) sparkle(g, x, y, 1, "glow2"); } if (f === 2) { orb(g, CX, ORB_Y - 2, 5.5, 0.2, 2); g.line([15, 13], [15, 15], "glow2"); for (const [x, y] of [[6, 2], [26, 3], [7, 12], [25, 10]]) sparkle(g, x, y, 1, "glow3"); } if (f === 3) { orb(g, CX, 4, 3.5, 0.3, 2); sparkle(g, 15, 0, 1, "glow2"); g.line([15, 9], [15, 13], "glow2"); g.line([16, 9], [16, 12], "glow1"); } if (f === 4) { g.line([15, 0], [15, 6], "n5"); g.line([16, 1], [16, 5], "glow3"); g.line([14, 2], [14, 4], "glow2"); g.line([15, 7], [15, 11], "glow2"); g.pixel(15, 13, "glow1"); for (const [x, y] of [[10, 4], [21, 3], [12, 9], [19, 8]]) g.pixel(x, y, "glow3"); } if (f === 5) { for (const [x, y, s] of [[15, 1, 1], [9, 3, 1], [22, 2, 1], [12, 8, 0], [19, 7, 0], [6, 8, 0], [25, 7, 0], [16, 5, 0]]) sparkle(g, x, y, s, s ? "glow2" : "glow3"); } if (f === 6) { for (const [x, y, c] of [[8, 7, "glow2"], [23, 6, "glow2"], [5, 13, "glow2"], [26, 12, "glow3"], [14, 4, "glow3"]]) g.pixel(x, y, c); } }); return; } drawEmpty(g, t);});function drawEmpty(g, t) { const f = Math.round(t * N_EMPTY) % N_EMPTY; pedestal(g, { rune: f === 4 || f === 5 ? 0 : -1, pool: -1 }); // residual glint: an ember of light left in the socket, flickering on then a small star on the lit lip g.detached("glint", () => { const e = [0, 1, 1, 2, 3, 2, 1, 0][f]; if (e > 0) g.pixel(17, 19, GL[e]); if (e > 2) g.pixel(16, 19, "glow1"); if (f === 5) sparkle(g, 12, 19, 1, "n4"); if (f === 6) g.pixel(12, 19, "n5"); });}submit(OrbPedestal, { size: [32, 48], origin: "top-left", kind: "icon", scale: 1, animate: { fps: 10, duration: N_IDLE / 10, loop: true }, anchor: { x: 16, y: GROUND }, variants: [ { name: "idle", args: ["idle"] }, { name: "take", args: ["take"], animate: { fps: 10, duration: N_TAKE / 10, loop: false } }, { name: "empty", args: ["empty"], animate: { fps: 10, duration: N_EMPTY / 10, loop: true } }, { name: "idle-violet", args: ["idle"], repalette: swap("glow", "v") }, { name: "idle-green", args: ["idle"], repalette: swap("glow", "e") }, ],});掲載されたコードでは、ビルドスクリプトが自動付与する共通部分を省略しています。具体的には 1 行のランタイムヘッダー、共有パレット、および汎用ヘルパー関数(contactShadow、litIndex、flame、hash、chk、TAU)です。表示されている行番号は実際のファイルと一致しています。これらのプログラムは AIエージェントが生成し、描画されたコマの仕上がりを確認しながら修正を加えたものです。
エージェントに、こうしたプログラムを書かせる。
Tessarune は描画 API のリファレンス、25 冊の制作マニュアル、結果を確かめるための描画エンジンをエージェントに渡します。