Tessarune
Graveyard & Chapel · soul_lantern.js

魂の灯籠

32×64 · JavaScript 225 行

32×64 の縦長の床置きの小物で、苔むした石の土台に鍛鉄の柱を立て、ねじった部分、槍の穂先の飾り、杖の先のように曲がった腕を付けた墓地の道の灯りです。籠に入ったランタンが 2 つの鎖の輪で鉤から下がり、中でろうそくが燃えています。ガラスが光り、籠の格子の影が地面のディザの光だまりに落ちます。変種はどれも 12 コマのループで、炎は階調を差し替えるだけで色を変え、新しい色は足していません。光の粒が昇る soul(魂火)、ときどき火の粉が飛ぶ fire(炎)、紫の witch(魔女火)です。

魂の灯籠 — 描画結果6×
このプログラムで描画(変種「soul」) · 32×64 · 12 コマ
Graveyard & Chapel動くスプライト素材: Graveyard & Chapel(itch.io)Vol.4・小物 12 点・33 アニメ

冒頭のコメント。これが指示書です。サイズ、視点、光の向き、すべての変種が書かれています。

soul_lantern.js · 1–9
// Soul lantern — 32x64 floor prop (tall), top-down 3/4, key light top-left.// A cemetery path lamp: a wrought-iron post on a mossy stone footing, a twisted section, a spear finial, and a// shepherd's-crook arm with a scroll brace. A caged lantern hangs from the hook on two chain links; a candle stub// burns inside. The glass panes glow and catch a glint of the key light; the cage throws bars of light on the// ground (a detached, dithered pool with the bar shadows radiating from under the lantern).// Variants (12f loops; the flame is the fl0..3 family, recoloured by ramp swap — no new colours)://   soul   blue soulfire, motes drift up and away//   fire   ordinary warm candle flame, one ember now and then//   witch  violet witchfire, motes drift up and away

最後の数行。submit でキャンバスの大きさと、描画エンジンが書き出す変種の一覧を指定します。

