Download live.js from IntelligenceLab/Terminal-Data-Website: direct link, hf CLI and curl.
- Browser
- Download file 28.2 kB
-
https://huggingface.co/spaces/IntelligenceLab/Terminal-Data-Website/resolve/main/live.js
- Command line
-
hf download hf://spaces/IntelligenceLab/Terminal-Data-Website/live.js
-
curl -L -o live.js https://huggingface.co/spaces/IntelligenceLab/Terminal-Data-Website/resolve/main/live.js
28.2 kB
| (() => { | |
| '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); | |
| } | |
| })(); | |