Graveyard & Chapel · coffin.js
棺
48×48 の床置きの小物で、縦長に置いた六角形の棺を凸形の立体の組み合わせとして作り、画素ごとに描いています。鉄の取っ手、紫の石をはめた真鍮の十字架が蓋にあり、内側にはボタン留めの暗いベルベットと白っぽい枕があります。蓋は剛体として動きます。closed(12 コマ)は静止したあと、内側から 2 回叩かれて蓋が跳ね、ほこりが舞います。open(10 コマの一回再生)は蓋が右へ滑り、傾いて落ち、側面に立てかかって止まります。open-idle(12 コマ)は内張りから紫の光が漏れ、光の粒が昇ります。
6×
動くスプライト素材: 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 冊の制作マニュアル、結果を確かめるための描画エンジンをエージェントに渡します。