File size: 8,132 Bytes
d32737a
 
 
 
 
 
 
 
 
2ce6518
 
 
 
 
 
 
 
 
 
 
 
 
 
d32737a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9dde5d4
d32737a
 
 
 
 
 
 
 
 
9dde5d4
d32737a
 
 
 
 
 
 
 
 
9dde5d4
 
d32737a
 
 
9dde5d4
 
 
d32737a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2ce6518
 
d32737a
 
9b672a8
d32737a
9b672a8
d32737a
 
 
9b672a8
 
 
 
 
 
 
 
d32737a
 
 
 
9b672a8
 
 
9dde5d4
 
 
9b672a8
 
2ce6518
d32737a
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
/* 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 };
})();