Seven widgets, in math

Interactive math in the Bartosz Ciechanowski register: vanilla JavaScript, inline SVG, CSS scroll-driven animations. Zero framework, zero build step.

1. Matrix multiplication — what each entry actually means

The static version on the math page shows row 1 × column 2 of a 2×2 product. It's fine. But matrix multiplication is the kind of operation where watching the row–column pairing slide across the output is more pedagogically useful than reading about it. Hover any of the four output cells below.

Widget 1 · Hover any output cell to see how it's computed. vanilla JS + CSS · ~50 lines
2
3
1
4
·
5
6
7
8
=
19
36
33
38
Hover any output cell. (Touch on mobile.)

Each output entry is row $i$ of $A$ dotted with column $j$ of $B$ — exactly the formula $(AB)_{ij} = \sum_k A_{ik} B_{kj}$.

2. EML's obstruction — drag $x$ to watch the construction fail

From §8 of the math page: the obvious EML construction $\mathrm{neg}(x) = (e - x) - e$ agrees with $-x$ when $x < e$, and disagrees on $x \geq e$ because of the junk-value extension of $\ln$. The static plot shows it. The draggable plot makes it tactile.

Widget 2 · Drag the slider to scrub $x$ from $-2$ to $6$. vanilla JS + SVG + range input · ~80 lines
x y −2 −1 0 1 2 y = −x neg(x) e e+1
$x = $ 0.00
Truth: $-x$
0.00
Construction: $\mathrm{neg}(x)$
0.00
Gap
0.00

For $x < e$, the gap stays at zero — the construction is honest. As $x$ crosses $e \approx 2.718$, the inner $\mathrm{sub}(e, x)$ goes negative; the outer $\ln$ takes the junk-value $0$; the construction collapses to $1 - e \approx -1.72$ and stays there. At $x = e+1 \approx 3.72$, the gap is exactly $2$.

3. The Khovanskii chain, revealed by scroll

The Khovanskii implication chain ends with the headline non-expressibility theorem. On the math page the four steps appear all at once. Below the same chain animates in as you scroll — pure CSS animation-timeline: view(), no JavaScript. Each step appears as it enters the viewport, paced to the reader.

↓ scroll slowly through this section ↓

Every term over the EML basis evaluates to a function with solvable monodromy.structural induction · Path-B Axioms 1–3
$\mathrm{quinticRoot}$ has $S_5$ monodromy, which is not solvable.classical Galois · Axiom 4
If some EML-term equalled $\mathrm{quinticRoot}$, it would have both solvable and non-solvable monodromy.contraposition
Therefore no EML-term globally represents the quintic root function. ∎

If you scrolled fast and the steps appeared all at once, scroll back up and try again slower — or you're on Firefox, which still gates scroll-driven animations behind layout.css.scroll-driven-animations.enabled. The fallback is a non-animated render that still reads correctly.

4. PolyAct — the polynomial-activation expressibility frontier

The freshest result on disk (2026-05-11): a polynomial-activation network of arity-1 operators, each applying a polynomial of degree at most $k$, composed to depth $d$, can represent exactly the polynomials of degree $\leq k^d$. The proof is mathlib's Polynomial.natDegree_comp_le applied at each tree level. Fully constructive — no axioms.

So whether $x^N$ is representable by such a network reduces to a single arithmetic comparison: $N \leq k^d$? The widget below makes the comparison tactile. Move the sliders; watch the representability frontier shift; see when your target polynomial drops in or out of range. The headline formalized theorem — $x^9$ is not representable at depth $3$ with degree-$\leq 2$ activations — pops out at $k=2,\, d=3$ (where $k^d = 8 < 9$).

Widget 4 · Drag the sliders to explore which polynomials a polynomial-activation network can represent. vanilla JS + range inputs + SVG · ~70 lines
2
3
9
0 4 8 12 16 20 24 28 32 representable: deg ≤ 8 N=9
Not representable. With $k = 2$ and $d = 3$, the network can represent polynomials of degree at most k^d = 2^3 = 8. The target $x^9$ has degree $9$, which exceeds $8$. x_pow_9_not_representable_at_depth_3  — the formalized headline theorem.

Try $k = 3, d = 3$ to see $x^{27}$ become reachable. Or $k = 2, d = 4$ for the depth-bump fix. The Lean proof of the degree-bound theorem is Term.eval_hasPolyDegLe in Sheffer/Examples/PolyAct.lean — six lines of induction over term structure.

5. Ryser's formula for the permanent

