Tessarune
Graveyard & Chapel · tombstone.js

墓石

48×48 · JavaScript 269 行

48×48 の床置きの小物で、草の生えた古い墓の盛り土の上に、輪十字の紋字と墓碑銘を刻み、肩と根元に苔の付いた、風化した丸い墓石が立っています。idle(16 コマ)は草の房が揺れ、淡い青の魂の火が墓石の後ろを通りながら周りを回ります。cracked(16 コマ)は墓石が割れて傾き、割れ目と紋字から紫の光が脈打ちながらにじみます。rise(10 コマの一回再生)は土が震えて盛り上がり、弾けて、魂の火が地面から昇り、idle の最初のコマで終わります。

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

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

tombstone.js · 1–8
// Tombstone — 48x48 floor prop, top-down 3/4, key light top-left.// A weathered round-topped headstone (you see its face, its lit top edge and its shaded right side) with an// engraved ring-cross rune and an epitaph, moss on the shoulder and the foot, over an old grassy grave mound.// Variants://   idle     16f loop  grass tufts sway, a faint soul-wisp (blue, unoutlined) circles the stone, passing behind it//   cracked  16f loop  the stone is split and leaning; violet light seeps from the crack and the rune, pulsing//   rise     10f once  the mound shivers, bulges, bursts (clods fly), settles; the wisp climbs out of the earth//                      and the last frame is idle frame 0

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

