Spaces:
Running
Running
| /* manimo playground — shared kit (no build step, no deps). | |
| * A tiny toolkit every widget uses: DOM helpers, editable number grids, a canvas helper, a | |
| * step engine, and the math used to drive BOTH these testers and the Manim video animations (the | |
| * computation is identical, so the website and the video explain a concept the exact same way). | |
| * Widgets register themselves into MANIMO.registry[<primitive>] = { title, mount(root, concept) }. | |
| */ | |
| window.MANIMO = window.MANIMO || {}; | |
| MANIMO.registry = MANIMO.registry || {}; | |
| /* A tiny shared data bus so a paper's concepts interconnect: each tester publishes its result and | |
| * downstream testers consume it — the token matrix X flows into Q·Kᵀ, whose scores flow into softmax, | |
| * whose weights flow into the weighted sum. The SAME numbers move through every card, exactly as they | |
| * move through the network in the video. Cleared by app.js whenever a new paper is rendered. */ | |
| MANIMO.bus = (function () { | |
| let vals = {}, subs = {}; | |
| return { | |
| set(key, val) { if (!key) return; vals[key] = val; (subs[key] || []).forEach((cb) => { try { cb(val); } catch (e) { /* noop */ } }); }, | |
| get(key) { return vals[key]; }, | |
| on(key, cb) { (subs[key] = subs[key] || []).push(cb); if (key in vals) { try { cb(vals[key]); } catch (e) { /* noop */ } } return cb; }, | |
| clear() { vals = {}; subs = {}; }, | |
| }; | |
| })(); | |
| MANIMO.kit = (function () { | |
| function el(tag, attrs, children) { | |
| const n = document.createElement(tag); | |
| if (attrs) for (const k in attrs) { | |
| if (k === "class") n.className = attrs[k]; | |
| else if (k === "style") n.style.cssText = attrs[k]; | |
| else if (k === "html") n.innerHTML = attrs[k]; | |
| else if (k.slice(0, 2) === "on") n.addEventListener(k.slice(2).toLowerCase(), attrs[k]); | |
| else n.setAttribute(k, attrs[k]); | |
| } | |
| (children || []).forEach((c) => n.appendChild(typeof c === "string" ? document.createTextNode(c) : c)); | |
| return n; | |
| } | |
| function clear(n) { while (n.firstChild) n.removeChild(n.firstChild); } | |
| const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v)); | |
| const lerp = (a, b, t) => a + (b - a) * t; | |
| function fmt(x) { | |
| if (!isFinite(x)) return "–"; | |
| const r = Math.round(x * 100) / 100; | |
| return Number.isInteger(r) ? String(r) : r.toFixed(2); | |
| } | |
| // An editable grid of numbers -> {node, get():number[][], set(m)}. | |
| function grid(rows, cols, values, opts) { | |
| opts = opts || {}; | |
| const inputs = []; | |
| const tbl = el("table", { class: "mx" }); | |
| for (let i = 0; i < rows; i++) { | |
| const tr = el("tr"); | |
| const row = []; | |
| for (let j = 0; j < cols; j++) { | |
| const v = values && values[i] && values[i][j] != null ? values[i][j] : 0; | |
| const inp = el("input", { class: "cell", type: "number", step: "any", value: String(v) }); | |
| if (opts.readonly) inp.readOnly = true; | |
| row.push(inp); | |
| tr.appendChild(el("td", null, [inp])); | |
| } | |
| inputs.push(row); | |
| tbl.appendChild(tr); | |
| } | |
| return { | |
| node: tbl, | |
| get: () => inputs.map((r) => r.map((i) => { const x = parseFloat(i.value); return Number.isFinite(x) ? x : 0; })), | |
| set: (m) => m.forEach((r, i) => r.forEach((v, j) => { if (inputs[i] && inputs[i][j]) inputs[i][j].value = fmt(v); })), | |
| cell: (i, j) => inputs[i][j], | |
| highlight: (cells, on) => { (cells || []).forEach(([i, j]) => { if (inputs[i] && inputs[i][j]) inputs[i][j].classList.toggle("hot", on !== false); }); }, | |
| }; | |
| } | |
| function button(label, onclick, cls) { return el("button", { class: "btn " + (cls || ""), onclick }, [label]); } | |
| function chip(text, accent) { return el("span", { class: "chip" + (accent ? " acc" : "") }, [text]); } | |
| function row(children, cls) { return el("div", { class: "row " + (cls || "") }, children); } | |
| function panel(title) { const p = el("div", { class: "steps", role: "log", "aria-live": "polite" }); if (title) p.appendChild(el("div", { class: "steps-h" }, [title])); return p; } | |
| function logLine(panel, html, accent) { panel.appendChild(el("div", { class: "step" + (accent ? " acc" : ""), html })); panel.scrollTop = panel.scrollHeight; } | |
| // Heatmap canvas: render a matrix as colored cells on the accent scale. | |
| function heatmap(canvas, m, theme, opts) { | |
| opts = opts || {}; | |
| const ctx = canvas.getContext("2d"); | |
| const R = m.length, C = m[0] ? m[0].length : 0; | |
| const W = canvas.width, H = canvas.height, cw = W / C, ch = H / R; | |
| let lo = Infinity, hi = -Infinity; | |
| m.forEach((r) => r.forEach((v) => { if (isFinite(v)) { lo = Math.min(lo, v); hi = Math.max(hi, v); } })); | |
| if (!isFinite(lo) || !isFinite(hi)) { lo = 0; hi = 1; } // all non-finite → neutral scale, never NaN colours | |
| const span = hi - lo || 1; | |
| ctx.clearRect(0, 0, W, H); | |
| for (let i = 0; i < R; i++) for (let j = 0; j < C; j++) { | |
| const v = m[i][j]; | |
| const t = isFinite(v) ? (v - lo) / span : 0; | |
| ctx.fillStyle = isFinite(v) ? mix(theme.bg, theme.accent, 0.12 + 0.85 * t) : theme.muted; | |
| ctx.fillRect(j * cw + 1, i * ch + 1, cw - 2, ch - 2); | |
| if (opts.values) { ctx.fillStyle = t > 0.55 ? theme.bg : theme.ink; ctx.font = Math.min(cw, ch) * 0.34 + "px system-ui"; ctx.textAlign = "center"; ctx.textBaseline = "middle"; ctx.fillText(fmt(m[i][j]), j * cw + cw / 2, i * ch + ch / 2); } | |
| } | |
| } | |
| function mix(a, b, t) { | |
| const pa = hex(a), pb = hex(b); | |
| const c = pa.map((v, i) => Math.round(lerp(v, pb[i], clamp(t, 0, 1)))); | |
| return "rgb(" + c.join(",") + ")"; | |
| } | |
| function hex(h) { | |
| h = (h || "#000000").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)); | |
| } | |
| // softmax + dot product — the shared math (the SAME steps the video animations narrate). | |
| function softmax(v) { const m = Math.max.apply(null, v); const ex = v.map((x) => Math.exp(x - m)); const s = ex.reduce((a, b) => a + b, 0); return ex.map((x) => x / s); } | |
| function dot(a, b) { let s = 0; for (let i = 0; i < a.length; i++) s += a[i] * b[i]; return s; } | |
| function matmul(A, B) { const m = A.length, k = B.length, n = B[0].length; const C = []; for (let i = 0; i < m; i++) { C[i] = []; for (let j = 0; j < n; j++) { let s = 0; for (let p = 0; p < k; p++) s += A[i][p] * B[p][j]; C[i][j] = s; } } return C; } | |
| function col(M, j) { return M.map((r) => r[j]); } | |
| function transpose(M) { return (M[0] || []).map((_, j) => M.map((r) => r[j])); } | |
| function sameShape(a, b) { return !!a && !!b && a.length === b.length && (a[0] || []).length === (b[0] || []).length; } | |
| // A simple async step runner: each click/auto-tick calls next(); returns controls. | |
| // play(ms, onDone) auto-advances to the end then calls onDone() once (used by "Play all"). | |
| function stepper(steps, render) { | |
| let i = 0, timer = null, done = null; | |
| function go(k) { i = clamp(k, 0, steps.length); render(i); } | |
| function next() { if (i < steps.length) go(i + 1); } | |
| function reset() { stop(); go(0); } | |
| function stop() { if (timer) { clearInterval(timer); timer = null; } done = null; } | |
| function play(ms, onDone) { | |
| stop(); done = onDone || null; | |
| timer = setInterval(() => { | |
| if (i >= steps.length) { const d = done; stop(); if (d) d(); return; } | |
| next(); | |
| }, ms || 700); | |
| } | |
| render(0); | |
| return { next, reset, play, stop, at: () => i, total: steps.length }; | |
| } | |
| // Wrap a stepper as a uniform "player" so the gallery can drive every widget the same way: | |
| // reset() to the start, play(done) runs to the end and calls done(), stop() cancels. | |
| function player(ctrl, ms) { | |
| // honour prefers-reduced-motion: snap through the steps near-instantly instead of animating. | |
| const reduce = typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches; | |
| return { play: (done) => ctrl.play(reduce ? 1 : (ms || 750), done), stop: () => ctrl.stop(), reset: () => ctrl.reset() }; | |
| } | |
| return { el, clear, clamp, lerp, fmt, grid, button, chip, row, panel, logLine, heatmap, mix, hex, softmax, dot, matmul, col, transpose, sameShape, stepper, player }; | |
| })(); | |