Tessarune
Graveyard & Chapel · coffin.js

棺

48×48 · JavaScript 410 行

48×48 の床置きの小物で、縦長に置いた六角形の棺を凸形の立体の組み合わせとして作り、画素ごとに描いています。鉄の取っ手、紫の石をはめた真鍮の十字架が蓋にあり、内側にはボタン留めの暗いベルベットと白っぽい枕があります。蓋は剛体として動きます。closed(12 コマ)は静止したあと、内側から 2 回叩かれて蓋が跳ね、ほこりが舞います。open(10 コマの一回再生)は蓋が右へ滑り、傾いて落ち、側面に立てかかって止まります。open-idle(12 コマ)は内張りから紫の光が漏れ、光の粒が昇ります。

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

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

coffin.js · 1–10
// Coffin — 48x48 floor prop, top-down 3/4, key light top-left. Lies lengthwise, head end away from the camera.// A classic six-sided (toe-pincher) coffin modelled as convex solids and ray-cast per pixel along the oblique view// ray (screen y = BASE - z - K*depth): a hollow plank box with a base trim, a bevelled lid slab one step larger than// the box, iron bar handles on the long walls and a ring pull on the foot end, a brass cross with a violet stone// inlaid in the lid, and inside: dark velvet lining (r0 / v0), button tufts and a pale pillow.// The lid is a rigid body: it slides across the rim, tips over the right wall and comes to rest leaning against it.// Variants://   closed     loop 12f   — still, then two knocks from inside: the lid jolts up (the 2nd time askew), dust puffs//   open       once 10f   — a thump, the lid slides right, tips off and lands against the wall; ends on open-idle f0//   open-idle  loop 12f   — lid leaning on the floor, violet glow breathing out of the lining, motes rising

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

