Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

3Blue1Brown, reproduced

Real 3Blue1Brown lessons rebuilt in Manic — the math visualised and narrated with Manic captions. From the Essence of Linear Algebra to the Riemann zeta function, each is a single readable .manic file you can open, run, and adapt.

Each block is the whole file — copy it into x.manic and run manic x.manic (live) or --record out (video).

vectors-basis-transformations

Essence of Linear Algebra, chapters 1–11 in one file: vectors → basis → the grid transforming (gridmap) → composition → determinant → inverse → the dot product (projection AND duality) → change of basis → eigenvectors → eigenbasis → functions as vectors.

// ============================================================================
//  Essence of Linear Algebra, chapters 1→2→3 in one arc.
//
//  Ch1  A vector is an arrow from the origin; its coordinates are [x, y].
//  Ch2  Those coordinates are SCALARS: v = x·î + y·ĵ — a linear combination of
//       the basis vectors î=(1,0), ĵ=(0,1).
//  Ch3  A matrix is a transformation: it says where î and ĵ LAND, and the whole
//       grid morphs to follow (`gridmap` animates it). Because v = x·î + y·ĵ,
//       it rides along, landing at x·(new î) + y·(new ĵ).
//
//  Example: v = [1, 2] under [[2,1],[1,2]] (î→(2,1), ĵ→(1,2)), so v lands at
//  1·(2,1) + 2·(1,2) = (4,5). Origin (400,470), 55 px/unit.
// ============================================================================
title("Vectors, basis, and transformations");
canvas(1280, 720);
template("black");

text(narr, (640, 696), "A vector is an arrow from the origin.", 24);

// the plane + basis, ready to be transformed in Ch3 (starts at the identity)
gridmap(gm, (400, 470), 55, 2, 1, 1, 2, 5);

// the vector v = [1,2] and its linear-combination scaffold
vector(v, (400, 470), (55, 110), yellow);
vector(jj, (455, 470), (0, 110), red);           // 2·j-hat, stacked on i-hat → reaches v
equation(vlab, (486, 352), `\vec{v}=[1,2]`, 30); color(vlab, yellow);
equation(ilab, (472, 500), `\hat{\imath}`, 26); color(ilab, lime);
equation(jlab, (366, 414), `\hat{\jmath}`, 26); color(jlab, red);
equation(mtx, (980, 165), `\begin{bmatrix}2&1\\1&2\end{bmatrix}`, 44);

// Ch4 — a second transform (90° rotation) composed on top: morph M1 → M2·M1
gridmap(gm2, (400, 470), 55, -1, -2, 2, 1, 5, 2, 1, 1, 2);
equation(comp4, (640, 96), `\begin{bmatrix}0&-1\\1&0\end{bmatrix}\begin{bmatrix}2&1\\1&2\end{bmatrix}=\begin{bmatrix}-1&-2\\2&1\end{bmatrix}`, 34);

// the determinant cell (Ch5), the projection (Ch7), and the alt-basis grid (Ch8)
determinant(dt, (400, 470), 55, 2, 1, 1, 2, gold);
project(pr, (400, 470), 55, (1, 2), (3, 1));       // project v=(1,2) onto span(w=(3,1))
squish(sq, (400, 470), 55, 1, 2, 5);               // Ch7 duality: dot with v = squish onto a line
eigen(ev, (400, 470), 55, 2, 1, 1, 2, gold);       // Ch9 eigenvectors of [[2,1],[1,2]] (λ = 3, 1)
diagonalise(dg, (400, 470), 55, 2, 1, 1, 2, gold); // Ch10 eigenbasis = a pure stretch
coords(cf, (700, 450), (-4, 4), (-3, 3), 46, 46, 0); // Ch11 a function graph…
plot(fn, (700, 450), 46, 46, "0.25*x*x*x - x", (-3.6, 3.6)); color(fn, mint);
equation(deq, (700, 130), `\tfrac{d}{dx}\;\leftrightarrow\;\begin{bmatrix}0&1&0\\0&0&2\\0&0&0\end{bmatrix}`, 32);
gridmap(cb, (400, 470), 55, 1, 0.7, 0.4, 1, 5);
vector(vb, (400, 470), (55, 110), yellow);

// --- initial visibility ---
hidden(gm.i); hidden(gm.j);                       // basis revealed in Ch2
hidden(v); hidden(vlab); hidden(jj); hidden(ilab); hidden(jlab); hidden(mtx);
hidden(dt); hidden(dt.unit); hidden(dt.val); hidden(pr);
hidden(sq); hidden(sq.line); hidden(sq.dual);
hidden(ev); hidden(dg); hidden(cf); hidden(fn); hidden(deq);
hidden(gm2); hidden(gm2.bg); hidden(gm2.i); hidden(gm2.j); hidden(comp4);
hidden(cb); hidden(cb.bg); hidden(cb.i); hidden(cb.j); hidden(vb);

// --- Ch1: a vector ---
wait(0.5);
par { show(v); show(vlab); }
wait(1.3);

// --- Ch2: coordinates are a linear combination of the basis ---
say(narr, "Its coordinates are scalars: v = 1·i-hat + 2·j-hat.");
par { show(gm.i); show(gm.j); show(ilab); show(jlab); }
wait(0.7);
show(jj);                                         // i-hat then two j-hats reach v
wait(1.6);

// --- Ch3: the matrix transforms space; v rides along ---
say(narr, "A matrix moves i-hat and j-hat — and the whole grid follows.");
show(mtx);
par { fade(jj); fade(ilab); fade(jlab); fade(vlab); }
par { to(gm, morph, 1, 2.2); grow(v, (620, 195), 2.2); }   // space deforms, v follows
wait(0.6);
say(narr, "v lands at 1·(new i-hat) + 2·(new j-hat) = (4,5).");
wait(1.6);