tombstone.js · 259–269
submit(Tombstone, {  size: [W, H], origin: "top-left", kind: "icon", scale: 1,  animate: { fps: 10, duration: 1.6, loop: true },  anchor: { x: CX, y: GROUND },  args: ["idle"],  variants: [    { name: "idle" },    { name: "cracked", args: ["cracked"] },    { name: "rise", args: ["rise"], animate: { fps: 10, duration: 1.0, loop: false } },  ],});
プログラム全体を表示 (269 行)
tombstone.js · 269 行
// Tombstone — 48x48 floor prop, top-down 3/4, key light top-left.// A weathered round-topped headstone (you see its face, its lit top edge and its shaded right side) with an// engraved ring-cross rune and an epitaph, moss on the shoulder and the foot, over an old grassy grave mound.// Variants://   idle     16f loop  grass tufts sway, a faint soul-wisp (blue, unoutlined) circles the stone, passing behind it//   cracked  16f loop  the stone is split and leaning; violet light seeps from the crack and the rune, pulsing//   rise     10f once  the mound shivers, bulges, bursts (clods fly), settles; the wisp climbs out of the earth//                      and the last frame is idle frame 0const W = 48, H = 48, CX = 24, GROUND = 45;const FX0 = 15, FX1 = 30, AY = 12.5, AR = 8, BASE = 34;   // stone face: columns, arch centre row, arch radius, footconst ACX = 23;                                           // arch centre x (pixel-centre coords)const DX = 2, DY = -2;                                    // stone thickness, seen up and to the rightconst MND = { cx: 24, cy: 39, rx: 14, ry: 5 };            // grave moundconst inFace = (x, y) => {  if (x < FX0 || x > FX1 || y > BASE) return false;  if (y >= AY) return true;  const dx = x + 0.5 - ACX, dy = y + 0.5 - AY;  return dx * dx + dy * dy <= AR * AR;};const inSlab = (x, y) => inFace(x, y) || inFace(x - 1, y + 1) || inFace(x - DX, y - DY);// ---- stone -------------------------------------------------------------------------------------------------// crack: a zigzag down the face (cracked variant); returns the crack column for a rowconst CRACK = [[27, 3], [28, 7], [27, 10], [28, 14], [27, 18], [25, 22], [26, 26], [24, 30], [25, 35]];function crackX(y) {  for (let i = 0; i + 1 < CRACK.length; i++) {    const [x0, y0] = CRACK[i], [x1, y1] = CRACK[i + 1];    if (y >= y0 && y <= y1) return Math.round(x0 + (x1 - x0) * (y - y0) / (y1 - y0));  }  return -99;}const RUNE = (() => {  // ring-cross carved into the face: a ring of radius 3 round (23,16), a shaft and a crossbar  const s = new Set();  for (let a = 0; a < 64; a++) {    const x = Math.round(22.5 + 3.4 * Math.cos(TAU * a / 64)), y = Math.round(15.5 + 3.4 * Math.sin(TAU * a / 64));    s.add(x + "," + y);  }  for (let y = 10; y <= 23; y++) s.add("23," + y);  for (let x = 18; x <= 27; x++) s.add(x + ",15");  for (const x of [20, 21, 22, 24, 26, 27]) s.add(x + ",26");            // epitaph: two lines of "words"  for (const x of [19, 20, 22, 23, 24, 26]) s.add(x + ",28");  return s;})();const isRune = (x, y) => RUNE.has(x + "," + y);function faceTone(x, y, cracked, pulse) {  const lEdge = !inFace(x - 1, y), tEdge = !inFace(x, y - 1), rEdge = !inFace(x + 1, y);  if (cracked) {    const c = crackX(y);    if (x === c) return pulse > 0.66 ? "v3" : pulse > 0.3 ? "v2" : "v1";   // the glowing split    if (x === c + 1) return "n1";                                         // right lip in the crack's shade    if (x === c - 1 && !lEdge) return pulse > 0.4 ? "v1" : "n2";           // light spilling onto the left lip  }  if (isRune(x, y)) {    if (cracked && y < 26) return pulse > 0.7 ? "v1" : "n0";    return y >= 26 ? "n1" : "n0";  }  if (!cracked && isRune(x - 1, y - 1) && y < 30) return "n3";            // the carving's lit lower-right wall  if (cracked && isRune(x - 1, y - 1) && y < 30) return "n3";  if (lEdge || (tEdge && x < ACX + 2)) return "n4";                       // lit arris on the top-left  if (rEdge) return "n1";  if (!inFace(x + 2, y) && chk(x, y)) return "n1";                        // turning away from the light  if (y >= BASE - 2) return "n1";                                         // damp foot  if (y >= BASE - 5) return (x & 1) === (y & 1) && y === BASE - 3 ? "n1" : "n2";  if (y < 9 || x <= FX0 + 1) return "n3";                                 // upper arch and left strip catch more light  return "n2";}function slabTone(x, y) {  // top edge (faces the sky, lit) and right side (turned away)  const dx = x + 0.5 - ACX - 1, dy = y + 0.5 - AY + 1;  if (dy < 0 && x <= FX1 + 1) return dx < -3 ? "n4" : dx < 3 ? "n3" : "n2";  return y > BASE - 4 ? "n0" : "n1";}function stone(g, cracked, pulse) {  g.part("stone", () => g.field({ x: [FX0 - 1, FX1 + DX + 1], y: [2, BASE] }, (x, y) => {    if (inFace(x, y)) return faceTone(x, y, cracked, pulse);    if (inSlab(x, y)) return slabTone(x, y);    return null;  }));  g.part("chips", () => {    // countable weathering: dents (dark top-left pixel, lit bottom-right pixel)    for (const [x, y] of [[17, 12], [28, 20], [19, 31], [27, 7]]) {      if (cracked && Math.abs(x - crackX(y)) < 2) continue;      g.pixel(x, y, "n1"); g.pixel(x + 1, y + 1, "n4");    }  });  g.part("moss", () => {    // on the lit shoulder, dripping down the left arris, and a band at the damp foot    for (const [x, y, k] of [[18, 5, "e1"], [19, 4, "e2"], [20, 4, "e2"], [21, 3, "e1"], [22, 3, "e1"], [17, 6, "e1"],      [16, 7, "e1"], [16, 8, "e0"], [15, 10, "e1"], [15, 11, "e1"], [15, 12, "e0"], [18, 6, "e0"], [20, 3, "e3"], [17, 5, "e2"]])      g.pixel(x, y, k);    for (let x = FX0; x <= FX1 + 1; x++) {      const h = Math.floor(hash(x, 5) * 4) - (x > 25 ? 1 : 0);      for (let i = 0; i < h; i++) g.pixel(x, BASE - i, i === h - 1 ? (x < 22 ? "e2" : "e1") : "e0");    }  });}// ---- grave mound -------------------------------------------------------------------------------------------------const DIRT = ["w0", "w1", "w2", "w3"];function mound(g, bulge) {  const { cx, cy, rx, ry } = MND;  const B = { cx: 24, cy: 39 - bulge, rx: 5 + bulge * 0.6, ry: 2.5 + bulge * 0.6 };  g.part("mound", () => g.field({ x: [cx - rx, cx + rx], y: [cy - ry - 5, cy + ry] }, (x, y) => {    if (bulge > 0) {      const bi = litIndex(x, y, B.cx, B.cy, B.rx, B.ry, 4, 0.05);      if (bi >= 0) return DIRT[bi];    }    const i = litIndex(x, y, cx, cy, rx, ry, 4, 0.1);    if (i < 0) return null;    // old grave: a grass crown with a wavy edge; bare earth on the flanks and the near face    const gx = (x + 0.5 - cx) / (rx - 2), gy = (y + 0.5 - (cy - 1.2)) / (ry - 1.3 + 0.6 * Math.sin(x * 1.3));    if (gx * gx + gy * gy <= 1) {      const gi = litIndex(x, y, cx - 1, cy - 1, rx - 2, ry - 1, 4, 0.05);      return ["e0", "e1", "e1", "e2"][Math.max(0, gi)];    }    return DIRT[i];  }));  g.part("blades", () => {    // countable marks on the crown: a few blade strokes and lit tips    for (const [x, y] of [[16, 37], [20, 36], [27, 36], [31, 38], [23, 40], [18, 40]]) { g.pixel(x, y, "e1"); g.pixel(x, y - 1, "e2"); }    for (const [x, y] of [[17, 36], [19, 37], [22, 35], [15, 38]]) g.pixel(x, y, "e3");  });  g.part("pebbles", () => {    for (const [x, y] of [[13, 41], [33, 40], [29, 43], [18, 43]]) { g.pixel(x, y, "n3"); g.pixel(x + 1, y, "n1"); }  });}function cracks(g, f) {  // shivering earth before the burst  if (f < 1 || f > 4) return;  g.part("fissures", () => {    const L = f === 1 ? [[[22, 39], [24, 38], [26, 39]]] :      [[[20, 38], [22, 37], [24, 38], [26, 37], [28, 38]], [[24, 38], [23, 40]]];    for (const path of L) for (let i = 0; i + 1 < path.length; i++) g.line(path[i], path[i + 1], "ink");  });}// ---- grass -------------------------------------------------------------------------------------------------------const TUFTS = [[12, 38, 0.0, 1], [14, 34, 0.3, 1], [33, 35, 0.55, 0], [36, 39, 0.8, 0], [20, 44, 0.15, 1], [30, 44, 0.65, 0], [10, 42, 0.4, 1]];function grass(g, t) {  g.part("grass", () => {    for (const [bx, by, ph, lit] of TUFTS) {      const sw = Math.round(Math.sin(TAU * (t + ph)) * 1.2);      for (const [dx, h, lean] of [[-1, 3, -1], [0, 4, 0], [1, 3, 1]]) {        const x0 = bx + dx, tip = [x0 + lean + sw, by - h];        g.line([x0, by], tip, "e1");        g.pixel(x0, by, "e0");        g.pixel(tip[0], tip[1], lit ? "e3" : "e2");      }    }  });}// ---- soul wisp -----------------------------------------------------------------------------------------------------const wispAt = (t) => [23 + 14 * Math.cos(TAU * t + 2.3), 21 + 6 * Math.sin(TAU * t + 2.3) - 2 * Math.sin(2 * TAU * t)];function wispBlob(g, x, y, tail, flick) {  // a soft glowing head (white core, blue body, dithered halo) and a tail of fading dots  x = Math.round(x); y = Math.round(y);  for (const [dx, dy] of [[-2, -1], [-1, -2], [3, 1], [1, 3], [-2, 2], [3, -1], [1, -2], [-1, 3], [0, -2], [-2, 0], [3, 0], [0, 3]])    if (chk(x + dx, y + dy + (flick ? 1 : 0))) g.pixel(x + dx, y + dy, "b1");  for (const [dx, dy] of [[0, -1], [1, -1], [-1, 0], [2, 0], [-1, 1], [2, 1], [0, 2], [1, 2]]) g.pixel(x + dx, y + dy, "b2");  g.pixel(x, y, "n5"); g.pixel(x + 1, y, flick ? "b3" : "n5"); g.pixel(x, y + 1, "b3"); g.pixel(x + 1, y + 1, "b2");  tail.forEach(([tx, ty], i) => g.pixel(Math.round(tx), Math.round(ty), i < 2 ? "b2" : "b1"));}function wisp(g, t) {  const [x, y] = wispAt(t);  const tail = [0.045, 0.07, 0.095, 0.12, 0.145].map((d) => wispAt(t - d)).map(([tx, ty], i) => [tx + 0.5, ty - 0.5 - i * 0.7]);  const behind = Math.sin(TAU * t + 2.3) < -0.15;  g.depth(behind ? 2 : -2, () => g.detached("wisp", () => wispBlob(g, x, y, tail, Math.sin(TAU * t * 4) > 0)));}// ---- rise -----------------------------------------------------------------------------------------------------------const RISE = 10;const BULGE = [0, 0, 1, 2, 3, 1, 1, 0, 0, 0];function clods(g, f) {  // [x0, vx, peak height, landing y]  const C = [[22, -7, 8, 38], [25, 7, 10, 39], [23, -2, 12, 36], [21, -11, 5, 42], [26, 10, 6, 42]];  if (f < 4 || f > 7) return;  const p = (f - 3) / 4;                                        // 0.25 .. 1  g.outlined("ink", () => g.part("clods", () => {    C.forEach(([x0, vx, hp, ly], i) => {      const x = Math.round(x0 + vx * p), y = Math.round(37 + (ly - 37) * p - hp * 4 * p * (1 - p));      if (f === 7) { g.pixel(x, y, "w2"); g.pixel(x + 1, y, "w1"); return; }   // landed crumbs      g.rect({ x: [x, x + 2], y: [y, y + 1] }, "w1", { fill: "w1" });      g.pixel(x, y, "w3"); g.pixel(x + 1, y, "w2"); g.pixel(x + 2, y + 1, "w0");      if (i < 3) { g.pixel(x + 1, y - 1, "w2"); g.pixel(x + 1, y + 2, "w0"); }      if (i === 1) g.pixel(x + 2, y - 1, "e1");                  // a clod still wearing its turf    });  }));  if (f <= 6) g.detached("dust", () => {    const r = [0, 0, 0, 0, 4, 7, 9][f];    for (let a = 0; a < 12; a++) {      const x = Math.round(24 + r * Math.cos(TAU * a / 12)), y = Math.round(38 + r * 0.45 * Math.sin(TAU * a / 12) - (f - 3));      if (chk(x, y) || f === 4) g.pixel(x, y, f === 6 ? "n2" : "n3");    }  });}function stateAt(state, t) {  t = ((t % 1) + 1) % 1;  if (state === "rise") {    const f = Math.min(RISE - 1, Math.round(t * RISE));    if (f === 0 || f === RISE - 1) return { state: "idle", t: 0, f: -1 };    return { state: "rise", t: 0, f };  }  return { state, t, f: -1 };}const Tombstone = sprite(pal, (g, t, state) => {  const S = stateAt(state, t);  const cracked = S.state === "cracked";  const pulse = 0.5 + 0.5 * Math.cos(TAU * S.t);  const f = S.f;  contactShadow(g, CX, GROUND - 1, 17, 2);  const shake = f === 2 || f === 4 ? 1 : f === 3 ? -1 : 0;  g.outlined("ink", () => {    if (cracked) g.at(23, BASE, () => g.rotate(8, () => g.at(-23, -BASE, () => stone(g, true, pulse))));    else g.at(shake, 0, () => stone(g, false, pulse));  });  g.outlined("ink", () => mound(g, f > 0 ? BULGE[f] : 0));  cracks(g, f);  if (f === 4 || f === 5) g.part("burst", () => {    // the earth tears open where the clods came from    g.line([22, 37 - BULGE[f]], [26, 37 - BULGE[f]], "ink"); g.line([23, 38 - BULGE[f]], [25, 38 - BULGE[f]], "w0");  });  grass(g, f > 0 ? 0 : S.t);  if (cracked) {    g.part("fallenChip", () => { g.pixel(33, 33, "n3"); g.pixel(34, 33, "n2"); g.pixel(33, 34, "n2"); g.pixel(34, 34, "n1"); });    g.detached("motes", () => {      for (let k = 0; k < 4; k++) {        const p = (S.t * 2 + k / 4) % 1;        const x = 28 + [-1, 1, 0, 2][k] + Math.round(Math.sin(TAU * (p + k * 0.3)) * 1.5);        const y = 12 + k * 5 - Math.round(p * 8);        if (p < 0.4) g.pixel(x, y, "v3"); else if (p < 0.8) g.pixel(x, y, "v2");      }    });    g.part("seep", () => {      // the glow reaches the ground at the stone's foot      const k = pulse > 0.5 ? "v2" : "v1";      g.pixel(24, 35, k); g.pixel(23, 36, "v1"); g.pixel(25, 36, "v1");      if (pulse > 0.66) g.pixel(24, 36, "v3");    });  } else if (f < 0) {    wisp(g, S.t);  }  if (f > 0) {    clods(g, f);    if (f >= 5) {      // the wisp climbs out of the burst point towards its idle frame-0 spot      const [ex, ey] = wispAt(0), k = (f - 4) / 5;      const x = 24 + (ex - 24) * k, y = 37 + (ey - 37) * k;      g.depth(-2, () => g.detached("wisp", () => wispBlob(g, x, y, [[x + (24 - ex) * 0.12, y + 2], [x + (24 - ex) * 0.2, y + 4]], true)));    }  }});submit(Tombstone, {  size: [W, H], origin: "top-left", kind: "icon", scale: 1,  animate: { fps: 10, duration: 1.6, loop: true },  anchor: { x: CX, y: GROUND },  args: ["idle"],  variants: [    { name: "idle" },    { name: "cracked", args: ["cracked"] },    { name: "rise", args: ["rise"], animate: { fps: 10, duration: 1.0, loop: false } },  ],});

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

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

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