soul_lantern.js · 215–225
submit(SoulLantern, {  size: [W, H], origin: "top-left", kind: "icon", scale: 1,  animate: { fps: 10, duration: 1.2, loop: true },  anchor: { x: PX, y: GROUND },  args: ["soul"],  variants: [    { name: "soul", repalette: swap("fl", "soul") },    { name: "fire", args: ["fire"] },    { name: "witch", args: ["witch"], repalette: swap("fl", "witch") },  ],});
プログラム全体を表示 (225 行)
soul_lantern.js · 225 行
// Soul lantern — 32x64 floor prop (tall), top-down 3/4, key light top-left.// A cemetery path lamp: a wrought-iron post on a mossy stone footing, a twisted section, a spear finial, and a// shepherd's-crook arm with a scroll brace. A caged lantern hangs from the hook on two chain links; a candle stub// burns inside. The glass panes glow and catch a glint of the key light; the cage throws bars of light on the// ground (a detached, dithered pool with the bar shadows radiating from under the lantern).// Variants (12f loops; the flame is the fl0..3 family, recoloured by ramp swap — no new colours)://   soul   blue soulfire, motes drift up and away//   fire   ordinary warm candle flame, one ember now and then//   witch  violet witchfire, motes drift up and awayconst W = 32, H = 64, GROUND = 61;const PX = 10;                                  // post centre column (the post is 3px: PX-1 .. PX+1)const LX = 22;                                  // lantern centre columnconst FC = { x: LX, y: 25 };                    // light centre (flame)const IRON = ["n0", "n1", "n2", "n3"];const L = [-0.74, -0.67];// iron bar swept along a path, shaded as a round bar lit from the top-leftfunction bar(g, path, w0, w1) {  const a = path[0], b = path[path.length - 1];  const dx = b[0] - a[0], dy = b[1] - a[1], len = Math.hypot(dx, dy) || 1;  const nl = (-dy / len) * L[0] + (dx / len) * L[1];  g.sweep(path, { w0, w1 }, (u, v) => {    const l = v * nl * 1.4 + 0.15;    return IRON[l > 0.5 ? 3 : l > -0.1 ? 2 : l > -0.7 ? 1 : 0];  });}// ---- stone footing, post, crook -----------------------------------------------------------------------------function footing(g) {  g.part("footing", () => {    // top face (lit, left brighter), then the front face    g.rect({ x: [PX - 5, PX + 5], y: [54, 55] }, "n3", { fill: "n3" });    g.line([PX - 5, 54], [PX - 1, 54], "n4");    g.rect({ x: [PX - 5, PX + 5], y: [56, 60] }, "n2", { fill: "n2" });    g.line([PX - 5, 56], [PX + 5, 56], "n1");                    // shadow under the top lip    g.line([PX + 4, 57], [PX + 4, 60], "n1"); g.line([PX + 5, 56], [PX + 5, 60], "n1");  // shaded right edge    g.line([PX - 5, 57], [PX - 5, 60], "n3");    // cracks and a chip, a tuft of moss in the corner    g.pixel(PX - 2, 58, "n1"); g.pixel(PX - 1, 59, "n1"); g.pixel(PX - 1, 57, "n1");    g.pixel(PX + 2, 55, "n2");    g.pixel(PX - 5, 60, "e1"); g.pixel(PX - 4, 60, "e0"); g.pixel(PX - 5, 59, "e2");    g.pixel(PX + 5, 54, "n2");  });}function post(g) {  g.part("post", () => {    for (let y = 8; y <= 53; y++) { g.pixel(PX - 1, y, "n2"); g.pixel(PX, y, "n1"); g.pixel(PX + 1, y, "n0"); }    for (let y = 30; y <= 44; y++) {                             // twisted section: a spiral of lit ridges      const k = (y - 30) % 3;      g.pixel(PX - 1 + k, y, k === 0 ? "n3" : k === 1 ? "n2" : "n1");    }    for (const y of [12, 13, 20, 21, 49, 50]) g.pixel(PX - 1, y, "n3");   // specular streaks  });  g.part("collars", () => {    for (const y0 of [28, 45, 52]) {      for (let x = PX - 2; x <= PX + 2; x++) {        const u = (x - PX) / 2;        g.pixel(x, y0, u < -0.4 ? "n4" : u < 0.4 ? "n3" : "n2");        g.pixel(x, y0 + 1, u < -0.4 ? "n2" : u < 0.4 ? "n1" : "n0");      }    }  });  g.part("finial", () => {    // spear point with a pair of barbs    g.pixel(PX, 1, "n3");    g.pixel(PX - 1, 2, "n3"); g.pixel(PX, 2, "n2");    g.pixel(PX - 1, 3, "n3"); g.pixel(PX, 3, "n2"); g.pixel(PX + 1, 3, "n1");    g.pixel(PX - 2, 4, "n3"); g.pixel(PX - 1, 4, "n2"); g.pixel(PX, 4, "n1"); g.pixel(PX + 1, 4, "n1"); g.pixel(PX + 2, 4, "n0");    g.pixel(PX, 5, "n1"); g.pixel(PX - 1, 6, "n3"); g.pixel(PX, 6, "n2"); g.pixel(PX + 1, 6, "n1");    g.pixel(PX, 7, "n1");  });}function crook(g) {  g.part("crook", () => {    // the arm leaves the post, runs out to the right and curls down into a hook    bar(g, [[PX + 1, 11], [14, 8], [20, 7], [24, 8], [26, 11], [25, 13], [23, 13]], 1.0, 0.6);    // scroll brace under the arm, rolling into a small curl against the post    bar(g, [[17, 8], [14, 11], [12, 15], [PX + 1, 19]], 0.6, 0.6);    g.pixel(13, 17, "n2"); g.pixel(14, 18, "n1"); g.pixel(14, 17, "n0");    // chain: two links from the arm down to the lantern ring    g.pixel(LX, 9, "n3"); g.pixel(LX, 10, "n1"); g.pixel(LX - 1, 11, "n2"); g.pixel(LX + 1, 11, "n0");    g.pixel(LX, 12, "n3");  });}// ---- the lantern ------------------------------------------------------------------------------------------const GT = 19, GB = 29;                                         // glass rowsfunction lantern(g, t) {  g.part("lanternCap", () => {    for (let y = 13; y <= 18; y++) {      const hw = y === 18 ? 6 : y - 13;      for (let x = LX - hw; x <= LX + hw; x++) {        const u = (x - LX) / (hw + 0.5);        let k = u < -0.35 ? "n3" : u <= 0.35 ? "n2" : "n1";        if (y === 18) k = u < -0.5 ? "n3" : u < 0.5 ? "n2" : "n1";        if (y === 13) k = "n3";        g.pixel(x, y, k);      }    }    g.pixel(LX - 5, 18, "n4"); g.pixel(LX - 2, 15, "n4");    g.pixel(LX, 16, "n0");                                      // a vent  });  const flick = 0.5 + 0.5 * Math.sin(TAU * (2 * t + 0.1)) * 0.7 + 0.15 * Math.sin(TAU * (3 * t + 0.6));  g.part("lanternCage", () => g.field({ x: [LX - 6, LX + 6], y: [GT, GB + 1] }, (x, y) => {    const hw = y === GB + 1 ? 5 : 5 - (y - GT >= 8 ? 0 : 0);    const dx = x - LX, ax = Math.abs(dx);    if (ax > hw) return null;    if (y === GB + 1) return dx < -2 ? "n3" : dx < 3 ? "n2" : "n1";   // bottom band    if (ax === 5) return dx < 0 ? "n2" : "n0";                     // corner posts    if (ax === 2) return dx < 0 ? "n1" : "n0";                     // cage bars (silhouetted by the glow)    if (y === 24) return dx < 0 ? "n1" : "n0";                     // cross bar    // glass: front pane brightest near the flame; side panes dimmer    const front = ax < 2, left = dx < 0;    const d = Math.hypot(x - FC.x, (y - FC.y) * 0.9);    // glint of the key light on the left pane (fixed, the glass is hard and cold)    if (left && !front && (x - (LX - 4)) + (y - GT) === 1 && y < GT + 3) return "n5";    if (front) return d < 3.2 + flick ? "fl2" : "fl1";    return d < 3.4 + flick ? "fl1" : "fl0";  }));  g.part("lanternBase", () => {    g.line([LX - 3, GB + 2], [LX + 3, GB + 2], "n1"); g.pixel(LX - 3, GB + 2, "n3"); g.pixel(LX - 2, GB + 2, "n2");    g.line([LX - 1, GB + 3], [LX + 1, GB + 3], "n1"); g.pixel(LX - 1, GB + 3, "n2");    g.pixel(LX, GB + 4, "n1");  });  g.part("candle", () => {    g.rect({ x: [LX - 1, LX + 1], y: [27, 29] }, "n4", { fill: "n4" });    g.line([LX + 1, 27], [LX + 1, 29], "n3");    g.pixel(LX - 1, 27, "n5"); g.pixel(LX, 27, "n5");    g.pixel(LX + 1, 28, "n4");                                  // a wax drip    g.pixel(LX, 26, "n0");                                      // wick  });}function flameShape(g, t, mode) {  const tall = mode === "fire" ? 4.2 : 5.2;  const hgt = tall + 0.9 * Math.sin(TAU * (2 * t + 0.1)) + 0.5 * Math.sin(TAU * (3 * t + 0.6));  const sway = mode === "fire" ? 0.6 : 1.1;  g.part("flame", () => g.field({ x: [LX - 3, LX + 3], y: [18, 26] }, (x, y) => {    const k = (25.5 - y) / hgt;    if (k < 0 || k > 1) return null;    const ax = LX + (sway * Math.sin(TAU * (t + 0.2)) + 0.4 * Math.sin(TAU * 2 * t)) * k * k;    const hw = k < 0.3 ? 1.1 + k : 1.4 * Math.pow((1 - k) / 0.7, 0.8);    const d = Math.abs(x - ax);    if (d > hw) return null;    if (k > 0.8) return "fl2";    return d < hw * 0.55 && k < 0.65 ? "fl3" : "fl2";  }));}const BAYER = [0, 8, 2, 10, 12, 4, 14, 6, 3, 11, 1, 9, 15, 7, 13, 5];const dith = (x, y, dens) => BAYER[(y & 3) * 4 + (x & 3)] / 16 < dens;function halo(g, t) {  const R = 11 + 0.7 * Math.sin(TAU * 2 * t) + 0.4 * Math.sin(TAU * (3 * t + 0.4));  g.detached("halo", () => g.field({ x: [LX - 12, W - 1], y: [12, 38] }, (x, y) => {    const r = Math.hypot(x - FC.x, (y - FC.y) * 0.85);    if (r > R) return null;    if (!dith(x, y, 0.55 * Math.pow(1 - r / R, 1.1))) return null;    return r < R * 0.7 ? "fl1" : "fl0";  }));}// light through the cage on the ground: an elliptical pool with radial bar shadows and a dark spot under the basefunction groundLight(g, t) {  const C = { x: LX - 1, y: 57 }, RX = 9.5, RY = 4.4;  const breathe = 1 + 0.06 * Math.sin(TAU * 2 * t);  g.detached("groundLight", () => g.field({ x: [LX - 10, W - 1], y: [51, 63] }, (x, y) => {    const nx = (x + 0.5 - C.x) / (RX * breathe), ny = (y + 0.5 - C.y) / (RY * breathe);    const r = Math.sqrt(nx * nx + ny * ny);    if (r > 1 || r < 0.2) return null;                                        // dark spot under the lantern base    const ang = Math.atan2(ny, nx);    if (r > 0.3 && Math.abs(Math.sin(3 * ang + 0.5)) < 0.26) return null;      // bar shadows radiate out    const dens = r < 0.5 ? 0.7 : r < 0.75 ? 0.45 : 0.22;    if (!dith(x, y, dens)) return null;    return r < 0.4 ? "fl2" : r < 0.7 ? "fl1" : "fl0";  }));}function motes(g, t, mode) {  g.detached("motes", () => {    if (mode === "fire") {                                     // one ember lifts off the candle      const p = (t * 2) % 1;      if (p < 0.55) g.pixel(LX + Math.round(Math.sin(TAU * p) * 2), 20 - Math.round(p * 16), p < 0.3 ? "fl3" : "fl2");      return;    }    for (let i = 0; i < 6; i++) {      const p = (t + i / 6) % 1;      const x = Math.round(LX + (i % 2 ? 1 : -1) * (2 + p * 5) * Math.sin(TAU * (p * 0.9 + i * 0.23)) + (i - 2.5) * 1.1);      const y = Math.round(31 - p * 30);      if (y < 0) continue;      if (p < 0.45) { g.pixel(x, y, "fl3"); g.pixel(x, y + 1, "fl2"); }      else if (p < 0.75) { g.pixel(x, y, "fl2"); g.pixel(x, y + 1, "fl0"); }      else if (chk(x, Math.floor(t * 12))) g.pixel(x, y, "fl1");    }  });}const SoulLantern = sprite(pal, (g, t, mode) => {  t = ((t % 1) + 1) % 1;  contactShadow(g, PX, GROUND, 7, 1);  g.depth(2, () => groundLight(g, t));  g.outlined("ink", () => {    post(g);    crook(g);    footing(g);    lantern(g, t);  });  flameShape(g, t, mode);  g.depth(2, () => halo(g, t));  motes(g, t, mode);});submit(SoulLantern, {  size: [W, H], origin: "top-left", kind: "icon", scale: 1,  animate: { fps: 10, duration: 1.2, loop: true },  anchor: { x: PX, y: GROUND },  args: ["soul"],  variants: [    { name: "soul", repalette: swap("fl", "soul") },    { name: "fire", args: ["fire"] },    { name: "witch", args: ["witch"], repalette: swap("fl", "witch") },  ],});

掲載されたコードでは、ビルドスクリプトが自動付与する共通部分を省略しています。具体的には 1 行のランタイムヘッダー、共有パレット、および汎用ヘルパー関数(contactShadow、litIndex、flame、hash、chk、TAU)です。表示されている行番号は実際のファイルと一致しています。これらのプログラムは AIエージェントが生成し、描画されたコマの仕上がりを確認しながら修正を加えたものです。

エージェントに、こうしたプログラムを書かせる。

Tessarune は描画 API のリファレンス、25 冊の制作マニュアル、結果を確かめるための描画エンジンをエージェントに渡します。