// --- Ch4: matrix multiplication is composition ---
say(narr, "Ch 4 — apply one transform, then another: that is matrix multiplication.");
show(comp4);                                       // the product, clearly placed up top
par { fade(gm); fade(v); }
show(gm2);                                          // starts at M1 (seamless with Ch3)
to(gm2, morph, 1, 1.8);                             // now rotate 90°: the grid is at M2·M1
wait(1.3);
to(gm2, morph, 0, 1.5);                             // undo the second transform, back to M1
par { fade(gm2); fade(comp4); }
par { show(gm); show(v); }
wait(0.6);

// --- Ch5: the determinant is the area scale factor ---
say(narr, "Ch 5 — the determinant is how much areas scale: 2·2 − 1·1 = 3.");
par { show(dt); show(dt.unit); show(dt.val); }
wait(1.9);
par { fade(dt); fade(dt.unit); fade(dt.val); }    // fade = timeline hide (hidden is base-state only)

// --- Ch6: column space + the inverse undoes the transform ---
say(narr, "Ch 6 — the columns span the whole plane, so the inverse sends space back.");
par { to(gm, morph, 0, 1.8); grow(v, (455, 360), 1.8); }   // reverse the morph = the inverse
wait(1.4);

// --- Ch7: the dot product — projection, and its dual (a squish onto a line) ---
say(narr, "Ch 7 — the dot product of v and w is a projection — w's shadow on v.");
par { fade(gm); fade(gm.bg); fade(v); fade(mtx); }
show(pr);
wait(1.6);
say(narr, "It's also a squish of the whole plane onto a line — the dual vector IS v.");
fade(pr);
par { show(sq); show(sq.line); show(sq.dual); }
to(sq, morph, 1, 1.9);                              // collapse space onto the number line
wait(1.0);
to(sq, morph, 0, 1.2);                              // and back — the dual vector remains
wait(0.8);

// --- Ch8: change of basis ---
say(narr, "Ch 8 — change of basis: the same arrow, read on a different grid.");
par { fade(sq); fade(sq.line); fade(sq.dual); }
par { show(cb); show(vb); }
to(cb, morph, 1, 1.8);                                       // the coordinate grid changes; v stays put
wait(1.8);

// --- Ch9: eigenvectors and eigenvalues ---
say(narr, "Ch 9 — some vectors keep to their own line: eigenvectors (λ = 3 and λ = 1).");
par { fade(cb); fade(vb); }
show(ev);
wait(2.0);

// --- Ch10: the eigenbasis is a pure stretch ---
say(narr, "Ch 10 — in the eigenbasis, the transformation is just a diagonal stretch.");
fade(ev);
show(dg);
wait(2.0);

// --- Ch11: abstract vector spaces ---
say(narr, "Ch 11 — even functions are vectors; the derivative is a linear map with a matrix.");
fade(dg);
par { show(cf); show(fn); show(deq); }
wait(2.2);

duality-dot-product

The dot product as DUALITY: the plane collapses onto a number line under a 1×2 map (squish), and the gold dual vector is what you dot with.

// ============================================================================
//  The dot product and duality (eola chapter 7 — the "why" behind chapter8p2).
//
//  A 1x2 matrix [a b] is a linear map from the plane to a NUMBER LINE: it sends
//  (x,y) to a·x + b·y and squishes all of space onto the line. Every such map
//  corresponds to a single 2D vector — the DUAL vector (a,b) — and applying the
//  map is exactly the dot product with (a,b). That correspondence is duality.
//
//  Built on the new `squish` builtin. Here [a b] = [2 1].
// ============================================================================
title("The dot product and duality");
canvas(1280, 720);
template("black");

text(narr, (640, 694), "A 1×2 matrix is a map from the plane to a number line.", 24);
equation(mtx, (980, 130), `\begin{bmatrix}2&1\end{bmatrix}`, 44);

squish(sq, (640, 360), 70, 2, 1, 4);

// --- reveal ---
hidden(sq.dual); hidden(mtx);

wait(0.6);
show(mtx);
wait(1.2);

say(narr, "It squishes all of space onto the line — i-hat to 2, j-hat to 1.");
to(sq, morph, 1, 2.4);
wait(1.2);

say(narr, "Every such map corresponds to a single 2D vector — the dual vector (2,1).");
to(sq, morph, 0, 1.6);                             // un-squish, back to the plane
show(sq.dual);
wait(1.4);

say(narr, "Applying the map is the same as dotting with (2,1). That is duality.");
pulse(sq.dual);
wait(1.8);

warp-grid

A complex-plane grid DEFORMS under z → z² (warp) — lines bend into the classic parabolic conformal web.

// ============================================================================
//  A coordinate grid over the complex plane, deforming under z -> f(z).
//
//  `warp` samples each grid line through the complex function, so straight lines
//  bend into curves. It starts as the identity grid and morphs to the warped
//  image when animated — `to(id, morph, 1, dur)`. Here z -> z^2, the classic
//  conformal map (the non-linear twin of `linmap`, and the deforming-grid
//  companion to `domaincolor`).
// ============================================================================
title("Complex grid warp");
canvas(1280, 720);
template("black");

warp(g, (640, 360), 82, `z*z`, 3, 40);
equation(fn, (640, 80), `z \mapsto z^{2}`, 46);
text(narr, (640, 672), "A grid over the complex plane.", 26);