The permanent of an $n \times n$ matrix is $\mathrm{perm}(X) = \sum_{\sigma \in S_n} \prod_{i=1}^{n} X_{i,\sigma(i)}$ — the determinant-without-the-signs. The naive formula has $n!$ terms; exponential in $n$. Ryser (1963) gave an alternative that has $2^n$ terms and admits a polynomial-size arithmetic circuit: $$\mathrm{perm}(X) \;=\; (-1)^n \sum_{S \subseteq [n]} (-1)^{|S|} \prod_{i=1}^{n} \!\left( \sum_{j \in S} X_{i,j} \right).$$ For a $3 \times 3$ matrix the two formulas have $6$ and $8$ terms respectively; the asymptotic gap opens at $n = 4$ ($24$ vs. $16$) and grows. Edit the matrix entries below — both formulas recompute, and the green chip confirms they agree.

Widget 5 · Edit the matrix entries; both formulas recompute live. vanilla JS + grid layout + number inputs · ~120 lines

Naive · $\sum_{\sigma \in S_3} \prod_i X_{i,\sigma(i)}$   (6 terms)

Sum: 0

Ryser · $(-1)^3 \sum_{S \subseteq [3]} (-1)^{|S|} \prod_i \sum_{j \in S} X_{i,j}$   (8 terms)

Sum: 0

checking…

The Ryser sum has more rows ($2^n$ vs. $n!$) at $n=3$ but each row computes via $n$ additions and $n$ multiplications — a polynomial-size arithmetic circuit. The team's permanentRyserAux_in_VPOver theorem in Sheffer/Foundations/PermanentVNP.lean proves the circuit has size $2n^2 + 5n + 2$, satisfying IsPolyBound with constant $k = 9$. That bound is the substance behind $\mathrm{perm} \in \mathrm{VNP}$.

6. NAND-tree from a truth table

Sheffer 1913 said every Boolean function on $n$ inputs is expressible as a tree built from only the NAND operator. The proof goes through disjunctive normal form: enumerate the rows of the truth table where the function outputs true; for each such row build a minterm (an AND of literals matching that row); OR the minterms together; then rewrite every AND, OR, and NOT in terms of NAND.

Click the output column of the truth table below to toggle each row. The widget recomputes the function's identity, builds the DNF, and constructs the explicit NAND-tree — using exactly the constructions notTree, andTree, orTree that Sheffer/Examples/Nand.lean proves are NAND-tree representations of their respective Boolean primitives.

Widget 6 · Toggle the output column; watch the DNF and the NAND-tree rebuild. vanilla JS + recursive tree formatting · ~160 lines
a
b
f(a, b)
0
0
0
0
1
1
1
0
1
1
1
0

Function

XORtruth: 0110

Disjunctive normal form

($\lnot a \wedge b$) $\vee$ ($a \wedge \lnot b$)

NAND-tree (expanded via notTree / andTree / orTree)

tree depth · 0 internal NAND nodes

Try toggling all four outputs to 1 — the function becomes the constant true, and the NAND-tree expands to a tautology over the four minterms. Or toggle one row at a time to follow a single-minterm function. Sheffer/Examples/Nand.lean proves sheffer_complete: this construction always terminates with a valid NAND-tree, with zero axioms.

7. Monodromy of $z \mapsto z^{1/n}$

The classical building block of Path A. The function $z \mapsto z^{1/n}$ is multivalued on $\mathbb{C} \setminus \{0\}$: at every nonzero $z$, there are $n$ distinct $n$-th roots. The covering map records this — the universal cover unfolds $\mathbb{C}^*$ into $n$ sheets, and dragging a path around the origin in the base space induces a permutation on the sheets. For $z^{1/n}$, that permutation is the cyclic shift $(\sigma : k \mapsto k+1 \bmod n)$ generated by a single counterclockwise loop. The monodromy group is $\mathbb{Z}/n$ — abelian, hence trivially solvable.

Drag the blue dot around the marked branch point at the origin. The widget tracks the cumulative winding number of your path; the sheet wheel on the right highlights which of the $n$ branches you'd currently be on. Loop once, advance one sheet. Loop $n$ times, return home.

Widget 7 · Drag the dot around the origin to wind the path. Watch the active sheet cycle. vanilla JS + pointer events + winding-number accumulation · ~140 lines
sheets of $z^{1/n}$ sheet 0 / n
Position $z$
0.70 + 0.00 i
$z$ in polar
r = 0.70, θ = 0.00
Winding number
0.00
Current value $z^{1/n}$
0.93

The simplest covering map. For $n = 2$, every counterclockwise loop flips the sign of the square root; two loops restore it (monodromy $\mathbb{Z}/2$). For $n = 5$, you need five loops to return to the starting sheet (monodromy $\mathbb{Z}/5$, still abelian — still solvable). The team's headline result is harder: the generic quintic root, parameterized by five coefficients, has monodromy group $S_5$ on a 5-dimensional base space — a non-solvable permutation group, which is what makes the EML obstruction work. The math is the same shape (paths $\to$ permutations on sheets); this widget shows the bedrock case that the Lean formalization in Sheffer/Foundations/MonodromyGroup.lean + MultivaluedAnalytic.lean builds on.