coffin.js · 401–410
submit(Coffin, {  size: [48, 48], origin: "top-left", kind: "icon", scale: 1,  animate: { fps: 10, duration: N_IDLE / 10, loop: true },  anchor: { x: 23, y: GROUND },  variants: [    { name: "closed", args: ["closed"] },    { name: "open", args: ["open"], animate: { fps: 10, duration: N_OPEN / 10, loop: false } },    { name: "open-idle", args: ["open-idle"] },  ],});
プログラム全体を表示 (410 行)
coffin.js · 410 行
// Coffin — 48x48 floor prop, top-down 3/4, key light top-left. Lies lengthwise, head end away from the camera.// A classic six-sided (toe-pincher) coffin modelled as convex solids and ray-cast per pixel along the oblique view// ray (screen y = BASE - z - K*depth): a hollow plank box with a base trim, a bevelled lid slab one step larger than// the box, iron bar handles on the long walls and a ring pull on the foot end, a brass cross with a violet stone// inlaid in the lid, and inside: dark velvet lining (r0 / v0), button tufts and a pale pillow.// The lid is a rigid body: it slides across the rim, tips over the right wall and comes to rest leaning against it.// Variants://   closed     loop 12f   — still, then two knocks from inside: the lid jolts up (the 2nd time askew), dust puffs//   open       once 10f   — a thump, the lid slides right, tips off and lands against the wall; ends on open-idle f0//   open-idle  loop 12f   — lid leaning on the floor, violet glow breathing out of the lining, motes risingconst CX = 22, GROUND = 44, K = 0.6;const BASE = GROUND - K * 18;                      // foot end (y = -18) sits on the ground lineconst HB = 7, LT = 2, T = 1.1, FLOOR = 1.4, RIM = 7.0;const N_IDLE = 12, N_OPEN = 10;const LV = norm3([-0.6, -0.25, 0.76]);             // toward the key light (x right, y away, z up)const WOOD = ["w0", "w1", "w2", "w3"], IRON = ["n0", "n1", "n2", "n3"], GOLD = ["g0", "g1", "g2", "g3"];function norm3(v) { const m = Math.hypot(v[0], v[1], v[2]); return [v[0] / m, v[1] / m, v[2] / m]; }const dotL = (n) => n[0] * LV[0] + n[1] * LV[1] + n[2] * LV[2];// ---- the hexagon (x across, y along; head at +y), counter-clockwiseconst hexPts = (hw, hs, hf, yh, ys, yf) => [[hf, yf], [hs, ys], [hw, yh], [-hw, yh], [-hs, ys], [-hf, yf]];const BOXHEX = hexPts(5.0, 8.0, 4.0, 18, 9, -18);const CAVHEX = hexPts(5.0 - T, 8.0 - T, 4.0 - T, 18 - T, 9, -18 + T);const LIDHEX = hexPts(5.8, 8.8, 4.8, 18.8, 9, -18.8);// half-width of the box's lower (shoulder -> foot) walls at yconst hwLow = (y) => 4.0 + ((y + 18) / 27) * 4;// convex prism as half-spaces [nx, ny, nz, d] (inside: n.p <= d); faces 0..5 = hex edges, 6 = bottom, 7 = topfunction prism(poly, z0, z1) {  const pl = [];  for (let i = 0; i < poly.length; i++) {    const a = poly[i], b = poly[(i + 1) % poly.length];    const ex = b[0] - a[0], ey = b[1] - a[1], m = Math.hypot(ex, ey);    const n = [ey / m, -ex / m, 0];    pl.push([n[0], n[1], 0, n[0] * a[0] + n[1] * a[1]]);  }  pl.push([0, 0, -1, -z0], [0, 0, 1, z1]);  return pl;}const boxPl = (x0, x1, y0, y1, z0, z1) => [[1, 0, 0, x1], [-1, 0, 0, -x0], [0, 1, 0, y1], [0, -1, 0, -y0], [0, 0, -1, -z0], [0, 0, 1, z1]];const BOX = prism(BOXHEX, 0, HB);const CAV = prism(CAVHEX, FLOOR, 99);const LID = prism(LIDHEX, 0, LT);const HANDLES = [];for (const s of [-1, 1]) for (const y of [-10, 2]) {  const w = hwLow(y);  HANDLES.push(boxPl(s < 0 ? -w - 1.3 : w - 0.2, s < 0 ? -w + 0.2 : w + 1.3, y - 2.2, y + 2.2, 2.9, 4.3));}const PILLOW = { c: [0, 13.2, FLOOR + 1.2], r: [3.2, 2.4, 1.5] };let TIN = 0, TOUT = 0, CIN = -1, COUT = -1;function convex(o, d, planes) {  let tin = -1e9, tout = 1e9, cin = -1, cout = -1;  for (let i = 0; i < planes.length; i++) {    const p = planes[i];    const den = p[0] * d[0] + p[1] * d[1] + p[2] * d[2];    const num = p[3] - (p[0] * o[0] + p[1] * o[1] + p[2] * o[2]);    if (den > -1e-9 && den < 1e-9) { if (num < 0) return false; continue; }    const t = num / den;    if (den < 0) { if (t > tin) { tin = t; cin = i; } } else if (t < tout) { tout = t; cout = i; }    if (tin > tout) return false;  }  TIN = tin; TOUT = tout; CIN = cin; COUT = cout;  return true;}function ellipsoid(o, d, e) {  const [cx, cy, cz] = e.c, [rx, ry, rz] = e.r;  const ox = (o[0] - cx) / rx, oy = (o[1] - cy) / ry, oz = (o[2] - cz) / rz;  const dx = d[0] / rx, dy = d[1] / ry, dz = d[2] / rz;  const A = dx * dx + dy * dy + dz * dz, B = 2 * (ox * dx + oy * dy + oz * dz), C = ox * ox + oy * oy + oz * oz - 1;  const disc = B * B - 4 * A * C;  if (disc < 0) return -1;  return (-B - Math.sqrt(disc)) / (2 * A);}// ---- lid pose: yaw about its centre, then pivot on its underside at local x = xc, which sits on the rim point// (RIM, ty, HB + lift); phi tips the right side down.function lidXform(ps) {  const cy = Math.cos(ps.yaw), sy = Math.sin(ps.yaw), cp = Math.cos(ps.phi), sp = Math.sin(ps.phi);  const rotN = (n) => { // local -> world (no translation)    const x1 = n[0] * cy - n[1] * sy, y1 = n[0] * sy + n[1] * cy, z1 = n[2];    return [x1 * cp + z1 * sp, y1, -x1 * sp + z1 * cp];  };  const invN = (v) => { // world -> local (no translation)    const x1 = v[0] * cp - v[2] * sp, z1 = v[0] * sp + v[2] * cp, y1 = v[1];    return [x1 * cy + y1 * sy, -x1 * sy + y1 * cy, z1];  };  const P = [RIM, ps.ty, HB + ps.lift];  const toL = (p) => { const q = invN([p[0] - P[0], p[1] - P[1], p[2] - P[2]]); q[0] += ps.xc * cy; q[1] += ps.xc * sy; return q; };  // (the pivot offset is applied before the yaw, so move it through the yaw too)  const toW = (q) => { const r = rotN([q[0] - ps.xc * cy, q[1] - ps.xc * sy, q[2]]); return [r[0] + P[0], r[1] + P[1], r[2] + P[2]]; };  return { rotN, invN, toL, toW };}const project = (p, dx) => [CX + p[0] - 0.5 + (dx || 0), BASE - p[2] - K * p[1] - 0.5, p[1] + 40];// ---- one frame: cast every pixelfunction castCoffin(ps, dx) {  const X = lidXform(ps);  const d = [0, 1, -K], dl = X.invN(d);  const buf = new Array(48 * 48).fill(null);  for (let py = 0; py < 48; py++) for (let px = 0; px < 48; px++) {    const o = [px + 0.5 - CX - dx, -40, BASE - (py + 0.5) + K * 40];    let bt = 1e9, hit = null;    if (convex(o, d, BOX)) {      const tb = TIN, cb = CIN, qx = o[0], qy = o[1] + tb;      let inside = false;      if (cb === 7 && convex([qx, qy, HB], [0, 0, -1], CAV.slice(0, 6))) inside = true;      if (inside && convex(o, d, CAV)) {        bt = TOUT; hit = { id: "cav", code: COUT };        const te = ellipsoid(o, d, PILLOW);        if (te > 0 && te < bt) { bt = te; hit = { id: "pillow" }; }      } else { bt = tb; hit = { id: "box", code: cb }; }    }    for (let i = 0; i < HANDLES.length; i++) if (convex(o, d, HANDLES[i]) && TIN < bt) { bt = TIN; hit = { id: "handle", code: CIN, i }; }    const ol = X.toL(o);    if (convex(ol, dl, LID) && TIN < bt) { bt = TIN; hit = { id: "lid", code: CIN }; }    if (!hit) continue;    const p = hit.id === "lid" ? [ol[0] + bt * dl[0], ol[1] + bt * dl[1], ol[2] + bt * dl[2]] : [o[0], o[1] + bt, o[2] - K * bt];    const c = classify(hit, p, X);    c.t = bt;    buf[py * 48 + px] = c;  }  return buf;}function planeN(pl, code) { const p = pl[code]; return [p[0], p[1], p[2]]; }// distance from (x, y) to the lid hexagon's edges: [distance, outward normal of the nearest edge]function lidEdge(x, y) {  let best = 1e9, bn = null;  for (let i = 0; i < 6; i++) {    const p = LID[i], dd = p[3] - (p[0] * x + p[1] * y);    if (dd < best) { best = dd; bn = [p[0], p[1]]; }  }  return [best, bn];}function classify(hit, p, X) {  const [x, y, z] = p;  if (hit.id === "box") {    const n = planeN(BOX, hit.code);    if (hit.code === 7) return { part: "box", mat: "wood", s: "rim", d: 0.95 };    const foot = hit.code === 5;    if (foot) {      // ring pull on the foot end: a small plate and a hanging ring      if (Math.abs(x) < 1.1 && Math.abs(z - 5.2) < 0.7) return { part: "handles", mat: "iron", d: dotL(n), plate: true };      const r = Math.hypot(x, (z - 3.4) * 1.1);      if (r > 0.9 && r < 1.9) return { part: "handles", mat: "iron", d: dotL(n), ring: true, top: z > 3.4 };    }    return { part: "box", mat: "wood", s: "wall" + hit.code, d: dotL(n), z, u: foot ? x : y };  }  if (hit.id === "cav") {    if (hit.code === 6) return { part: "lining", mat: "velvet", s: "floor", x, y };    const n = planeN(CAV, hit.code);    return { part: "lining", mat: "velvet", s: "wall", d: dotL([-n[0], -n[1], 0.2]), z };  }  if (hit.id === "pillow") {    const c = PILLOW.c, r = PILLOW.r;    const n = norm3([(x - c[0]) / (r[0] * r[0]), (y - c[1]) / (r[1] * r[1]), (z - c[2]) / (r[2] * r[2])]);    return { part: "pillow", mat: "pillow", d: dotL(n), seam: Math.abs(y - c[1]) < 0.35 };  }  if (hit.id === "handle") {    const pl = HANDLES[hit.i], n = planeN(pl, hit.code);    return { part: "handles", mat: "iron", d: dotL(n), bar: true };  }  // lid: local coordinates  let n = planeN(LID, hit.code);  if (hit.code === 7) {    const [e, en] = lidEdge(x, y);    // brass cross + violet stone (inlaid, flush)    const vb = Math.abs(x) < 1.05 && y > -3 && y < 14.5, hb = Math.abs(y - 9) < 1.2 && Math.abs(x) < 4.2;    if (Math.abs(x) < 1.05 && Math.abs(y - 9) < 1.2) return { part: "gem", mat: "gem", lx: x, ly: y };    if (vb || hb) {      const edgeL = vb && hb ? 0 : vb ? (x < -0.45 ? 1 : x > 0.45 ? -1 : 0) : 0;      const edgeT = hb && !vb ? (y > 9.6 ? 1 : y < 8.4 ? -1 : 0) : vb && !hb ? (y > 13.7 ? 1 : y < -2.2 ? -1 : 0) : 0;      const tipL = hb && !vb && x < -3.5, tipR = hb && !vb && x > 3.5;      return { part: "cross", mat: "brass", edge: edgeL + edgeT + (tipL ? 1 : 0) - (tipR ? 1 : 0), d: dotL(X.rotN([0, 0, 1])) };    }    if (e < 1.05) n = norm3([en[0] * 0.8, en[1] * 0.8, 0.6]);     // bevel    const groove = e > 2.3 && e < 2.95;    const grain = (Math.abs(x + 3.2) < 0.35 && hash(1, Math.floor(y / 5)) > 0.35) || (Math.abs(x - 2.8) < 0.35 && hash(2, Math.floor((y + 2) / 6)) > 0.45);    return { part: "lid", mat: "wood", s: "lidtop", d: dotL(X.rotN(n)), bevel: e < 1.05, groove, grain: grain && e >= 2.95, lid: true };  }  return { part: "lid", mat: "wood", s: "lidside" + hit.code, d: dotL(X.rotN(n)), edgeTop: z > LT - 0.7, lid: true };}function level(d) { return d > 0.8 ? 3 : d > 0.45 ? 2 : d > 0.08 ? 1 : 0; }function tone(buf, fx) {  const keys = new Array(48 * 48).fill(null);  for (let i = 0; i < 48 * 48; i++) {    const b = buf[i]; if (!b) continue;    const px = i % 48, py = (i - px) / 48;    const up = py > 0 ? buf[i - 48] : null, dn = py < 47 ? buf[i + 48] : null, lf = px > 0 ? buf[i - 1] : null;    let key;    if (b.mat === "wood") {      let lv = level(b.d);      if (b.s === "rim") lv = !lf || lf.part !== "box" || !up || up.part !== "box" ? 3 : 2;      else if (b.s === "lidtop") {        lv = b.bevel ? level(b.d) : b.groove ? 0 : 1;        if (b.grain) lv = 0;        else if (!b.bevel && !b.groove && lf && lf.groove) lv = 2;   // far wall of the groove catches light      } else if (b.s.startsWith("lidside")) {        lv = Math.max(0, level(b.d) - 1) + (b.edgeTop ? 1 : 0);      } else {        // box walls: base trim, one plank seam, the lid's overhang shades the top row        lv = Math.max(0, lv - 1);        if (b.z < 1.2) lv = Math.max(0, lv - 1);        else if (b.z < 1.9) lv = Math.min(3, lv + 1);        else if (Math.abs(b.z - 4.4) < 0.5) lv = Math.max(0, lv - 1);        if (up && up.lid) lv = 0;      }      key = WOOD[Math.max(0, Math.min(3, lv))];    } else if (b.mat === "iron") {      let lv = level(b.d);      const same = (q) => q && q.part === "handles";      if (b.bar) lv = !same(up) ? (b.d > 0 ? 3 : 2) : !same(dn) ? 0 : 1;      if (b.plate) lv = 2;      if (b.ring) lv = b.top ? 3 : 1;      key = IRON[Math.max(0, Math.min(3, lv))];    } else if (b.mat === "brass") {      let lv = 2 + b.edge;      if (fx.glint) lv = Math.min(3, lv + 1);      key = GOLD[Math.max(0, Math.min(3, lv - (b.d < 0.6 ? 1 : 0)))];    } else if (b.mat === "gem") {      const hi = b.lx < 0 && b.ly > 9;      const p = fx.gem || 0;      key = hi ? (p > 0.5 ? "n5" : "v3") : b.lx > 0.2 && b.ly < 9 ? (p > 0.5 ? "v2" : "v0") : p > 0.5 ? "v3" : "v1";    } else if (b.mat === "velvet") {      if (b.s === "wall") key = up && up.part === "box" ? "v0" : b.d > 0.3 ? "r0" : "v0";      else {        // floor: the left wall throws a shadow; button tufts on a diamond lattice        const gap = b.x + hwLow(Math.min(b.y, 10)) - T;        const u = (b.x + 0.5) / 3.2 + b.y / 5.2, v = (b.x + 0.5) / 3.2 - b.y / 5.2;        const btn = Math.abs(u - Math.round(u)) < 0.16 && Math.abs(v - Math.round(v)) < 0.16;        key = gap < 1.6 ? "v0" : btn ? "r1" : "r0";        const gl = fx.glow && gap >= 1.6 ? fx.glow(b.x, b.y, px, py) : 0;        if (gl === 2) key = btn ? "v3" : "v2"; else if (gl === 1) key = "v1";      }    } else if (b.mat === "pillow") {      key = b.seam ? "n1" : b.d > 0.6 ? "n4" : b.d > 0.15 ? "n3" : "n2";    }    keys[i] = key;  }  return keys;}const PARTS = ["box", "lining", "pillow", "handles", "lid", "cross", "gem"];function drawCoffin(g, ps, fx, dx) {  const buf = castCoffin(ps, dx || 0);  const keys = tone(buf, fx);  const seen = new Set();  for (let i = 0; i < 48 * 48; i++) if (buf[i]) seen.add(buf[i].part);  g.outlined("ink", () => {    for (const part of PARTS) if (seen.has(part)) g.part(part, () => g.field({ x: [0, 47], y: [0, 47] }, (x, y) => {      const b = buf[y * 48 + x];      return b && b.part === part ? keys[y * 48 + x] : null;    }));  });  return buf;}// ---- effectsfunction visible(buf, x, y, depth, slack) {  if (x < 0 || y < 0 || x > 47 || y > 47) return false;  const b = buf[y * 48 + x];  return !b || depth < b.t + slack;}// a dust puff: p 0..1 — a tight grey burst, a looser cloud, then a few thinning specks; dir drifts it sidewaysconst PUFF = [  [[0, 0, "n4"], [1, 0, "n4"], [2, 0, "n3"], [1, -1, "n5"]],  [[1, -2, "n4"], [0, -1, "n4"], [2, -1, "n5"], [3, -1, "n4"], [1, 0, "n3"], [3, 0, "n3"], [4, 0, "n3"]],  [[1, -3, "n3"], [3, -2, "n4"], [0, -1, "n3"], [4, -1, "n3"], [2, 0, "n3"]],  [[2, -4, "n3"], [5, -2, "n3"], [0, -2, "n3"]],];function puff(g, x, y, p, dir) {  if (p < 0 || p >= 1) return;  const st = Math.min(3, Math.floor(p * 4));  const ox = x + Math.round(dir * p * 2), oy = y - Math.round(p * 1.5);  for (const [a, b, k] of PUFF[st]) g.pixel(ox + a * dir, oy + b, k);}// the lining's breath: violet wisps curling up out of the opening, motes rising (cavity-floor glow is in tone())const WISPS = [[-2, -3, 0.0], [2, 3, 0.33], [0, -10, 0.67]];const MOTES = [[-3, 5, 0.1], [3, -6, 0.45], [1, -13, 0.8], [-1, 9, 0.62]];function glow(g, buf, t) {  g.detached("glow", () => {    for (const [wx, wy, ph] of WISPS) {      const p = (t + ph) % 1;      const x = wx + Math.sin(TAU * (p * 0.9 + ph)) * 1.6, z = FLOOR + 3 + p * 24;      const [sx, sy, dep] = project([x, wy, z]);      const px = Math.round(sx), py = Math.round(sy);      const put = (a, b, k) => { if (visible(buf, a, b, dep, 0.5)) g.pixel(a, b, k); };      if (p < 0.4) { put(px, py, "v2"); put(px + 1, py, "v2"); put(px, py + 1, "v1"); put(px + 1, py + 1, "v1"); put(px, py - 1, "v1"); }      else if (p < 0.72) { put(px, py, "v1"); put(px + 1, py - 1, "v2"); if (chk(px, py)) put(px - 1, py + 1, "v1"); }      else if (chk(px, Math.floor(t * 12))) put(px, py, "v1");    }  });  g.detached("motes", () => {    for (const [mx, my, ph] of MOTES) {      const p = (t + ph) % 1;      if (p > 0.85) continue;      const z = FLOOR + 1 + p * 22, x = mx + Math.round(Math.sin(TAU * (p + ph)) * 1.2);      const [sx, sy, dep] = project([x, my, z]);      const px = Math.round(sx), py = Math.round(sy);      if (!visible(buf, px, py, dep, 0.5)) continue;      g.pixel(px, py, p < 0.3 ? "n5" : p < 0.6 ? "v3" : "v2");    }  });}// the fallen lid's own dithered shadow on the floor to the rightfunction lidShadow(g) {  g.part("lidshadow", () => {    for (let y = GROUND - 12; y <= GROUND; y++) for (let x = Math.round(CX + 9); x <= Math.round(CX + 25) && x < 48; x++) {      const u = (x - (CX + 17)) / 8.5, v = (y - (GROUND - 6)) / 7;      if (u * u + v * v <= 1 && chk(x, y)) g.pixel(x, y, "ink");    }  });}// ---- statesconst CLOSED = { xc: RIM, phi: 0, yaw: 0, ty: 0, lift: 0 };const PHI_REST = Math.asin(HB / 17.6);const OPENED = { xc: -8.8, phi: PHI_REST, yaw: 0.08, ty: -1.2, lift: 0 };const OPEN_KEYS = [  // xc, phi, yaw, ty, lift  [RIM, 0, 0, 0, 0],  [RIM, 0, 0.03, 0, 1.6],          // a thump from inside: the lid jumps  [RIM - 2.2, 0, 0.05, -0.3, 0.3],   // and starts to slide  [RIM - 5.6, 0, 0.07, -0.6, 0],  [RIM - 9.5, 0.06, 0.08, -0.9, 0],  [-5.6, 0.2, 0.08, -1.1, 0],      // over the edge  [-8.0, 0.33, 0.08, -1.2, 0],  [-8.8, PHI_REST, 0.08, -1.2, 0],  // lands  [-8.8, PHI_REST - 0.07, 0.08, -1.2, 0], // bounces  [OPENED.xc, OPENED.phi, OPENED.yaw, OPENED.ty, OPENED.lift],   // == open-idle f0];// the knocks in the closed loop: [frame, lift, yaw, shiftX]const KNOCK = { 5: [1.4, 0, 0.5], 6: [0, 0, 0], 8: [2.2, 0.05, -0.6], 9: [0.4, 0.02, 0] };// violet light pooled on the cavity floor, breathing once per loop: 2 = core, 1 = dithered halo, 0 = nonefunction floorGlow(t) {  const k = 0.5 - 0.5 * Math.cos(TAU * t);  return (x, y, px, py) => {    const r = Math.hypot(x / 2.6, (y + 1) / 8);    if (r < 0.35 + 0.25 * k) return 2;    if (r < 0.7 + 0.3 * k && chk(px, py)) return 1;    return 0;  };}const Coffin = sprite(pal, (g, t, state) => {  contactShadow(g, 23, GROUND - 1, 12, 2);  if (state === "closed") {    const f = Math.round(t * N_IDLE) % N_IDLE;    const kk = KNOCK[f] || [0, 0, 0];    const ps = { xc: RIM - kk[2] * 0.6, phi: 0, yaw: kk[1], ty: 0, lift: kk[0] };    const shake = f === 8 ? 1 : 0;    drawCoffin(g, ps, { gem: f === 5 || f === 8 || f === 9 ? 1 : 0 }, shake);    g.detached("dust", () => {      // puffs squeezed out from under the lid at each knock, left and right seams and the foot corners      const seam = (y) => BASE - HB - K * y;      for (const [f0, amp] of [[5, 1], [8, 2]]) {        const p = (f - f0) / 4;        if (p < 0 || p >= 1) continue;        puff(g, Math.round(CX - 9), Math.round(seam(4)), p, -1);        puff(g, Math.round(CX + 8), Math.round(seam(-4)), p, 1);        if (amp > 1) { puff(g, Math.round(CX - 7), Math.round(seam(-14)), p, -1); puff(g, Math.round(CX + 6), Math.round(seam(12)), p, 1); }      }    });    return;  }  if (state === "open") {    const f = Math.min(N_OPEN - 1, Math.round(t * N_OPEN));    if (f >= 7) lidShadow(g);    const [xc, phi, yaw, ty, lift] = OPEN_KEYS[f];    const buf = drawCoffin(g, { xc, phi, yaw, ty, lift }, {      gem: f === 1 ? 1 : 0,      glow: f >= 3 ? floorGlow(f === N_OPEN - 1 ? 0 : 0.5) : null,    });    g.detached("dust", () => {      if (f >= 1 && f <= 3) { const p = (f - 1) / 3; puff(g, Math.round(CX - 9), Math.round(BASE - HB - K * 4), p, -1); }      if (f === 7 || f === 8) {                     // gone again on the last frame (== open-idle f0)        const p = (f - 7) / 2;        for (const yy of [-10, 2, 12]) puff(g, Math.round(CX + 20), Math.round(BASE - K * yy) - 1, p, 1);      }    });    if (f === N_OPEN - 1) glow(g, buf, 0);    return;  }  lidShadow(g);  const buf = drawCoffin(g, OPENED, { glow: floorGlow(t) });  glow(g, buf, t);});submit(Coffin, {  size: [48, 48], origin: "top-left", kind: "icon", scale: 1,  animate: { fps: 10, duration: N_IDLE / 10, loop: true },  anchor: { x: 23, y: GROUND },  variants: [    { name: "closed", args: ["closed"] },    { name: "open", args: ["open"], animate: { fps: 10, duration: N_OPEN / 10, loop: false } },    { name: "open-idle", args: ["open-idle"] },  ],});

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

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

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