(() => { 'use strict'; const REDUCE = window.matchMedia('(prefers-reduced-motion: reduce)').matches; const SC = ['#0d9488', '#2563eb', '#7c3aed', '#db2777', '#d97706', '#059669']; const DRC = ['#fbbf24', '#2dd4bf', '#f472b6', '#60a5fa', '#a78bfa', '#34d399', '#fb7185', '#22d3ee']; const clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v)); const easeOut = (t) => 1 - Math.pow(1 - clamp(t, 0, 1), 3); const easeInOut = (t) => { const x = clamp(t, 0, 1); return x < 0.5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2; }; const fmt = (n) => n.toLocaleString('en-US'); const hexRgb = (h) => [1, 3, 5].map((i) => parseInt(h.slice(i, i + 2), 16)); function decode(b64, Type) { const bin = atob(b64); const u8 = new Uint8Array(bin.length); for (let i = 0; i < bin.length; i += 1) u8[i] = bin.charCodeAt(i); return new Type(u8.buffer); } function make(tag, cls, parent, text) { const node = document.createElement(tag); if (cls) node.className = cls; if (text != null) node.textContent = text; if (parent) parent.appendChild(node); return node; } // One animation loop drives every visible view. const active = new Set(); let raf = 0; function loop(now) { active.forEach((view) => { if (view.ready) view.render(now); }); raf = active.size && !document.hidden ? requestAnimationFrame(loop) : 0; } function wake() { if (!raf && active.size && !document.hidden) raf = requestAnimationFrame(loop); } document.addEventListener('visibilitychange', () => { if (document.hidden) { cancelAnimationFrame(raf); raf = 0; } else { wake(); } }); class View { constructor(box, { canvas = true, hero = false } = {}) { this.box = box; this.hero = hero; this.t0 = performance.now() - (REDUCE ? 600000 : 0); this.w = 1; this.h = 1; if (canvas) { this.canvas = make('canvas', 'lv-canvas', box); this.ctx = this.canvas.getContext('2d'); } } start() { this.ro = new ResizeObserver(() => this.resize()); this.ro.observe(this.box); this.io = new IntersectionObserver((entries) => { if (!REDUCE && entries.some((e) => e.isIntersecting)) { active.add(this); wake(); } else { active.delete(this); } }, { rootMargin: '60px' }); this.io.observe(this.box); this.resize(); return this; } resize() { const r = this.box.getBoundingClientRect(); this.w = Math.max(1, r.width); this.h = Math.max(1, r.height); if (this.canvas) { const dpr = Math.min(2, window.devicePixelRatio || 1); const W = Math.round(this.w * dpr); const H = Math.round(this.h * dpr); if (this.canvas.width !== W || this.canvas.height !== H) { this.canvas.width = W; this.canvas.height = H; } this.ctx.setTransform(dpr, 0, 0, dpr, 0, 0); } if (this.w < 8 || this.h < 8) return; this.layout(); this.ready = true; this.render(performance.now()); } layout() {} render() {} elapsed(now) { return now - this.t0; } caption(text) { if (!this.cap) this.cap = make('span', 'lv-cap', this.box); if (this.cap.textContent !== text) this.cap.textContent = text; } dispose() { active.delete(this); if (this.io) this.io.disconnect(); if (this.ro) this.ro.disconnect(); this.box.replaceChildren(); } } // OpenSCAD meshes: rotate, alternating public runs at one shared scale. class MeshLive extends View { constructor(box, v, opts) { super(box, opts); this.base = hexRgb(opts.color || '#7c3aed'); this.meshes = v.meshes.map((m) => { const T = decode(m.tris, Float32Array); const mn = [Infinity, Infinity, Infinity]; const mx = [-Infinity, -Infinity, -Infinity]; for (let i = 0; i < T.length; i += 3) { for (let k = 0; k < 3; k += 1) { const x = T[i + k]; if (x < mn[k]) mn[k] = x; if (x > mx[k]) mx[k] = x; } } return { id: m.id, T, n: T.length / 9, ctr: [0, 1, 2].map((k) => (mn[k] + mx[k]) / 2), rad: Math.hypot(mx[0] - mn[0], mx[1] - mn[1], mx[2] - mn[2]) / 2 || 1, }; }); this.rad = Math.max(...this.meshes.map((m) => m.rad)); const maxN = Math.max(...this.meshes.map((m) => m.n)); this.tmp = new Float32Array(9); this.depth = new Float32Array(maxN); this.order = new Uint32Array(maxN); this.pts = new Float32Array(maxN * 6); this.col = new Array(maxN); this.cycle = 6500; this.start(); } render(now) { const { ctx, w, h, tmp, pts, depth, order } = this; const t = this.elapsed(now); const mesh = this.meshes[Math.floor(t / this.cycle) % this.meshes.length]; const ph = (t % this.cycle) / this.cycle; const alpha = REDUCE ? 1 : clamp(Math.min(ph, 1 - ph) / 0.07, 0, 1); ctx.clearRect(0, 0, w, h); const m = Math.min(w, h); const ox = w / 2; const oy = h / 2; const shadow = ctx.createRadialGradient(ox, oy + h * 0.33, 0, ox, oy + h * 0.33, m * 0.45); shadow.addColorStop(0, 'rgba(15,23,42,.24)'); shadow.addColorStop(1, 'rgba(15,23,42,0)'); ctx.fillStyle = shadow; ctx.beginPath(); ctx.ellipse(ox, oy + h * 0.33, m * 0.45, m * 0.07, 0, 0, Math.PI * 2); ctx.fill(); const s = (m * 0.5) / this.rad; const yaw = -0.75 + t * 0.00045; const cy = Math.cos(yaw); const sy = Math.sin(yaw); const cp = Math.cos(0.42); const sp = Math.sin(0.42); const [mx, my, mz] = mesh.ctr; let lx = -0.45; let ly = -0.72; let lz = 0.55; const ll = Math.hypot(lx, ly, lz); lx /= ll; ly /= ll; lz /= ll; let hx = lx; let hy = ly - 1; let hz = lz; const hl = Math.hypot(hx, hy, hz); hx /= hl; hy /= hl; hz /= hl; const [br, bg, bb] = this.base; const { T, n } = mesh; for (let f = 0; f < n; f += 1) { const b = f * 9; for (let k = 0; k < 3; k += 1) { const x = T[b + 3 * k] - mx; const y = T[b + 3 * k + 1] - my; const z = T[b + 3 * k + 2] - mz; const x1 = x * cy - y * sy; const y1 = x * sy + y * cy; tmp[3 * k] = x1; tmp[3 * k + 1] = y1 * cp - z * sp; tmp[3 * k + 2] = y1 * sp + z * cp; } const ux = tmp[3] - tmp[0]; const uy = tmp[4] - tmp[1]; const uz = tmp[5] - tmp[2]; const vx = tmp[6] - tmp[0]; const vy = tmp[7] - tmp[1]; const vz = tmp[8] - tmp[2]; let nx = uy * vz - uz * vy; let ny = uz * vx - ux * vz; let nz = ux * vy - uy * vx; const nl = Math.hypot(nx, ny, nz) || 1; nx /= nl; ny /= nl; nz /= nl; if (ny > 0) { nx = -nx; ny = -ny; nz = -nz; } const dif = Math.max(0, nx * lx + ny * ly + nz * lz); const spec = Math.pow(Math.max(0, nx * hx + ny * hy + nz * hz), 30) * 140; const k = 0.2 + 0.8 * dif; this.col[f] = `rgb(${Math.min(255, br * k + spec) | 0},${Math.min(255, bg * k + spec) | 0},${Math.min(255, bb * k + spec) | 0})`; depth[f] = tmp[1] + tmp[4] + tmp[7]; const o = f * 6; pts[o] = ox + tmp[0] * s; pts[o + 1] = oy - tmp[2] * s; pts[o + 2] = ox + tmp[3] * s; pts[o + 3] = oy - tmp[5] * s; pts[o + 4] = ox + tmp[6] * s; pts[o + 5] = oy - tmp[8] * s; order[f] = f; } const sorted = order.subarray(0, n).sort((p, q) => depth[q] - depth[p]); ctx.globalAlpha = alpha; ctx.lineJoin = 'round'; ctx.lineWidth = 0.6; for (let j = 0; j < n; j += 1) { const f = sorted[j]; const o = f * 6; ctx.beginPath(); ctx.moveTo(pts[o], pts[o + 1]); ctx.lineTo(pts[o + 2], pts[o + 3]); ctx.lineTo(pts[o + 4], pts[o + 5]); ctx.closePath(); ctx.fillStyle = this.col[f]; ctx.strokeStyle = this.col[f]; ctx.fill(); ctx.stroke(); } ctx.globalAlpha = 1; this.caption(`${mesh.id} · ${fmt(mesh.n)} facets`); } } // OpenFOAM: the mesh draws in, then tracer particles flow inlet to outlet. class FoamLive extends View { constructor(box, v, opts) { super(box, opts); this.P = decode(v.points, Float32Array); this.Q = decode(v.quads, Uint16Array); const pal = { inlet: '#2563eb', outlet: '#d97706' }; this.patches = v.patches.map((p) => ({ E: decode(p.edges, Uint16Array), color: pal[p.name] || (p.type === 'wall' ? '#1e293b' : '#db2777'), })); let x0 = Infinity; let x1 = -Infinity; let y0 = Infinity; let y1 = -Infinity; for (let i = 0; i < this.P.length; i += 2) { const x = this.P[i]; const y = this.P[i + 1]; if (x < x0) x0 = x; if (x > x1) x1 = x; if (y < y0) y0 = y; if (y > y1) y1 = y; } this.bb = [x0, y0, x1, y1]; const bins = 160; const lo = new Float32Array(bins).fill(Infinity); const hi = new Float32Array(bins).fill(-Infinity); for (let i = 0; i < this.P.length; i += 2) { const b = clamp(Math.round(((this.P[i] - x0) / (x1 - x0)) * (bins - 1)), 0, bins - 1); lo[b] = Math.min(lo[b], this.P[i + 1]); hi[b] = Math.max(hi[b], this.P[i + 1]); } for (let b = 1; b < bins; b += 1) { if (!Number.isFinite(lo[b])) { lo[b] = lo[b - 1]; hi[b] = hi[b - 1]; } } this.bins = bins; this.lo = lo; this.hi = hi; this.maxSpan = Math.max(...Array.from(hi, (v2, b) => v2 - lo[b])); this.cells = v.n_cells; this.off = document.createElement('canvas'); this.parts = Array.from({ length: opts.hero ? 280 : 120 }, () => this.spawn(true)); this.last = 0; this.start(); } spawn(scatter) { return { f: scatter ? Math.random() : -Math.random() * 0.04, u: 0.05 + Math.random() * 0.9, v: 0.7 + Math.random() * 0.6, ph: Math.random() * 6.3 }; } layout() { const dpr = Math.min(2, window.devicePixelRatio || 1); const pad = this.hero ? 30 : 14; const [x0, y0, x1, y1] = this.bb; this.s = Math.min((this.w - 2 * pad) / (x1 - x0), (this.h - 2 * pad) / (y1 - y0)); this.ox = this.w / 2 - ((x0 + x1) / 2) * this.s; this.oy = this.h / 2 + ((y0 + y1) / 2) * this.s; const off = this.off; off.width = Math.round(this.w * dpr); off.height = Math.round(this.h * dpr); const c = off.getContext('2d'); c.setTransform(dpr, 0, 0, dpr, 0, 0); c.clearRect(0, 0, this.w, this.h); const { P, Q } = this; const X = (x) => this.ox + x * this.s; const Y = (y) => this.oy - y * this.s; c.lineWidth = 0.45; c.strokeStyle = 'rgba(5,150,105,.5)'; c.beginPath(); for (let q = 0; q < Q.length; q += 4) { c.moveTo(X(P[2 * Q[q]]), Y(P[2 * Q[q] + 1])); for (let k = 1; k < 4; k += 1) c.lineTo(X(P[2 * Q[q + k]]), Y(P[2 * Q[q + k] + 1])); c.closePath(); } c.stroke(); c.lineWidth = this.hero ? 2.4 : 1.6; for (const p of this.patches) { c.strokeStyle = p.color; c.beginPath(); for (let e = 0; e < p.E.length; e += 2) { c.moveTo(X(P[2 * p.E[e]]), Y(P[2 * p.E[e] + 1])); c.lineTo(X(P[2 * p.E[e + 1]]), Y(P[2 * p.E[e + 1] + 1])); } c.stroke(); } } at(p) { const [x0, , x1] = this.bb; const b = clamp(Math.round(clamp(p.f, 0, 1) * (this.bins - 1)), 0, this.bins - 1); const span = this.hi[b] - this.lo[b]; const y = this.lo[b] + span * (0.04 + 0.92 * p.u); return { x: this.ox + (x0 + clamp(p.f, 0, 1) * (x1 - x0)) * this.s, y: this.oy - y * this.s, span }; } render(now) { const { ctx, w, h } = this; const t = this.elapsed(now); const dt = clamp(now - (this.last || now), 0, 48); this.last = now; ctx.clearRect(0, 0, w, h); const reveal = REDUCE ? 1 : easeOut(t / 1500); ctx.save(); ctx.beginPath(); ctx.rect(0, 0, w * reveal, h); ctx.clip(); ctx.drawImage(this.off, 0, 0, w, h); ctx.restore(); this.caption(`${fmt(this.cells)} cells`); if (reveal < 1) return; ctx.lineCap = 'round'; ctx.lineWidth = this.hero ? 1.8 : 1.4; for (const p of this.parts) { const a = this.at(p); const speed = 0.00007 * p.v * (this.maxSpan / Math.max(a.span, 1e-6)) ** 0.7; p.f += speed * dt; if (p.f > 1) Object.assign(p, this.spawn(false)); const b = this.at(p); const wobble = Math.sin(t * 0.002 + p.ph) * 0.6; const len = clamp((b.x - a.x) * 5, 2, 14); ctx.strokeStyle = 'rgba(14,165,233,.75)'; ctx.beginPath(); ctx.moveTo(b.x - len, b.y + wobble); ctx.lineTo(b.x, b.y + wobble); ctx.stroke(); } } } // KiCad: a design-rule scan sweeps the board and lights up each finding. class DrcLive extends View { constructor(box, v, opts) { super(box, opts); this.dots = v.dots; let x0 = Infinity; let x1 = -Infinity; let y0 = Infinity; let y1 = -Infinity; for (const [x, y] of this.dots) { x0 = Math.min(x0, x); x1 = Math.max(x1, x); y0 = Math.min(y0, y); y1 = Math.max(y1, y); } const pad = 14; this.bb = [x0 - pad, y0 - pad, x1 + pad, y1 + pad]; this.sweepMs = 3200; this.cycle = 4600; this.start(); } layout() { const margin = this.hero ? 26 : 12; const [x0, y0, x1, y1] = this.bb; this.s = Math.min((this.w - 2 * margin) / (x1 - x0), (this.h - 2 * margin) / (y1 - y0)); this.bw = (x1 - x0) * this.s; this.bh = (y1 - y0) * this.s; this.bx = (this.w - this.bw) / 2; this.by = (this.h - this.bh) / 2; } render(now) { const { ctx, w, h, s, bx, by, bw, bh } = this; const [x0, y0] = this.bb; const t = this.elapsed(now) % this.cycle; const sweep = REDUCE ? 1.2 : t / this.sweepMs; const beam = bx + sweep * bw; ctx.clearRect(0, 0, w, h); ctx.save(); ctx.beginPath(); if (ctx.roundRect) ctx.roundRect(bx, by, bw, bh, 8); else ctx.rect(bx, by, bw, bh); ctx.fillStyle = '#05271f'; ctx.fill(); ctx.strokeStyle = 'rgba(52,211,153,.5)'; ctx.lineWidth = 1; ctx.stroke(); ctx.clip(); ctx.strokeStyle = 'rgba(94,234,212,.1)'; ctx.lineWidth = 0.6; ctx.beginPath(); const step = 10 * s; for (let gx = bx; gx < bx + bw; gx += step) { ctx.moveTo(gx, by); ctx.lineTo(gx, by + bh); } for (let gy = by; gy < by + bh; gy += step) { ctx.moveTo(bx, gy); ctx.lineTo(bx + bw, gy); } ctx.stroke(); if (sweep <= 1) { const band = ctx.createLinearGradient(beam - 46, 0, beam + 6, 0); band.addColorStop(0, 'rgba(45,212,191,0)'); band.addColorStop(1, 'rgba(45,212,191,.28)'); ctx.fillStyle = band; ctx.fillRect(beam - 46, by, 52, bh); ctx.fillStyle = 'rgba(153,246,228,.9)'; ctx.fillRect(beam, by, 1.5, bh); } ctx.restore(); let found = 0; const r = this.hero ? 3.4 : 2.6; for (const [x, y, g] of this.dots) { const px = bx + (x - x0) * s; const py = by + (y - y0) * s; const color = DRC[g % DRC.length]; const passed = px <= beam; if (passed) found += 1; ctx.globalAlpha = passed ? 1 : 0.22; ctx.fillStyle = color; ctx.beginPath(); ctx.arc(px, py, r, 0, Math.PI * 2); ctx.fill(); const since = ((beam - px) / bw) * this.sweepMs; if (passed && since < 700 && sweep <= 1) { const k = since / 700; ctx.globalAlpha = 1 - k; ctx.strokeStyle = color; ctx.lineWidth = 1.4; ctx.beginPath(); ctx.arc(px, py, r + 2 + k * 9, 0, Math.PI * 2); ctx.stroke(); } } ctx.globalAlpha = 1; this.caption(`DRC scan · ${found} findings`); } } // Container: layers build bottom-up, then the attestation is written. class ContainerLive extends View { constructor(box, v, opts) { super(box, { ...opts, canvas: false }); this.sc = v.scenarios; const wrap = make('div', 'lv-container', box); this.code = make('div', 'lv-code', wrap); this.stack = make('div', 'lv-stack', wrap); this.hash = make('div', 'lv-hash', wrap); this.layers = []; this.idx = -1; this.cycle = 3800; this.start(); } render(now) { const t = this.elapsed(now); const idx = REDUCE ? 0 : Math.floor(t / this.cycle) % this.sc.length; const lt = REDUCE ? this.cycle : t % this.cycle; const sc = this.sc[idx]; if (idx !== this.idx) { this.idx = idx; this.code.textContent = `FROM ${sc.base} · ${sc.id}`; this.layers = Array.from({ length: sc.layers }, (_, k) => { const el = make('div', 'lv-layer', null, k === sc.layers - 1 ? `L1 · base ${sc.base}` : `L${sc.layers - k}`); el.style.setProperty('--k', String(k)); return el; }); this.stack.replaceChildren(...this.layers); } const n = this.layers.length; this.layers.forEach((el, k) => el.classList.toggle('on', lt > 180 + (n - 1 - k) * 260)); const typeStart = 180 + n * 260 + 120; const chars = clamp(Math.floor((lt - typeStart) / 42), 0, sc.attestation.length); const cursor = chars > 0 && chars < sc.attestation.length ? '▍' : ''; const text = `attestation ${sc.attestation.slice(0, chars)}${cursor}`; if (this.hash.textContent !== text) this.hash.textContent = text; this.box.classList.toggle('lv-fade', !REDUCE && lt > this.cycle - 320); } } // Blender: rotation dial sweeps out to each scenario's evaluated rotation. class DialLive extends View { constructor(box, v, opts) { super(box, opts); this.scenes = v.scenes; this.cycle = 4800; this.start(); } render(now) { const { ctx, w, h } = this; const t = this.elapsed(now) % this.cycle; ctx.clearRect(0, 0, w, h); const cx = w / 2; const cy = h / 2 - (this.hero ? 0 : 8); const R = Math.min(w, h) * (this.hero ? 0.3 : 0.32); ctx.fillStyle = 'rgba(255,255,255,.92)'; ctx.strokeStyle = 'rgba(100,116,139,.35)'; ctx.lineWidth = 1.2; ctx.beginPath(); ctx.arc(cx, cy, R, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); ctx.strokeStyle = 'rgba(100,116,139,.45)'; for (let k = 0; k < 36; k += 1) { const a = (k / 36) * Math.PI * 2; const r1 = R - (k % 9 === 0 ? 10 : 5); ctx.lineWidth = k % 9 === 0 ? 1.4 : 0.9; ctx.beginPath(); ctx.moveTo(cx + R * Math.cos(a), cy - R * Math.sin(a)); ctx.lineTo(cx + r1 * Math.cos(a), cy - r1 * Math.sin(a)); ctx.stroke(); } this.scenes.forEach((sc, i) => { const local = t - i * 220; let k; if (REDUCE) k = 1; else if (local < 1400) k = easeInOut(local / 1400); else if (local < 3700) k = 1; else k = 1 - easeInOut((local - 3700) / 800); const a = sc.rotation_z * clamp(k, 0, 1); const color = SC[i % SC.length]; ctx.strokeStyle = color; ctx.lineCap = 'round'; ctx.lineWidth = 3; ctx.globalAlpha = 0.85; ctx.beginPath(); ctx.arc(cx, cy, R + 8 + i * 7, 0, -a, true); ctx.stroke(); ctx.globalAlpha = 1; const tip = R - 16; ctx.lineWidth = 3.2; ctx.beginPath(); ctx.moveTo(cx, cy); ctx.lineTo(cx + tip * Math.cos(a), cy - tip * Math.sin(a)); ctx.stroke(); ctx.fillStyle = color; ctx.beginPath(); ctx.arc(cx + tip * Math.cos(a), cy - tip * Math.sin(a), 5, 0, Math.PI * 2); ctx.fill(); }); ctx.fillStyle = '#0f172a'; ctx.beginPath(); ctx.arc(cx, cy, 6, 0, Math.PI * 2); ctx.fill(); this.caption(`rotation_z ${this.scenes.map((sc) => sc.rotation_z).join(' · ')} rad`); } } // MuJoCo: joint-position trajectories draw in across every public scenario. class LinesLive extends View { constructor(box, v, opts) { super(box, opts); this.series = v.series; const xs = this.series.flatMap((sr) => sr.step); const ys = this.series.flatMap((sr) => sr.qpos); this.x0 = Math.min(...xs); this.x1 = Math.max(...xs); let y0 = Math.min(...ys); let y1 = Math.max(...ys); const pad = (y1 - y0) * 0.1 || 1; this.y0 = y0 - pad; this.y1 = y1 + pad; this.cycle = 5200; this.start(); } render(now) { const { ctx, w, h } = this; const t = this.elapsed(now) % this.cycle; const p = REDUCE ? 1 : easeInOut(t / 2600); const fade = REDUCE || t < this.cycle - 500 ? 1 : (this.cycle - t) / 500; ctx.clearRect(0, 0, w, h); const L = this.hero ? 40 : 22; const Rm = this.hero ? 30 : 22; const T = this.hero ? 30 : 22; const B = this.hero ? 34 : 24; const X = (v) => L + ((v - this.x0) / ((this.x1 - this.x0) || 1)) * (w - L - Rm); const Y = (v) => T + (1 - (v - this.y0) / (this.y1 - this.y0)) * (h - T - B); ctx.strokeStyle = 'rgba(148,163,184,.25)'; ctx.lineWidth = 1; for (let k = 0; k <= 3; k += 1) { const y = T + (k / 3) * (h - T - B); ctx.beginPath(); ctx.moveTo(L, y); ctx.lineTo(w - Rm, y); ctx.stroke(); } const cursorX = L + p * (w - L - Rm); ctx.strokeStyle = 'rgba(15,23,42,.12)'; ctx.setLineDash([3, 4]); ctx.beginPath(); ctx.moveTo(cursorX, T - 6); ctx.lineTo(cursorX, h - B + 6); ctx.stroke(); ctx.setLineDash([]); ctx.globalAlpha = fade; ctx.lineCap = 'round'; ctx.lineJoin = 'round'; this.series.forEach((sr, i) => { const n = sr.step.length - 1; const reach = p * n; const color = SC[i % SC.length]; ctx.strokeStyle = color; ctx.lineWidth = this.hero ? 2.8 : 2.2; ctx.beginPath(); let hx = X(sr.step[0]); let hy = Y(sr.qpos[0]); ctx.moveTo(hx, hy); for (let j = 1; j <= n; j += 1) { if (j <= reach) { hx = X(sr.step[j]); hy = Y(sr.qpos[j]); ctx.lineTo(hx, hy); } else { const f = reach - (j - 1); if (f > 0) { hx = X(sr.step[j - 1]) + (X(sr.step[j]) - X(sr.step[j - 1])) * f; hy = Y(sr.qpos[j - 1]) + (Y(sr.qpos[j]) - Y(sr.qpos[j - 1])) * f; ctx.lineTo(hx, hy); } break; } } ctx.stroke(); ctx.fillStyle = color; ctx.beginPath(); ctx.arc(hx, hy, this.hero ? 4.2 : 3.2, 0, Math.PI * 2); ctx.fill(); }); ctx.globalAlpha = 1; this.caption(`qpos over the trajectory · ${this.series.length} public scenarios`); } } // Image tasks: public outputs cycle with a scan line or photometry aperture. class ImagesLive extends View { constructor(box, v, opts, mode) { super(box, { ...opts, canvas: false }); this.mode = mode; this.images = v.images; this.wrap = make('div', `lv-images lv-${mode}`, box); const slots = { micro: 3, astro: 4, ocr: 1, color: 6 }[mode]; this.slots = Array.from({ length: slots }, (_, j) => { const cell = make('div', 'lv-cell', this.wrap); const a = make('img', 'on', cell); const b = make('img', '', cell); [a, b].forEach((img) => { img.alt = ''; img.decoding = 'async'; }); const first = this.images[j % this.images.length]; a.src = first.src; if (mode === 'astro') make('span', 'lv-aperture', cell); if (mode === 'ocr' || mode === 'micro') make('span', 'lv-scan', cell); if (mode === 'color') { const sw = make('span', 'lv-swatch', cell); sw.style.background = first.m; } return { cell, layers: [a, b], front: 0, idx: j % this.images.length }; }); this.period = mode === 'ocr' ? 3200 : 2600; this.start(); } show(slot, idx) { if (slot.idx === idx) return; slot.idx = idx; const back = slot.layers[1 - slot.front]; back.src = this.images[idx].src; back.classList.add('on'); slot.layers[slot.front].classList.remove('on'); slot.front = 1 - slot.front; } render(now) { const t = this.elapsed(now); const n = this.images.length; if (this.mode === 'color') { const hot = REDUCE ? 0 : Math.floor(t / 900) % n; this.slots.forEach((slot, j) => slot.cell.classList.toggle('hot', j === hot)); const im = this.images[hot]; this.caption(`${im.id} · mean swatch ${im.m}`); return; } this.slots.forEach((slot, j) => { const step = REDUCE ? 0 : Math.floor((t + j * 450) / this.period); this.show(slot, (j + step) % n); }); const im = this.images[this.slots[0].idx]; if (this.mode === 'ocr') this.caption(`${im.id} · ${im.m} text lines`); else if (this.mode === 'micro') this.caption(`${im.id} · ${im.m} objects segmented`); else this.caption(`${im.id} · flux ${im.m.toFixed(1)}`); } } const RENDERERS = { stl: (box, v, o) => new MeshLive(box, v, o), foam: (box, v, o) => new FoamLive(box, v, o), drc: (box, v, o) => new DrcLive(box, v, o), container: (box, v, o) => new ContainerLive(box, v, o), blender: (box, v, o) => new DialLive(box, v, o), robot: (box, v, o) => new LinesLive(box, v, o), micro: (box, v, o) => new ImagesLive(box, v, o, 'micro'), astro: (box, v, o) => new ImagesLive(box, v, o, 'astro'), ocr: (box, v, o) => new ImagesLive(box, v, o, 'ocr'), color: (box, v, o) => new ImagesLive(box, v, o, 'color'), }; function mount(box, kind, opts = {}) { const data = window.IL_VISUALS && window.IL_VISUALS[kind]; const build = RENDERERS[kind]; if (!data || !build) return null; box.dataset.kind = kind; try { return build(box, data, opts); } catch (err) { box.replaceChildren(); console.error(`live preview failed for ${kind}`, err); return null; } } function mountCards() { document.querySelectorAll('.ex-card[data-kind]').forEach((card) => { const thumb = card.querySelector('.ex-thumb'); if (!thumb || thumb.querySelector('.ex-live')) return; const box = make('div', 'ex-live', thumb); box.setAttribute('aria-hidden', 'true'); const color = getComputedStyle(card).getPropertyValue('--dc').trim(); if (mount(box, card.dataset.kind, { color })) { requestAnimationFrame(() => thumb.classList.add('is-live')); } else { box.remove(); } }); } function loadData() { if (window.IL_VISUALS) return Promise.resolve(); return new Promise((resolve, reject) => { const script = document.createElement('script'); script.src = 'assets/examples/visuals.js'; script.async = true; script.onload = resolve; script.onerror = reject; document.head.appendChild(script); }); } const api = { ready: false, mount }; window.ILLive = api; const section = document.getElementById('examples'); if (!section) return; const go = () => loadData() .then(() => { api.ready = true; mountCards(); document.dispatchEvent(new CustomEvent('il-live-ready')); }) .catch(() => {}); if (!('IntersectionObserver' in window)) { go(); } else { const io = new IntersectionObserver((entries) => { if (!entries.some((e) => e.isIntersecting)) return; io.disconnect(); go(); }, { rootMargin: '900px 0px' }); io.observe(section); } })();