8. Techniques used — and why they were enough

The research report from earlier this evening evaluated nine different stacks. For these three widgets, the answer turned out to be the simplest one: the page already had the entire stack. No new framework, no React, no build step, no install. Just three techniques layered on what was already there.

Technique 1 · CSS class-driven highlights
Widget 1 (matrix multiplication) toggles a few CSS classes on the parent based on which output cell is hovered. Each cell knows its row and column index via data-* attributes. The visual highlight (yellow row, green column, orange output) is pure CSS. The dot-product readout is updated by setting innerHTML. Total: ~50 lines of vanilla JS. The pattern transfers to any "highlight a relationship" interaction.
Technique 2 · SVG manipulation via vanilla JS
Widget 2 (EML obstruction) uses a single inline SVG and a range input. Each input event recomputes $\mathrm{neg}(x)$ in JavaScript and updates the positions of three SVG elements (vertical line, two dots, gap connector) by setting their cx/cy attributes. The piecewise definition of $\mathrm{neg}(x)$ is two lines: if (x < e) return -x; else return 1 - e;. Total: ~80 lines including the SVG markup. This is the Bartosz Ciechanowski pattern — inline SVG plus targeted attribute updates.
Technique 3 · Scroll-driven CSS animations (zero JavaScript)
Widget 3 (Khovanskii chain) uses animation-timeline: view() with animation-range: entry 0% entry 60%. Each step has a slideUp keyframe; the browser drives the animation as the element enters the viewport. Josh Comeau's article is the best teaching reference. The fallback for browsers without support is a no-op (the @supports not rule), so the content reads correctly even when the animation doesn't run.
Technique 4 · Multi-input range-driven number-line widget
Widget 4 (PolyAct) extends the SVG-attribute-update pattern to three coupled range inputs. The math is k^d — one line; the rendering is two SVG attribute updates per slider event (the green shaded rectangle's width, the orange target marker's $x$). Verdict text and pin (x_pow_9_not_representable_at_depth_3) update as innerHTML. Total: ~70 lines of vanilla JS. The pattern transfers to any "controllable parameter → visualized frontier" widget — likely the most reused interactive shape on this site.
Technique 5 · Side-by-side recomputing tables driven by editable inputs
Widget 5 (Ryser) reads a $3 \times 3$ matrix of <input type="number"> entries, computes both the $n!$-permutation sum and the $2^n$-subset sum, renders each as a list of contribution rows, totals them, and verifies the two totals match. Each input fires an input event that recomputes everything in ~120 lines of vanilla JS — no virtual DOM, no reactivity framework, no observer plumbing. The two sums are kept in adjacent panels so the structural contrast (exponential vs. polynomial structure) is visible at every keystroke.
Technique 6 · Recursive expression-tree builder rendered as indented text
Widget 6 (NAND-tree) builds a small JavaScript expression tree from the truth-table bits — first as a DNF tree of Var / Not / And / Or nodes, then transformed recursively into a NAND-only tree via the three direct constructions notTree / andTree / orTree. The output is rendered as indented monospaced text that mirrors the shape of a Lean Term NandBasis n: each NAND at one level, its two children indented one step. The depth count under the tree matches what Sheffer.Term.depth would compute on the corresponding Lean term. ~160 lines including the tree-builder algebra and the renderer.
Technique 7 · Pointer-event drag with winding-number accumulation
Widget 7 (monodromy) captures pointer events on an SVG plane, converts pointer pixel coordinates to math-space via the viewBox transform, and accumulates a signed winding number at every move. The trick: compute the signed angle delta as the shorter of the two arcs between successive angles (capping |Δθ| < π), divide by , accumulate. The current sheet of the n-th-root covering is just floor(winding) mod n. The sheet arcs are rendered as SVG path elements coloured along an OkLCH hue ring; the active arc lights up via a CSS class swap on every redraw. ~140 lines including SVG layout, pointer plumbing, and the trail-rendering.

What this proves, in the small: the recommended-tools list in the research report is real, but for this specific page — Markdown-pandoc-shaped, single document, no framework — the right answer is to not adopt any of them yet. The existing stack plus three small additions (range inputs, SVG attribute manipulation, CSS scroll timelines) reaches the Ciechanowski register without crossing into Mafs/React/Astro territory.

The day that changes is the day there are five interactive widgets per page across ten pages, and the cost of writing each from scratch outweighs the cost of adopting Mafs. We're not there. Until we are, this is the path.