wait(0.6);
say(narr, "Every point z moves to z squared — lines bend into parabolas.");
to(g, morph, 1, 2.6);
wait(1.6);

zeta

The Riemann zeta function by domaincolor (with a real zeta(z) in the complex evaluator): the pole at s=1, the critical line Re=½, and the first non-trivial zeros marked.

// ============================================================================
//  The Riemann zeta function ζ(s), visualised by domain colouring.
//
//  ζ is a genuine complex function now (`zeta(z)` in the complex evaluator, via
//  Borwein's accelerated eta series — valid through the critical strip). Domain
//  colouring paints the complex plane: hue = phase of ζ(s), brightness = |ζ(s)|,
//  so ZEROS are dark points and the POLE at s=1 is bright. Every non-trivial
//  zero found sits on the critical line Re = 1/2 — the Riemann Hypothesis.
// ============================================================================
title("The Riemann zeta function");
canvas(1280, 720);
template("black");

// the s-plane, coloured by ζ(s) — a tall strip up the imaginary axis so the
// first non-trivial zeros (½ ± 14.13i) are in frame
domaincolor(zp, (500, 330), 240, 640, `zeta(z)`, 5.4);
colorwheel(cw, (980, 470), 72);                 // phase → hue legend

equation(def, (960, 150), `\zeta(s)=\sum \frac{1}{n^{s}}`, 40);
text(narr, (640, 700), "Colour = phase of zeta; brightness = size.", 24);

// the critical line Re = 1/2 and the strip 0 < Re < 1
line(cl, (511, 12), (511, 648)); color(cl, gold); dashed(cl);
text(cllab, (700, 250), "critical line  Re = 1/2", 20); color(cllab, gold);

// the first non-trivial zeros, on the critical line
dot(z1, (511, 16)); color(z1, gold);
dot(z2, (511, 644)); color(z2, gold);
text(zlab, (690, 60), "zeta = 0  (½ ± 14.13i)", 20); color(zlab, gold);

// the pole at s = 1
circle(pole, (522, 330), 10); outlined(pole); color(pole, cyan);
text(plab, (720, 330), "pole at s = 1", 20); color(plab, cyan);

// --- reveal ---
hidden(cl); hidden(cllab); hidden(z1); hidden(z2); hidden(zlab);
hidden(pole); hidden(plab); hidden(def); hidden(cw);

wait(0.4);
show(zp);
say(narr, "The Riemann zeta function, 1 + 1/2^s + 1/3^s + ...");
show(def);
wait(1.2);

say(narr, "Defined for Re(s) > 1, then continued everywhere but a pole at s = 1.");
par { show(pole); show(plab); }
wait(1.2);

say(narr, "Its zeros are the dark points.");
show(cw);
wait(1.0);

say(narr, "Every non-trivial zero found lies on the line Re = 1/2.");
par { show(cl); show(cllab); }
wait(0.7);
par { show(z1); show(z2); show(zlab); pulse(z1); pulse(z2); }
wait(1.6);

euler-characteristic

Euler’s formula V − E + F = 2 on a wheel graph: V and E counted, faces filled + counted by regions, and 6 − 10 + 6 = 2 assembled.

// ============================================================================
//  Euler's characteristic formula, V - E + F = 2, on a planar graph.
//
//  The graph is a wheel W5 (a hub joined to a 5-point rim). We count its
//  vertices, its edges, and its faces — the faces detected and filled by
//  `regions` (5 inner triangles), plus the one outer face — and watch
//  V - E + F land on 2.
// ============================================================================
title("Euler's characteristic");
canvas(1280, 720);
template("black");

equation(thm, (760, 70), `V - E + F = 2`, 46);

// --- the wheel graph W5 ---
polygon(rim, (760,150),(560,295),(637,530),(883,530),(960,295));
color(rim, cyan);
line(k1, (760,360),(760,150)); tag(k1, spokes); color(k1, cyan);
line(k2, (760,360),(560,295)); tag(k2, spokes); color(k2, cyan);
line(k3, (760,360),(637,530)); tag(k3, spokes); color(k3, cyan);
line(k4, (760,360),(883,530)); tag(k4, spokes); color(k4, cyan);
line(k5, (760,360),(960,295)); tag(k5, spokes); color(k5, cyan);

regions(cells, rim, spokes);   // fills + counts the 5 inner faces

dot(hub,(760,360));
dot(p1,(760,150)); dot(p2,(560,295)); dot(p3,(637,530));
dot(p4,(883,530)); dot(p5,(960,295));

// --- the running tally, on the left ---
equation(tv, (250, 235), `V = 6`, 44);
equation(te, (250, 345), `E = 10`, 44);
equation(tf, (250, 455), `F = 6`, 44);
text(fnote, (250, 500), "5 inside + 1 outside", 18);
equation(sum, (760, 660), `6 - 10 + 6 = 2`, 52);

// --- reveal ---
untraced(rim); untraced(k1); untraced(k2); untraced(k3); untraced(k4); untraced(k5);
untraced(cells);
hidden(hub); hidden(p1); hidden(p2); hidden(p3); hidden(p4); hidden(p5);
hidden(tv); hidden(te); hidden(tf); hidden(fnote); hidden(sum);

wait(0.4);
par { draw(rim, 1.0); draw(k1, 1.0); draw(k2, 1.0); draw(k3, 1.0); draw(k4, 1.0); draw(k5, 1.0); }
par { show(hub); show(p1); show(p2); show(p3); show(p4); show(p5); }
wait(0.4);

