/* 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[] = { 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 }; })();