Tessarune
Dungeon Mechanisms · clay_pot.js

壺

32×32 · JavaScript 418 行

32×32 の壊せる素焼きの壺で、描いた帯にクリーム色の点が並んでいます。中が空洞の一つの立体の模型から描くので、置いた壺も、叩かれたときの姿勢も、ぎざぎざに割れた底も同じ形から出てきます。idle(8 コマ)は光の当たる肩を光沢が滑り、hit(6 コマの一回再生)はつぶれて底を軸に揺れます。break(10 コマの一回再生)はひびが入り、6 つの破片と 3 枚の硬貨が弧を描いて飛び、土ぼこりが立ちます。broken(8 コマ)は床の底と破片と硬貨で、硬貨が光ります。破片は壺そのものの画素を、まっすぐな線で切り分けたものです。

壺 — 描画結果6×
このプログラムで描画(変種「break」) · 32×32 · 10 コマ
Dungeon Mechanisms動くスプライト素材: Dungeon Mechanisms(itch.io)Vol.2・罠と扉と壊れる物 12 点

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

clay_pot.js · 1–12
// Clay pot (breakable terracotta urn) — 32x32, top-down 3/4, key light top-left.// The urn is a hollow solid of revolution, ray-marched per pixel (camera elevation: ellipse ratio 0.35, as in// Vol.1), so the same model gives the resting pot, every squash/rock pose of the hit, and the jagged broken base// (a height cut around the axis, open so the dark inside shows). Tones come from the surface normal through a// terracotta ramp (w0 w1 w2 r2 + r3 sheen); the painted band and its cream dots are countable marks.// Shards are the SAME pixels as the intact pot, cut along straight Voronoi edges, so what flies is visibly the pot;// once a shard is off the pot it is re-lit as a chunk (lit top-left edge, dark lower edge) so it reads on the floor.// Variants://   idle    loop 8f      — a sheen glint slides down the lit shoulder//   hit     one-shot 6f  — squash on impact, rocks right/left on its foot, settles on idle frame 0//   break   one-shot 10f — cracks, bursts into 6 shards + 3 coins that arc out and land; dust puff; ends on broken f0//   broken  loop 8f      — base, shards and coins on the floor; a coin glints

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