// count the vertices
par { show(tv); pulse(hub); pulse(p1); pulse(p2); pulse(p3); pulse(p4); pulse(p5); }
wait(0.5);

// count the edges
par { show(te); pulse(rim); pulse(k1); pulse(k2); pulse(k3); pulse(k4); pulse(k5); }
wait(0.5);

// count the faces — 5 filled by `regions`, plus the outer face
draw(cells, 1.3);
par { show(tf); show(fnote); }
wait(0.8);

// and the characteristic lands on 2
show(sum);
wait(1.4);

euler-proof

The spanning-tree / dual-tree PROOF of V − E + F = 2, built on spantree (run on the primal AND the dual) and dual.

// ============================================================================
//  Why V - E + F = 2 — the spanning-tree / dual-tree proof, narrated by Manic.
//
//  A spanning tree of the graph uses V-1 edges (every vertex, no loop). The
//  edges it leaves out are in bijection with the bounded faces: their duals form
//  a spanning tree of the DUAL graph, using F-1 edges. Every edge is in exactly
//  one of the two trees, so (V-1) + (F-1) = E, i.e. V - E + F = 2.
//
//  Built on `spantree` (run on the primal AND the dual) and `dual`.
// ============================================================================
title("Euler's formula — a proof");
canvas(1280, 720);
template("black");

equation(thm, (300, 66), `V - E + F = 2`, 42);
text(narr, (760, 682), "A connected planar graph.", 26);

// --- the wheel graph W5 ---
polygon(rim, (760,150),(560,295),(637,530),(883,530),(960,295));
color(rim, cyan);
line(k1, (760,360),(760,150)); tag(k1, spokes); color(k1, cyan);
line(k2, (760,360),(560,295)); tag(k2, spokes); color(k2, cyan);
line(k3, (760,360),(637,530)); tag(k3, spokes); color(k3, cyan);
line(k4, (760,360),(883,530)); tag(k4, spokes); color(k4, cyan);
line(k5, (760,360),(960,295)); tag(k5, spokes); color(k5, cyan);

regions(cells, rim, spokes);              // the F faces (faint context)
opacity(cells, 0.18);

dot(hub,(760,360));
dot(p1,(760,150)); dot(p2,(560,295)); dot(p3,(637,530));
dot(p4,(883,530)); dot(p5,(960,295));

// tally
equation(tv, (135, 200), `V = 6`, 34);
equation(te, (135, 275), `E = 10`, 34);
equation(tf, (135, 350), `F = 6`, 34);

// the two trees (declared now, revealed in sequence)
spantree(pt, spokes, rim);                // primal tree {pt} (green) + co-tree {pt.co} (orange)
dual(du, rim, spokes);                     // dual: {du} edges + {du.nodes} dots
spantree(dt, du);                          // dual's spanning tree {dt}
color(dt, teal);

equation(sum, (760, 632), `(V-1) + (F-1) = E`, 40);
equation(nums, (760, 632), `5 + 5 = 10`, 40);

// --- initial visibility ---
untraced(rim); untraced(k1); untraced(k2); untraced(k3); untraced(k4); untraced(k5);
untraced(cells); untraced(pt); untraced(pt.co); untraced(dt);
hidden(hub); hidden(p1); hidden(p2); hidden(p3); hidden(p4); hidden(p5);
hidden(tv); hidden(te); hidden(tf);
hidden(du); hidden(du.nodes); hidden(dt.co);   // substrate + dual co-tree stay hidden
hidden(sum); hidden(nums);

// 1. the graph
wait(0.4);
par { draw(rim,1.0); draw(k1,1.0); draw(k2,1.0); draw(k3,1.0); draw(k4,1.0); draw(k5,1.0); }
par { show(hub); show(p1); show(p2); show(p3); show(p4); show(p5); }
wait(0.6);

// 2. count V, E, F
say(narr, "V vertices, E edges, F faces.");
draw(cells, 0.9);
par { show(tv); show(te); show(tf); }
wait(1.0);

// 3. a spanning tree — V-1 edges
say(narr, "A spanning tree: every vertex, no loops — V-1 = 5 edges.");
par { draw(pt, 1.4); }
wait(1.0);

// 4. the leftover (co-tree) edges
say(narr, "That leaves E - (V-1) = 5 edges.");
par { draw(pt.co, 1.0); }
wait(1.0);

// 5. the dual — one node per face
say(narr, "The dual graph: one node inside each face.");
par { show(du.nodes); }
wait(1.0);

// 6. a spanning tree of the dual — F-1 edges
say(narr, "Its spanning tree pairs with the leftovers — F-1 = 5 edges.");
par { draw(dt, 1.4); }
wait(1.0);

// 7. the two trees tile every edge
say(narr, "Every edge lies in exactly one tree.");
show(sum);
wait(1.0);
par { fade(sum); show(nums); }
wait(1.0);

// 8. conclusion
say(narr, "So V - E + F = 2.");
pulse(thm);
wait(1.6);

pythagorean-proof

The dissection proof of a² + b² = c²: two identical (a+b)-squares, four triangles each, leaving c² on one and a²+b² on the other.

// ============================================================================
//  The Pythagorean theorem by dissection, a^2 + b^2 = c^2.
//
//  Two identical squares of side (a+b), each filled with four copies of the same
//  right triangle. On the left the triangles leave a tilted square on the
//  hypotenuse — c^2. On the right, the same four triangles regroup to leave a
//  square on each leg — a^2 and b^2. Same square, same triangles, so a^2+b^2=c^2.
//
//  a = 120, b = 180 (so the tilted square's side is c = sqrt(120^2+180^2)).
// ============================================================================
title("Pythagoras by dissection");
canvas(1280, 720);
template("black");

