Tessarune
Graveyard & Chapel · open_grave.js

掘りかけの墓穴

64×48 · JavaScript 244 行

64×48 の床置きの小物で、踏み固めた土の縁、根の見える土の層の奥の壁、真っ暗な底を持つ掘りたての四角い穴と、その横の土の山があります。山のてっぺんには鋤が刺さり、柄に小さな鉄のランタンが下がっています。idle(12 コマ)は山から穴へ土がこぼれ落ち、ランタンの炎が暖かく揺れます。dig(10 コマの一回再生)は鋤がひとりでに抜けて穴を掘り、土の塊を山へ放り、また刺さって、idle の最初のコマで終わります。

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

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

open_grave.js · 1–7
// Open grave — 64x48 floor prop, top-down 3/4, key light top-left.// A freshly dug rectangular pit (trampled earth rim, a far wall of cut soil strata with roots, black depth),// the spoil heap beside it with a spade stuck in its crown, and a small iron lantern hung on the spade's grip.// Variants://   idle  12f loop  loose soil trickles off the heap into the pit, the lantern flame flickers warm//   dig   10f once  the spade pulls itself out, bites into the pit, flings a clod onto the heap and plants//                   itself again; ends on idle frame 0

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

open_grave.js · 235–244
submit(OpenGrave, {  size: [W, H], origin: "top-left", kind: "icon", scale: 1,  animate: { fps: 10, duration: 1.2, loop: true },  anchor: { x: CX, y: GROUND },  args: ["idle"],  variants: [    { name: "idle" },    { name: "dig", args: ["dig"], animate: { fps: 10, duration: 1.0, loop: false } },  ],});
プログラム全体を表示 (244 行)
open_grave.js · 244 行
// Open grave — 64x48 floor prop, top-down 3/4, key light top-left.// A freshly dug rectangular pit (trampled earth rim, a far wall of cut soil strata with roots, black depth),// the spoil heap beside it with a spade stuck in its crown, and a small iron lantern hung on the spade's grip.// Variants://   idle  12f loop  loose soil trickles off the heap into the pit, the lantern flame flickers warm//   dig   10f once  the spade pulls itself out, bites into the pit, flings a clod onto the heap and plants//                   itself again; ends on idle frame 0const W = 64, H = 48, CX = 32, GROUND = 45;const PIT = { x0: 5, x1: 31, y0: 20, y1: 36 };      // the openingconst WALL = 11;                                     // far wall height seen in the openingconst RIM = 2;                                       // trampled earth band around the opening// spoil heap: overlapping lumps [cx, cy, rx, ry, light bias]const LUMPS = [[47, 35, 12, 7, 0.12], [42, 30, 6, 4, 0.1], [49, 28, 6, 5, 0.1], [55, 32, 5, 4, 0], [39, 36, 5, 4, 0.05], [53, 38, 7, 4, 0]];const REST = { x: 48, y: 29, a: -23 };               // spade: blade tip position and handle angle (deg) when plantedconst DIRT = ["w0", "w1", "w2", "w3"];const inOpen = (x, y) => x >= PIT.x0 && x <= PIT.x1 && y >= PIT.y0 && y <= PIT.y1;const inRimBand = (x, y) => {  if (inOpen(x, y)) return false;  const dx = Math.max(PIT.x0 - RIM - x, x - PIT.x1 - RIM, 0), dy = Math.max(PIT.y0 - RIM - y, y - PIT.y1 - RIM - 1, 0);  if (dx > 0 || dy > 0) return false;  // clip the corners so the band reads as dug earth, not a picture frame  const cx = x < PIT.x0 ? PIT.x0 - x : x > PIT.x1 ? x - PIT.x1 : 0, cy = y < PIT.y0 ? PIT.y0 - y : y > PIT.y1 ? y - PIT.y1 : 0;  if (cx + cy > RIM + 1) return false;  // a ragged outer edge: some columns / rows lose their outermost pixel  if ((y === PIT.y0 - RIM || y === PIT.y1 + RIM + 1) && Math.sin(x * 1.7) > 0.55) return false;  if ((x === PIT.x0 - RIM || x === PIT.x1 + RIM) && Math.sin(y * 2.1) > 0.6) return false;  return true;};// ---- pit ------------------------------------------------------------------------------------------------------function pitTone(x, y) {  const d = y - PIT.y0;                                // depth down the far wall  const sh = x - PIT.x0 < 2 + d * 0.45 ? 1 : 0;        // the left rim throws a shadow down and to the right  if (d < WALL) {    // cut soil strata with wavy seams: dark topsoil, a paler clay band, then earth fading into the dark    const b = d + Math.round(Math.sin(x * 0.7) * 0.6 + Math.sin(x * 0.23 + 1) * 0.8);    let i = b < 2 ? 1 : b < 4 ? 2 : b < 6 ? 1 : b < 8 ? 0 : -1;    i -= sh;    if (i >= 0) return DIRT[i];    return d >= WALL - 2 ? "ink" : "n0";  }  return "ink";                                        // the bottom of the grave}function rimTone(x, y) {  const inner = inOpen(x - 1, y) || inOpen(x + 1, y) || inOpen(x, y - 1) || inOpen(x, y + 1);  const near = y > PIT.y1, far = y < PIT.y0, left = x < PIT.x0;  if (inner && far) return "w3";                       // the far lip catches the light  if (inner && left) return "w3";  if (inner && near) return "w1";                      // near lip: we look over its dark edge  if (far || left) return "w2";  if (near && y === PIT.y1 + RIM + 1) return "w0";  return "w1";}function pit(g) {  g.part("rim", () => g.field({ x: [PIT.x0 - RIM - 1, PIT.x1 + RIM + 1], y: [PIT.y0 - RIM - 1, PIT.y1 + RIM + 1] }, (x, y) =>    inRimBand(x, y) ? rimTone(x, y) : null));  g.part("pit", () => g.field({ x: [PIT.x0, PIT.x1], y: [PIT.y0, PIT.y1] }, (x, y) => pitTone(x, y)));}function pitDetail(g) {  g.part("roots", () => {    // pale roots hanging out of the far wall, stones in the strata, clods on the bottom    for (const [x, y, n] of [[12, 21, 3], [21, 21, 2], [27, 22, 3]]) for (let i = 0; i < n; i++) g.pixel(x + (i === n - 1 ? 1 : 0), y + i, i === 0 ? "w4" : "w3");    for (const [x, y] of [[16, 23], [24, 24], [29, 26]]) { g.pixel(x, y, "n3"); g.pixel(x + 1, y, "n1"); }    for (const [x, y] of [[14, 34], [22, 33], [27, 35]]) g.pixel(x, y, "n0");    // vertical bites of the spade in the far wall: a shadowed stroke with a lit edge beside it    for (const x of [10, 18, 25]) { g.line([x, PIT.y0 + 3], [x, PIT.y0 + 6], "w0"); g.pixel(x + 1, PIT.y0 + 3, "w3"); }  });  g.part("turf", () => {    // torn grass on the outer edge of the rim    for (const [x, y, k] of [[4, 18, "e1"], [5, 17, "e2"], [9, 17, "e1"], [10, 17, "e2"], [18, 17, "e1"], [25, 17, "e1"], [26, 17, "e2"],      [2, 25, "e1"], [2, 26, "e0"], [2, 32, "e1"], [8, 39, "e0"], [9, 39, "e1"], [20, 39, "e0"], [29, 39, "e1"]]) g.pixel(x, y, k);  });}// ---- spoil heap -----------------------------------------------------------------------------------------------function heap(g) {  g.part("heap", () => g.field({ x: [26, 62], y: [20, 43] }, (x, y) => {    // a spoil heap built from overlapping lumps, each lit as an ellipsoid; clods and stones added as marks    let i = -1;    for (const [cx, cy, rx, ry, b] of LUMPS) i = Math.max(i, litIndex(x, y, cx, cy, rx, ry, 4, b));    if (i < 0) return null;    return DIRT[i];  }));  g.part("clods", () => {    for (const [x, y] of [[41, 29], [53, 27], [44, 37], [56, 34], [49, 40], [37, 36]]) { g.pixel(x, y, "w3"); g.pixel(x + 1, y, "w1"); g.pixel(x, y + 1, "w1"); }    for (const [x, y] of [[46, 33], [58, 31], [41, 40]]) { g.pixel(x, y, "n3"); g.pixel(x + 1, y, "n1"); }    g.pixel(51, 38, "w0"); g.pixel(52, 38, "w0");  });}function hole(g, open) {  // the slot the spade leaves in the crown when it is pulled out  if (!open) return;  g.part("slot", () => { g.line([REST.x - 2, REST.y - 2], [REST.x + 2, REST.y - 2], "w0"); g.line([REST.x - 1, REST.y - 1], [REST.x + 1, REST.y - 1], "ink"); g.pixel(REST.x - 2, REST.y - 1, "w3"); });}function trickle(g, t) {  // three grains roll down the heap's left flank and drop into the pit  const P = [[39, 30], [37, 31], [36, 32], [35, 33], [33, 33], [32, 34], [31, 35]];  g.detached("trickle", () => {    for (let k = 0; k < 3; k++) {      const p = (t + k / 3) % 1;      if (p > 0.85) continue;      const s = p / 0.85 * (P.length - 1), i = Math.floor(s);      const [x, y] = P[i];      g.pixel(x, y, i < 3 ? "w3" : "w2");      if (i < 2 && k === 0) g.pixel(x + 1, y, "w1");    }  });}// ---- spade and lantern ----------------------------------------------------------------------------------------------const rot = (px, py, a) => { const c = Math.cos(a * Math.PI / 180), s = Math.sin(a * Math.PI / 180); return [px * c - py * s, px * s + py * c]; };function spadeLocal(g, loaded) {  // local frame: blade tip at the origin, handle up the -y axis  g.part("blade", () => {    g.fillPoly([[-2, -7], [2, -7], [2, -2], [1, 0], [-1, 0], [-2, -2]], "n2");    g.line([-2, -7], [-2, -2], "n4");                   // lit left edge    g.line([2, -7], [2, -2], "n1");    g.pixel(-1, -6, "n3"); g.pixel(1, -1, "n1");    g.line([-1, -8], [1, -8], "n1"); g.pixel(-1, -8, "n3");   // socket    if (loaded) { g.rect({ x: [-2, 2], y: [-5, -3] }, "w2", { fill: "w2" }); g.pixel(-1, -5, "w3"); g.pixel(2, -3, "w0"); g.pixel(1, -6, "w1"); }  });  g.part("shaft", () => {    g.line([0, -9], [0, -21], "w3");    g.line([1, -9], [1, -21], "w1");    g.pixel(0, -14, "w4"); g.pixel(1, -17, "w0");    // T-grip: a short crossbar, lit on its left end    g.line([0, -22], [1, -22], "w2");    g.rect({ x: [-2, 3], y: [-24, -23] }, "w2", { fill: "w2" });    g.line([-2, -24], [1, -24], "w3"); g.pixel(-2, -23, "w3"); g.line([2, -23], [3, -23], "w1");  });}function lanternAt(g, lx, ly, sw, t) {  // hook, cap, glass with flame, base; sw = swing offset of the body (px)  const flick = [0, 1, 0, 2, 1, 0, 1, 2, 0, 1, 0, 1][Math.floor(((t % 1) + 1) % 1 * 12) % 12];  const bx = lx + sw;  g.outlined("ink", () => g.part("lantern", () => {    g.line([lx, ly], [lx, ly + 1], "n2");    g.pixel(bx, ly + 2, "n3");    g.line([bx - 1, ly + 3], [bx + 1, ly + 3], "n3"); g.pixel(bx + 1, ly + 3, "n1");    g.line([bx - 2, ly + 4], [bx + 2, ly + 4], "n2"); g.pixel(bx - 2, ly + 4, "n4");    g.rect({ x: [bx - 1, bx + 1], y: [ly + 5, ly + 8] }, "r3", { fill: "r3" });    g.line([bx - 2, ly + 5], [bx - 2, ly + 8], "n2"); g.line([bx + 2, ly + 5], [bx + 2, ly + 8], "n1");    g.pixel(bx, ly + 6, "g3"); g.pixel(bx, ly + 7, flick === 2 ? "g3" : "g2");    if (flick === 1) g.pixel(bx, ly + 5, "g2");    if (flick === 2) g.pixel(bx - 1, ly + 8, "r2");    g.pixel(bx - 1, ly + 5, "g3");                       // glint on the glass    g.line([bx - 2, ly + 9], [bx + 2, ly + 9], "n1"); g.pixel(bx - 2, ly + 9, "n3");  }));  g.detached("glow", () => {    // a few warm sparks of halo beside the glass, breathing with the flicker    const k = flick === 0 ? "r1" : "r2";    g.pixel(bx - 4, ly + 6, k); g.pixel(bx + 4, ly + 7, k);    if (flick === 2) { g.pixel(bx - 4, ly + 8, "r1"); g.pixel(bx + 4, ly + 5, "r1"); }  });}function spade(g, S, t) {  const { x, y, a } = S;  g.outlined("ink", () => g.at(x, y, () => g.rotate(a, () => spadeLocal(g, S.loaded))));  const [hx, hy] = rot(-2, -22, a);  lanternAt(g, Math.round(x + hx), Math.round(y + hy) + 1, S.sw, t);}function collar(g) {  // a little cone of soil heaped round the planted blade (covers its lower half)  g.part("collar", () => g.field({ x: [REST.x - 5, REST.x + 5], y: [REST.y - 4, REST.y + 2] }, (x, y) => {    const i = litIndex(x, y, REST.x + 0.5, REST.y - 0.5, 4.5, 3, 4, 0.15);    return i < 0 ? null : DIRT[i];  }));}// ---- dig -------------------------------------------------------------------------------------------------------------const DIG = 10;// per frame: blade tip x, y, handle angle, loaded, lantern swingconst KEYS = [  null,  [47, 25, -25, 0, 1],     // pulls itself out  [40, 29, -45, 0, 2],     // tips over the pit  [31, 32, -58, 0, 1],     // bites into the pit  [33, 27, -50, 1, -1],    // lifts a load  [42, 23, -32, 1, -2],    // swings back towards the heap  [51, 22, -42, 0, -1],    // flings (the clod leaves the blade)  [50, 26, -30, 0, 1],     // comes down  [48, 28, -24, 0, 0],     // plants itself  null,];function stateAt(state, t) {  t = ((t % 1) + 1) % 1;  if (state === "dig") {    const f = Math.min(DIG - 1, Math.round(t * DIG));    if (f === 0 || f === DIG - 1) return stateAt("idle", 0);    const [x, y, a, loaded, sw] = KEYS[f];    return { x, y, a, loaded: !!loaded, sw, f, t: f / DIG, planted: f === 8 };  }  return { x: REST.x, y: REST.y, a: REST.a, loaded: false, sw: 0, f: -1, t, planted: true };}function flung(g, f) {  // the clod arcs onto the heap's crown and breaks  const path = { 6: [[54, 17, 3]], 7: [[56, 21, 3]], 8: [[55, 27, 2], [58, 29, 1], [52, 28, 1]] }[f];  if (!path) return;  g.outlined("ink", () => g.part("clod", () => {    for (const [x, y, s] of path) {      if (s >= 2) { g.rect({ x: [x, x + s - 1], y: [y, y + 1] }, "w1", { fill: "w1" }); g.pixel(x, y, "w3"); g.pixel(x + s - 1, y + 1, "w0"); }      else g.pixel(x, y, "w2");    }  }));  if (f === 8) g.detached("puff", () => { for (const [x, y] of [[53, 25], [58, 26], [60, 28], [51, 28]]) g.pixel(x, y, "n3"); });}function pitSpray(g, f) {  // the blade's bite throws a few crumbs inside the pit  if (f !== 3 && f !== 4) return;  g.detached("spray", () => {    const pts = f === 3 ? [[27, 28], [24, 27], [29, 26]] : [[26, 25], [23, 23], [30, 22]];    for (const [x, y] of pts) g.pixel(x, y, "w2");  });}const OpenGrave = sprite(pal, (g, t, state) => {  const S = stateAt(state, t);  const f = S.f;  contactShadow(g, CX, GROUND - 1, 30, 2);  g.outlined("ink", () => { pit(g); heap(g); });  pitDetail(g);  hole(g, !S.planted);  spade(g, S, S.t);  if (S.planted) collar(g);                  // soil over the buried half of the blade  trickle(g, f < 0 ? S.t : 0);  if (f > 0) { flung(g, f); pitSpray(g, f); }  if (S.planted) g.part("lampLight", () => {    // the lantern's warm light on the heap's shoulder below it    const on = Math.floor(S.t * 12) % 3 !== 1;    for (const [x, y] of [[40, 27], [42, 26], [38, 29]]) g.pixel(x, y, on ? "w4" : "w3");  });});submit(OpenGrave, {  size: [W, H], origin: "top-left", kind: "icon", scale: 1,  animate: { fps: 10, duration: 1.2, loop: true },  anchor: { x: CX, y: GROUND },  args: ["idle"],  variants: [    { name: "idle" },    { name: "dig", args: ["dig"], animate: { fps: 10, duration: 1.0, loop: false } },  ],});

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

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

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