A matrix library in 5 lines of code
Here’s an animation of a pulsating rectangle, spinning around the origin. It’s nothing special, except that it’s drawn with the power of ✨matrix math✨, using my own matrix library (5 lines of code!).
For years, on and off, I tried to internalize matrix operations, but it never stuck. I remember watching a Khan Academy linear algebra course around ten years ago, where Sal’s guidance was basically, “A matrix is a grid of numbers. Here is the matrix multiplication number-crunching algorithm.”
Sal Khan is a great teacher, but this approach was wrong. Recently, I watched Grant Sanderson’s Essence of Linear Algebra course, which made things not just clear, but almost ... obvious! Here is the fundamental insight: a matrix describes a linear function by recording where the basis vectors move to. This insight is so important that I’ll say it again:
A matrix describes a linear function by recording where the basis vectors move to.
Everything else follows smoothly from this definition of a matrix. Armed with just this insight, I no longer have to remember how to “multiply matrices”. Instead, I can just work it out from the definition (which is that matrix “multiplication” is actually function composition.) Here is my tiny matrix math library:
const zipWith = (f, a, b) => a.map((k, i) => f(k, b[i])); // helper // Vector ops const vecScale = (n, v) => v.map(c => n*c); const vecAdd = (v1, v2) => zipWith((c1,c2)=>c1+c2, v1, v2); // Matrix ops const matApply = (mat, vec) => zipWith(vecScale, vec, mat).reduce(vecAdd); const matMul = (m2, m1) => m1.map(v => matApply(m2,v));
In this system, matrices are written in “column-major format”: a list of basis vectors. Here are some examples for 2D matrix math:
const identity = [ [1, 0], // the x basis vector ("i-hat"). It hasn't moved anywhere. [0, 1], // the y basis vector ("j-hat"). It hasn't moved anywhere. ]; const rotateClockwise90 = [ [-1, 0], // the rotated x basis vector ("i-hat") [ 1, 0], // the rotated y basis vector ("j-hat") ]; // Common matrix constructors const rotate = a => [[Math.cos(a), Math.sin(a)], [-Math.sin(a), Math.cos(a)]]; const scaleSep = (s) => [ [s, 0], [0, s] ]; const scale = s => scaleSep([s,s]);
For my purposes (graphics programming), linear functions have at least two big deficiencies. Linear functions can’t describe translation (that is, moving stuff!), because they preserve the origin point. And linear functions can’t describe projection (that is, simulating a camera), because they keep parallel lines parallel.
In my next post, I describe homogeneous coordinates, a mathematical hack that builds on top of plain linear algebra, and allows describing translation and projection. Stay tuned!