equation(thm, (640, 96), `a^2 + b^2 = c^2`, 50);
text(narr, (640, 672), "Two identical squares, side a + b.", 26);

// ---- LEFT square: four triangles + c^2 ----
polygon(sqL, (120,210),(420,210),(420,510),(120,510)); outlined(sqL); color(sqL, dim);
// four corner triangles (legs a,b)
polygon(la1, (120,210),(240,210),(120,390), teal);
polygon(la2, (420,210),(420,330),(240,210), teal);
polygon(la3, (420,510),(300,510),(420,330), teal);
polygon(la4, (120,510),(120,390),(300,510), teal);
opacity(la1, 0.4); opacity(la2, 0.4); opacity(la3, 0.4); opacity(la4, 0.4);
// the tilted square on the hypotenuse
polygon(csq, (240,210),(420,330),(300,510),(120,390), gold);
equation(clab, (270, 360), `c^2`, 40);

// ---- RIGHT square: the same four triangles + a^2 + b^2 ----
polygon(sqR, (600,210),(900,210),(900,510),(600,510)); outlined(sqR); color(sqR, dim);
// a^2 (top-left) and b^2 (bottom-right)
polygon(asq, (600,210),(720,210),(720,330),(600,330), blue);
polygon(bsq, (720,330),(900,330),(900,510),(720,510), violet);
equation(alab, (660, 270), `a^2`, 34);
equation(blab, (810, 420), `b^2`, 40);
// the four triangles filling the two leftover rectangles
polygon(ra1, (720,210),(900,210),(900,330), teal);
polygon(ra2, (720,210),(900,330),(720,330), teal);
polygon(ra3, (600,330),(720,330),(600,510), teal);
polygon(ra4, (720,330),(720,510),(600,510), teal);
opacity(ra1, 0.4); opacity(ra2, 0.4); opacity(ra3, 0.4); opacity(ra4, 0.4);

// side = a + b, under each square
brace(brL, (120,528),(420,528), "down"); text(brLt, (270,556), "a + b", 20);
brace(brR, (600,528),(900,528), "down"); text(brRt, (750,556), "a + b", 20);

// ---- reveal ----
hidden(la1); hidden(la2); hidden(la3); hidden(la4);
untraced(csq); hidden(clab);
untraced(asq); untraced(bsq); hidden(alab); hidden(blab);
hidden(ra1); hidden(ra2); hidden(ra3); hidden(ra4);
hidden(brL); hidden(brLt); hidden(brR); hidden(brRt);

wait(0.4);
par { show(sqL); show(sqR); }
par { show(brL); show(brR); show(brLt); show(brRt); }
wait(0.7);

say(narr, "Fill each with four copies of the same right triangle.");
par { show(la1); show(la2); show(la3); show(la4); show(ra1); show(ra2); show(ra3); show(ra4); }
wait(0.9);

say(narr, "On the left they leave a square on the hypotenuse: c squared.");
draw(csq, 1.0); show(clab);
wait(1.0);

say(narr, "Rearranged, the same four leave a square on each leg: a squared, b squared.");
par { draw(asq, 0.9); draw(bsq, 0.9); }
par { show(alab); show(blab); }
wait(1.1);

say(narr, "Same square, same four triangles — so a squared plus b squared is c squared.");
pulse(thm);
wait(0.6);
par { pulse(csq); pulse(asq); pulse(bsq); }
wait(1.4);

region-partition

regions fills + counts the areas a pentagram’s chords cut a disk into (16) — the planar-arrangement engine behind Moser/Euler/dissection scenes.

// ============================================================================
//  Region partition: five points on a circle, every chord drawn between them,
//  and `regions` fills + counts the areas they cut the disk into. The pentagon
//  + its diagonals (a pentagram) partition the circle into 16 regions.
//
//  `regions(id, boundary, dividers)` computes the planar arrangement of the
//  boundary and every divider, then fills each enclosed face — the areas are
//  detected for you, no coordinates for the pieces.
// ============================================================================
title("Region partition");
canvas(1280, 720);
template("black");

circle(disk, (640, 360), 240);

// five points on the circle
dot(pa, (640, 120)); dot(pb, (412, 286)); dot(pc, (499, 554));
dot(pd, (781, 554)); dot(pe, (868, 286));

// all ten chords (5 sides + 5 diagonals), tagged so `regions` takes them at once
line(s0, (640,120), (412,286)); tag(s0, chords);
line(s1, (412,286), (499,554)); tag(s1, chords);
line(s2, (499,554), (781,554)); tag(s2, chords);
line(s3, (781,554), (868,286)); tag(s3, chords);
line(s4, (868,286), (640,120)); tag(s4, chords);
line(g0, (640,120), (499,554)); tag(g0, chords);
line(g1, (640,120), (781,554)); tag(g1, chords);
line(g2, (412,286), (781,554)); tag(g2, chords);
line(g3, (412,286), (868,286)); tag(g3, chords);
line(g4, (499,554), (868,286)); tag(g4, chords);

regions(rg, disk, chords);   // fills + counts every enclosed area (16)

light-and-brachistochrone

The Brachistochrone’s optics arc: refract (real Snell’s law + angle sweep) → the native brachistochrone race where the cycloid wins.

