zli12321's picture
Deploy IntelligenceLab website
fadbe09 verified
Raw History Blame Contribute Delete
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);
}
})();