Tessarune
Dungeon Mechanisms · orb_pedestal.js

宝珠の台座

32×48 · JavaScript 241 行

32×48 の床置きの小物で、段のある台座、紋を彫って真鍮の帯を巻いた丸い柱、浅い受け口のある広がった皿から成る石の柱の上に、ガラスと光の宝珠が浮かんでいます。宝珠は向かい合う面を照らし、受け口の光だまり、皿の奥の壁、柱の紋の溝が光ります。idle(12 コマ)は宝珠が 1px 上下し、渦が回り、光の粒が昇ります。take(8 コマの一回再生)は宝珠が光って浮き上がり、一筋に縮んで光の粒になって弾けます。empty(8 コマ)は空の受け口です。idle-violet と idle-green は光の色を差し替えたものです。

宝珠の台座 — 描画結果6×
このプログラムで描画(変種「idle」) · 32×48 · 12 コマ
Dungeon Mechanisms動くスプライト素材: 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 冊の制作マニュアル、結果を確かめるための描画エンジンをエージェントに渡します。