// ============================================================================
//  Light and the brachistochrone — the optics half of the 2016 video.
//
//  Bernoulli solved "what curve gives the fastest slide?" by a leap: a sliding
//  bead is like a ray of light, which always takes the FASTEST path. Light
//  bends when it enters a slower medium (Snell's law); a bead falling through
//  ever-faster layers bends the same way — and the limiting curve is a cycloid.
//
//  Built on the optics kit's `refract` (real Snell's law, angle sweep) and the
//  physics kit's `brachistochrone` (four beads race, the cycloid wins).
// ============================================================================
title("Light and the brachistochrone");
canvas(1280, 720);
template("black");

text(narr, (640, 694), "Which path through the water is fastest?", 24);

refract(rf, (640, 360), 1.0, 1.5);            // air over glass/water — light bends
equation(snell, (640, 84), `\frac{\sin\theta_1}{v_1}=\frac{\sin\theta_2}{v_2}`, 40);
brachistochrone(bc, (450, 230));              // A→B racing curves (shown in act 2)

// --- act 1: light refracts ---
hidden(snell); hidden(bc);

wait(0.6);
say(narr, "Crossing into water light slows, and bends toward the normal.");
wait(1.6);

say(narr, "Steeper in means steeper out — always by Snell's law.");
run(rf, 3.2);
show(snell);
wait(1.2);

// --- act 2: Bernoulli's leap → the cycloid ---
say(narr, "Bernoulli's leap: a sliding bead is like light through faster and faster layers.");
par { fade(rf); fade(snell); }
wait(0.8);

show(bc);
say(narr, "Its fastest path is a cycloid — the brachistochrone.");
run(bc, 3.6);
wait(1.6);

snell-to-cycloid

The derivation that connects them: layers of ever-faster media, light bending at each boundary (Snell), the zig-zag smoothing into a cycloid where sin θ/√y is constant.

// ============================================================================
//  Snell's law → the cycloid — the derivation that connects light to the
//  brachistochrone (3b1b's multilayered.py).
//
//  Slice the medium into horizontal layers, each one letting light move faster
//  than the last (a falling bead speeds up: v = sqrt(y)). Light bends at every
//  boundary by Snell's law, sin(θ)/v = const. The bent path zig-zags down; as
//  the layers get thinner it smooths into a single curve — a CYCLOID — on which
//  sin(θ)/sqrt(y) is constant everywhere. That curve is the brachistochrone.
//
//  The bend angles below are the real Snell solution: v_i = sqrt(i+1),
//  sin(θ_i) = sin(20°)·v_i, so θ steepens 20° → 57° from the vertical.
// ============================================================================
title("Snell's law becomes a cycloid");
canvas(1280, 720);
template("black");

text(narr, (640, 694), "A falling bead speeds up — so slice the fall into faster and faster layers.", 22);

// six layers, each faster (lighter) than the one above
rect(l0, (640, 178), 1280, 77); color(l0, indigo); opacity(l0, 0.32);
rect(l1, (640, 255), 1280, 77); color(l1, blue);   opacity(l1, 0.32);
rect(l2, (640, 332), 1280, 77); color(l2, blue);   opacity(l2, 0.42);
rect(l3, (640, 409), 1280, 77); color(l3, teal);   opacity(l3, 0.42);
rect(l4, (640, 486), 1280, 77); color(l4, teal);   opacity(l4, 0.55);
rect(l5, (640, 563), 1280, 77); color(l5, cyan);   opacity(l5, 0.55);

// the discrete light path — bends once per boundary (Snell at each)
line(s0, (200,140), (228,217)); tag(s0, ray); color(s0, gold);
line(s1, (228,217), (270,294)); tag(s1, ray); color(s1, gold);
line(s2, (270,294), (327,371)); tag(s2, ray); color(s2, gold);
line(s3, (327,371), (399,448)); tag(s3, ray); color(s3, gold);
line(s4, (399,448), (491,525)); tag(s4, ray); color(s4, gold);
line(s5, (491,525), (609,602)); tag(s5, ray); color(s5, gold);

// the smooth limit: a cycloid through the same endpoints
param(cyc, (200,140), 130, 231, `t - sin(t)`, `cos(t) - 1`, (0, pi)); color(cyc, yellow);

// the invariant, and one angle to illustrate it
equation(inv, (960, 250), `\frac{\sin\theta}{\sqrt{y}} = \text{const}`, 40);
line(nrm, (274,320), (274,430)); color(nrm, dim); dashed(nrm);   // vertical (normal)
line(tan, (240,300), (312,428)); color(tan, red);                // tangent along the curve
text(thlab, (300, 340), "θ", 26); color(thlab, red);

// --- reveal ---
untraced(s0); untraced(s1); untraced(s2); untraced(s3); untraced(s4); untraced(s5);
untraced(cyc);
hidden(inv); hidden(nrm); hidden(tan); hidden(thlab);

wait(0.6);
par { show(l0); show(l1); show(l2); show(l3); show(l4); show(l5); }
wait(0.8);

say(narr, "Light bends at every boundary — Snell's law, sin(θ)/v constant.");
draw(s0, 0.4); draw(s1, 0.4); draw(s2, 0.4); draw(s3, 0.4); draw(s4, 0.4); draw(s5, 0.4);
wait(0.8);

say(narr, "Thinner and thinner layers — the zig-zag smooths into one curve.");
par { fade(ray); draw(cyc, 1.4); }
wait(1.0);

say(narr, "A cycloid — and along it sin(θ)/sqrt(y) stays constant everywhere.");
par { show(inv); show(nrm); show(tan); show(thlab); }
wait(1.8);

cross-product-3d

The 3D CROSS product (cross3): v × w perpendicular to the parallelogram v,w span, its length = that area, orbited to reveal the geometry behind the determinant formula.