clay_pot.js · 409–418
submit(Pot, {  size: [W, H], origin: "top-left", kind: "icon", scale: 1,  anchor: { x: CX, y: GROUND },  variants: [    { name: "idle", args: ["idle"], animate: { fps: 10, duration: N_IDLE / 10, loop: true } },    { name: "hit", args: ["hit"], animate: { fps: 10, duration: N_HIT / 10, loop: false } },    { name: "break", args: ["break"], animate: { fps: 10, duration: N_BREAK / 10, loop: false } },    { name: "broken", args: ["broken"], animate: { fps: 10, duration: N_BROKEN / 10, loop: true } },  ],});
プログラム全体を表示 (418 行)
clay_pot.js · 418 行
// Clay pot (breakable terracotta urn) — 32x32, top-down 3/4, key light top-left.// The urn is a hollow solid of revolution, ray-marched per pixel (camera elevation: ellipse ratio 0.35, as in// Vol.1), so the same model gives the resting pot, every squash/rock pose of the hit, and the jagged broken base// (a height cut around the axis, open so the dark inside shows). Tones come from the surface normal through a// terracotta ramp (w0 w1 w2 r2 + r3 sheen); the painted band and its cream dots are countable marks.// Shards are the SAME pixels as the intact pot, cut along straight Voronoi edges, so what flies is visibly the pot;// once a shard is off the pot it is re-lit as a chunk (lit top-left edge, dark lower edge) so it reads on the floor.// Variants://   idle    loop 8f      — a sheen glint slides down the lit shoulder//   hit     one-shot 6f  — squash on impact, rocks right/left on its foot, settles on idle frame 0//   break   one-shot 10f — cracks, bursts into 6 shards + 3 coins that arc out and land; dust puff; ends on broken f0//   broken  loop 8f      — base, shards and coins on the floor; a coin glintsconst W = 32, H = 32, CX = 16, REFY = 27.6, GROUND = 29;const SE = 0.35, CE = Math.sqrt(1 - SE * SE);          // camera elevation (sin, cos)const HTOP = 20.6, WALL = 1.1, FLOOR = 1.2;const LIGHT = (() => { const v = [-0.55, 0.5, 0.68], m = Math.hypot(...v); return v.map((c) => c / m); })();   // view space: right, up, toward cameraconst N_IDLE = 8, N_HIT = 6, N_BREAK = 10, N_BROKEN = 8;const TER = ["w0", "w1", "w2", "r2", "r3"];// ---- profile: radius along the height (foot, belly, shoulder, neck, flared lip)const PROF = [[0, 4.4], [0.8, 5.0], [2.5, 6.6], [5, 8.0], [7.5, 8.5], [10, 8.1], [12.5, 6.9], [14.5, 5.3], [16.2, 3.9],  [17.4, 3.5], [18.6, 3.9], [19.6, 5.0], [20.6, 5.4]];const wallAt = (h) => WALL + Math.max(0, h - 18.8) * 1.3;   // the lip thickens into a flat rolled rimconst RT = [];for (let i = 0; i <= 420; i++) {  const h = i * 0.05;  let r = PROF[PROF.length - 1][1];  for (let k = 1; k < PROF.length; k++) {    const [h0, r0] = PROF[k - 1], [h1, r1] = PROF[k];    if (h <= h1) { const u = (h - h0) / (h1 - h0), s = u * u * (3 - 2 * u); r = r0 + (r1 - r0) * (0.5 * u + 0.5 * s); break; }  }  RT.push(r);}const R = (h) => RT[Math.max(0, Math.min(420, Math.round(h * 20)))];const dR = (h) => (R(h + 0.4) - R(h - 0.4)) / 0.8;// jagged break line of the base (height as a function of the angle around the axis; 0 = facing the camera)const CUT = (th) => {  const k = ((th / TAU) * 6 + 10.15) % 1;                 // 6 teeth around  return 3.1 + (k < 0.5 ? k : 1 - k) * 4.2;};// ---- ray march: world (x right, h up, z toward camera) -> object via the posefunction toObj(x, h, z, T) {  const x1 = x - T.dx - T.piv, h1 = h - T.hop;  const c = Math.cos(T.ang), s = Math.sin(T.ang);  const xo = x1 * c - h1 * s + T.piv, ho = x1 * s + h1 * c;  return [xo / T.sx, ho / T.sy, z / T.sx];}function nToView(n, T) {  let a = n[0] / T.sx, b = n[1] / T.sy, c3 = n[2] / T.sx;  const m = Math.hypot(a, b, c3); a /= m; b /= m; c3 /= m;  const c = Math.cos(T.ang), s = Math.sin(T.ang);  const wx = a * c + b * s, wh = -a * s + b * c;  return [wx, wh * CE - c3 * SE, wh * SE + c3 * CE];}function solid(x, h, z, cut) {  if (h < 0 || h > HTOP) return null;  const rho = Math.hypot(x, z), r = R(h);  if (rho > r) return null;  const rin = r - wallAt(h);  if (h > FLOOR && rho < rin) return null;  const th = Math.atan2(x, z);  if (cut && h > CUT(th)) return null;  // which surface did the ray enter through: the nearest one  let s = "out", best = r - rho;  if (HTOP - h < best) { s = "top"; best = HTOP - h; }  if (h > FLOOR) { if (rho - rin < best) { s = "in"; best = rho - rin; } }  else if (rho < rin && FLOOR - h < best) { s = "floor"; best = FLOOR - h; }  if (cut && CUT(th) - h < best) { s = "cut"; best = CUT(th) - h; }  return { s, rho, th, h, x, z };}function cast(T, cut) {  const buf = new Array(W * H).fill(null);  for (let py = 0; py < H; py++) for (let px = 0; px < W; px++) {    const X = px + 0.5 - CX, Y = py + 0.5 - REFY;    if (Math.abs(X - T.dx) > 11) continue;    for (let d = 16; d >= -16; d -= 0.15) {      const wh = -Y * CE + d * SE, wz = Y * SE + d * CE;      const [xo, ho, zo] = toObj(X, wh, wz, T);      const hit = solid(xo, ho, zo, cut);      if (hit) { buf[py * W + px] = hit; break; }    }  }  return tone(buf, T, cut);}// ---- tones.  Each pixel keeps its ramp level (lv) and material (m) so shards can be re-lit later.function level(d) { return d > 0.93 ? 4 : d > 0.56 ? 3 : d > 0.2 ? 2 : d > -0.15 ? 1 : 0; }function bandKey(lv) { return lv >= 3 ? "r1" : lv >= 1 ? "r0" : "w0"; }function tone(buf, T, cutMode) {  const out = new Array(W * H).fill(null);  for (let i = 0; i < W * H; i++) {    const b = buf[i]; if (!b) continue;    const px = i % W, py = (i - px) / W;    const up = py > 0 ? buf[i - W] : null;    if (b.s === "out") {      const n = nToView([b.x / (b.rho || 1), -dR(b.h), b.z / (b.rho || 1)], T);      const d = n[0] * LIGHT[0] + n[1] * LIGHT[1] + n[2] * LIGHT[2];      let lv = level(d);      if (b.h < 1.3 && lv > 0) lv -= 1;                                   // the foot sits in its own shadow      if (lv === 4 && !(b.h > 6.5 && b.h < 11)) lv = 3;                    // sheen only on the belly      if (!up && lv >= 2 && lv < 4) lv += 1;                               // the top silhouette catches the light      if (b.h > 11.6 && b.h < 13.9) {                                      // painted band (dark slip)        let key = bandKey(lv), m = "band";        const a = ((b.th / (TAU / 12)) % 1 + 1) % 1;                        // a cream dot every 30 degrees        if (Math.abs(b.h - 12.75) < 0.6 && Math.abs(a - 0.5) < 0.17 && d > -0.3) { key = lv >= 2 ? "w4" : "w3"; m = "dot"; }        out[i] = { key, part: "band", m, lv, h: b.h, th: b.th, ext: true };      } else out[i] = { key: TER[lv], part: "pot", m: "clay", lv, h: b.h, th: b.th, ext: true };    } else if (b.s === "top") {      // the rolled rim: bright on the lit (left / far) side, mid on the right      const lv = b.x - b.z * 0.6 < -1 ? 4 : b.x < 2.2 ? 3 : 2;      out[i] = { key: TER[lv], part: "pot", m: "clay", lv, h: b.h, th: b.th, ext: true, rim: true };    } else if (b.s === "cut") {      out[i] = { key: b.x < 2 ? "w3" : "w2", part: "pot", m: "cut", lv: 3, h: b.h, th: b.th, ext: true };    } else if (b.s === "in") {      // inner wall: the far side faces the camera; its right half catches the light over the rim      out[i] = { key: b.x > 0.5 && b.z < 0 ? "w1" : "w0", part: "inside", m: "in", lv: 0, h: b.h, th: b.th, ext: false };    } else {      out[i] = { key: "w0", part: "inside", m: "in", lv: 0, h: b.h, th: b.th, ext: false };    }  }  if (cutMode) {    // the broken bowl sits lower in the light: lift its outer wall one step, then draw the broken edge as one    // pale jagged line (fresh clay): outer wall pixels with nothing (or the inside) above them, and the far wall's top    const snap = out.map((o) => (o ? { part: o.part } : null));    for (let i = 0; i < W * H; i++) {      const o = out[i]; if (!o) continue;      if (o.m === "clay" && o.h > 0.9) { o.lv = Math.min(3, o.lv + 2); o.key = TER[o.lv]; }      if (i < W) continue;      const a = snap[i - W], x = i % W;      if (o.part !== "inside") {        if (!a || a.part === "inside") { o.key = x < CX - 1 ? "w4" : x < CX + 4 ? "w3" : "w2"; o.m = "cut"; o.part = "pot"; }      } else if (!a) { o.key = x < CX ? "w3" : "w2"; o.part = "pot"; o.m = "cut"; }    }  }  return out;}// ---- compositing: my own occupancy buffer mirrors what the renderer holds, so overlapping chunks get an ink seamfunction markOcc(occ, idx) {  occ[idx] = 1;  const x = idx % W, y = (idx - x) / W;  for (const [nx, ny] of [[x + 1, y], [x - 1, y], [x, y + 1], [x, y - 1]]) if (nx >= 0 && ny >= 0 && nx < W && ny < H) occ[ny * W + nx] = 1;}function emitLayer(g, occ, name, pix, detached) {  // pix: Map idx -> key  const seam = [];  for (const idx of pix.keys()) {    const x = idx % W, y = (idx - x) / W;    for (const [nx, ny] of [[x + 1, y], [x - 1, y], [x, y + 1], [x, y - 1]]) {      if (nx < 0 || ny < 0 || nx >= W || ny >= H) continue;      const k = ny * W + nx;      if (!pix.has(k) && occ[k]) seam.push(k);    }  }  for (const k of seam) pix.set(k, "ink");  let x0 = W, x1 = -1, y0 = H, y1 = -1;  for (const idx of pix.keys()) { const x = idx % W, y = (idx - x) / W; x0 = Math.min(x0, x); x1 = Math.max(x1, x); y0 = Math.min(y0, y); y1 = Math.max(y1, y); }  if (x1 < 0) return;  const body = () => g.outlined("ink", () => g.field({ x: [x0, x1], y: [y0, y1] }, (x, y) => pix.get(y * W + x) ?? null));  if (detached) g.detached(name, body); else g.part(name, body);  for (const idx of pix.keys()) markOcc(occ, idx);}function drawPot(g, occ, buf, over) {  // intact pot (or the base): one outlined body, emitted per part so the report lists them  let x0 = W, x1 = -1, y0 = H, y1 = -1;  for (let i = 0; i < W * H; i++) if (buf[i]) { const x = i % W, y = (i - x) / W; x0 = Math.min(x0, x); x1 = Math.max(x1, x); y0 = Math.min(y0, y); y1 = Math.max(y1, y); }  const keyAt = (i) => (over && over.has(i) ? over.get(i) : buf[i].key);  g.outlined("ink", () => {    for (const p of ["pot", "band", "inside"]) {      if (!buf.some((b) => b && b.part === p)) continue;      g.part(p, () => g.field({ x: [x0, x1], y: [y0, y1] }, (x, y) => {        const b = buf[y * W + x];        return b && b.part === p ? keyAt(y * W + x) : null;      }));    }  });  for (let i = 0; i < W * H; i++) if (buf[i]) markOcc(occ, i);}// ---- poses (cached)const REST = { sx: 1, sy: 1, ang: 0, piv: 0, hop: 0, dx: 0 };const CACHE = {};const intact = () => CACHE.intact || (CACHE.intact = cast(REST, false));const baseBuf = () => CACHE.base || (CACHE.base = cast(REST, true));const HIT_KEYS = [  // sx, sy, ang(deg), pivot side, dx  [1.1, 0.9, 0, 0, 0],  [0.95, 1.06, 9, 1, 1],  [1, 1, -6, -1, 0],  [1, 1, 3.5, 1, 0],  [1.02, 0.98, -1.5, -1, 0],  [1, 1, 0, 0, 0],];function hitPose(f) {  const key = "hit" + f;  if (CACHE[key]) return CACHE[key];  const [sx, sy, deg, side, dx] = HIT_KEYS[f];  if (f === N_HIT - 1) return (CACHE[key] = intact());  return (CACHE[key] = cast({ sx, sy, ang: (deg * Math.PI) / 180, piv: side * 4.4, hop: 0, dx }, false));}// ---- shards: exterior pixels above the break line, split by straight Voronoi edgesconst SEEDS = [[16, 10], [16, 21], [10, 19], [22, 19]];function shards() {  if (CACHE.shards) return CACHE.shards;  const buf = intact();  const list = SEEDS.map(() => []);  for (let i = 0; i < W * H; i++) {    const b = buf[i]; if (!b || !b.ext || b.h <= CUT(b.th) + 0.15) continue;    const x = i % W, y = (i - x) / W;    let best = 0, bd = 1e9;    SEEDS.forEach(([sx, sy], k) => { const d = (x - sx) ** 2 + ((y - sy) * 1.15) ** 2; if (d < bd) { bd = d; best = k; } });    list[best].push([x, y, b]);  }  const out = list.map((px) => {    let cx = 0, cy = 0;    for (const [x, y] of px) { cx += x + 0.5; cy += y + 0.5; }    cx /= px.length; cy /= px.length;    const m = new Map(px.map(([x, y, b]) => [`${x},${y}`, b]));    return { px, m, cx, cy };  });  return (CACHE.shards = out);}// crack pixels for the impact frame: pot pixels whose right or lower neighbour belongs to another shardfunction crackMap() {  if (CACHE.cracks) return CACHE.cracks;  const owner = new Map();  shards().forEach((s, k) => { for (const [x, y] of s.px) owner.set(y * W + x, k); });  const m = new Map();  for (const [i, k] of owner) {    const x = i % W;    const r = owner.get(i + 1), d = owner.get(i + W);    if ((x < W - 1 && r !== undefined && r !== k) || (d !== undefined && d !== k)) m.set(i, "w0");  }  return (CACHE.cracks = m);}// where each shard lands: [x, y, spin (deg), landing frame, peak (px), behind the base?, flat (y scale at rest)]const LAND = [  [8, 17, -12, 6, 6, true, 0.8],       // neck + shoulder with the lip and painted band: back left  [25, 18, 85, 6, 6, true, 0.8],       // belly front: back right  [5, 28, -6, 5, 3, false, 0.55],       // belly left: front left  [27, 28, 6, 5, 3, false, 0.55],       // belly right: front right];const COINS = [  // start x (mouth), rest x, rest y, landing frame, peak  [15, 16, 30, 6, 7],                 // rolls out in front  [17, 17, 26, 8, 6],                 // falls back into the broken bowl (the one that glints)];// re-light a shard as a chunk: top/left edge lit, bottom/right edge dark, interior keeps its pot tone (clamped)function chunkKey(b, pix, x, y, lit) {  const has = (dx, dy) => pix.has((y + dy) * W + (x + dx));  let lv = Math.max(1, Math.min(3, b.lv + lit));  if (!has(0, -1) || !has(-1, 0)) lv = Math.min(4, lv + 1);  else if (!has(0, 1) || !has(1, 0)) lv = Math.max(0, lv - 1);  if (b.m === "band") return bandKey(lv);  if (b.m === "dot") return lv >= 2 ? "w4" : "w3";  if (b.m === "cut") return lv >= 3 ? "w3" : "w2";  return TER[lv];}function shardPixels(s, tx, ty, deg, lit, sy) {  const a = (deg * Math.PI) / 180, c = Math.cos(a), sn = Math.sin(a);  const src = new Map();  const r = 8, bx = Math.round(tx), by = Math.round(ty);  for (let y = by - r; y <= by + r; y++) for (let x = bx - r; x <= bx + r; x++) {    if (x < 0 || y < 0 || x >= W || y >= H) continue;    const dx = x + 0.5 - tx, dy = (y + 0.5 - ty) / (sy || 1);    const lx = dx * c + dy * sn, ly = -dx * sn + dy * c;    const b = s.m.get(`${Math.floor(lx + s.cx)},${Math.floor(ly + s.cy)}`);    if (b) src.set(y * W + x, b);  }  if (lit === null) { const m = new Map(); for (const [k, b] of src) m.set(k, b.key); return m; }  const pix = new Map();  for (const [k, b] of src) { const x = k % W, y = (k - x) / W; pix.set(k, chunkKey(b, src, x, y, lit)); }  return pix;}function shardState(k, f) {  const s = shards()[k], [rx, ry, spin, lf, peak, , flat] = LAND[k];  if (f <= 0) return { x: s.cx, y: s.cy, a: 0, lit: null, sy: 1 };  const u = Math.min(1, (f - 1) / (lf - 1)), e = 1 - (1 - u) * (1 - u);  // burst: the first moving frame pushes every piece out from the pot's centre  const ox = s.cx - 16, oy = s.cy - 17, ol = Math.hypot(ox, oy) || 1;  const x0 = s.cx + (ox / ol) * 2, y0 = s.cy + (oy / ol) * 1.5;  let x = x0 + (rx - x0) * e, y = y0 + (ry - y0) * u - peak * 4 * u * (1 - u);  if (f === lf + 1) y -= 1;                               // one small bounce after landing  return { x, y, a: spin * e, lit: f === 1 ? null : 0, sy: 1 + (flat - 1) * u };}function coin(g, occ, name, x, y, face) {  // face: 0 = lying (3x2), 1 = spinning face-on (3x3 disc), 2 = edge-on (1x3)  const pix = new Map();  const put = (px, py, k) => { if (px >= 0 && py >= 0 && px < W && py < H) pix.set(py * W + px, k); };  if (face === 0) { put(x - 1, y, "g2"); put(x, y, "g3"); put(x + 1, y, "g2"); put(x - 1, y + 1, "g1"); put(x, y + 1, "g1"); put(x + 1, y + 1, "g0"); }  else if (face === 1) { put(x, y - 1, "g2"); put(x - 1, y, "g3"); put(x, y, "g2"); put(x + 1, y, "g1"); put(x, y + 1, "g0"); }  else { put(x, y - 1, "g3"); put(x, y, "g2"); put(x, y + 1, "g1"); }  emitLayer(g, occ, name, pix, true);}function star(g, x, y, size) {  g.detached("glint", () => {    if (size === 0) { g.pixel(x, y, "g3"); return; }    g.pixel(x, y, "n5");    for (const [dx, dy] of [[1, 0], [-1, 0], [0, 1], [0, -1]]) g.pixel(x + dx, y + dy, "g3");  });}function dust(g, f) {  // two soft dust clouds roll out from the foot: each is three round clumps, a small n4 core in a checker of n3,  // drifting outward and up, thinning to a sparse n2 dither and gone  const k = f - 1;  if (k < 0 || k > 4) return;  const CLUMPS = [[0, 0, 2.0, 0], [-2.6, 0.7, 1.5, 1], [2.3, -1.0, 1.4, 1]];   // outward, down, radius, dither parity  g.detached("dust", () => {    const seen = new Set();    for (const s of [-1, 1]) for (const [ox, oy, r0, par] of CLUMPS) {      const r = r0 + k * 0.4, cx = CX + s * (10.5 + k * 1.7 + ox), cy = GROUND - 1.2 - k * 0.8 + oy;      for (let py = Math.floor(cy - r - 1); py <= cy + r + 1; py++) for (let px = Math.floor(cx - r - 1); px <= cx + r + 1; px++) {        const d = Math.hypot(px + 0.5 - cx, (py + 0.5 - cy) * 1.1) / r;        if (d > 1 || px < 0 || px >= W || py < 0 || py >= H || seen.has(py * W + px)) continue;        const on = ((px + py + par) & 1) === 0;        let key = null;        if (k === 0) key = d < 0.5 ? "n4" : on ? "n3" : null;        else if (k === 1) key = d < 0.35 ? "n4" : on ? (d < 0.7 ? "n3" : "n2") : null;        else if (k === 2) key = on ? (d < 0.55 ? "n3" : "n2") : null;        else if (k === 3) key = on && (px + 2 * py) % 3 !== 0 ? "n2" : null;        else key = on && (px + 2 * py) % 4 === 0 ? "n2" : null;        if (key) { seen.add(py * W + px); g.pixel(px, py, key); }      }    }  });}// sheen glint: slides down the highlight on the lit belly over idle frames 2..5function glintPath() {  if (CACHE.glint) return CACHE.glint;  const buf = intact(), hl = [];  for (let i = 0; i < W * H; i++) if (buf[i] && buf[i].lv === 4 && buf[i].part === "pot") hl.push(i);  hl.sort((a, b) => a - b);  const pick = (u) => hl[Math.min(hl.length - 1, Math.round(u * (hl.length - 1)))];  return (CACHE.glint = [null, null, pick(0), pick(0.35), pick(0.7), pick(1), null, null]);}function drawShard(g, occ, k, st) {  emitLayer(g, occ, "shard" + k, shardPixels(shards()[k], st.x, st.y, st.a, st.lit, st.sy), true);}function drawBroken(g, occ, f) {  const rest = (k) => ({ x: LAND[k][0], y: LAND[k][1], a: LAND[k][2], lit: 0, sy: LAND[k][6] });  const order = LAND.map((l, k) => k).sort((a, b) => LAND[a][1] - LAND[b][1]);  for (const k of order) if (LAND[k][5]) drawShard(g, occ, k, rest(k));        // behind the base  drawPot(g, occ, baseBuf());  for (const k of order) if (!LAND[k][5]) drawShard(g, occ, k, rest(k));  COINS.forEach(([, x, y], i) => coin(g, occ, "coin" + i, x, y, 0));  if (f !== null) {    const [x, y] = [COINS[1][1], COINS[1][2]];    if (f === 3 || f === 5) star(g, x, y - 1, 0);    if (f === 4) star(g, x, y - 1, 1);  }}const Pot = sprite(pal, (g, t, mode) => {  const occ = new Uint8Array(W * H);  const fb = Math.min(N_BREAK - 1, Math.round(t * N_BREAK));  // the shadow widens as the pieces spread over the floor  contactShadow(g, CX, GROUND, mode === "broken" || (mode === "break" && fb >= 5) ? 14 : mode === "break" && fb >= 2 ? 12 : 10, 2);  if (mode === "idle") {    const f = Math.round(t * N_IDLE) % N_IDLE;    const over = new Map(), gp = glintPath()[f];    if (gp !== null && gp !== undefined) { over.set(gp, "n5"); over.set(gp - W, "r3"); over.set(gp + W, "r3"); }    drawPot(g, occ, intact(), over);    return;  }  if (mode === "hit") {    const f = Math.min(N_HIT - 1, Math.round(t * N_HIT));    drawPot(g, occ, hitPose(f));    if (f === 0) star(g, 27, 15, 1);    if (f === 1) g.detached("chips", () => { g.pixel(28, 13, "n4"); g.pixel(29, 17, "n4"); g.pixel(27, 11, "n3"); });    return;  }  if (mode === "break") {    const f = Math.min(N_BREAK - 1, Math.round(t * N_BREAK));    if (f === N_BREAK - 1) { drawBroken(g, occ, 0); return; }    if (f === 0) { drawPot(g, occ, cast({ sx: 1.08, sy: 0.93, ang: 0, piv: 0, hop: 0, dx: 0 }, false), crackMap()); star(g, 27, 15, 1); return; }    const S = shards().map((s, k) => ({ k, st: shardState(k, f) }));    S.sort((a, b) => a.st.y - b.st.y);    const behind = (k) => LAND[k][5] && f >= LAND[k][3];    for (const { k, st } of S) if (behind(k)) drawShard(g, occ, k, st);    drawPot(g, occ, baseBuf());    for (const { k, st } of S) if (!behind(k)) drawShard(g, occ, k, st);    COINS.forEach(([sx, rx, ry, lf, peak], i) => {      const u = Math.min(1, (f - 1) / (lf - 1));      const x = Math.round(sx + (rx - sx) * u), y = Math.round(12 + (ry - 12) * u - peak * 4 * u * (1 - u));      coin(g, occ, "coin" + i, x, y, u >= 1 ? 0 : (f + i) % 2 ? 1 : 2);    });    dust(g, f);    return;  }  drawBroken(g, occ, Math.round(t * N_BROKEN) % N_BROKEN);});submit(Pot, {  size: [W, H], origin: "top-left", kind: "icon", scale: 1,  anchor: { x: CX, y: GROUND },  variants: [    { name: "idle", args: ["idle"], animate: { fps: 10, duration: N_IDLE / 10, loop: true } },    { name: "hit", args: ["hit"], animate: { fps: 10, duration: N_HIT / 10, loop: false } },    { name: "break", args: ["break"], animate: { fps: 10, duration: N_BREAK / 10, loop: false } },    { name: "broken", args: ["broken"], animate: { fps: 10, duration: N_BROKEN / 10, loop: true } },  ],});

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

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

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