// Generated by tools/build_hf_space.py from website/app.js. // Edit the website component, then regenerate this file. (() => { 'use strict'; const D = {"repack":{"seeds":[13,29,41,53,67,83],"ax0":[0.8232041876661003,0.8233935994406542,0.8219183080748164,0.8238790235613092,0.8301391905794541,0.8255466957887013],"ax1":[0.8232724456635336,0.8233549147844315,0.8223302837069276,0.823957710136404,0.8301796226451794,0.8256099889365335],"pk0":[0.8253415346145629,0.8195828790465991,0.825005172441403,0.8185641605406999,0.8264244556427002,0.8223262195204751],"pk1":[0.7731130219375094,0.7535358890270193,0.7723392908771832,0.7799754473112029,0.7801123619700472,0.7906655114804761],"chg_ax":[0.0059668223063151045,0.008325576782226562,0.0075499216715494795,0.006144841512044271,0.0062719980875651045,0.00698089599609375],"chg_pk":[1.1211490631103516,1.2609418233235676,1.41387939453125,1.150795618693034,1.2623087565104167,1.1498578389485676],"gap_training":-0.001806097217432523}}; const $ = (s, el = document) => el.querySelector(s); const $$ = (s, el = document) => Array.from(el.querySelectorAll(s)); const RM = window.matchMedia('(prefers-reduced-motion: reduce)').matches; // say "tap" where nothing can hover const HOVERLESS = window.matchMedia('(hover: none)').matches; const MINUS = '−'; const tok = (n) => getComputedStyle(document.documentElement).getPropertyValue(n).trim(); const isDark = () => { const t = document.documentElement.getAttribute('data-theme'); if (t === 'dark' || t === 'light') return t === 'dark'; return window.matchMedia('(prefers-color-scheme: dark)').matches; }; const sgn = (v, d) => (v < 0 ? MINUS : '+') + Math.abs(v).toFixed(d); const fix = (v, d) => (v < 0 ? MINUS : '') + Math.abs(v).toFixed(d); const mean = (a) => a.reduce((s, v) => s + v, 0) / a.length; const intf = (v) => Math.round(v).toLocaleString('en-US'); const clamp = (v, a, b) => Math.max(a, Math.min(b, v)); // easeOut answers the reader, as --ease does; ease is for motion nobody asked for const ease = (t) => (t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2); const easeOut = (t) => 1 - Math.pow(1 - t, 5); function hex2rgb(h) { h = h.replace('#', ''); if (h.length === 3) h = h.split('').map((c) => c + c).join(''); return [0, 2, 4].map((i) => parseInt(h.slice(i, i + 2), 16) / 255); } const toLin = (c) => (c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4)); const toGam = (c) => (c <= 0.0031308 ? 12.92 * c : 1.055 * Math.pow(c, 1 / 2.4) - 0.055); function rgb2lab(rgb) { const [r, g, b] = rgb.map(toLin); const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b); const m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b); const s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b); return [0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s, 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s, 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s]; } function lab2rgb(lab) { const [L, a, b] = lab; const l = Math.pow(L + 0.3963377774 * a + 0.2158037573 * b, 3); const m = Math.pow(L - 0.1055613458 * a - 0.0638541728 * b, 3); const s = Math.pow(L - 0.0894841775 * a - 1.291485548 * b, 3); return [4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s, -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s, -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s].map((c) => clamp(toGam(clamp(c, 0, 1)), 0, 1)); } const lab = (hex) => rgb2lab(hex2rgb(hex)); const mixLab = (a, b, t) => [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t]; const css = (labv) => { const c = lab2rgb(labv); return `rgb(${Math.round(c[0] * 255)},${Math.round(c[1] * 255)},${Math.round(c[2] * 255)})`; }; function ramp(hexes) { const L = hexes.map(lab); return (t) => { t = clamp(t, 0, 1); const x = t * (L.length - 1), i = Math.min(L.length - 2, Math.floor(x)); return mixLab(L[i], L[i + 1], x - i); }; } const RAMPS = { geo: () => ramp(isDark() ? ['#0C3B32', '#0D6B58', '#18A385', '#62D7BC', '#C6F4E9'] : ['#E2F6F0', '#9DE2D0', '#3DC6A6', '#009078', '#00493A']), // memory order (--mem-0 to --mem-4): the opening and the end mem: () => ramp([0, 1, 2, 3, 4].map((i) => tok('--mem-' + i))), }; // "r,g,b", for a canvas color with an alpha of its own const rgbOf = (hex) => hex2rgb(hex).map((c) => Math.round(c * 255)).join(','); const motion = (() => { let paused = RM; try { if (!RM && localStorage.getItem('pack3d-motion') === 'paused') paused = true; } catch (e) { /* storage blocked */ } const cbs = []; const label = () => $$('[data-motion]').forEach((b) => { b.textContent = paused ? 'Play motion' : 'Pause motion'; }); const api = { get paused() { return paused; }, set(p) { p = !!p; if (p === paused) return; paused = p; try { localStorage.setItem('pack3d-motion', p ? 'paused' : 'on'); } catch (e) { /* storage blocked */ } label(); cbs.forEach((f) => f(p)); }, on(cb) { cbs.push(cb); }, }; document.addEventListener('click', (ev) => { if (ev.target.closest('[data-motion]')) api.set(!paused); }); label(); return api; })(); const themeCbs = []; function onTheme(cb) { themeCbs.push(cb); } const themeChanged = () => themeCbs.forEach((f) => { try { f(); } catch (e) { console.error(e); } }); window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', themeChanged); const themeWatch = new MutationObserver(themeChanged); themeWatch.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] }); // set the page's theme ('light', 'dark', or null for the system's), every figure repainted before this returns function setTheme(t) { const h = document.documentElement; if (h.getAttribute('data-theme') === t) return; if (t === null) h.removeAttribute('data-theme'); else h.setAttribute('data-theme', t); themeWatch.takeRecords(); themeChanged(); } // ====================================================================== device pixel ratio changes // a window dragged to a screen of another density keeps its CSS size, so no ResizeObserver fires const dprCbs = []; function onDpr(cb) { dprCbs.push(cb); } (function watchDpr() { const mq = window.matchMedia(`(resolution: ${window.devicePixelRatio || 1}dppx)`); const f = () => { mq.removeEventListener('change', f); dprCbs.forEach((cb) => cb()); watchDpr(); }; mq.addEventListener('change', f); })(); function initRepackStage() { const stage = $('#repackStage'), cellsEl = stage && $('.sw-cells', stage); let canvas = $('#swCanvas'); if (!stage || !cellsEl || !canvas) return; const R = D.repack, G = 64, NV = G * G * G; const rateAx = mean(R.chg_ax), ratePk = mean(R.chg_pk); const nAx = Math.round(rateAx / 100 * NV), nPk = Math.round(ratePk / 100 * NV); $('#swRA').textContent = `${rateAx.toFixed(4)}%`; $('#swRB').textContent = `${ratePk.toFixed(2)}%`; canvas.setAttribute('aria-label', `Two copies of one cell, ${intf(NV)} positions each, under the same repack. About ${nAx} predictions change under PACK3D, each circled, and about ${intf(nPk)} under packed RoPE. ${HOVERLESS ? 'Swipe sideways' : 'Drag'}, or use the arrow keys, to turn them.`); const go = $('#swGo'), stateEl = $('#swState'), stateT = $('#swStateT'), stateX = $('#swStateX'), cA = $('#swNA'), cB = $('#swNB'); const cells = [$('#swA'), $('#swB')]; let strips = [$('#swStripA'), $('#swStripB')]; cA.dataset.max = intf(nAx); cB.dataset.max = intf(nPk); // each count is as wide as its final value (the CSS reads it) // the theme's ink and the changes' colors, read again when the theme changes; a token that is not a hex color keeps // the light page's const hexOr = (c, alt) => { const v = hex2rgb(c || ''); return v.every((x) => x >= 0) ? v : alt; }; let INK = true, INKC = [0.067, 0.067, 0.067], PAPER = [1, 1, 1], SPARK = null; const colors = () => { INK = !isDark(); INKC = hexOr(tok('--ink'), [0.067, 0.067, 0.067]); PAPER = hexOr(tok('--surface'), [1, 1, 1]); SPARK = { ax: hexOr(tok('--spark-a'), [0, 0.584, 0.478]), pk: hexOr(tok('--spark-b'), [0.878, 0.31, 0.333]) }; }; colors(); // a new canvas in place of an old one, focus kept, and the old store let go at once (iOS counts a detached canvas // against its memory until it is collected) function swap(old) { const c = old.cloneNode(false), had = document.activeElement === old; old.replaceWith(c); old.width = old.height = 0; if (had) c.focus({ preventScroll: true }); return c; } // ---------------------------------------------------------------- a cell, as a segmented volume (made once) // It runs in a worker, from its own source, so it uses nothing from outside itself. Square roots, not // Math.hypot, whose rounding differs between engines: the same seed gives the same cell and sparks everywhere. function makeScene(G, nAx, nPk) { const NV = G * G * G; let seed = 7; const rnd = () => { seed = (seed + 0x6D2B79F5) | 0; let t = Math.imul(seed ^ (seed >>> 15), 1 | seed); t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; const wob = (x, y, z) => 0.5 * Math.sin(3.1 * x + 1.7) * Math.sin(2.6 * y + 0.4) * Math.sin(2.9 * z + 2.1) + 0.3 * Math.sin(5.3 * x + 1.1 * y + 2.0) * Math.cos(4.1 * z - 0.7 * y); const cls = new Uint8Array(NV); // 0 outside, 1 cytoplasm, 2 nucleus, 3 organelle const at = (x, y, z) => (z * G + y) * G + x, P = (v) => (v - G / 2 + 0.5) / (G / 2); const len = (a, b, c) => Math.sqrt(a * a + b * b + c * c); // the cell and its nucleus, two wobbly ellipsoids; the wobble moves their surfaces by less than 0.0561 // and 0.0401, so it is worked out only near them for (let z = 0; z < G; z++) for (let y = 0; y < G; y++) for (let x = 0; x < G; x++) { const px = P(x), py = P(y), pz = P(z), d = len(px / 0.93, py / 0.74, pz / 0.66); if (d >= 1.0561 || (d >= 0.9439 && d + 0.07 * wob(px, py, pz) >= 1)) continue; const e = len((px - 0.16) / 0.42, (py + 0.04) / 0.35, (pz - 0.02) / 0.36); cls[at(x, y, z)] = e < 0.9599 || (e < 1.0401 && e + 0.05 * wob(py, pz, px) < 1) ? 2 : 1; } for (let m = 0; m < 40; m++) { // mitochondria: short walks through the cytoplasm let x = 0, y = 0, z = 0, tries = 0; do { x = 4 + Math.floor(rnd() * (G - 8)); y = 4 + Math.floor(rnd() * (G - 8)); z = 4 + Math.floor(rnd() * (G - 8)); } while (cls[at(x, y, z)] !== 1 && ++tries < 300); let dx = rnd() - 0.5, dy = rnd() - 0.5, dz = (rnd() - 0.5) * 0.6; for (let s = 0; s < 14; s++) { const i = at(Math.round(x), Math.round(y), Math.round(z)); if (cls[i] !== 1) break; cls[i] = 3; dx += (rnd() - 0.5) * 0.5; dy += (rnd() - 0.5) * 0.5; const l = len(dx, dy, dz) || 1; x += dx / l; y += dy / l; z += (dz / l) * 0.8; } } // the classes counted along each row of G positions in memory, for the memory strips: a strip's pixels then mix // 4,096 rows on the main thread instead of reading all 262,144 positions const rows = new Uint8Array(NV / G * 4); for (let r = 0, i = 0; r < NV / G; r++) for (let x = 0; x < G; x++, i++) rows[r * 4 + cls[i]]++; // the six neighbours on the flat index (x ± 1, y ± G, z ± G²); past a face of the volume is outside const GG = G * G, M = G - 1; const differs = (i, x, y, z, c) => (x < M ? cls[i + 1] !== c : c !== 0) || (x > 0 ? cls[i - 1] !== c : c !== 0) || (y < M ? cls[i + G] !== c : c !== 0) || (y > 0 ? cls[i - G] !== c : c !== 0) || (z < M ? cls[i + GG] !== c : c !== 0) || (z > 0 ? cls[i - GG] !== c : c !== 0); const touches = (i, x, y, z, c) => (x < M && cls[i + 1] === c) || (x > 0 && cls[i - 1] === c) || (y < M && cls[i + G] === c) || (y > 0 && cls[i - G] === c) || (z < M && cls[i + GG] === c) || (z > 0 && cls[i - GG] === c); const memb = [], env = [], edge = [], pos = [], col = [], size = [], kind = []; const C = { cyto: [0.42, 0.30, 0.86], memb: [0.66, 0.54, 1.0], nuc: [0.08, 0.62, 0.52], env: [0.30, 0.92, 0.76], mito: [1.0, 0.86, 0.78] }; const KINDS = [[C.memb, 0.36], [C.cyto, 0.03], [C.env, 0.26], [C.nuc, 0.085], [C.mito, 0.4]]; const put = (x, y, z, kd, s) => { const j = () => (rnd() - 0.5) * 0.9 / (G / 2); const [rgb, k] = KINDS[kd]; pos.push(P(x) + j(), P(y) + j(), P(z) + j()); col.push(rgb[0] * k, rgb[1] * k, rgb[2] * k); size.push(s); kind.push(kd); }; for (let z = 0, i = 0; z < G; z++) for (let y = 0; y < G; y++) for (let x = 0; x < G; x++, i++) { const c = cls[i]; if (c === 0) { if (differs(i, x, y, z, 0)) edge.push([x, y, z]); continue; } if (c === 1) { if (differs(i, x, y, z, 1) && !touches(i, x, y, z, 2) && !touches(i, x, y, z, 3)) { memb.push([x, y, z]); if (rnd() < 0.55) put(x, y, z, 0, 1.5); } else if (rnd() < 0.08) put(x, y, z, 1, 3.4); } else if (c === 2) { if (differs(i, x, y, z, 2)) { env.push([x, y, z]); if (rnd() < 0.6) put(x, y, z, 2, 1.6); } else if (rnd() < 0.2) put(x, y, z, 3, 2.6); } else put(x, y, z, 4, 1.4); } // the predictions the repack changes, at the paper's rates over all 262,144 positions const pick = (list, k) => { const a = list.slice(), out = []; for (let i = 0; i < k && a.length; i++) out.push(a.splice(Math.floor(rnd() * a.length), 1)[0]); return out; }; const sparksPk = pick(memb.concat(env, edge), nPk), sparksAx = pick(env, nAx); const sparkSet = (list) => { const p = new Float32Array(list.length * 3), t = new Float32Array(list.length); list.forEach(([x, y, z], i) => { p.set([P(x), P(y), P(z)], i * 3); t[i] = rnd() * 0.75; }); return { n: list.length, pos: p, t }; }; return { rows, NP: size.length, pos: new Float32Array(pos), col: new Float32Array(col), size: new Float32Array(size), kind: new Uint8Array(kind), kinds: KINDS.map(([rgb, k]) => rgb.map((v) => v * k)), pk: sparkSet(sparksPk), ax: sparkSet(sparksAx), }; } // ---------------------------------------------------------------- the camera (shared by both renderers) const persp = (f, a, nn, fa) => { const t = 1 / Math.tan(f / 2); return [t / a, 0, 0, 0, 0, t, 0, 0, 0, 0, (fa + nn) / (nn - fa), -1, 0, 0, 2 * fa * nn / (nn - fa), 0]; }; const mul = (a, b) => { const o = new Array(16).fill(0); for (let c = 0; c < 4; c++) for (let r = 0; r < 4; r++) for (let k = 0; k < 4; k++) o[c * 4 + r] += a[k * 4 + r] * b[c * 4 + k]; return o; }; const viewM = (yaw, pitch, d) => { const cy = Math.cos(yaw), sy = Math.sin(yaw), cp = Math.cos(pitch), sp = Math.sin(pitch); return [cy, sp * sy, -cp * sy, 0, 0, cp, sp, 0, sy, -sp * cy, cp * cy, 0, 0, 0, -d, 1]; }; // one cell's viewport, in device pixels from the canvas's top left, and its camera function camera(v, dpr, yaw, pitch) { const w = v.w * dpr, h = v.h * dpr, dist = 3.45 / Math.min(1, w / h); return { x: v.x * dpr, y: v.y * dpr, w, h, dist, mvp: mul(persp(0.62, w / h, 0.1, 40), viewM(yaw, pitch, dist)), vox: (2 / G) * (h / 2) / Math.tan(0.31) }; } // ---------------------------------------------------------------- WebGL // u_ink 1: ink on the light page (v_col the hue laid, v_w its weight); 0: light on the dark one (v_col the light). // u_mode 0 the cell, 1 the changes, 2 their rings, 3 the paper disc under a ring. The mode reaches the fragment // shader as varyings: a uniform declared in both shaders must match their precisions. const VS = `attribute vec3 a_pos; attribute vec3 a_col; attribute float a_size; attribute float a_t; uniform mat4 u_mvp; uniform float u_vox, u_k, u_time, u_mode, u_dist, u_maxpt, u_ink; uniform vec3 u_spark, u_inkc; varying vec3 v_col; varying float v_ring, v_w, v_ink; void main() { vec4 p = u_mvp * vec4(a_pos, 1.0); gl_Position = p; float fog = clamp((p.w - u_dist + 0.95) / 1.9, 0.0, 1.0); float size; v_ink = u_ink; if (u_mode < 0.5) { // the cell in one tone: the light a point gives on the dark page is the ink it lays on the light one v_w = dot(a_col, vec3(0.2126, 0.7152, 0.0722)) * 1.5 * mix(1.2, 0.5, fog); v_col = u_ink > 0.5 ? u_inkc : vec3(v_w); v_ring = 0.0; size = a_size * u_vox / p.w; } else { float d = (u_k - a_t) * 7.0; // d * d, not pow(d, 2.0): pow is undefined for d < 0 float k = clamp((u_k - a_t) * 3.0, 0.0, 1.0); float flare = 1.0 + 1.6 * exp(-d * d) * step(a_t, u_k); float tw = 0.85 + 0.15 * sin(u_time * 5.0 + a_t * 40.0); v_w = k * tw * flare * mix(1.0, 0.6, fog); v_col = u_ink > 0.5 ? u_spark : u_spark * v_w; v_ring = u_mode > 2.5 ? 2.0 : u_mode > 1.5 ? 1.0 : 0.0; size = (u_mode > 2.5 ? 5.8 : u_mode > 1.5 ? 5.2 : (1.5 + 1.1 * (flare - 1.0))) * u_vox / p.w * k; } gl_PointSize = min(size, u_maxpt); }`; const FS = `precision mediump float; varying vec3 v_col; varying float v_ring, v_w, v_ink; void main() { float r = length(gl_PointCoord - 0.5) * 2.0; float a = v_ring > 1.5 ? 1.0 - smoothstep(0.8, 0.98, r) : v_ring > 0.5 ? smoothstep(0.74, 0.82, r) * (1.0 - smoothstep(0.88, 0.98, r)) * 0.95 : exp(-r * r * 4.0) * step(r, 1.0); if (v_ink > 0.5) { float al = clamp(v_w * a, 0.0, 1.0); // a point: what it takes from the paper, multiplied in; a ring or its disc: laid over, premultiplied gl_FragColor = v_ring > 0.5 ? vec4(v_col * al, al) : vec4((1.0 - v_col) * al, 1.0); } else { vec3 c = v_col * a; gl_FragColor = vec4(c, max(c.r, max(c.g, c.b))); // valid premultiplied light over the dark page } }`; const GL_ATTRS = { alpha: true, premultipliedAlpha: true, antialias: false, depth: false, stencil: false, preserveDrawingBuffer: false, powerPreference: 'low-power' }; // a WebGL renderer on canvas cv, or null. setup() can run again on the same context after a restore. function glRenderer(cv, S) { let gl = null; try { gl = cv.getContext('webgl', GL_ATTRS) || cv.getContext('experimental-webgl', GL_ATTRS); } catch (e) { gl = null; } if (!gl || gl.isContextLost()) return null; let X = null; function setup() { const sh = (t, s) => { const o = gl.createShader(t); gl.shaderSource(o, s); gl.compileShader(o); if (!gl.getShaderParameter(o, gl.COMPILE_STATUS) && !gl.isContextLost()) throw new Error(gl.getShaderInfoLog(o)); return o; }; const prog = gl.createProgram(); gl.attachShader(prog, sh(gl.VERTEX_SHADER, VS)); gl.attachShader(prog, sh(gl.FRAGMENT_SHADER, FS)); gl.linkProgram(prog); if (!gl.getProgramParameter(prog, gl.LINK_STATUS) && !gl.isContextLost()) throw new Error(gl.getProgramInfoLog(prog)); gl.useProgram(prog); const U = {}; ['u_mvp', 'u_vox', 'u_k', 'u_time', 'u_mode', 'u_dist', 'u_spark', 'u_maxpt', 'u_ink', 'u_inkc'].forEach((n) => { U[n] = gl.getUniformLocation(prog, n); }); const buf = (arr, n, name) => { const b = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, b); gl.bufferData(gl.ARRAY_BUFFER, arr, gl.STATIC_DRAW); return { b, n, l: gl.getAttribLocation(prog, name) }; }; const set = (p, t, n) => [buf(p, 3, 'a_pos'), buf(new Float32Array(n * 3), 3, 'a_col'), buf(new Float32Array(n).fill(1), 1, 'a_size'), buf(t, 1, 'a_t')]; X = { U, cell: [buf(S.pos, 3, 'a_pos'), buf(S.col, 3, 'a_col'), buf(S.size, 1, 'a_size'), buf(new Float32Array(S.NP), 1, 'a_t')], pk: set(S.pk.pos, S.pk.t, S.pk.n), ax: set(S.ax.pos, S.ax.t, S.ax.n), maxpt: (gl.getParameter(gl.ALIASED_POINT_SIZE_RANGE) || [1, 64])[1], }; gl.disable(gl.DEPTH_TEST); gl.enable(gl.BLEND); gl.blendFunc(gl.ONE, gl.ONE); gl.clearColor(0, 0, 0, 0); } try { setup(); } catch (e) { console.warn('repack stage: WebGL setup failed', e); return null; } const bind = (bs) => bs.forEach((x) => { if (x.l < 0) return; gl.bindBuffer(gl.ARRAY_BUFFER, x.b); gl.enableVertexAttribArray(x.l); gl.vertexAttribPointer(x.l, x.n, gl.FLOAT, false, 0, 0); }); return { lost: () => gl.isContextLost(), dispose() { const x = gl.getExtension('WEBGL_lose_context'); if (x && !gl.isContextLost()) x.loseContext(); }, // free the GPU now, not at GC restore() { try { setup(); return true; } catch (e) { console.warn('repack stage: WebGL restore failed', e); return false; } }, draw(f) { const { U } = X; gl.viewport(0, 0, cv.width, cv.height); // ink: the sheet's paper, opaque, that each point multiplies (its alpha kept at one); light: nothing, added to if (INK) gl.clearColor(PAPER[0], PAPER[1], PAPER[2], 1); else gl.clearColor(0, 0, 0, 0); gl.clear(gl.COLOR_BUFFER_BIT); const mult = () => (INK ? gl.blendFuncSeparate(gl.ZERO, gl.ONE_MINUS_SRC_COLOR, gl.ZERO, gl.ONE) : gl.blendFunc(gl.ONE, gl.ONE)); const over = () => (INK ? gl.blendFuncSeparate(gl.ONE, gl.ONE_MINUS_SRC_ALPHA, gl.ZERO, gl.ONE) : gl.blendFunc(gl.ONE, gl.ONE)); gl.uniform1f(U.u_ink, INK ? 1 : 0); gl.uniform3f(U.u_inkc, ...INKC); f.views.forEach((v, side) => { const c = camera(v, f.dpr, f.yaw, f.pitch); if (c.w < 2 || c.h < 2) return; gl.viewport(c.x, cv.height - c.y - c.h, c.w, c.h); gl.uniformMatrix4fv(U.u_mvp, false, c.mvp); gl.uniform1f(U.u_dist, c.dist); gl.uniform1f(U.u_vox, c.vox); gl.uniform1f(U.u_maxpt, X.maxpt); gl.uniform1f(U.u_time, (f.now / 1000) % 1256.6370614359173); // a whole number of twinkles, so the wrap is seamless mult(); gl.uniform1f(U.u_mode, 0); bind(X.cell); gl.drawArrays(gl.POINTS, 0, S.NP); gl.uniform1f(U.u_k, f.kSpark); if (side === 1) { gl.uniform3f(U.u_spark, ...SPARK.pk); gl.uniform1f(U.u_mode, 1); bind(X.pk); gl.drawArrays(gl.POINTS, 0, S.pk.n); } else { bind(X.ax); // circled: one in 14,000 is too small to see. On paper each stands on a disc of the paper, so it reads if (INK) { over(); gl.uniform3f(U.u_spark, ...PAPER); gl.uniform1f(U.u_mode, 3); gl.drawArrays(gl.POINTS, 0, S.ax.n); } mult(); gl.uniform3f(U.u_spark, ...SPARK.ax); gl.uniform1f(U.u_mode, 1); gl.drawArrays(gl.POINTS, 0, S.ax.n); over(); gl.uniform1f(U.u_mode, 2); gl.drawArrays(gl.POINTS, 0, S.ax.n); } }); }, }; } // ---------------------------------------------------------------- Canvas 2D: the same points, for a reader without WebGL // drawn only when something changes, so the figure at rest costs nothing function ctx2dRenderer(cv, S) { const g = cv.getContext('2d'); if (!g) return null; const SPR = 32; // the fragment shader, at 32 x 32: light (rgb holding its strength), premultiplied by hand, or ink (rgb the hue, w // the weight). shape: a soft dot, a ring (true) or a disc ('disc') const bake = (rgb, ring, w) => { const c = document.createElement('canvas'); c.width = c.height = SPR; const x = c.getContext('2d'), img = x.createImageData(SPR, SPR); for (let j = 0; j < SPR; j++) for (let i = 0; i < SPR; i++) { const r = Math.hypot((i + 0.5) / SPR - 0.5, (j + 0.5) / SPR - 0.5) * 2; const ss = (a, b, v) => { const t = clamp((v - a) / (b - a), 0, 1); return t * t * (3 - 2 * t); }; const a = ring === 'disc' ? 1 - ss(0.8, 0.98, r) : ring ? ss(0.74, 0.82, r) * (1 - ss(0.88, 0.98, r)) * 0.95 : (r <= 1 ? Math.exp(-r * r * 4) : 0); if (w !== undefined) { const al = Math.min(1, w * a); if (al > 0) img.data.set([rgb[0] * 255, rgb[1] * 255, rgb[2] * 255, al * 255], (j * SPR + i) * 4); continue; } const p = rgb.map((v) => Math.min(1, v * a)), al = Math.max(...p); if (al > 0) img.data.set([p[0] / al * 255, p[1] / al * 255, p[2] / al * 255, al * 255], (j * SPR + i) * 4); } x.putImageData(img, 0, 0); return c; }; // the cell in one tone, as the shader has it const tone = (rgb) => (0.2126 * rgb[0] + 0.7152 * rgb[1] + 0.0722 * rgb[2]) * 1.5 * 1.2; let cellSpr = null, pkSpr = null, axSpr = null, ringSpr = null, discSpr = null; const project = (m, x, y, z) => { const w = m[3] * x + m[7] * y + m[11] * z + m[15]; return [(m[0] * x + m[4] * y + m[8] * z + m[12]) / w, (m[1] * x + m[5] * y + m[9] * z + m[13]) / w, w]; }; function dots(c, pos, n, each) { for (let i = 0; i < n; i++) { const [nx, ny, w] = project(c.mvp, pos[i * 3], pos[i * 3 + 1], pos[i * 3 + 2]); if (w <= 0) continue; each(i, c.x + (nx * 0.5 + 0.5) * c.w, c.y + (0.5 - ny * 0.5) * c.h, w, clamp((w - c.dist + 0.95) / 1.9, 0, 1)); } } // a layer kept until the camera or the layout changes; a drag draws every third point, the release all of them const layer = document.createElement('canvas'), lg = layer.getContext('2d'); let key = ''; cv.addEventListener('contextrestored', () => { key = ''; kick(); }); // a 2D context given back blank function sprites() { // again when the theme changes cellSpr = S.kinds.map((rgb) => { const w = tone(rgb); return INK ? bake(INKC, false, w) : bake([w, w, w]); }); pkSpr = INK ? bake(SPARK.pk, false, 1) : bake(SPARK.pk); axSpr = INK ? bake(SPARK.ax, false, 1) : bake(SPARK.ax); ringSpr = INK ? bake(SPARK.ax, true, 1) : bake(SPARK.ax, true); discSpr = INK ? bake(PAPER, 'disc', 1) : null; key = ''; } sprites(); function cellsInto(f, stride) { if (layer.width !== cv.width || layer.height !== cv.height) { layer.width = cv.width; layer.height = cv.height; } lg.setTransform(1, 0, 0, 1, 0, 0); lg.globalCompositeOperation = 'source-over'; lg.globalAlpha = 1; lg.clearRect(0, 0, layer.width, layer.height); if (INK) { lg.fillStyle = `rgb(${PAPER.map((v) => Math.round(v * 255))})`; lg.fillRect(0, 0, layer.width, layer.height); } lg.globalCompositeOperation = INK ? 'multiply' : 'lighter'; f.views.forEach((v) => { const c = camera(v, f.dpr, f.yaw, f.pitch); if (c.w < 2 || c.h < 2) return; lg.save(); lg.beginPath(); lg.rect(c.x, c.y, c.w, c.h); lg.clip(); for (let i = 0; i < S.NP; i += stride) { const [nx, ny, w] = project(c.mvp, S.pos[i * 3], S.pos[i * 3 + 1], S.pos[i * 3 + 2]); if (w <= 0) continue; const x = c.x + (nx * 0.5 + 0.5) * c.w, y = c.y + (0.5 - ny * 0.5) * c.h, fog = clamp((w - c.dist + 0.95) / 1.9, 0, 1); const s = Math.max(1, S.size[i] * c.vox / w); lg.globalAlpha = Math.min(1, (1.2 + (0.5 - 1.2) * fog) / 1.2 * (stride > 1 ? 1.8 : 1)); lg.drawImage(cellSpr[S.kind[i]], x - s / 2, y - s / 2, s, s); } lg.restore(); }); lg.globalCompositeOperation = 'source-over'; lg.globalAlpha = 1; } return { still: true, lost: () => false, restore: () => true, dispose() { layer.width = layer.height = 0; }, recolor: sprites, draw(f) { const stride = f.drag ? 3 : 1; const k = [f.yaw, f.pitch, f.dpr, cv.width, cv.height, stride, f.lv].join(); if (k !== key) { key = k; cellsInto(f, stride); } g.setTransform(1, 0, 0, 1, 0, 0); g.globalCompositeOperation = 'source-over'; g.globalAlpha = 1; g.clearRect(0, 0, cv.width, cv.height); g.drawImage(layer, 0, 0); if (f.kSpark <= 0) return; const add = INK ? 'multiply' : 'lighter', lay = INK ? 'source-over' : 'lighter'; f.views.forEach((v, side) => { const c = camera(v, f.dpr, f.yaw, f.pitch); if (c.w < 2 || c.h < 2) return; const set = side ? S.pk : S.ax, spr = side ? pkSpr : axSpr; dots(c, set.pos, set.n, (i, x, y, w, fog) => { const kk = clamp((f.kSpark - set.t[i]) * 3, 0, 1); if (kk <= 0) return; const s = Math.max(1, 1.5 * c.vox / w * kk), r = 5.2 * c.vox / w * kk; g.globalAlpha = Math.min(1, kk * (1 + (0.6 - 1) * fog)); if (!side && discSpr) { const d = r * 5.8 / 5.2; g.globalCompositeOperation = lay; g.drawImage(discSpr, x - d / 2, y - d / 2, d, d); } g.globalCompositeOperation = add; g.drawImage(spr, x - s / 2, y - s / 2, s, s); if (!side) { g.globalCompositeOperation = lay; g.drawImage(ringSpr, x - r / 2, y - r / 2, r, r); } }); }); g.globalCompositeOperation = 'source-over'; g.globalAlpha = 1; }, }; } // ---------------------------------------------------------------- the story: the training layout, the repack, the changes // Each phase eases from where the figure stands, so a press midway turns it there; a press eases out, faster. const D_ = { repack: 1700, restore: 1100, fade: 500 }, BY_HAND = { repack: 1000, restore: 700 }; const STATE = { before: 'Training layout: raster order', repack: 'Repacking: the same 262,144 tokens, moved in memory in 16 groups', after: 'Repacked. The same cell, the same two models, a new memory order', restore: 'Back to the training layout', }; const HEARD = `. ${intf(nAx)} predictions changed under PACK3D, ${intf(nPk)} under packed RoPE.`; // read out after "Repacked"; on screen the counts say it let phase = 'before', t0 = 0, from = { s: 0, k: 0, c: 0 }, cur = from, played = false, curve = ease, span = D_.repack; let S = null, rd = null, raf = 0, last = 0, shown = [0, 0], stripsAt = -1; let yaw = 0.6, pitch = 0.42, drag = null, dpr = 1, views = [], lv = 0, onScreen = false, wellInView = false, autoT = 0; const write = (el, t) => { if (el && el.textContent !== t) el.textContent = t; }; // the markup holds the two longest states unseen; written from STATE, so they cannot drift $$('.sw-hold', stateEl).forEach((el, i) => write(el, [STATE.repack, STATE.after][i])); function setPhase(p, now = performance.now(), byHand = false) { from = cur; // where the figure stands now phase = RM && p === 'repack' ? 'after' : RM && p === 'restore' ? 'before' : p; t0 = now; if (phase === 'repack' || phase === 'restore') { curve = byHand ? easeOut : ease; span = (byHand ? BY_HAND : D_)[phase]; } stateEl.classList.toggle('hot', phase === 'after'); write(stateT, STATE[phase]); write(stateX, phase === 'after' ? HEARD : ''); write(go, phase === 'after' || phase === 'repack' ? 'Restore the training layout' : 'Repack the memory'); kick(); } // where the figure stands at a time: s how far the memory is repacked, k the sparks, c the counts function standAt(now) { const e = Math.max(0, now - t0), fade = Math.max(0, 1 - e / D_.fade); if (phase === 'repack') return { s: from.s + (1 - from.s) * curve(Math.min(1, e / span)), k: from.k * fade, c: from.c * fade }; if (phase === 'after') return { s: 1, k: RM ? 1.4 : Math.min(1.4, e / 900), c: RM ? 1 : Math.min(1, e / 1100) }; if (phase === 'restore') return { s: from.s * (1 - curve(Math.min(1, e / span))), k: from.k * fade, c: from.c * fade }; return { s: 0, k: 0, c: 0 }; } // the status line speaks only for the reader: playing by itself turns its voice off, a press turns it on const voice = (on) => stateEl.setAttribute('aria-live', on ? 'polite' : 'off'); go.addEventListener('click', () => { played = true; voice(true); setPhase(phase === 'after' || phase === 'repack' ? 'restore' : 'repack', performance.now(), true); }); function kick() { if (!raf && !(rd && rd.lost())) raf = requestAnimationFrame(frame); } // no renderer at all: the counts and strips still run function frame(now) { raf = 0; if (rd && rd.lost()) return; const dt = last ? Math.min(0.05, (now - last) / 1000) : 0; last = now; if (phase === 'repack' && now - t0 > span) setPhase('after', now); else if (phase === 'restore' && now - t0 > span) setPhase('before', now); cur = standAt(now); const still = !rd || rd.still || motion.paused; // paused, reduced or drawn in 2D: the cells turn only by hand if (!still && !drag) yaw += dt * 0.12; paint(now); const moving = phase === 'repack' || phase === 'restore' || (phase === 'after' && now - t0 < 1500); if (onScreen && (!still || moving || drag)) kick(); } // counts written only when they change, strips only once the repack has moved; it reads no layout function paint(now) { const nA = Math.round(cur.c * nAx), nB = Math.round(cur.c * nPk); if (nA !== shown[0]) { shown[0] = nA; cA.textContent = intf(nA); } if (nB !== shown[1]) { shown[1] = nB; cB.textContent = intf(nB); } if (rd) rd.draw({ views, dpr, yaw, pitch, kSpark: cur.k, now, drag: !!drag, lv }); if (cur.s !== stripsAt) drawStrips(cur.s); } // ---------------------------------------------------------------- the memory strips: all positions in raster order const SEG = 16, perm = [9, 3, 14, 0, 11, 6, 1, 13, 4, 15, 7, 2, 12, 5, 10, 8]; // each class's tone along memory, from the memory ramp: outside nearly the sheet, then the cytoplasm, the organelles // and, deepest, the nucleus. Read again when the theme changes. let SCOL = null; const stripColors = () => { const m = RAMPS.mem(), rgb = (l) => lab2rgb(l).map((c) => Math.round(c * 255)); SCOL = [rgb(mixLab(lab(tok('--surface') || '#FFFFFF'), m(0), 0.35)), rgb(m(0.12)), rgb(m(1)), rgb(m(0.55))]; }; stripColors(); let stripSrc = null, sg = []; function stripImage(w) { // each pixel the mean color of the positions it covers if (stripSrc) stripSrc.width = 0; const can = document.createElement('canvas'); can.width = w; can.height = 1; const c2 = can.getContext('2d'); if (!c2) return null; // from the worker's counts per row of 64 positions; a row split by a pixel's edge counts by the part it covers const img = c2.createImageData(w, 1), n = new Float64Array(4), rows = S.rows, nr = rows.length / 4; for (let px = 0; px < w; px++) { const a = px / w * nr, b = (px + 1) / w * nr; n.fill(0); for (let r = Math.floor(a); r < b; r++) { const f = Math.min(b, r + 1) - Math.max(a, r); for (let c = 0; c < 4; c++) n[c] += rows[r * 4 + c] * f; } const k = n[0] + n[1] + n[2] + n[3] || 1; for (let ch = 0; ch < 3; ch++) img.data[px * 4 + ch] = (n[0] * SCOL[0][ch] + n[1] * SCOL[1][ch] + n[2] * SCOL[2][ch] + n[3] * SCOL[3][ch]) / k; img.data[px * 4 + 3] = 255; } c2.putImageData(img, 0, 0); return can; } function drawStrips(s) { stripsAt = s; if (!stripSrc) return; strips.forEach((cv, i) => { const g2 = sg[i]; if (!g2) return; const w = cv.width, h = cv.height, sw = w / SEG; g2.clearRect(0, 0, w, h); g2.imageSmoothingEnabled = false; for (let k = 0; k < SEG; k++) g2.drawImage(stripSrc, k * stripSrc.width / SEG, 0, stripSrc.width / SEG, 1, (k + (perm.indexOf(k) - k) * s) * sw, 0, sw + 0.5, h); }); } // measured when sizes change, never in a frame; drawn at once, since a resized canvas is blank function layout() { const cr = cellsEl.getBoundingClientRect(), W = cr.width, H = cr.height; dpr = Math.min(2, window.devicePixelRatio || 1, Math.sqrt(3e6 / Math.max(1, W * H))); // at most 3 Mpx const bw = Math.round(W * dpr), bh = Math.round(H * dpr); if (canvas.width !== bw || canvas.height !== bh) { canvas.width = bw; canvas.height = bh; } views = cells.map((el) => { const r = el.getBoundingClientRect(); return { x: r.left - cr.left, y: r.top - cr.top, w: r.width, h: r.height }; }); lv++; const sd = Math.min(2, window.devicePixelRatio || 1); strips.forEach((s) => { const w = Math.round(s.clientWidth * sd), h = Math.round(s.clientHeight * sd); if (s.width !== w || s.height !== h) { s.width = w; s.height = h; } }); const sw = Math.max(64, strips[0].width); if (S && (!stripSrc || stripSrc.width !== sw)) stripSrc = stripImage(sw); // only when the width changes stripsAt = -1; if (!(rd && rd.lost())) paint(performance.now()); kick(); } // ---------------------------------------------------------------- the renderer: WebGL, else Canvas 2D, else words let fails = 0, born = 0, lostT = 0; function useGL() { // always on a new canvas if (fails >= 3) return false; // three contexts refused, or lost young: Canvas 2D from here on const old = rd; canvas = swap(canvas); if (old) old.dispose(); // after the swap, so its 'lost' event is ignored rd = glRenderer(canvas, S); if (!rd) { fails++; return false; } born = performance.now(); watch(canvas); stage.classList.remove('no-canvas'); return true; } function use2D() { const old = rd; canvas = swap(canvas); if (old) old.dispose(); rd = ctx2dRenderer(canvas, S); stage.classList.toggle('no-canvas', !rd); // no canvas at all: the words in the cells' place; the counts, strips and button still run return !!rd; } function start() { // WebGL now, again a frame later, then Canvas 2D if (useGL()) return; // the observer's first call lays it out requestAnimationFrame(() => { if (!useGL()) use2D(); layout(); }); } function watch(cv) { cv.addEventListener('webglcontextlost', (e) => { e.preventDefault(); // ask the browser to give it back if (cv !== canvas) return; // a canvas already replaced cancelAnimationFrame(raf); raf = 0; fails = performance.now() - born < 5000 ? fails + 1 : 0; if (!document.hidden) recoverSoon(); }); cv.addEventListener('webglcontextrestored', () => { if (cv !== canvas) return; clearTimeout(lostT); born = performance.now(); if (rd.restore()) layout(); else replace(); }); } // a context lost and not given back (iOS often never gives one back): a new canvas and a new context function replace() { clearTimeout(lostT); if (!useGL()) use2D(); layout(); } // a loss in view may be given back in a moment (a GPU process restarting); one found on return seldom is function recoverSoon(ms = 1500) { clearTimeout(lostT); lostT = setTimeout(() => { if (rd && rd.lost()) replace(); }, ms); } const onReturn = () => { if (!document.hidden && rd && rd.lost()) recoverSoon(300); }; function build() { // the strips' contexts, made once; a strip that is refused one is given a new canvas, then put away sg = strips.map((s, i) => s.getContext('2d') || (strips[i] = swap(s)).getContext('2d')); strips.forEach((s, i) => { if (!sg[i]) s.parentElement.hidden = true; }); strips.forEach((s) => s.addEventListener('contextrestored', () => { stripsAt = -1; kick(); })); start(); // one layout per frame, starting a frame later, so it stays out of the task that took the volume in const ro = new ResizeObserver(() => layout()); requestAnimationFrame(() => requestAnimationFrame(() => [cellsEl, ...cells, ...strips.map((s) => s.parentElement)].forEach((el) => ro.observe(el)))); onDpr(layout); document.addEventListener('visibilitychange', onReturn); window.addEventListener('pageshow', onReturn); autoplay(); } // ---------------------------------------------------------------- the volume: made in a worker, once, when the reader gets near function makeSoon() { let w = null, url = '', over = false, late = 0; const done = (s) => { if (over) return; over = true; clearTimeout(late); if (w) w.terminate(); if (url) URL.revokeObjectURL(url); if (s) { S = s; build(); } else setTimeout(() => { S = makeScene(G, nAx, nPk); build(); }, 0); // no worker: here, once }; try { const src = `'use strict';const makeScene=${makeScene};self.onmessage=(e)=>{const s=makeScene(...e.data);self.postMessage(s,[s.rows,s.pos,s.col,s.size,s.kind,s.pk.pos,s.pk.t,s.ax.pos,s.ax.t].map((a)=>a.buffer));};`; url = URL.createObjectURL(new Blob([src], { type: 'text/javascript' })); w = new Worker(url); w.onmessage = (e) => done(e.data); w.onerror = w.onmessageerror = (e) => { e.preventDefault(); done(null); }; late = setTimeout(() => done(null), 8000); // a worker that never answers w.postMessage([G, nAx, nPk]); } catch (e) { done(null); } } new IntersectionObserver((es, io) => { if (es.some((e) => e.isIntersecting)) { io.disconnect(); makeSoon(); } }, { rootMargin: '1500px 0px' }).observe(stage); // ---------------------------------------------------------------- turning the cells by hand // on the cells' box, so a new canvas needs no listeners of its own const turned = () => { played = true; motion.set(true); }; cellsEl.addEventListener('pointerdown', (e) => { if (e.target !== canvas || e.button !== 0) return; // a right click opens the menu; it does not turn if (drag) { dropDrag(); return; } // a second finger: a pinch, which zooms the page drag = { x: e.clientX, y: e.clientY, yaw, pitch, id: e.pointerId, moved: false, finger: e.pointerType !== 'mouse' }; try { canvas.setPointerCapture(e.pointerId); } catch (err) { /* the pointer is already gone */ } canvas.classList.add('grabbing'); }); cellsEl.addEventListener('pointermove', (e) => { if (!drag || e.pointerId !== drag.id) return; if (e.pointerType === 'mouse' && !(e.buttons & 1)) { endDrag(); return; } // the button came up where we could not hear it const dx = e.clientX - drag.x, dy = e.clientY - drag.y, ax = Math.abs(dx), ay = Math.abs(dy); if (!drag.moved && (drag.finger ? ax < 8 || ax < ay : ax < 4 && ay < 4)) return; // a finger turns only sideways drag.moved = true; yaw = drag.yaw + dx * 0.008; pitch = clamp(drag.pitch + dy * 0.006, -0.4, 1.3); kick(); }); const endDrag = () => { if (!drag) return; if (drag.moved) turned(); drag = null; canvas.classList.remove('grabbing'); kick(); }; const dropDrag = () => { if (!drag) return; ({ yaw, pitch } = drag); drag = null; canvas.classList.remove('grabbing'); kick(); }; cellsEl.addEventListener('pointerup', endDrag); cellsEl.addEventListener('lostpointercapture', endDrag); cellsEl.addEventListener('pointercancel', dropDrag); cellsEl.addEventListener('keydown', (e) => { const d = e.target === canvas && !e.altKey && !e.ctrlKey && !e.metaKey && { ArrowLeft: [-0.2, 0], ArrowRight: [0.2, 0], ArrowUp: [0, -0.1], ArrowDown: [0, 0.1] }[e.key]; if (!d) return; e.preventDefault(); turned(); yaw += d[0]; pitch = clamp(pitch + d[1], -0.4, 1.3); kick(); }); // ---------------------------------------------------------------- playing by itself function autoplay() { if (played || autoT || motion.paused || !wellInView || !S) return; autoT = setTimeout(() => { autoT = 0; if (!played && phase === 'before' && !motion.paused && wellInView) { played = true; voice(false); setPhase('repack'); } }, 700); } new IntersectionObserver((es) => { const e = es[es.length - 1]; onScreen = e.isIntersecting; wellInView = onScreen && e.intersectionRatio > 0.45; if (onScreen) { last = 0; kick(); autoplay(); } }, { threshold: [0, 0.45, 0.7] }).observe(stage); motion.on((paused) => { last = 0; kick(); if (!paused) autoplay(); }); // focus in the window stops its autoplay, so the button's name never changes under focus stage.addEventListener('focusin', () => { if (!played) { played = true; clearTimeout(autoT); autoT = 0; } }); onTheme(() => { colors(); stripColors(); if (S && stripSrc) stripSrc = stripImage(stripSrc.width); stripsAt = -1; if (rd && rd.recolor) rd.recolor(); if (!(rd && rd.lost())) paint(performance.now()); kick(); }); setPhase(RM ? 'after' : 'before'); // reduced motion: a still of the result, which still answers the button if (RM) played = true; stateEl.setAttribute('role', 'status'); // from here on a change of state is read out; the first, above, is not } initRepackStage(); for (const key of ['ax0', 'ax1', 'pk0', 'pk1']) { document.getElementById(key).textContent = mean(D.repack[key]).toFixed(4); } })();