// ============================================================================
//  The cross product in 3D (eola chapter8p2 — the geometric part).
//
//  v × w is the vector PERPENDICULAR to the plane of v and w, whose LENGTH is
//  the area of the parallelogram they span (and whose sign follows the right
//  hand). That geometry is what the î-ĵ-k̂ determinant formula computes.
//
//  Example: v=(2,1,0), w=(-1,2,0), so v×w=(0,0,5) — straight up, length 5 = the
//  parallelogram's area (2·2 − 1·(−1)). Built on the new `cross3` builtin.
// ============================================================================
title("The cross product in 3D");
canvas(1280, 720);
template("black");

text(narr, (640, 694), "Two vectors v and w in three-dimensional space.", 24);
equation(det, (960, 130),
  `\vec v\times\vec w=\det\!\begin{bmatrix}\hat\imath&\hat\jmath&\hat k\\2&1&0\\-1&2&0\end{bmatrix}`, 30);

cross3(cr, (0, 0, 0), (2, 1, 0), (-1, 2, 0));
camera3((5.5, -5.5, 5.5), (0.2, 0.4, 1.6));

// --- reveal ---
hidden(cr.p); hidden(cr.e1); hidden(cr.e2); hidden(det);

wait(0.6);
wait(1.4);                                        // v (green) and w (red) are up

say(narr, "They span a parallelogram.");
par { show(cr.e1); show(cr.e2); }
wait(1.4);

say(narr, "Their cross product v × w points perpendicular to both.");
show(cr.p);
wait(1.2);

say(narr, "Its length equals the area of that parallelogram.");
orbit3(75, 22, 9.5, 3.2);                          // orbit to reveal the perpendicular
wait(1.2);

say(narr, "That geometry is exactly what the determinant formula computes.");
show(det);
wait(1.8);

mnist-network

Neural Networks ch.1 & 3: a stylised digit (digit) feeds a real 144→16→16→10 network — the pixels ARE the inputs — then the forward pass computes, loss compares to the true label, and backward sends gradients back. The network builds layer by layer, not fully-formed.

// ============================================================================
//  What IS a neural network — and how does it learn? 3Blue1Brown's Neural
//  Networks (2017), chapters 1 & 3, as one journey:
//
//    build   — a network is layers of neurons joined by weighted connections,
//              revealed one layer at a time (not dumped fully-formed);
//    forward — a handwritten digit feeds in, each pixel an input, values flowing
//              forward through the layers to ten output probabilities;
//    loss    — compare the guess with the true label;
//    backward— backpropagation sends the error back as gradients, layer by layer.
//
//  A stylised "5" (`tensor`) feeds a real deterministic `network` via `feed`,
//  which flattens the pixels into the 144 inputs AND traces the lit strokes into
//  the input column. Every value shown is computed, not decorative.
// ============================================================================
title("What is a neural network — and how it learns");
canvas("16:9");
template("black");

// the handwritten digit as pixel brightnesses (12×12 = 144 inputs).
// Change "5" to any 0-9 to feed a different digit — or draw your own shape with
// `tensor(... `#`=ink `.`=blank ...)`.
digit(img, (250, 340), "5", 18, cyan);

// 144 pixels → 16 → 16 → 10 digit classes (the real MNIST shape, abbreviated ⋮)
network(net, (830, 340), "144 16 16 10", "sigmoid sigmoid softmax", 620, 500, 7);

text(narr, (640, 672), "A handwritten digit is just a grid of pixel brightnesses.", 24);

hidden(img);
hidden(net);
hidden(narr);

wait(0.4);
show(narr);
show(img);
wait(1.8);

// --- BUILD: the network appears one layer at a time (the journey) ---
say(narr, "A network is layers of neurons, joined by weighted connections.");
show(net.layer0);
wait(0.7);
par { show(net.transition0); show(net.layer1); }
wait(0.5);
par { show(net.transition1); show(net.layer2); }
wait(0.5);
par { show(net.transition2); show(net.layer3); }
par { show(net.probabilities); show(net.status); }
wait(1.0);

// --- FORWARD: the digit feeds in and values flow forward ---
say(narr, "Feed the digit in: each pixel is an input, and the values flow forward.");
feed(net, img, 3.6, smooth);
wait(0.6);

// --- LOSS: compare the guess with the true label ---
say(narr, "Compare the output with the true label — a five.");
loss(net, "0 0 0 0 1 0 0 0 0 0", crossentropy, 1.6);
wait(0.8);

// --- BACKWARD: backpropagation sends the error back as gradients ---
say(narr, "Backpropagation sends the error back through the layers, as gradients.");
backward(net, 3.4, smooth);
wait(1.0);

say(narr, "Forward to predict, backward to learn — that is how a network trains.");
wait(2.0);

gradient-partials

How a network learns: f(x,y) as a cost landscape; slice3 holds one variable constant so a cross-section’s steepness IS the partial derivative; gradient3 stacks them into ∇f (uphill); descend3 rolls a ball down −∇f into the valley — with a movie camera that swings to reveal the stack.

// ============================================================================
//  How a neural network learns — the gradient, and gradient descent. 3Blue1Brown's
//  "Gradient descent" segment (gradient.py), the full arc:
//
//    cost      — a network's cost is a function of MANY weights; picture it as a
//                landscape over just two;
//    partials  — to read ∂f/∂y, HOLD x constant → the surface slices to one curve,
//                and its steepness IS the partial (slice3); likewise ∂f/∂x;
//    gradient  — stack the two partials → ∇f, the direction of steepest ascent;
//    descent   — to LEARN, step the other way: follow −∇f DOWNHILL until the ball
//                settles in a valley — a minimum of the cost (descend3).
//
//  Narrated with captions (no character rigs). Every curve, slope, and step is
//  computed from the real surface.
// ============================================================================
title("How a neural network learns: gradient descent");
canvas("16:9");
template("black");

