Arcane Curiosities Vol.1 · mimic.js
ミミック
48×48 のミミックで、宝箱と同じ作りを少しゆがめています。腹がふくらんで息をし、板は曲がり、蓋の合わせ目に不ぞろいな歯が並び、前に舌が垂れ、すき間で片目が光ります。宝箱と同じく、4 方向は一つのミミックを床の上で回したもので、世界の側の左上から光が当たります。各方向に idle(12 コマ、蓋が 1〜2px 開いて息をし、腹がふくらみ、舌が揺れ、歯を光が走る)と bite(10 コマのループ、蓋が大きく開いて飛びかかり、つぶれながら噛み閉じる)があります。
6×
動くスプライト素材: Arcane Curiosities Vol.1(itch.io)動く小物 12 点・52 アニメ
冒頭のコメント。これが指示書です。サイズ、視点、光の向き、すべての変種が書かれています。
mimic.js · 1–9
// Mimic — 48x48, top-down 3/4, key light top-left. The treasure chest's design language (same plank box, barrel// lid on a back hinge, iron bands, brass caps and lock), slightly warped: the belly bows out and breathes, the// planks run crooked, uneven teeth line the lid seam, a tongue lolls over the front and one eye glints in the gap.// Built like chest.js: every pixel casts the oblique view ray (screen y = BASE - z - K*depth) into a few convex// solids, so the four facings are one mimic turned on the floor, lit top-left in world space (right is not a// mirrored left). Teeth are flat tooth sheets on the seam planes; the tongue is a chain of spheres.// Variants (f = down / up / left / right):// idle-f loop, 12f — lid breathes 1-2px open, belly swells, tongue sways, a glint runs along the teeth// bite-f loop, 10f — lid snaps wide (teeth rows, palate, tongue lashing up), lunges, chomps shut with a squash最後の数行。submit でキャンバスの大きさと、描画エンジンが書き出す変種の一覧を指定します。
mimic.js · 434–442
submit(Mimic, { size: [48, 48], origin: "top-left", kind: "icon", scale: 1, animate: { fps: 10, duration: N_IDLE / 10, loop: true }, anchor: { x: 24, y: GROUND }, variants: [ ...FACES.map((f) => ({ name: `idle-${f}`, args: ["idle", f] })), ...FACES.map((f) => ({ name: `bite-${f}`, args: ["bite", f], animate: { fps: 10, duration: N_BITE / 10, loop: true } })), ],});プログラム全体を表示 (442 行)
mimic.js · 442 行
// Mimic — 48x48, top-down 3/4, key light top-left. The treasure chest's design language (same plank box, barrel// lid on a back hinge, iron bands, brass caps and lock), slightly warped: the belly bows out and breathes, the// planks run crooked, uneven teeth line the lid seam, a tongue lolls over the front and one eye glints in the gap.// Built like chest.js: every pixel casts the oblique view ray (screen y = BASE - z - K*depth) into a few convex// solids, so the four facings are one mimic turned on the floor, lit top-left in world space (right is not a// mirrored left). Teeth are flat tooth sheets on the seam planes; the tongue is a chain of spheres.// Variants (f = down / up / left / right):// idle-f loop, 12f — lid breathes 1-2px open, belly swells, tongue sways, a glint runs along the teeth// bite-f loop, 10f — lid snaps wide (teeth rows, palate, tongue lashing up), lunges, chomps shut with a squashconst CX = 24.5, GROUND = 45, K = 0.4;const L = 31, D = 19, H = 12, WALL = 3, R = 5;const HL = L / 2, HD = D / 2, T = 1.6, LIDZ = H + WALL, BX = 8;const N_IDLE = 12, N_BITE = 10, WIDE = 66;const LV = norm3([-0.6, -0.25, 0.76]);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 FACINGS = { down: { toL: (v) => [v[0], v[1], v[2]], toW: (v) => [v[0], v[1], v[2]], ext: HD, shadow: 17 }, up: { toL: (v) => [-v[0], -v[1], v[2]], toW: (v) => [-v[0], -v[1], v[2]], ext: HD, shadow: 17 }, right: { toL: (v) => [v[1], -v[0], v[2]], toW: (v) => [-v[1], v[0], v[2]], ext: HL, shadow: 12 }, left: { toL: (v) => [-v[1], v[0], v[2]], toW: (v) => [v[1], -v[0], v[2]], ext: HL, shadow: 12 },};// ---- solidsconst quad = (c, r) => ({ c, inv2: r.map((v) => (isFinite(v) ? 1 / (v * v) : 0)) });const BODY = [-HL, HL, -HD, HD, 0, H];const CAV = [-HL + T, HL - T, -HD + T, HD - T, T, 99];const LOCK = [-3.5, 3.5, -HD - 0.8, -HD, H - 8, H - 3];const HANDLES = [[HL, HL + 1, -1.5, 1.5, H - 4, H - 2], [-HL - 1, -HL, -1.5, 1.5, H - 4, H - 2]];const KNUCKLES = [[2.5, 5.5, HD, HD + 1, H - 1, H + 1], [-5.5, -2.5, HD, HD + 1, H - 1, H + 1]];const LIDBOX = [-HL, HL, -HD, HD, H, LIDZ];const BARREL = [-HL, HL, -Infinity, Infinity, LIDZ, Infinity], BARREL_Q = quad([0, 0, LIDZ], [Infinity, HD, R]);const ANY = [-99, 99, -99, 99, -99, 99];const EYE_X = -5.25, EYE_Q = quad([EYE_X, -HD + 2.6, H + 0.8], [1.9, 1.4, 1.6]);const BOUND_C = [-HL - 1.1, HL + 1.1, -HD - 5, HD + 1.1, 0, H + 11];const BOUND_L = [-HL - 0.4, HL + 0.4, -HD - 0.4, HD + 0.1, H - 3.8, LIDZ + R + 0.1];// teeth: flat sheets on the seam planes. [centre along the edge, half-width, length]; uneven on purposeconst UPPER_FRONT = [[-13.6, 1.1, 2.2], [-11, 1.4, 4.4], [-8, 1.5, 2.7], [-2, 1.5, 2.9], [1.5, 1.5, 2.5], [5, 1.5, 3], [8, 1.5, 2.6], [11, 1.4, 4.2], [13.6, 1.1, 2.3]]; // fangs at +-11, a gap at x=-5 where the eye looks outconst LOWER_FRONT = [-11.5, -8.5, -2, 1, 7.5, 10.5].map((c, i) => [c, 1.5, 2.2 + 0.7 * hash(i, 11)]);const UPPER_SIDE = [-7.5, -4, -0.5].map((c, i) => [c, 1.7, 2.6 + 0.7 * hash(i, 13)]);const LOWER_SIDE = [-5.75, -2.25, 1.25].map((c, i) => [c, 1.5, 2.2 + 0.5 * hash(i, 17)]);// [axis of the sheet plane, plane value, outward sign, edge axis, teeth, grows down (upper) or up (lower)]const UPPER_SHEETS = [[1, -HD - 0.25, -1, 0, UPPER_FRONT], [0, HL + 0.25, 1, 1, UPPER_SIDE], [0, -HL - 0.25, -1, 1, UPPER_SIDE]];const LOWER_SHEETS = [[1, -HD + 0.9, -1, 0, LOWER_FRONT], [0, HL - 0.9, 1, 1, LOWER_SIDE], [0, -HL + 0.9, -1, 1, LOWER_SIDE]];let TIN = 0, TOUT = 0, CIN = -1, COUT = -1;function convex(o, d, b, q) { let tin = -1e9, tout = 1e9, cin = -1, cout = -1; for (let i = 0; i < 3; i++) { const lo = b[2 * i], hi = b[2 * i + 1], di = d[i], oi = o[i]; if (lo === -Infinity && hi === Infinity) continue; if (di < 1e-9 && di > -1e-9) { if (oi < lo || oi > hi) return false; continue; } let t1 = (lo - oi) / di, t2 = (hi - oi) / di, c1 = 2 * i, c2 = 2 * i + 1; if (t1 > t2) { const k = t1; t1 = t2; t2 = k; c1 = 2 * i + 1; c2 = 2 * i; } if (t1 > tin) { tin = t1; cin = c1; } if (t2 < tout) { tout = t2; cout = c2; } if (tin > tout) return false; } if (q) { const r = q.inv2, c = q.c; const ox = o[0] - c[0], oy = o[1] - c[1], oz = o[2] - c[2]; const A = d[0] * d[0] * r[0] + d[1] * d[1] * r[1] + d[2] * d[2] * r[2]; const B = 2 * (ox * d[0] * r[0] + oy * d[1] * r[1] + oz * d[2] * r[2]); const C = ox * ox * r[0] + oy * oy * r[1] + oz * oz * r[2] - 1; const disc = B * B - 4 * A * C; if (disc < 0) return false; const sq = Math.sqrt(disc), r1 = (-B - sq) / (2 * A), r2 = (-B + sq) / (2 * A); if (r1 > tin) { tin = r1; cin = 6; } if (r2 < tout) { tout = r2; cout = 6; } if (tin > tout) return false; } TIN = tin; TOUT = tout; CIN = cin; COUT = cout; return true;}function normalOf(code, p, q) { if (code < 6) { const n = [0, 0, 0]; n[code >> 1] = code & 1 ? 1 : -1; return n; } return norm3([(p[0] - q.c[0]) * q.inv2[0], (p[1] - q.c[1]) * q.inv2[1], (p[2] - q.c[2]) * q.inv2[2]]);}// ray vs one tooth sheet: returns [t, tooth index] or null. `down` teeth hang from z=H, others rise from it.// Seen edge-on (the ray runs along the sheet) a tooth is a 1px sliver: march the ray through the sheet's thickness.function toothAt(teeth, s, h) { if (h < 0) return -1; for (let i = 0; i < teeth.length; i++) { const [c, w, len] = teeth[i], u = Math.abs(s - c); if (u < w && h <= len * (1 - u / w)) return i; } return -1;}function sheetHit(o, d, sh, down) { const [ax, at, , ea, teeth] = sh; if (d[ax] < 1e-6 && d[ax] > -1e-6) { if (Math.abs(o[ax] - at) > 0.6) return null; const zA = down ? H - 4.6 : H, zB = down ? H : H + 3.2; // tooth band heights const tA = (zB - o[2]) / d[2], tB = (zA - o[2]) / d[2]; // d[2] < 0: the ray descends for (let t = Math.min(tA, tB); t <= Math.max(tA, tB); t += 0.25) { const i = toothAt(teeth, o[ea] + t * d[ea], down ? H - (o[2] + t * d[2]) : o[2] + t * d[2] - H); if (i >= 0) return [t, i]; } return null; } const t = (at - o[ax]) / d[ax]; const i = toothAt(teeth, o[ea] + t * d[ea], down ? H - (o[2] + t * d[2]) : o[2] + t * d[2] - H); return i >= 0 ? [t, i] : null;}// the tongue: a chain of spheres along 4 control points, between hanging (u=0) and lashing up (u=1)function tongueChain(u, sw) { const lerp3 = (a, b) => a.map((v, i) => v + (b[i] - v) * u); const P = [ lerp3([2, -1, H - 2.5], [1, 1, H - 2]), lerp3([4, -HD + 0.4, H + 0.6], [2, -3, H + 4]), lerp3([4.5 + 0.6 * sw, -HD - 1.4, H - 3.5], [3 + 1.5 * sw, -HD, H + 9]), lerp3([5 + 1.8 * sw, -HD - 1.8, H - 8], [1 + 3 * sw, -HD - 3, H + 11]), ]; const out = []; for (let k = 0; k < 9; k++) { const f = (k / 8) * 3, i = Math.min(2, Math.floor(f)), a = f - i; const c = P[i].map((v, j) => v + (P[i + 1][j] - v) * a); const rad = 2.8 - 1.0 * (k / 8); out.push(quad(c, [rad, rad * 0.8, rad * 0.85])); } return out;}function castMimic(face, st) { const F = FACINGS[face], BASE = GROUND - K * F.ext; const a = (st.th * Math.PI) / 180, ca = Math.cos(a), sa = Math.sin(a); const lidNormal = (n) => [n[0], n[1] * ca + n[2] * sa, -n[1] * sa + n[2] * ca]; const d = F.toL([0, 1 / st.sx, -K / st.sz]); const dl = [d[0], d[1] * ca - d[2] * sa, d[1] * sa + d[2] * ca]; const ex = F.toL([1, 0, 0]), ez = F.toL([0, 0, 1]); const tongue = tongueChain(st.tongue, st.sway); const proj = (q) => { const w = F.toW([q[0], q[1] - st.lunge, q[2]]); const sy = BASE - st.hop - w[2] * st.sz - K * w[1] * st.sx; const zf = GROUND - sy - st.hop, bow = 1 + st.bulge * Math.sin(Math.PI * Math.max(0, Math.min(1, zf / (H + 4)))); return [CX + w[0] * st.sx * bow - 0.5, sy - 0.5, w[1] * st.sx]; }; // screen rectangle of a (finite) box in the chest or lid frame: pixels outside it skip that solid entirely const rect = (b, lid) => { const r = [99, -99, 99, -99]; for (const x of [b[0], b[1]]) for (const y of [b[2], b[3]]) for (const z of [b[4], b[5]]) { const [sx, sy] = proj(lid ? lidToChest([x, y, z], st.th) : [x, y, z]); r[0] = Math.min(r[0], sx); r[1] = Math.max(r[1], sx); r[2] = Math.min(r[2], sy); r[3] = Math.max(r[3], sy); } return [Math.floor(r[0]) - 2, Math.ceil(r[1]) + 2, Math.floor(r[2]) - 2, Math.ceil(r[3]) + 2]; }; const qbox = (q) => q.c.flatMap((c, i) => [c - 1 / Math.sqrt(q.inv2[i]), c + 1 / Math.sqrt(q.inv2[i])]); const tb = [99, -99, 99, -99, 99, -99]; for (const q of tongue) { const b = qbox(q); for (let i = 0; i < 6; i += 2) { tb[i] = Math.min(tb[i], b[i]); tb[i + 1] = Math.max(tb[i + 1], b[i + 1]); } } const R_BODY = rect(BODY, false), R_LOCK = rect(LOCK, false), R_EYE = rect(qbox(EYE_Q), false), R_TONGUE = rect(tb, false); const R_HANDLES = HANDLES.map((h) => rect(h, false)), R_KNUCKLES = KNUCKLES.map((k) => rect(k, false)); const R_LOWER = rect([-HL, HL, -HD, HD, H, H + 3.4], false), R_UPPER = rect([-HL - 0.6, HL + 0.6, -HD - 0.6, HD, H - 4.6, H], true); const R_LID = rect([-HL, HL, -HD, HD, H, LIDZ + R], true); const o = [0, 0, 0], ol = [0, 0, 0]; const buf = new Array(48 * 48).fill(null); for (let py = 0; py < 48; py++) { // the warp: the belly bows out, most at mid-height of the near face const zf = GROUND - 0.5 - py - st.hop, bow = 1 + st.bulge * Math.sin(Math.PI * Math.max(0, Math.min(1, zf / (H + 4)))); for (let px = 0; px < 48; px++) { const Xw = (px + 0.5 - CX) / (st.sx * bow), Zw = (BASE - py - 0.5 - st.hop) / st.sz; for (let i = 0; i < 3; i++) o[i] = ex[i] * Xw + ez[i] * Zw; o[1] += st.lunge; ol[0] = o[0]; ol[1] = HD + (o[1] - HD) * ca - (o[2] - H) * sa; ol[2] = H + (o[1] - HD) * sa + (o[2] - H) * ca; const inR = (r) => px >= r[0] && px <= r[1] && py >= r[2] && py <= r[3]; const inC = convex(o, d, BOUND_C, null), inL = convex(ol, dl, BOUND_L, null); if (!inC && !inL) continue; let bt = 1e9, bid = null, blid = false, bcode = -1, bq = null, bflip = false, bextra = 0; const hit = (id, lid, box, q) => { if (convex(lid ? ol : o, lid ? dl : d, box, q) && TIN < bt) { bt = TIN; bid = id; blid = lid; bcode = CIN; bq = q; bflip = false; } }; if (inC) { if (inR(R_BODY) && convex(o, d, BODY, null)) { const tb = TIN, cb = CIN, qx = o[0] + tb * d[0], qy = o[1] + tb * d[1]; if (cb === 5 && Math.abs(qx) < HL - T && Math.abs(qy) < HD - T) { if (convex(o, d, CAV, null) && TOUT < bt) { bt = TOUT; bid = "inner"; blid = false; bcode = COUT; bq = null; bflip = true; } } else if (tb < bt) { bt = tb; bid = "body"; blid = false; bcode = cb; bq = null; bflip = false; } } if (inR(R_LOCK)) hit("lock", false, LOCK, null); for (let i = 0; i < 2; i++) { if (inR(R_HANDLES[i])) hit("handle", false, HANDLES[i], null); if (inR(R_KNUCKLES[i])) hit("knuckle", false, KNUCKLES[i], null); } if (inR(R_EYE)) hit("eye", false, ANY, EYE_Q); if (inR(R_TONGUE)) for (const q of tongue) hit("tongue", false, ANY, q); if (inR(R_LOWER)) for (const sh of LOWER_SHEETS) { const r = sheetHit(o, d, sh, false); if (r && r[0] < bt) { bt = r[0]; bid = "lower"; blid = false; bcode = 2 * sh[0] + (sh[2] > 0 ? 1 : 0); bq = null; bflip = false; bextra = r[1]; } } } if (inL) { if (inR(R_LID)) { hit("lidbox", true, LIDBOX, null); hit("barrel", true, BARREL, BARREL_Q); } if (inR(R_UPPER)) for (const sh of UPPER_SHEETS) { const r = sheetHit(ol, dl, sh, true); if (r && r[0] < bt) { bt = r[0]; bid = "upper"; blid = true; bcode = 2 * sh[0] + (sh[2] > 0 ? 1 : 0); bq = null; bflip = false; bextra = r[1]; } } } if (!bid) continue; const oo = blid ? ol : o, dd = blid ? dl : d; const p = [oo[0] + bt * dd[0], oo[1] + bt * dd[1], oo[2] + bt * dd[2]]; let n = normalOf(bcode, p, bq); if (bflip) n = [-n[0], -n[1], -n[2]]; const nw = F.toW(blid ? lidNormal(n) : n); const c = classify(bid, p, n, bextra); c.lid = blid; c.t = bt; c.d = nw[0] * LV[0] + nw[1] * LV[1] + nw[2] * LV[2]; buf[py * 48 + px] = c; } } return { buf, proj };}// a point in the lid's rest frame -> chest-local, with the lid open by th degreesfunction lidToChest(q, th) { const a = (th * Math.PI) / 180, dy = q[1] - HD, dz = q[2] - H; return [q[0], HD + dy * Math.cos(a) + dz * Math.sin(a), H - dy * Math.sin(a) + dz * Math.cos(a)];}function classify(id, p, n, extra) { const x = p[0], y = p[1], z = p[2], ax = Math.abs(x), ay = Math.abs(y); const band = Math.abs(ax - BX) <= 1.5, hinge = ax >= 2.5 && ax <= 5.5; const rivet = Math.abs(ax - BX) < 0.5 && Math.abs((((z % 4) + 4) % 4) - 1.5) < 0.5; const face = n[2] > 0.5 ? "top" : n[2] < -0.5 ? "under" : n[1] < -0.5 ? "front" : n[1] > 0.5 ? "back" : n[0] > 0 ? "endR" : "endL"; const u = face === "front" ? x : face === "back" ? -x : y; const CAP = { part: "caps", mat: "brass" }, BAND = { part: "bands", mat: "iron", rivet }; if (id === "upper" || id === "lower") return { part: "teeth", mat: "tooth", s: id, k: extra }; if (id === "tongue") return { part: "tongue", mat: "tongue" }; if (id === "eye") return { part: "eye", mat: "eye", x }; if (id === "body") { if (face === "top") return { part: "maw", mat: "gum" }; const end = face === "endL" || face === "endR"; if (z < 3.5 && (end ? ay > HD - 3.5 : ax > HL - 3.5)) return CAP; if (!end && band) return BAND; if (face === "back" && hinge && z > H - 4) return { part: "hinges", mat: "iron", rivet: Math.abs(ax - 4) < 0.5 && Math.abs(z - 9.5) < 0.5 }; if (end) { const ry = y / 2.5, rz = (z - (H - 6)) / 2, rr = ry * ry + rz * rz; if (rr > 0.3 && rr < 1.1) return { part: "handles", mat: "iron", ring: true }; } // crooked planks: the seams slope across the face return { part: "body", mat: "wood", s: face, k: Math.floor((z + 0.09 * u + 0.6) / 4), u, z }; } if (id === "inner") return { part: "maw", mat: "maw", s: face }; if (id === "lock") return { part: "lock", mat: "brass", key: face === "front" && ax < 0.5 && z > 5.5 && z < 7.5 }; if (id === "handle") return { part: "handles", mat: "iron", ring: true }; if (id === "knuckle") return { part: "hinges", mat: "iron", knuckle: true, z }; if (id === "lidbox") { if (face === "under") return { part: "maw", mat: "palate", s: "under", k: Math.floor((y + HD) / 4), x, y }; const end = face === "endL" || face === "endR"; if (end ? ay > HD - 3.5 : ax > HL - 3.5) return CAP; if (z < H + 1) return { part: "bands", mat: "iron", rim: true }; if (!end && band) return BAND; if (face === "back" && hinge) return { part: "hinges", mat: "iron" }; return { part: "lid", mat: "wood", s: "wall" + face, k: 0, u }; } if (Math.abs(n[0]) > 0.9) { const e = Math.hypot(y / HD, (z - LIDZ) / R); if (e > 0.8) return { part: "bands", mat: "iron", rim: true }; return { part: "lid", mat: "wood", s: face, k: 0, u: y }; } const phi = Math.atan2((z - LIDZ) / R, y / HD); if (band) return BAND; if (hinge && phi < 0.45) return { part: "hinges", mat: "iron" }; return { part: "lid", mat: "wood", s: "barrel", k: Math.floor((phi + 0.012 * x) / (Math.PI / 3)), u: x };}function level(d) { return d > 0.78 ? 3 : d > 0.45 ? 2 : d > 0.1 ? 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; const lf = px > 0 ? buf[i - 1] : null, rt = px < 47 ? buf[i + 1] : null; const samePart = (q) => q && q.part === b.part; const glint = fx.glint ? fx.glint(px, py) : 0; let key; if (b.mat === "wood") { let lv = level(b.d); const seam = (q) => q && q.s === b.s && q.k !== b.k; if (seam(rt) || seam(dn)) lv = 0; else if (seam(up) || seam(lf)) lv = Math.min(3, lv + 1); else if (b.z !== undefined && Math.floor(b.z) % 4 === 2 && hash(b.k * 3 + (b.s === "front" ? 0 : 11), Math.floor((b.u + 40) / 4)) > 0.62 && Math.floor(b.u + 40) % 4 !== 0) lv = Math.max(0, lv - 1); if (!b.lid && up && up.lid && b.s !== "top") lv = 0; if (!up && lv > 0) lv = Math.min(3, lv + 1); key = WOOD[Math.max(0, Math.min(3, lv))]; } else if (b.mat === "iron") { let lv = level(b.d); if (!samePart(lf) && !b.rim) lv = Math.min(3, lv + 1); else if (!samePart(rt) && !b.rim) lv = Math.max(0, lv - 1); if (b.rim && !samePart(up)) lv = Math.min(3, lv + 1); if (b.ring) lv = Math.max(lv, 2) + (!samePart(up) || !samePart(lf) ? 1 : 0) - (!samePart(dn) ? 1 : 0); if (b.knuckle) lv = b.z > H ? 3 : 1; lv = Math.max(0, Math.min(3, lv)); key = b.rivet ? "n4" : lv === 3 && (b.d > 0.85 || (b.knuckle && !samePart(lf))) ? "n4" : IRON[lv]; } else if (b.mat === "brass") { const same = (q) => q && q.mat === "brass"; let lv = b.d > 0.5 ? 2 : 1; if (!same(up)) lv = b.d > 0.1 ? 3 : 2; else if (!same(lf)) lv = 2; else if (!same(dn) || !same(rt)) lv = 0; key = b.key ? "ink" : GOLD[lv]; } else if (b.mat === "tooth") { // bone: lit left edge, shaded right edge, dark root in the gum line; the glint turns a tooth white-hot const sameT = (q) => q && q.mat === "tooth" && q.s === b.s && q.k === b.k; let k = "n4"; if (!sameT(lf)) k = b.s === "upper" ? "n5" : "n4"; else if (!sameT(rt)) k = "n3"; if (b.s === "upper" ? !sameT(dn) && sameT(up) : !sameT(up) && sameT(dn)) k = k === "n5" ? "n4" : "n3"; if (glint > 0) k = "n5"; key = k; } else if (b.mat === "tongue") { const lv = b.d > 0.72 ? 3 : b.d > 0.4 ? 2 : b.d > 0.05 ? 1 : 0; key = ["r0", "r1", "r2", "v3"][lv]; if (lv === 3 && samePart(up)) key = "r2"; // only the top rim gets the wet highlight } else if (b.mat === "eye") { const cxp = Math.abs(b.x - EYE_X); // a slit pupil once the eye shows more than a sliver; in the idle gap it is just a hot amber glint const tall = samePart(up) && samePart(dn); key = cxp < 0.5 && tall ? "ink" : fx.eyeGlint && !samePart(up) && !samePart(lf) ? "n5" : cxp < 1.2 ? "g3" : "g2"; } else if (b.mat === "maw") { key = b.s === "top" ? "ink" : b.s === "endL" || b.s === "endR" ? "r0" : samePart(up) ? "v0" : "r0"; } else if (b.mat === "gum") { key = "r0"; } else if (b.mat === "palate") { // roof of the mouth: warm near the teeth, dark down the throat, a groove down the middle, ridges across const seam = (q) => q && q.s === b.s && q.k !== b.k; const deep = (b.y + HD) / D; // 0 at the front edge (teeth), 1 at the hinge (throat) const zone = deep < 0.3 ? 2 : deep < 0.62 ? 1 : 0; const ramp = ["ink", "v0", "r0", "r1"]; key = ramp[zone + 1]; if (seam(dn) || seam(rt) || Math.abs(b.x + 0.5) < 0.6) key = ramp[zone]; } keys[i] = key; } return keys;}const PARTS = ["body", "lid", "maw", "tongue", "eye", "teeth", "bands", "hinges", "handles", "caps", "lock"];function drawMimic(g, face, st, fx) { const fr = castMimic(face, st); const buf = fr.buf, keys = tone(buf, fx); let x0 = 47, x1 = 0, y0 = 47, y1 = 0; const seen = new Set(); // only declare the parts this view actually shows (an occluded part is not a silent one) for (let i = 0; i < 48 * 48; i++) if (buf[i]) { const x = i % 48, y = (i - x) / 48; x0 = Math.min(x0, x); x1 = Math.max(x1, x); y0 = Math.min(y0, y); y1 = Math.max(y1, y); seen.add(buf[i].part); } g.outlined("ink", () => { for (const part of PARTS) if (seen.has(part)) g.part(part, () => g.field({ x: [x0, x1], y: [y0, y1] }, (x, y) => { const b = buf[y * 48 + x]; return b && b.part === part ? keys[y * 48 + x] : null; })); }); return fr;}function 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;}function star(g, buf, x, y, depth, size) { const put = (px, py, k) => { if (visible(buf, px, py, depth, 1)) g.pixel(px, py, k); }; put(x, y, "n5"); for (const [dx, dy] of [[1, 0], [-1, 0], [0, 1], [0, -1]]) put(x + dx, y + dy, size > 1 ? "n5" : "n4"); if (size > 1) for (const [dx, dy] of [[2, 0], [-2, 0], [0, 2], [0, -2]]) put(x + dx, y + dy, "n4");}// ---- statesconst BITE = [ // th, sx, sz, hop, lunge, bulge [3.5, 1, 1, 0, 0, 0.04], [16, 0.98, 1.05, 0, 0, 0.05], // the lid starts to rise, the body stretches [50, 0.97, 1.07, 1, 0.6, 0.05], [WIDE, 1, 1.04, 2, 1.5, 0.06], // wide open, lunging: teeth rows, palate, tongue lashing up [WIDE + 2, 1, 1.02, 2, 1.5, 0.06], [30, 1, 1, 1, 1, 0.05], // snapping down [0, 1.07, 0.86, 0, 0.5, 0.09], // CHOMP: squash [0, 0.97, 1.05, 0, 0, 0.03], // rebound [2, 1.01, 0.98, 0, 0, 0.04], [3, 1, 1, 0, 0, 0.04],];const Mimic = sprite(pal, (g, t, state, face) => { contactShadow(g, 24, GROUND, FACINGS[face].shadow, 2); if (state === "idle") { const breath = 0.5 - 0.5 * Math.cos(TAU * t); const st = { th: 3.5 + 3 * breath, sx: 1, sz: 1, hop: 0, lunge: 0, bulge: 0.035 + 0.03 * breath, tongue: 0, sway: Math.sin(TAU * t) }; // a glint runs along the teeth left to right, then rests const gp = 10 + (t / 0.7) * 30; const fr = drawMimic(g, face, st, { glint: t < 0.7 ? (x, y) => (Math.abs(x + (y - 30) * 0.6 - gp) < 1.8 ? 1 : 0) : null, eyeGlint: t > 0.55 && t < 0.9, }); g.detached("glint", () => { if (t >= 0.7) return; // a star pops at the tip of each fang as the glint passes it for (const [c, , len] of UPPER_FRONT) { if (len < 3) continue; const [sx, sy, dep] = fr.proj(lidToChest([c, -HD - 0.3, H - len + 0.8], st.th)); const x = Math.round(sx), y = Math.round(sy); if (Math.abs(x + (y - 30) * 0.6 - gp) < 1.8) star(g, fr.buf, x, y, dep - 1, 1); } }); return; } const f = Math.min(N_BITE - 1, Math.round(t * N_BITE)); const [th, sx, sz, hop, lunge, bulge] = BITE[f]; const st = { th, sx, sz, hop, lunge, bulge, tongue: Math.min(1, th / WIDE), sway: Math.sin(TAU * t + 1) }; const fr = drawMimic(g, face, st, { eyeGlint: f >= 2 && f <= 4 }); if (f === 6) { // chomp: clack marks burst from the front corners of the seam g.detached("clack", () => { for (const s of [-1, 1]) { const [sx0, sy0, dep] = fr.proj([s * (HL + 1), -HD - 1, H + 0.5]); const x = Math.round(sx0), y = Math.round(sy0); for (let k = 1; k <= 2; k++) { for (const [dx, dy] of [[s * (k + 1), -k], [s * (k + 2), 0], [s * (k + 1), k]]) { if (visible(fr.buf, x + dx, y + dy, dep - 3, 0)) g.pixel(x + dx, y + dy, k === 1 ? "n5" : "n4"); } } } }); }});const FACES = ["down", "up", "left", "right"];submit(Mimic, { size: [48, 48], origin: "top-left", kind: "icon", scale: 1, animate: { fps: 10, duration: N_IDLE / 10, loop: true }, anchor: { x: 24, y: GROUND }, variants: [ ...FACES.map((f) => ({ name: `idle-${f}`, args: ["idle", f] })), ...FACES.map((f) => ({ name: `bite-${f}`, args: ["bite", f], animate: { fps: 10, duration: N_BITE / 10, loop: true } })), ],});掲載されたコードでは、ビルドスクリプトが自動付与する共通部分を省略しています。具体的には 1 行のランタイムヘッダー、共有パレット、および汎用ヘルパー関数(contactShadow、litIndex、flame、hash、chk、TAU)です。表示されている行番号は実際のファイルと一致しています。これらのプログラムは AIエージェントが生成し、描画されたコマの仕上がりを確認しながら修正を加えたものです。
エージェントに、こうしたプログラムを書かせる。
Tessarune は描画 API のリファレンス、25 冊の制作マニュアル、結果を確かめるための描画エンジンをエージェントに渡します。