Dungeon Mechanisms · clay_pot.js
壺
32×32 の壊せる素焼きの壺で、描いた帯にクリーム色の点が並んでいます。中が空洞の一つの立体の模型から描くので、置いた壺も、叩かれたときの姿勢も、ぎざぎざに割れた底も同じ形から出てきます。idle(8 コマ)は光の当たる肩を光沢が滑り、hit(6 コマの一回再生)はつぶれて底を軸に揺れます。break(10 コマの一回再生)はひびが入り、6 つの破片と 3 枚の硬貨が弧を描いて飛び、土ぼこりが立ちます。broken(8 コマ)は床の底と破片と硬貨で、硬貨が光ります。破片は壺そのものの画素を、まっすぐな線で切り分けたものです。
6×
動くスプライト素材: 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 冊の制作マニュアル、結果を確かめるための描画エンジンをエージェントに渡します。