jacooob521's picture
fix 14 medium review findings (correctness/robustness/a11y)
9dde5d4 verified
Raw
History Blame Contribute Delete
8.13 kB
/* 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 };
})();