camera3((6, -8.5, 5.5), (0, 0, 0.7), 40);
axes3(world, (0, 0, 0), 2.4);
surface3(f, "exp(-x*x + cos(2*y))", (-2, 2), (-2, 2), 34);
opacity(f, 0.42);

slice3(sx, f, x, 0.6, 0.4, #ff3ea5);   // hold x → cross-section along y (∂f/∂y)
slice3(sy, f, y, 0.4, 0.6, #ffb020);   // hold y → cross-section along x (∂f/∂x)
gradient3(grad, f, 0.6, 0.4);
descend3(desc, f, 0.7, 0.35, 0.25, 45, #7cff3e);

equation(feq, (232, 96), `f(x,y)=e^{-x^2+\cos 2y}`, 28);
equation(ceq, (232, 150), `\text{cost } C(w_1,\dots,w_n)`, 24); color(ceq, dim);
equation(geq, (1040, 632), `\nabla f=\begin{bmatrix}\partial f/\partial x\\ \partial f/\partial y\end{bmatrix}`, 28);
text(narr, (640, 668), "A network's cost is one number over millions of weights.", 24);

hidden(sx); hidden(sx.slope);
hidden(sy); hidden(sy.slope);
hidden(grad); hidden(geq); hidden(narr);
hidden(desc); hidden(desc.ball);
untraced(desc); untraced(desc.ball);   // start un-drawn so the descent can roll on

// A MOVIE camera runs concurrently with the story: it swings around and up so the
// stacked slices + gradient are never hidden behind the peak, then drops low to
// follow the ball rolling into the valley. (orbit3 composes in `par`.)
par {
  seq {
    orbit3(-18, 26, 9.5, 5.0, smooth);   // establish: front-left, slightly high
    orbit3(40, 36, 9.2, 6.5, smooth);    // swing right & rise to reveal the STACK
    orbit3(62, 22, 8.4, 6.0, smooth);    // drop low, come round to watch the descent
    orbit3(74, 24, 8.4, 4.5, smooth);    // settle on the valley
  }
  seq {
    wait(0.5);
    show(narr);
    show(ceq);
    wait(2.2);

    say(narr, "Picture it as a landscape over just two of them.");
    wait(2.0);

    say(narr, "To read ∂f/∂y, hold x constant — that slices the surface to one curve.");
    par { show(sx); show(sx.slope); }
    wait(2.4);

    say(narr, "Hold y constant instead, and this slice's steepness is ∂f/∂x.");
    par { show(sy); show(sy.slope); }
    wait(2.4);

    say(narr, "Stack the two partials and you get the gradient — steepest ascent.");
    par { show(grad); show(geq); }
    wait(2.8);

    say(narr, "To LEARN, step the other way: downhill, along −∇f.");
    par { show(desc); show(desc.ball); }
    par { draw(desc, 3.0); draw(desc.ball, 3.0); }   // the ball rolls, the trail draws
    wait(0.6);

    say(narr, "It rolls into a valley — a minimum of the cost. That is learning.");
    pulse(desc.ball);
    wait(2.6);
  }
}

high-dimensions

Thinking outside the 10-D box: a point in N-dimensional space drawn as N dials (sliders). Put it on the unit sphere and Σxᵢ²=1 is a fixed budget — spread evenly every coordinate is tiny, and the corner sits √N away.

// ============================================================================
//  Thinking in high dimensions — 3Blue1Brown's "10-dimensional box" device: a
//  point in N-dimensional space is just N numbers, drawn as N dials. You can't
//  picture 7 axes, but you CAN watch 7 sliders move together.
//
//  Put the point on the unit sphere and the coordinates obey x₁²+…+x₇² = 1 — a
//  fixed budget of "real estate" spread across the axes. Spread it evenly and
//  every coordinate is tiny (1/√7); pile it into a few and the rest go slack.
//  The corner (1,1,…,1) sits √7 away — and √N in N-D — which is why high
//  dimensions feel so vast.
//
//  `sliders` is the rack; `setsliders` drives all dials at once and shows Σxᵢ².
// ============================================================================
title("Thinking in high dimensions");
canvas("16:9");
template("black");

sliders(pt, 7, (640, 340), 820, 360, gold);
text(narr, (640, 656), "A point in 7-D space is just seven numbers — one dial each.", 24);

hidden(narr);

wait(0.4);
show(narr);
wait(2.4);

say(narr, "Put it on the unit sphere: x₁² + … + x₇² = 1 — a fixed budget.");
setsliders(pt, "0.38 0.38 0.38 0.38 0.38 0.38 0.38", 1.6);
wait(2.2);

say(narr, "Spread evenly, every coordinate is tiny — only 1 over root 7.");
wait(2.2);

say(narr, "Pile the budget into a few, and the rest fall slack — Σx² stays 1.");
setsliders(pt, "0.86 0.42 0.22 0.13 0.10 0.06 0.04", 1.6);
wait(2.4);

say(narr, "Swing it around — the total never leaves the sphere.");
setsliders(pt, "-0.30 0.55 -0.62 0.28 0.34 -0.18 0.12", 1.6);
wait(2.2);

say(narr, "And the corner (1,…,1) sits √7 away — √N in N-D. High space is vast.");
setsliders(pt, "1 1 1 1 1 1 1", 1.8);
wait(2.6);