Tessarune
Graveyard & Chapel · cemetery_gate.js

墓地の門

64×48 · JavaScript 262 行

64×48 の床置きの小物で、正面を向いています。縁取りのある笠石と石の壺を載せた切石の門柱 2 本の間に、槍の穂先の鉄棒が渦巻きのアーチまで伸びる鍛鉄の両開きの扉があります。扉は細い鉄なので、すき間を埋めてしまう輪郭線は使わず、鉄棒を一本ずつ明暗の対で描き、明るい床でも暗い床でも見えるようにしています。closed(12 コマ)は錆びた鎖と南京錠が揺れ、枯れ葉が転がります。open(10 コマの一回再生)は錠が落ち、鎖が滑り落ちて、扉が内側へ開きます。open-idle(10 コマ)は鎖が地面に残ったままです。

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

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

cemetery_gate.js · 1–10
// Cemetery gate — 64x48 floor prop, top-down 3/4, key light top-left. Camera-facing.// Two ashlar piers with moulded caps carry stone urns; between them hangs a pair of wrought-iron gate leaves:// spear-tipped bars rising to a scroll arch, three rails, C-scrolls in the lunette. The leaves are thin iron, so// they are NOT ink-outlined (an outline would fill the gaps between bars): every bar is a lit/shadow pair instead// (n3 left, n0 right), which reads on light and dark floors alike.// Variants://   closed     12f loop  a rusty chain and padlock across the meeting bars sway; dead leaves skitter along the base//   open       10f once  the padlock drops, the chain slithers off, the leaves swing inward (overshoot, settle);//                        ends on open-idle frame 0//   open-idle  10f loop  gate open, the chain and padlock lie on the ground, dead leaves skitter through

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

