Linear algebra & tables
Each block is the whole file — copy it into x.manic and run manic x.manic (live) or --record out (video).
linear-algebra — the whole subject in five ideas
A guided lesson, not a feature demo: five chapters that build linear algebra as
one connected story. Chapters 1–3 view the same matrix [[2,1],[1,2]]
through three lenses — a transformation of space (linmap), the determinant as
area scaling (determinant), and its eigenvectors / diagonalisation
(diagonalise) — then it moves on to solving Ax = b (linsolve → rref) and
projection / least-squares (project). Start here.
// ============================================================================
// linear-algebra.manic — Linear Algebra in five ideas (a lesson)
// ----------------------------------------------------------------------------
// A guided tour of the whole subject, not a feature demo. One stage, five
// chapters; chapters 1-3 view the SAME matrix A = [[2,1],[1,2]] through three
// lenses, so the ideas connect:
// 1. a matrix TRANSFORMS space (linmap)
// 2. the DETERMINANT is how area scales (determinant) -> det = 3
// 3. EIGENVECTORS only stretch (diagonalise) -> lambda = 3, 1
// 4. SOLVING A x = b (linsolve -> rref)
// 5. PROJECTION: the closest answer (project)
//
// Reveal recipe (works for lines, arrows, fills, and text alike, preserving
// each element's opacity): `untraced(tag)` at build time, `draw(tag)` to reveal,
// `fade(tag)` to clear before the next chapter. `tag` broadcasts to every entity
// carrying it (see the ergonomics in the manic guide).
// ============================================================================
title("Linear Algebra, in five ideas");
canvas("16:9");
let sx = cx;
let sy = cy + 44; // shared stage center
let u = 44;
text(cap, (cx, h - 46), ""); size(cap, 22); color(cap, dim); display(cap);
// ---- build every chapter's visual up front, drawn-blank (untraced) ----
// A coordinate grid draws in with each chapter for consistent visual feedback.
// Chapters 1 & 3 bring their own (the deformed grid / the eigen-grid); chapters
// 2, 4, 5 get a plain reference grid that traces in at the start of the section.
plane(g2, (sx, sy), 176, 176, u); untraced(g2); // determinant — count the cells
plane(g4, (sx, sy), 240, 230, u); untraced(g4); // solving — read off (1, 3)
plane(g5, (sx, sy), 200, 200, u); untraced(g5); // projection
// 1. transformation of A = [[2,1],[1,2]]
linmap(lm, (sx, sy), u, 2, 1, 1, 2);
untraced(lm);
// 2. determinant of the same A (parallelogram is the bare id `dt`)
determinant(dt, (sx, sy), u, 2, 1, 1, 2);
untraced(dt); untraced(dt.unit); untraced(dt.val);
// 3. eigenvectors / diagonalisation of the same A
diagonalise(dg, (sx, sy), u, 2, 1, 1, 2);
untraced(dg);
// 4a. a 2x2 system as two lines (solution dot is the bare id `sys`)
linsolve(sys, (sx, sy), u, 2, 1, 1, 3, 5, 10);
untraced(sys.r1); untraced(sys.r2); hidden(sys); hidden(sys.val);
// 4b. the same system reduced by elimination
rref(rr, "2 1 5 ; 1 3 10", (sx, sy - 24), 120, 60);
untraced(rr.lbrack); untraced(rr.rbrack);
// 5. projection of a vector onto a line
project(pj, (sx, sy), u, (1, 3), (3, 1));
untraced(pj);
// ============================ the lesson ==================================
section("Transformations");
draw(lm, 1.2);
say(cap, "a matrix sends every point to a new one — and it does so LINEARLY", 0.5);
wait(1.8);
say(cap, "the grid stays parallel and evenly spaced; i-hat, j-hat land on its columns", 0.5);
wait(2.2);
fade(lm, 0.6);
wait(0.3);
say(cap, "", 0.2);
section("The determinant");
draw(g2, 0.7);
draw(dt.unit, 0.5); draw(dt, 0.7); draw(dt.val, 0.4);
say(cap, "how much does it stretch area? the unit square becomes this parallelogram", 0.5);
wait(2.0);
say(cap, "that area IS the determinant = 3 — three grid cells; a flip would be negative", 0.5);
wait(2.2);
fade(dt, 0.6); fade(dt.unit, 0.6); fade(dt.val, 0.6); fade(g2, 0.6);
wait(0.3);
say(cap, "", 0.2);
section("Eigenvectors");
draw(dg, 1.2);
say(cap, "most vectors change direction — but a few only STRETCH: the eigenvectors", 0.5);
wait(2.2);
say(cap, "in their basis A is pure scaling: A = P D P^-1, D = diag(3, 1)", 0.5);
wait(2.2);
fade(dg, 0.6);
wait(0.3);
say(cap, "", 0.2);
section("Solving A x = b");
draw(g4, 0.7);
draw(sys.r1, 0.7); draw(sys.r2, 0.7);
show(sys, 0.4); show(sys.val, 0.4);
say(cap, "a system of equations is a set of lines; the solution is where they meet", 0.5);
wait(2.2);
fade(sys, 0.5); fade(sys.r1, 0.5); fade(sys.r2, 0.5); fade(sys.val, 0.5); fade(g4, 0.5);
say(cap, "elimination finds it by reducing [ A | b ] to the identity beside the answer", 0.5);
draw(rr.lbrack, 0.4); draw(rr.rbrack, 0.4);
show(rr.s0, 0.4); show(rr.op0, 0.3);
wait(1.6);
par { show(rr.s4, 0.5); fade(rr.s0, 0.5); } show(rr.op4, 0.4); fade(rr.op0, 0.3);
wait(2.0);
fade(rr.s4, 0.5); fade(rr.op4, 0.4); fade(rr.lbrack, 0.4); fade(rr.rbrack, 0.4);
wait(0.3);
say(cap, "", 0.2);
section("Projection");
draw(g5, 0.7);
draw(pj, 1.0);
say(cap, "when no exact answer exists, take the CLOSEST point of the subspace", 0.5);
wait(2.0);
say(cap, "the error is perpendicular — that principle IS least-squares, the best fit", 0.5);
wait(2.4);
textbook-matrix-inverses
Two exam-style inverse proofs animated through structure rather than row reduction.
The first scales to an integer matrix, proves its columns are orthogonal, and gets
A⁻¹ = Aᵀ from AᵀA = I; the second computes A², observes A³ = I, and
concludes A⁻¹ = A². One persistent proof stage keeps every unchanged expression.
// ============================================================================
// textbook-matrix-inverses.manic
// Two textbook inverse proofs, animated as structural shortcuts:
// (i) orthogonal columns give A^T A = I;
// (ii) a finite-order matrix gives A^3 = I.
// ============================================================================
title("Two Matrix Inverse Proofs — See the Structure");
canvas("9:16");
template("paper");
watermark(manicMark,(220,175),"Made With Manic");
creator(me,"@anish2good name=Manic_Algebra tagline=Textbooks_in_motion yt=zarigatongy x=@anish2good web=8gwifi.org/manic accent=cyan secondary=magenta footer=social safe=reels");
socials(me);
text(kicker,(540,150),"MATRICES · INVERSES · STRUCTURE");
size(kicker,21); bold(kicker); color(kicker,cyan); hidden(kicker);
text(headline,(540,218),"Two proofs without row reduction");
size(headline,38); bold(headline); hidden(headline);
text(part,(540,300),"(i) TEST THE COLUMNS");
size(part,23); bold(part); color(part,gold); hidden(part);
rect(stage,(540,805),930,930);
color(stage,panel); outline(stage,dim); opacity(stage,0.72); hidden(stage);
equation(given,(540,500),
`A=\frac{1}{9}\begin{bmatrix}-8&1&4\\4&4&7\\1&-8&4\end{bmatrix}`,
40);
hidden(given);
equation(work,(540,800),`A^{-1}\stackrel{?}{=}A^{\mathsf T}`,48);
hidden(work);
text(reason,(540,1095),"The inverse appears when a product becomes the identity.");
size(reason,24); wrap(reason,820); color(reason,dim); hidden(reason);
line(rule,(170,1235),(910,1235));
color(rule,cyan); stroke(rule,2); dashed(rule,12,9); opacity(rule,0.32);
untraced(rule);
equation(summary,(540,1435),
`\boxed{A^{\mathsf T}A=I\Rightarrow A^{-1}=A^{\mathsf T}}`,
39);
hidden(summary);
text(takeaway,(540,1540),"LOOK FOR A SHORT PRODUCT THAT RETURNS TO I");
size(takeaway,18); bold(takeaway); color(takeaway,gold); hidden(takeaway);
step("read the two inverse claims") {
seq {
par {
show(kicker,0.35); show(headline,0.55); show(part,0.40);
show(stage,0.45); draw(rule,0.75); show(reason,0.40);
}
show(given,0.65);
show(work,0.55);
wait(0.85);
}
}
step("factor out the scale") {
seq {
say(reason,"Write A = M/9. Then ask whether M-transpose times M equals 81I.",0.50,smooth);
rewrite(work,
`M=\begin{bmatrix}-8&1&4\\4&4&7\\1&-8&4\end{bmatrix},\qquad A=\frac{1}{9}M`,
1.05,smooth);
wait(0.70);
}
}
step("recognize orthogonal columns") {
seq {
say(reason,"Every column has squared length 81, and distinct columns have dot product 0.",0.55,smooth);
rewrite(work,
`\begin{aligned}
\|c_1\|^2&=64+16+1=81\\
\|c_2\|^2&=1+16+64=81\\
\|c_3\|^2&=16+49+16=81
\end{aligned}`,
1.10,smooth);
wait(0.55);
rewrite(work,
`\begin{aligned}
c_1^{\mathsf T}c_2&=-8+16-8=0\\
c_1^{\mathsf T}c_3&=-32+28+4=0\\
c_2^{\mathsf T}c_3&=4+28-32=0
\end{aligned}`,
1.10,smooth);
wait(0.70);
}
}
step("the transpose is the inverse") {
seq {
say(reason,"Those six dot products are exactly the entries of M-transpose times M.",0.50,smooth);
rewrite(work,
`M^{\mathsf T}M=\begin{bmatrix}81&0&0\\0&81&0\\0&0&81\end{bmatrix}=81I`,
1.00,smooth);
wait(0.45);
rewrite(work,
`A^{\mathsf T}A=\frac{1}{81}M^{\mathsf T}M=I`,
0.90,smooth);
show(summary,0.55);
par { pulse(work,0.75); pulse(summary,0.75); }
wait(1.15);
}
}
step("begin the second matrix") {
seq {
par {
fade(summary,0.35);
say(part,"(ii) FIND A POWER THAT RETURNS TO I",0.45,smooth);
say(reason,"For the second matrix, compute powers instead of augmenting [A | I].",0.55,smooth);
rewrite(given,
`A=\begin{bmatrix}1&-1&1\\2&-1&0\\1&0&0\end{bmatrix}`,
0.95,smooth);
rewrite(work,`A^{-1}\stackrel{?}{=}A^2`,0.80,smooth);
}
wait(0.85);
}
}
step("square the matrix") {
seq {
say(reason,"First multiply A by itself. Keep this result for one more multiplication.",0.55,smooth);
rewrite(work,
`A^2=\begin{bmatrix}0&0&1\\0&-1&2\\1&-1&1\end{bmatrix}`,
1.00,smooth);
wait(0.80);
}
}
step("the third power is identity") {
seq {
say(reason,"Now A squared times A returns exactly to the identity matrix.",0.50,smooth);
rewrite(work,
`A^3=A^2A=\begin{bmatrix}1&0&0\\0&1&0\\0&0&1\end{bmatrix}=I`,
1.10,smooth);
wait(0.65);
rewrite(work,`AA^2=A^3=I`,0.80,smooth);
rewrite(summary,
`\boxed{AA^2=I\Rightarrow A^{-1}=A^2}`,
0.85,smooth);
show(summary,0.50);
par { pulse(work,0.75); pulse(summary,0.75); }
wait(1.10);
}
}
step("compare the two shortcuts") {
seq {
par {
show(takeaway,0.45);
say(part,"TWO STRUCTURES · ONE DEFINITION",0.45,smooth);
say(reason,"The inverse is whichever matrix multiplies A to give I.",0.50,smooth);
rewrite(given,
`\text{(i) orthogonality}\qquad\text{(ii) finite order}`,
0.90,smooth);
rewrite(work,
`\begin{aligned}
A^{\mathsf T}A=I&\Rightarrow A^{-1}=A^{\mathsf T}\\
AA^2=I&\Rightarrow A^{-1}=A^2
\end{aligned}`,
1.10,smooth);
fade(summary,0.45);
}
par { pulse(work,0.90); pulse(takeaway,0.90); }
wait(2.00);
}
}
linear-map
What a 2×2 matrix does to space: the grid deforms and the basis lands on its
columns (linmap), the unit square’s area becomes the determinant
(determinant), and two directions only stretch — the eigenvectors (eigen).
title("What a matrix does to space");
canvas("16:9");
let ox = cx - 60;
let oy = cy + 40;
text(hdr, (cx, 54), "the matrix [[2,1],[1,2]] — grid, determinant, eigenvectors");
size(hdr, 24); color(hdr, cyan); bold(hdr); display(hdr); hidden(hdr);
text(cap, (cx, h - 50), "");
size(cap, 22); color(cap, dim); display(cap);
// the plane deformed by the matrix (î, ĵ land on its columns)
linmap(lm, (ox, oy), 46, 2, 1, 1, 2, 2);
// the unit square's image — area = determinant
determinant(dt, (ox, oy), 46, 2, 1, 1, 2);
hidden(dt); hidden(dt.unit); hidden(dt.val);
// the two invariant directions (eigenvectors)
eigen(ev, (ox, oy), 46, 2, 1, 1, 2);
hidden(ev); hidden(ev.l0); hidden(ev.l1);
// ---- timeline ----
show(hdr, 0.6);
say(cap, "the matrix bends the grid; i-hat and j-hat land on its columns", 0.5);
wait(1.4);
say(cap, "the unit square maps to a parallelogram — its area IS the determinant", 0.5);
show(dt, 0.6);
show(dt.unit, 0.4);
show(dt.val, 0.5);
wait(1.4);
say(cap, "two directions only stretch, never turn: the eigenvectors", 0.5);
show(ev, 0.6);
show(ev.l0, 0.4);
show(ev.l1, 0.4);
wait(1.0);
linear-system
The geometry of solving and spanning, in three panels: a 2×2 system as two lines
crossing at the solution (linsolve), two independent vectors reaching the whole
plane, and two parallel vectors collapsing to a line — rank 1 (span).
// ============================================================================
// linear-system.manic — the geometry of solving & spanning (a template)
// ----------------------------------------------------------------------------
// Three side-by-side panels, each built up with animation:
// A) a 2x2 system as two lines meeting at the solution (linsolve)
// B) two independent vectors spanning the whole plane (span)
// C) two parallel vectors collapsing to a single line, rank 1 (span)
//
// HOW TO ADAPT THIS FILE:
// * Change the system in panel A: linsolve(sys, center, unit, a,b,c,d, e,f)
// draws a*x + b*y = e and c*x + d*y = f, meeting at the solution.
// * Change the vectors in panels B/C: span(id, center, unit, (vx,vy),(wx,wy)).
// * Everything is positioned from cx/cy/w/h + a per-panel origin (ax/bx/dx)
// and a `unit` = pixels-per-grid-step, so it scales cleanly. Grid point
// (gx,gy) maps to screen (origin_x + gx*u, origin_y - gy*u) [math y-up].
// * OPTIONAL: shade the closed region the two lines bound with the axes —
// see the commented `polygon(feasible, ...)` block in Panel A below.
// ============================================================================
title("Solving & spanning, visually");
canvas("16:9");
let y0 = cy + 20; // shared vertical center of the three panels
let ax = 280; // panel A origin x — a system as two lines
let bx = 640; // panel B origin x — span = the whole plane
let dx = 1000; // panel C origin x — parallel vectors collapse to a line
let u = 24; // pixels per grid unit (shared scale)
text(hdr, (cx, 46), "linear algebra: the geometry of solving & spanning");
size(hdr, 24); color(hdr, cyan); bold(hdr); display(hdr);
// per-panel captions, revealed as each panel animates
text(la, (ax, 96), "a system = two lines"); size(la, 18); color(la, dim); display(la); hidden(la);
text(lb, (bx, 96), "span of two vectors"); size(lb, 18); color(lb, dim); display(lb); hidden(lb);
text(lc, (dx, 96), "parallel = rank 1"); size(lc, 18); color(lc, dim); display(lc); hidden(lc);
// faint coordinate backdrops (drawn once, static)
plane(pa, (ax, y0), 150, 190, u);
plane(pb, (bx, y0), 150, 190, u);
plane(pc, (dx, y0), 150, 190, u);
// ---- Panel A — the system 2x + y = 5 and x + 3y = 10, meeting at (1, 3) ----
linsolve(sys, (ax, y0), u, 2, 1, 1, 3, 5, 10);
untraced(sys.r1); untraced(sys.r2); // start blank, drawn in on the timeline
hidden(sys); hidden(sys.val); // the solution dot + its label fade in
// -- OPTIONAL: fill the closed area the two lines bound with the axes --------
// The two rows, the x-axis and the y-axis enclose a quadrilateral (the classic
// "feasible region"). Its corners, in grid coordinates, are:
// (0,0) -> (2.5,0) -> (1,3) -> (0,3.33)
// origin line1 hits the two line2 hits
// the x-axis lines meet the y-axis
// Uncomment to shade it (points are origin + gridX*u, origin - gridY*u):
//
// polygon(feasible,
// (ax, y0), // (0, 0) the origin
// (ax + 2.5*u, y0), // (2.5, 0) line 1 crosses the x-axis
// (ax + u, y0 - 3*u), // (1, 3) the two lines meet
// (ax, y0 - 3.333*u), // (0, 3.33) line 2 crosses the y-axis
// lime);
// opacity(feasible, 0.18); // translucent, so grid + lines show through
// z(feasible, -1); // sit behind the lines
// (for an animated reveal instead of a static fill, also `hidden(feasible);`
// above, then `to(feasible, opacity, 0.18, 0.6);` in Panel A's timeline.)
// ---------------------------------------------------------------------------
// ---- Panel B — two independent vectors reach every point: the whole plane --
span(fill, (bx, y0), u, (3, 1), (-1, 2));
untraced(fill.v); untraced(fill.w); // vectors draw in
hidden(fill.plane); // faint region fades to 0.14
// ---- Panel C — two parallel vectors only reach one line: rank 1 ------------
span(rank, (dx, y0), u, (2, 1), (-2, -1), gold);
untraced(rank.v); untraced(rank.w); untraced(rank.line);
text(cap, (cx, h - 44), ""); size(cap, 22); color(cap, dim); display(cap);
// ---- Panel A timeline: two lines draw in, then the solution pops -----------
show(la, 0.4);
say(cap, "two equations are two lines; where they cross solves the system", 0.5);
par { draw(sys.r1, 0.8); draw(sys.r2, 0.8); }
show(sys, 0.4);
show(sys.val, 0.4);
pulse(sys);
wait(1.6);
// ---- Panel B timeline: two vectors grow out, then the plane fills in -------
show(lb, 0.4);
say(cap, "two independent vectors combine to reach every point: the whole plane", 0.5);
par { draw(fill.v, 0.7); draw(fill.w, 0.7); }
to(fill.plane, opacity, 0.14, 0.9);
wait(1.6);
// ---- Panel C timeline: two parallel vectors, then their line (collapse) ----
show(lc, 0.4);
say(cap, "but parallel vectors only reach one line: rank 1, a collapse", 0.5);
par { draw(rank.v, 0.7); draw(rank.w, 0.7); }
draw(rank.line, 0.8);
wait(1.8);
diagonalise
A = P D P⁻¹ made visual: every real-diagonalisable matrix has a basis — its
eigenvectors — in which it does nothing but stretch each axis. The unit
eigen-cell stretches by λ along each eigenvector, with no rotation or shear
(diagonalise).
// ============================================================================
// diagonalise.manic — A = P D P^-1 made visual (a template)
// ----------------------------------------------------------------------------
// The big idea of diagonalisation: every (real-diagonalisable) matrix has a
// basis — its EIGENVECTORS — in which it does nothing but STRETCH each axis.
// No rotation, no shear: just a diagonal scaling D = diag(lambda1, lambda2).
//
// `diagonalise(id, (cx,cy), unit, a,b,c,d, [color])` draws, for [[a,b],[c,d]]:
// * the (generally skewed) eigen-grid — the coordinate frame of the eigenbasis
// * the two eigen-axes dg.axis1 / dg.axis2
// * the unit eigen-cell dg.cell and its image under A dg.img
// * the eigenvector images as arrows dg.v1 / dg.v2 (+ labels dg.v1l / dg.v2l)
// Complex or repeated eigenvalues (no real 2-D eigenbasis) leave a note instead.
//
// TO ADAPT: change the four matrix numbers below. Try a non-symmetric matrix
// like (2,1,0,3) to see a SKEWED eigenbasis, or (0,-1,1,0) to see the
// "no real eigenbasis" note (a pure rotation).
// ============================================================================
title("Diagonalisation: a matrix in its own basis");
canvas("16:9");
let ox = cx - 40;
let oy = cy + 20;
text(hdr, (cx, 54), "A = P D P^-1 — in the eigenbasis, A is only a stretch");
size(hdr, 24); color(hdr, cyan); bold(hdr); display(hdr);
// the matrix [[2,1],[1,2]] — eigenvalues 3 (along (1,1)) and 1 (along (1,-1))
diagonalise(dg, (ox, oy), 60, 2, 1, 1, 2);
// the faint eigen-grid stays as static context; reveal the rest in beats:
untraced(dg.axis1); untraced(dg.axis2); // eigen-axes draw in
hidden(dg.cell); hidden(dg.img); // unit cell fades in; image fades to 0.4
untraced(dg.v1); untraced(dg.v2); // eigenvector arrows draw in
hidden(dg.v1l); hidden(dg.v2l); // lambda labels fade in
text(cap, (cx, h - 50), ""); size(cap, 22); color(cap, dim); display(cap);
// ---- timeline ----
say(cap, "every matrix has special directions — its eigenvectors", 0.5);
par { draw(dg.axis1, 0.7); draw(dg.axis2, 0.7); }
wait(1.2);
say(cap, "build the unit cell from those two directions", 0.5);
show(dg.cell, 0.6);
wait(1.4);
say(cap, "apply A: the cell only STRETCHES along each axis — never rotates", 0.5);
to(dg.img, opacity, 0.4, 0.8);
par { draw(dg.v1, 0.7); draw(dg.v2, 0.7); }
wait(1.2);
say(cap, "the stretch factors ARE the eigenvalues — the diagonal of D", 0.5);
show(dg.v1l, 0.4); show(dg.v2l, 0.4);
wait(1.6);
rref
Gaussian elimination, animated: an augmented matrix [A | b] is reduced to
reduced row-echelon form one row operation at a time, the numbers transforming
in place until the left block is the identity and the last column is the
solution (rref).
// ============================================================================
// rref.manic — Gaussian elimination, animated (a template)
// ----------------------------------------------------------------------------
// Reduce an augmented matrix [A | b] to reduced row-echelon form, one row
// operation at a time. Each intermediate state is drawn at the same spot, so
// cross-fading s{k-1} -> s{k} makes the numbers transform IN PLACE.
//
// `rref(id, "row ; row ; ...", (cx,cy), [cellw], [rowh])` draws, for the given
// matrix (rows split on `;`, entries on spaces/commas):
// * static brackets (the frame the numbers fill)
// * one matrix per elimination state, tagged rr.s0, rr.s1, ... (hidden)
// * the row-op caption for each state: rr.op0, rr.op1, ...
// rr.s0 is the untouched input; the LAST state is the RREF (for [A|b] its final
// column is the solution). Reveal the states in order to animate the reduction.
//
// TO ADAPT: change the matrix string. A different system takes a different
// NUMBER of steps — add/remove reveal beats below to match (the ones past the
// last real state are harmless no-ops, so a few extra never hurt).
// ============================================================================
title("rref: Gaussian elimination, animated");
canvas("16:9");
text(hdr, (cx, 60), "reduce [ A | b ] to reduced row-echelon form");
size(hdr, 24); color(hdr, cyan); bold(hdr); display(hdr);
// the augmented matrix of the system 2x + y = 5, x + 3y = 10
rref(rr, "2 1 5 ; 1 3 10", (cx, cy - 10), 120, 64);
text(cap, (cx, h - 56), ""); size(cap, 22); color(cap, dim); display(cap);
// ---- reveal each state in place, captioned with its row operation ----
// Per beat: fade the old op, CROSS-FADE the matrix state (identical cells sit
// still, only the changed ones morph), then show the new op.
say(cap, "start with the augmented matrix [ A | b ]", 0.5);
show(rr.s0, 0.5); show(rr.op0, 0.4);
wait(1.6);
fade(rr.op0, 0.25); par { show(rr.s1, 0.5); fade(rr.s0, 0.5); } show(rr.op1, 0.4);
wait(1.5);
fade(rr.op1, 0.25); par { show(rr.s2, 0.5); fade(rr.s1, 0.5); } show(rr.op2, 0.4);
wait(1.5);
fade(rr.op2, 0.25); par { show(rr.s3, 0.5); fade(rr.s2, 0.5); } show(rr.op3, 0.4);
wait(1.5);
say(cap, "the left block is now the identity — the last column is the solution", 0.5);
fade(rr.op3, 0.25); par { show(rr.s4, 0.5); fade(rr.s3, 0.5); } show(rr.op4, 0.4);
wait(2.2);
projection
One idea, two faces: orthogonal projection drops a vector onto a subspace
(the shadow is the closest point, the error meets the space at a right angle),
and least-squares fits a line to data the same way — minimising the squared
residuals (project, leastsquares).
// ============================================================================
// projection.manic — projection & least-squares (a template)
// ----------------------------------------------------------------------------
// One idea, two faces. ORTHOGONAL PROJECTION drops a vector onto a subspace;
// the shadow p is the closest point, and the error b - p meets the subspace at
// a right angle. LEAST-SQUARES fits a line to data the SAME way — the best line
// is the one that minimises the (squared) residuals, i.e. the projection of the
// data onto the space of lines.
//
// `project(id, (cx,cy), unit, (bx,by), (ax,ay), [color])` draws:
// id.line (the subspace = span of a), id.b, id.p (the shadow), id.res (error),
// id.rt (right-angle mark), id.blabel / id.plabel.
// `leastsquares(id, (cx,cy), unit, "x1 y1 x2 y2 ...", [color])` draws:
// id.line (best fit), id.points (dots), id.residuals (verticals), id.eq.
//
// TO ADAPT: change b/a in `project`, or the point list in `leastsquares`.
// ============================================================================
title("Projection & least-squares");
canvas("16:9");
text(hdr, (cx, 50), "the closest point is a projection — and so is the best-fit line");
size(hdr, 23); color(hdr, cyan); bold(hdr); display(hdr);
// ---- LEFT: project vector b onto the line spanned by a ----
plane(pa, (cx - 330, cy + 10), 150, 210, 40);
project(pj, (cx - 330, cy + 10), 40, (1, 3), (3, 1));
untraced(pj.line); untraced(pj.b); untraced(pj.p); untraced(pj.res); untraced(pj.rt);
hidden(pj.blabel); hidden(pj.plabel);
// ---- RIGHT: fit a line to a point cloud ----
plane(pb, (cx + 310, cy + 90), 150, 150, 34);
leastsquares(ls, (cx + 310, cy + 90), 34, "1 2 2 3 3 5 4 4 5 6");
hidden(ls.points); untraced(ls.line); untraced(ls.residuals); hidden(ls.eq);
text(cap, (cx, h - 46), ""); size(cap, 22); color(cap, dim); display(cap);
// ---- Panel A: the projection ----
say(cap, "drop b onto the line spanned by a — its shadow is the projection p", 0.5);
draw(pj.line, 0.6);
draw(pj.b, 0.6);
wait(0.6);
par { draw(pj.p, 0.6); draw(pj.res, 0.6); }
draw(pj.rt, 0.3);
show(pj.blabel, 0.3); show(pj.plabel, 0.3);
wait(1.4);
say(cap, "the error b - p meets the line at a right angle: p is the nearest point", 0.5);
wait(1.8);
// ---- Panel B: least-squares is the same idea ----
say(cap, "fitting a line works the SAME way: minimise the squared residuals", 0.5);
show(ls.points, 0.5);
wait(0.7);
draw(ls.line, 0.7);
draw(ls.residuals, 0.6);
show(ls.eq, 0.4);
wait(2.0);
matrix
A bracketed matrix, rows/columns addressable via tags.
// Matrix — a bracketed grid of entries, addressable by row and column via tag
// broadcast (à la Manim's Matrix + set_row_colors / set_column_colors).
//
// manic examples/matrix.manic
// manic examples/matrix.manic --record out --fps 60
//
// Rows are separated by ';', entries by spaces/commas. Entry ids m.r{i}c{j};
// tags m.row{i} / m.col{j} / m.entries.
title("Matrix");
canvas(1280, 720);
text(head, (640, 130), "rows and columns you can address");
display(head); color(head, cyan); size(head, 28); hidden(head);
text(cap, (640, 620), ""); color(cap, dim); size(cap, 22);
matrix(m, "2 0 4; -1 1 5; 3 -2 0", (640, 370));
untraced(m.lbrack); untraced(m.rbrack);
hidden(m.entries);
show(head, 0.5);
say(cap, "a 3x3 matrix");
par { draw(m.lbrack, 0.5); draw(m.rbrack, 0.5); }
seq { show(m.row0, 0.35); show(m.row1, 0.35); show(m.row2, 0.35); }
wait(0.5);
section("Columns");
say(cap, "colour a column — set_column_colors");
recolor(m.col1, magenta, 0.4);
flash(m.col2, cyan);
wait(0.5);
section("Rows");
say(cap, "and highlight a row — set_row_colors");
recolor(m.row0, lime, 0.4);
par { pulse(m.r0c0); pulse(m.r0c1); pulse(m.r0c2); }
wait(1.2);
matrix_addition
Two matrices summed, cell by cell.
// Matrix Addition — A + B = C, computed entry by entry. Each matching pair of
// entries flashes, then their sum pops into the result matrix. A teaching
// animation: it shows *why* matrix addition is element-wise.
//
// manic examples/matrix_addition.manic
// manic examples/matrix_addition.manic --record out --fps 60
title("Matrix Addition");
canvas(1280, 720);
text(head, (640, 120), "add two matrices, entry by entry");
display(head); color(head, cyan); size(head, 28); hidden(head);
text(cap, (640, 600), ""); color(cap, dim); size(cap, 26);
// A + B = C
matrix(A, "2 1; 0 3", (280, 350), 74, 66);
matrix(B, "1 4; 5 2", (640, 350), 74, 66);
matrix(C, "3 5; 5 5", (1000, 350), 74, 66);
text(plus, (460, 350), "+"); display(plus); color(plus, magenta); size(plus, 44); hidden(plus);
text(eq, (820, 350), "="); display(eq); color(eq, magenta); size(eq, 44); hidden(eq);
// A and B trace/fade in; C is built up during the sweep
untraced(A.lbrack); untraced(A.rbrack);
untraced(B.lbrack); untraced(B.rbrack);
untraced(C.lbrack); untraced(C.rbrack);
hidden(A.entries); hidden(B.entries); hidden(C.entries);
// --- reveal the two matrices ---
show(head, 0.5);
say(cap, "two matrices, A and B");
par { draw(A.lbrack, 0.4); draw(A.rbrack, 0.4); draw(B.lbrack, 0.4); draw(B.rbrack, 0.4); }
par { show(A.entries, 0.4); show(B.entries, 0.4); }
show(plus, 0.3);
wait(0.5);
// --- add entry by entry ---
section("Entry by entry");
say(cap, "add matching entries, position by position");
par { show(eq, 0.3); draw(C.lbrack, 0.4); draw(C.rbrack, 0.4); }
seq {
par { flash(A.r0c0, lime); flash(B.r0c0, lime); }
say(cap, "2 + 1 = 3");
par { show(C.r0c0, 0.3); pulse(C.r0c0); }
par { flash(A.r0c1, lime); flash(B.r0c1, lime); }
say(cap, "1 + 4 = 5");
par { show(C.r0c1, 0.3); pulse(C.r0c1); }
par { flash(A.r1c0, lime); flash(B.r1c0, lime); }
say(cap, "0 + 5 = 5");
par { show(C.r1c0, 0.3); pulse(C.r1c0); }
par { flash(A.r1c1, lime); flash(B.r1c1, lime); }
say(cap, "3 + 2 = 5");
par { show(C.r1c1, 0.3); pulse(C.r1c1); }
}
wait(0.4);
// --- the result ---
section("Result");
say(cap, "A + B — every entry, all at once");
recolor(C.entries, cyan, 0.4);
par { pulse(C.r0c0); pulse(C.r0c1); pulse(C.r1c0); pulse(C.r1c1); }
wait(1.5);
matrix_addition_plane
The same sum, laid out on a coordinate plane.
// Matrix Addition, Geometrically — a 2x1 matrix IS a vector. Adding two of them
//
// [3] [1] [4]
// [1] + [2] = [3]
//
// is the same as sliding one arrow onto the tip of the other (tip-to-tail) and
// reading off where you land. The column matrices at the top stay in lockstep
// with the arrows on the plane, so you see the algebra and the geometry at once.
//
// manic examples/matrix_addition_plane.manic
// manic examples/matrix_addition_plane.manic --record out --fps 60
title("Matrix Addition on the Plane");
canvas(1280, 720);
text(head, (640, 96), "a 2x1 matrix is a vector — adding them is tip-to-tail");
display(head); color(head, cyan); size(head, 26); hidden(head);
text(cap, (640, 686), ""); color(cap, dim); size(cap, 24);
// --- the equation, as column matrices across the top ---
matrix(MA, "3; 1", (452, 186), 44, 42); color(MA, cyan);
text(plus, (528, 186), "+"); display(plus); color(plus, dim); size(plus, 40); hidden(plus);
matrix(MB, "1; 2", (600, 186), 44, 42); color(MB, magenta);
text(eq, (676, 186), "="); display(eq); color(eq, dim); size(eq, 40); hidden(eq);
matrix(MC, "4; 3", (748, 186), 44, 42); color(MC, lime);
// --- the plane, centred low so the arrows have room to climb ---
plane(pl, (640, 438), 560, 250, 48);
// vectors from the plane's origin (dy is up); unit = 48 px
vector(va, (640, 438), (144, 48), cyan); // a = (3, 1) -> tip (784, 390)
vector(vb, (640, 438), (48, 96), magenta); // b = (1, 2) -> tip (688, 342)
vector(vs, (640, 438), (192, 144), lime); // a+b = (4, 3) -> tip (832, 294)
// the two translated copies that build the parallelogram
arrow(vb2, (784, 390), (832, 294)); color(vb2, magenta); glow(vb2, 0);
arrow(va2, (688, 342), (832, 294)); color(va2, cyan); glow(va2, 0);
// everything but the plane grid starts hidden / untraced
untraced(pl.x); untraced(pl.y); hidden(pl.grid);
untraced(va); untraced(vb); untraced(vs); untraced(vb2); untraced(va2);
untraced(MA.lbrack); untraced(MA.rbrack); hidden(MA);
untraced(MB.lbrack); untraced(MB.rbrack); hidden(MB);
untraced(MC.lbrack); untraced(MC.rbrack); hidden(MC);
// --- reveal the plane ---
show(head, 0.5);
section("The plane");
say(cap, "a cartesian grid, arrows pinned to the origin");
show(pl.grid, 0.6);
par { draw(pl.x, 0.5); draw(pl.y, 0.5); }
wait(0.3);
// --- vector a ---
section("Vector a");
say(cap, "a = [3, 1] — three right, one up");
par { draw(MA.lbrack, 0.3); draw(MA.rbrack, 0.3); }
par { show(MA, 0.3); draw(va, 0.6); }
wait(0.4);
// --- vector b ---
section("Vector b");
say(cap, "b = [1, 2] — one right, two up");
show(plus, 0.3);
par { draw(MB.lbrack, 0.3); draw(MB.rbrack, 0.3); }
par { show(MB, 0.3); draw(vb, 0.6); }
wait(0.4);
// --- tip to tail ---
section("Tip to tail");
say(cap, "slide b so its tail sits on the tip of a");
draw(vb2, 0.7);
wait(0.5);
// --- the sum ---
section("The sum");
say(cap, "the arrow to that new point is a + b = [4, 3]");
show(eq, 0.3);
par { draw(MC.lbrack, 0.3); draw(MC.rbrack, 0.3); }
par { show(MC, 0.3); draw(vs, 0.8); }
par { pulse(vs); flash(MC, lime); }
wait(0.6);
// --- parallelogram ---
section("Either order");
say(cap, "slide a onto b instead — same point. a + b = b + a");
draw(va2, 0.7);
wait(0.4);
say(cap, "the two paths frame a parallelogram; a + b is its diagonal");
par { pulse(va); pulse(vb); pulse(vs); }
wait(1.4);
linear_transform
A 2x2 matrix shearing a grid + basis vectors.
// Linear Transformation — a 2x2 matrix bends the whole plane. The grid, the
// basis vectors i-hat / j-hat, and a sample point all carry the tag `pl`, so a
// single `transform` applies the matrix to everything at once (Manim's
// ApplyMatrix). Straight lines stay straight; the grid shears / rotates.
//
// manic examples/linear_transform.manic
// manic examples/linear_transform.manic --template blueprint
title("Linear Transformation");
canvas("16:9");
let ox = cx; let oy = cy;
text(head, (cx, 84), "a matrix bends the whole plane -- watch the grid");
display(head); color(head, cyan); size(head, 24); hidden(head);
text(cap, (cx, 672), ""); color(cap, dim); size(cap, 23);
// the plane (its grid + axes are all tagged `pl`)
plane(pl, (ox, oy), 580, 320, 60);
// basis vectors + a sample point, all tagged `pl` so they transform together
vector(vi, (ox, oy), (120, 0), cyan); stroke(vi, 4); tag(vi, pl);
vector(vj, (ox, oy), (0, -120), magenta); stroke(vj, 4); tag(vj, pl);
dot(mark, (ox + 180, oy - 100), 9); color(mark, lime); glow(mark, 1.6); tag(mark, pl);
// --- script ---
show(head, 0.5);
say(cap, "the identity grid, with i-hat (cyan) and j-hat (magenta)");
wait(0.7);
section("Shear");
say(cap, "shear: i-hat stays put, j-hat leans over");
transform(pl, (ox, oy), 1, 0.5, 0, 1, 1.4, smooth);
wait(0.8);
section("Undo");
say(cap, "the inverse matrix brings it right back");
transform(pl, (ox, oy), 1, -0.5, 0, 1, 1.4, smooth);
wait(0.6);
section("Rotate");
say(cap, "a rotation matrix turns the whole plane");
transform(pl, (ox, oy), 0.707, -0.707, 0.707, 0.707, 1.5, smooth);
wait(1.3);
table
A ruled table; cells, rows, columns, labels all addressable.
// Tables — a ruled grid of entries with row/column headers, manic's Table /
// MathTable / IntegerTable. This is an addition table: each body cell is
// row + column. We reveal it, then "look up" 2 + 5 by flashing that row and
// column and lighting the answer — a demo of the table's tag addressing
// (row{i} / col{j} / the labels / the grid lines are all recolourable).
//
// manic examples/table.manic
// manic examples/table.manic --record out --fps 60
title("Tables");
canvas(1280, 720);
text(head, (640, 110), "a grid you can read by row and column");
display(head); color(head, cyan); size(head, 28); hidden(head);
text(cap, (640, 620), ""); color(cap, dim); size(cap, 24);
// body cells are the sums; headers are the addends (top-left corner is blank)
table(t, "0 5 10; 2 7 12; 4 9 14", (640, 372), 120, 78, "0 5 10", "0 2 4");
untraced(t.lines);
hidden(t.labels); hidden(t.entries);
// --- reveal ---
show(head, 0.5);
say(cap, "rule the grid, then fill it in");
draw(t.lines, 1.0);
show(t.labels, 0.5);
show(t.entries, 0.5);
wait(0.5);
// --- a lookup: 2 + 5 = 7 ---
section("Look it up");
say(cap, "read a cell as row + column");
par { flash(t.rowlabel1, lime); flash(t.collabel1, lime); }
say(cap, "row 2, column 5 ...");
par { flash(t.row1, cyan); flash(t.col1, cyan); }
say(cap, "2 + 5 = 7");
recolor(t.r1c1, lime, 0.3);
pulse(t.r1c1);
wait(1.4);
table_braces
A table annotated with braces.
// Table + Braces — a quarterly sales table, annotated with curly braces that
// group its columns (the four quarters into two halves of the year) and its
// rows (the two regions). A practical pattern: use a table for the data and
// braces to call out how its rows/columns cluster.
//
// The brace coordinates are aligned to the table's grid lines by hand — the
// table is centred at (640,360) with 110x70 cells, so its vertical rules fall
// at x = 365 + k*110 and its rows span y = 325..465.
//
// manic examples/table_braces.manic
// manic examples/table_braces.manic --record out --fps 60
title("Sales by Region");
canvas(1280, 720);
text(head, (640, 96), "a data table, with its groups braced");
display(head); color(head, cyan); size(head, 26); hidden(head);
text(cap, (640, 640), ""); color(cap, dim); size(cap, 24);
// rows = regions, columns = quarters; body cells are sales (in $k)
table(t, "12 15 18 20; 9 11 14 16", (640, 360), 110, 70, "Q1 Q2 Q3 Q4", "North South");
untraced(t.lines);
hidden(t.labels); hidden(t.entries);
// column braces over the header row (bulge up: points run right -> left)
bracelabel(h1, (695, 248), (475, 248), "H1", 26); color(h1, magenta); hidden(h1);
bracelabel(h2, (915, 248), (695, 248), "H2", 26); color(h2, lime); hidden(h2);
// a vertical brace to the left of the row labels, grouping the two regions
bracelabel(reg, (356, 325), (356, 465), "Regions", 26); color(reg, cyan); hidden(reg);
// --- reveal the table ---
show(head, 0.5);
say(cap, "quarterly sales for two regions");
draw(t.lines, 1.0);
show(t.labels, 0.5);
show(t.entries, 0.5);
wait(0.4);
// --- brace the columns into halves of the year ---
section("Halves of the year");
say(cap, "Q1-Q2 are the first half, Q3-Q4 the second");
par { flash(t.col0, magenta); flash(t.col1, magenta); }
show(h1, 0.5);
par { flash(t.col2, lime); flash(t.col3, lime); }
show(h2, 0.5);
wait(0.4);
// --- brace the rows into regions ---
section("The regions");
say(cap, "and the two rows are the regions");
par { flash(t.rowlabel0, cyan); flash(t.rowlabel1, cyan); }
show(reg, 0.6);
wait(1.4);