Learn more about Russian war crimes in Ukraine.

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], [0, s[1]] ];
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!

What can computers do? What are the limits of mathematics? And just how busy can a busy beaver be? This year, I’m writing Busy Beavers, a unique interactive book on computability theory. You and I will take a practical and modern approach to answering these questions — or at least learning why some questions are unanswerable!

It’s only $19, and you can get 50% off if you find the discount code ... Not quite. Hackers use the console!

After months of secret toil, I and Andrew Carr released Everyday Data Science, a unique interactive online course! You’ll make the perfect glass of lemonade using Thompson sampling. You’ll lose weight with differential equations. And you might just qualify for the Olympics with a bit of statistics!

It’s $29, but you can get 50% off if you find the discount code ... Not quite. Hackers use the console!

More by Jim

Tagged #mathematics, #programming, #js, #graphics. All content copyright James Fisher 2020. This post is not associated with my employer. Found an error? Edit this page.