cemetery_gate.js · 252–262
submit(Gate, {  size: [W, H], origin: "top-left", kind: "icon", scale: 1,  animate: { fps: 10, duration: N_IDLE / 10, loop: true },  anchor: { x: CX, y: GROUND },  args: ["closed"],  variants: [    { name: "closed", args: ["closed"] },    { name: "open", args: ["open"], animate: { fps: 10, duration: N_OPEN / 10, loop: false } },    { name: "open-idle", args: ["open-idle"], animate: { fps: 10, duration: N_OI / 10, loop: true } },  ],});
プログラム全体を表示 (262 行)
cemetery_gate.js · 262 行
// Cemetery gate — 64x48 floor prop, top-down 3/4, key light top-left. Camera-facing.// Two ashlar piers with moulded caps carry stone urns; between them hangs a pair of wrought-iron gate leaves:// spear-tipped bars rising to a scroll arch, three rails, C-scrolls in the lunette. The leaves are thin iron, so// they are NOT ink-outlined (an outline would fill the gaps between bars): every bar is a lit/shadow pair instead// (n3 left, n0 right), which reads on light and dark floors alike.// Variants://   closed     12f loop  a rusty chain and padlock across the meeting bars sway; dead leaves skitter along the base//   open       10f once  the padlock drops, the chain slithers off, the leaves swing inward (overshoot, settle);//                        ends on open-idle frame 0//   open-idle  10f loop  gate open, the chain and padlock lie on the ground, dead leaves skitter throughconst W = 64, H = 48, CX = 32, GROUND = 45;const PIERS = [3, 52];                     // left x of each 9px pier faceconst LL = 12, LR = 51, LEAF = 20;         // gate span: left leaf LL..LL+19, right leaf LR-19..LRconst RAILS = [22, 31, 40];                // rail rows (2px each: lit top, dark bottom)const BARS = [0, 4, 8, 12, 16];            // bar offsets from the hinge (2px each; 0 = hinge stile, 16 = meeting bar)const K = 0.3;                             // screen rise per px of depthconst N_IDLE = 12, N_OPEN = 10, N_OI = 10, REST = 58;const OPEN_TH = [0, 0, 3, 11, 24, 38, 50, 59, 63, REST];const STONE = ["n0", "n1", "n2", "n3", "n4"];// ---- piers and urns -------------------------------------------------------------------------------------------function pierTone(px, x, y) {  const u = x - px;                                                     // 0..8 on the face  if (y >= 12 && y <= 15) {                                             // cap: wider by one px each side    if (u < -1 || u > 9) return null;    if (y === 12) return u === -1 ? "n4" : u === 9 ? "n3" : "n4";       // top face    if (y === 13) return u === 9 ? "n2" : "n3";    if (y === 14) return u === -1 ? "n3" : u === 9 ? "n1" : "n2";    return u < 0 || u > 8 ? null : "n0";                                // shadow under the cap  }  if (y >= 41 && y <= 44) {                                             // plinth    if (u < -1 || u > 9) return null;    if (y === 41) return u === 9 ? "n2" : "n4";    if (y === 44) return u === 9 ? "n0" : "n1";    return u === -1 ? "n3" : u === 9 ? "n1" : "n2";  }  if (y < 16 || y > 40 || u < 0 || u > 8) return null;  if (y === 16) return "n1";  const c = Math.floor((y - 17) / 5), k = (y - 17) % 5;  if (k === 4) return "n1";                                             // mortar  if ((c & 1) && u === 4) return "n1";                                  // staggered joint  let i = u === 0 ? 4 : u === 8 ? 2 : 3;  if (k === 0 && u < 8) i = Math.min(4, i + 1);  if (hash(c, px) < 0.35 && u > 0 && u < 8 && k > 0) i -= 1;           // a darker block  if (u === 7 && k > 0) i = 2;  return STONE[i];}function urnTone(px, x, y) {  const cx = px + 4.5;  if (y === 11) return x >= px + 1 && x <= px + 7 ? (x === px + 1 ? "n4" : x === px + 7 ? "n1" : "n2") : null;  // foot slab  if (y === 10) return x >= px + 3 && x <= px + 5 ? (x === px + 3 ? "n3" : "n1") : null;                          // stem  if (y === 9) return x >= px + 2 && x <= px + 6 ? (x === px + 2 ? "n3" : x === px + 6 ? "n1" : "n2") : null;  if (y >= 5 && y <= 8) {                                                // bowl    const i = litIndex(x, y, cx, 6.6, 4.7, 2.9, 4, 0.1);    if (i < 0) return null;    if (y === 7 && x > px && x < px + 8) return i >= 2 ? "n4" : i === 1 ? "n2" : "n1";   // carved gadroon band    return STONE[Math.min(4, i + 1)];  }  if (y === 4) return x >= px + 2 && x <= px + 6 ? (x === px + 2 ? "n2" : "n0") : null;                           // shadow under the lip  if (y === 3) return x >= px && x <= px + 8 ? (x <= px + 1 ? "n4" : x >= px + 7 ? "n2" : "n3") : null;           // flared lip  if (y === 2) return x >= px + 2 && x <= px + 6 ? (x <= px + 3 ? "n4" : x === px + 6 ? "n2" : "n3") : null;       // lid  if (y === 1) return x >= px + 4 && x <= px + 4 ? "n4" : null;                                                    // knob  return null;}function piers(g) {  for (const px of PIERS) {    g.part(px < CX ? "pierL" : "pierR", () => g.field({ x: [px - 1, px + 9], y: [12, 44] }, (x, y) => pierTone(px, x, y)));    g.part(px < CX ? "urnL" : "urnR", () => g.field({ x: [px, px + 9], y: [1, 11] }, (x, y) => urnTone(px, x, y)));  }}function pierDetail(g) {  g.part("moss", () => {    for (const [x, y, k] of [[3, 40, "e1"], [4, 40, "e2"], [3, 39, "e1"], [5, 40, "e1"], [2, 43, "e1"], [3, 44, "e2"],      [60, 40, "e1"], [59, 40, "e1"], [60, 39, "e2"], [61, 44, "e1"], [4, 12, "e2"], [5, 12, "e1"], [54, 12, "e1"],      [3, 7, "e1"], [4, 8, "e2"], [59, 5, "e1"]]) g.pixel(x, y, k);    g.pixel(56, 25, "n1"); g.pixel(57, 26, "n1"); g.pixel(57, 27, "n0"); g.pixel(56, 28, "n1");   // crack    g.pixel(8, 33, "n2"); g.pixel(6, 20, "n4");                                                  // chips  });  g.part("hinges", () => {    for (const y of [23, 38]) { g.pixel(12, y, "n2"); g.pixel(51, y, "n1"); }  });}// ---- gate ---------------------------------------------------------------------------------------------------------const topY = (x) => Math.round(18 - 8 * (1 - ((x + 0.5 - CX) / 20.5) ** 2));   // bar tops follow the arch// leaf-local pixel list: [u, y, key] with u = distance from the hinge (0..19); built oncefunction leafPixels(left) {  const P = [];  const sx = (u) => (left ? LL + u : LR - u);  const add = (u, y, k) => P.push([u, y, k]);  // bars: lit pixel on the screen-left, dark on the screen-right  for (const b of BARS) for (const d of [0, 1]) {    const u = left ? b + d : b + 1 - d;                                  // keep the lit pixel on the screen-left    const x = sx(u), t = topY(x), heavy = b === 0 || b === 16;    for (let y = t; y <= 41; y++) add(u, y, d === 0 ? (heavy ? "n3" : "n2") : (heavy ? "n1" : "n0"));    if (d === 0) add(u, t - 1, "n3");  }  // spear tips  for (const b of BARS) {    const uL = left ? b : b + 1, uR = left ? b + 1 : b;                  // screen-left / screen-right pixel of the bar    const t = topY(sx(uL));    add(uL, t - 2, "n4"); add(uR, t - 2, "n2");    add(uL, t - 3, "n4");    const out = left ? uL - 1 : uL + 1, outR = left ? uR + 1 : uR - 1;   // barbs either side    if (out >= 0 && out < LEAF) add(out, t - 1, "n2");    if (outR >= 0 && outR < LEAF) add(outR, t - 1, "n0");  }  // rails and the arched top rail  for (const r of RAILS) for (let u = 0; u <= 17; u++) { add(u, r, u % 4 === 1 ? "n2" : "n3"); add(u, r + 1, "n0"); }  for (let u = 0; u <= 17; u++) { const t = topY(sx(u)) + 2; add(u, t, "n3"); add(u, t + 1, "n1"); }  // C-scrolls in the lunette between the arch rail and the top rail (two per leaf), and a band of rings low down  const scroll = (uc, yc, r) => {    for (let a = 0; a < 16; a++) {      const ang = a / 16 * TAU;      if (a >= 10 && a <= 13) continue;                                  // open side of the C      const u = Math.round(uc + Math.cos(ang) * r), y = Math.round(yc + Math.sin(ang) * r);      add(left ? u : u, y, Math.sin(ang) < -0.3 || Math.cos(ang) < -0.5 ? "n3" : "n1");    }  };  scroll(14.5, 17.5, 1.5);  for (const [uc, ry] of [[2.5, 36], [6.5, 36], [10.5, 36], [14.5, 36], [2.5, 27], [6.5, 27], [10.5, 27], [14.5, 27]]) for (const [du, dy, k] of [[-1, 0, "n2"], [0, -1, "n3"], [1, 0, "n0"], [0, 1, "n0"]])    add(Math.round(uc + du + 0.01), ry + dy, k);  return P;}const LEAF_PX = { L: leafPixels(true), R: leafPixels(false) };function gateMap(thL, thR) {  // project both leaves: a leaf turned by th keeps its hinge; each u moves to hinge + u cos th and rises u sin th K  const M = new Map();  for (const [left, th] of [[true, thL], [false, thR]]) {    const a = th * Math.PI / 180, c = Math.cos(a), s = Math.sin(a);    const dark = th > 70 ? 1 : 0;    for (const [u, y, k] of LEAF_PX[left ? "L" : "R"]) {      const x = left ? LL + Math.round(u * c) : LR - Math.round(u * c);      const yy = y - Math.round(u * s * K);      if (x < 0 || x >= W || yy < 0) continue;      const key = dark && k !== "n0" ? STONE[Math.max(0, STONE.indexOf(k) - 1)] : k;      const id = x + "," + yy, prev = M.get(id);      // where turned bars bunch up, keep the lighter pixel (the nearer, lit edge wins)      if (!prev || STONE.indexOf(key) > STONE.indexOf(prev)) M.set(id, key);    }  }  return M;}function gate(g, thL, thR) {  const M = gateMap(thL, thR);  g.part("gate", () => g.field({ x: [LL - 1, LR + 1], y: [0, 44] }, (x, y) => M.get(x + "," + y) ?? null));}// ---- chain, padlock, leaves --------------------------------------------------------------------------------------function chain(g, sway) {  // links wrapped round both meeting bars, a drooping loop and a padlock hanging from it  const dx = Math.round(sway * 1.4);  g.part("chain", () => {    const links = [[28, 33], [29, 33], [30, 34], [31, 34], [32, 34], [33, 34], [34, 33], [35, 33]];    links.forEach(([x, y], i) => g.pixel(x, y, i & 1 ? "w1" : "w3"));    const drop = [[29, 34], [30, 35], [31 + Math.round(sway * 0.6), 36], [32 + Math.round(sway * 0.6), 36], [33, 35], [34, 34]];    drop.forEach(([x, y], i) => g.pixel(x, y, i & 1 ? "w3" : "w1"));  });  g.outlined("ink", () => g.part("padlock", () => padlock(g, 31 + dx, 37)));}function padlock(g, x, y) {  // iron shackle over a squat iron body scabbed with rust, keyhole  g.pixel(x, y, "n4"); g.pixel(x + 1, y, "n3");  g.pixel(x - 1, y + 1, "n4"); g.pixel(x + 2, y + 1, "n2");  g.rect({ x: [x - 1, x + 2], y: [y + 2, y + 5] }, "n2", { fill: "n2" });  g.line([x - 1, y + 2], [x + 1, y + 2], "n4"); g.pixel(x + 2, y + 2, "n3");  g.pixel(x - 1, y + 3, "n3"); g.line([x - 1, y + 5], [x + 2, y + 5], "n1");  g.pixel(x + 2, y + 3, "w1"); g.pixel(x - 1, y + 4, "w2"); g.pixel(x + 2, y + 4, "w0");   // rust  g.pixel(x, y + 3, "ink"); g.pixel(x, y + 4, "ink");                                      // keyhole}function lockOnGround(g, x) {  // padlock on its side in the dirt, chain coiled beside it  g.outlined("ink", () => g.part("padlock", () => {    g.rect({ x: [x, x + 3], y: [42, 44] }, "n2", { fill: "n2" });    g.line([x, 42], [x + 3, 42], "n4"); g.pixel(x + 3, 44, "n0"); g.pixel(x, 43, "w2"); g.pixel(x + 3, 43, "w1");    g.pixel(x + 4, 43, "n3"); g.pixel(x + 5, 43, "n2"); g.pixel(x + 4, 42, "n4"); g.pixel(x + 2, 43, "ink");  }));  g.part("chain", () => {    for (const [dx, dy, k] of [[-1, 44, "w2"], [-2, 43, "w1"], [-3, 44, "w2"], [-4, 44, "n2"], [-5, 43, "w2"], [-6, 44, "w1"], [-7, 44, "w2"], [6, 44, "w2"], [7, 44, "w1"]])      g.pixel(x + dx, dy, k);  });}function chainFalling(g, f) {  // f1: lock drops, chain slides down the bars · f2..: the lock lies on the ground  if (f === 1) {    g.part("chain", () => {      for (const [x, y, k] of [[29, 36], [30, 37], [31, 38], [32, 38], [33, 37], [34, 36], [28, 35], [35, 35]].map(([x, y], i) => [x, y, i & 1 ? "w1" : "w2"])) g.pixel(x, y, k);    });    g.outlined("ink", () => g.part("padlock", () => padlock(g, 31, 39 - 1)));  } else lockOnGround(g, 30);}function deadLeaves(g, t, closedGate) {  // two leaves blow across the base once per loop (hidden behind the pier plinths at each end), one flips in place  g.detached("leaves", () => {    for (const [ph, y0, dir, k1, k2] of [[0, 43, 1, "w3", "w2"], [0.5, 44, 1, "g1", "w2"]]) {      const p = (t + ph) % 1;      const x = Math.round(dir > 0 ? 8 + p * 48 : 56 - p * 48);      const hop = Math.round(Math.abs(Math.sin(p * Math.PI * 3)) * 2);      if (x < 12 || x > 49) continue;                                     // behind the plinths      const flip = Math.floor(p * 12) & 1;      const y = y0 - hop;      if (flip) { g.pixel(x, y, k1); g.pixel(x + 1, y, k1); g.pixel(x + 2, y, k2); g.pixel(x + 1, y - 1, k2); }      else { g.pixel(x, y, k2); g.pixel(x + 1, y - 1, k1); g.pixel(x, y - 1, k1); g.pixel(x + 1, y - 2, k2); }    }    // a leaf caught against the right plinth, lifting and settling    const q = Math.sin(TAU * t);    g.pixel(49, 44, "r1"); g.pixel(50, 44, "r0"); g.pixel(48, 44, q > 0.3 ? "r0" : "r1");    if (q > 0.3) { g.pixel(49, 43, "r1"); g.pixel(50, 42, "r2"); } else g.pixel(50, 43, "r1");    g.pixel(17, 44, "w2"); g.pixel(18, 44, "w1"); g.pixel(18, 43, "w3");       // a settled one    g.pixel(40, 44, "g1"); g.pixel(41, 44, "g0");  });}// ---- states -------------------------------------------------------------------------------------------------------function stateAt(state, t) {  t = ((t % 1) + 1) % 1;  if (state === "closed") {    const f = Math.round(t * N_IDLE) % N_IDLE, tt = f / N_IDLE;    return { th: 0, lock: "hang", sway: Math.sin(TAU * tt), lt: tt };  }  if (state === "open-idle") {    const f = Math.round(t * N_OI) % N_OI, tt = f / N_OI;    return { th: REST, lock: "ground", lt: tt };  }  const f = Math.min(N_OPEN - 1, Math.round(t * N_OPEN));  if (f === 0) return stateAt("closed", 0);  if (f === N_OPEN - 1) return stateAt("open-idle", 0);  return { th: OPEN_TH[f], thR: OPEN_TH[Math.max(0, f - 1)] * 0.4 + OPEN_TH[f] * 0.6, lock: "fall", f, lt: f / N_OI };}const Gate = sprite(pal, (g, t, state) => {  const S = stateAt(state, t);  contactShadow(g, CX, GROUND, 30, 2);  // gate shadow on the ground: a dithered band under the closed leaves, shrinking to the hinges as they open  const a = S.th * Math.PI / 180, reach = Math.round(LEAF * Math.cos(a));  g.part("gateShadow", () => {    for (let x = LL; x <= LR; x++) {      const inL = x - LL < reach, inR = LR - x < reach;      if ((inL || inR) && chk(x, 42)) g.pixel(x, 43, "ink");    }  });  gate(g, S.th, S.thR ?? S.th);  g.outlined("ink", () => piers(g));  pierDetail(g);  if (S.lock === "hang") chain(g, S.sway);  else if (S.lock === "fall") chainFalling(g, S.f);  else lockOnGround(g, 30);  deadLeaves(g, S.lt, S.th === 0);});submit(Gate, {  size: [W, H], origin: "top-left", kind: "icon", scale: 1,  animate: { fps: 10, duration: N_IDLE / 10, loop: true },  anchor: { x: CX, y: GROUND },  args: ["closed"],  variants: [    { name: "closed", args: ["closed"] },    { name: "open", args: ["open"], animate: { fps: 10, duration: N_OPEN / 10, loop: false } },    { name: "open-idle", args: ["open-idle"], animate: { fps: 10, duration: N_OI / 10, loop: true } },  ],});

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

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

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