Fun with Matrices

     

This post is a little bit contrived.

Since I’ve been playing around with 3D graphics and GPUs, I’ve become fascinated with matrices (and maths in general). On top of that, I’ve been looking for a reason to play with Jupyter Notebooks. I’ve become interested in literate programming as well - it seems like a fantastic teaching tool.

So instead of doing what I was supposed to be doing this lovely Saturday, I decided to try to write a super basic (non compatible) version of Jupyter Notebooks for Javascript posts (I call it Kale). I decided to write a simple post about using Matrices in Javascript to try it out.

When you click the run button on any of the code blocks on this post, the output of one block will be the input to the next block. You’ll need to execute them in order for them to work - if you skip one you’ll get an error.

One last thing - I am not a maths major or anything of the sort. If you’ve found this while doing homework for school, you’re probably better off reading something else. If you’re just a tinkerer / hacker / explorer you might like it.

Ok, lets try out this slightly interactive post…

Why a Matrix is Useful

I think of matrices as little black boxes that transform a thing (usually a set of numbers) into another thing (usually a set of numbers). I believe the proper term is System of Equations.

Imagine you have a multi-threaded application that can run a set of functions in parallel. You could define all the functions, store them in a table, and then run them all at the same time.

If you wired up a bunch of these, you could make some interesting things - like maybe a GPU or a quantum computer.

It’ll be clearer with some examples.

Anatomy of a Matrix

For this post I am going to be using a matrix layout that is useful for 2D programming. This was the easiest for me to get started with, and it scales to more dimensions once you have it down.

The first matrix we’ll talk about is the identity matrix. It looks like this:

$$ I= \begin{bmatrix} 1 & 0 & 0 \\ 0 & 1 & 0 \\ 0 & 0 & 1 \\ \end{bmatrix} $$

The identity matrix is like a 1 in standard maths. If you multiply anything by 1, you’ll get that same number back. Same thing with the identity matrix. Anything you multiply by the identity matrix will give you back the original matrix.

Also note that the main feature of a matrix is made up of rows and columns. That’s all that is up there. It’s just an array of numbers layed out in rows and columns - called a 3x3 matrix.

To represent this in some code, let’s make a simple class in Javascript (click run after having a look (you can make the textarea bigger)):

class Matrix {
  /** Create a new matrix with the given row and column size */
  constructor(row, col, data) {
    if (!col || !row) {
      throw Error("Need column and row size");
    }
    this.rc = [row, col];
    if (data) {
      this.d = data;
    } else {
      this.d = new Array(row * col);
    }
  }
}

return Matrix;

Use a Matrix to Move Stuff Around

On top of the identity matrix, there are a few other commonly used matrices that are used for translation (moving on the x and y axis), rotation, and scale. Here are what those look like:

$$ T= \begin{bmatrix} 1 & 0 & tx \\ 0 & 1 & ty \\ 0 & 0 & 1 \\ \end{bmatrix} $$ $$ S= \begin{bmatrix} sx & 0 & 0 \\ 0 & sy & 0 \\ 0 & 0 & 1 \\ \end{bmatrix} $$ $$ R= \begin{bmatrix} \cos(\theta) & -\sin(\theta) & 0 \\ \sin(\theta) & \cos(\theta) & 0 \\ 0 & 0 & 1 \\ \end{bmatrix} $$

Where tx and ty are translation on the x and y axis, sx and sy are scale on the x and y axis, and theta is the degree in radians to rotate. The coolest one being rotation.

One thing you can do with these matrices is multiply them together to describe how something should move from one place to another. Using the class we made above, lets build a few matrices to describe a translation and rotation:

const [Matrix, me] = arguments;

// Translate to 30,30
const m1 = new Matrix(3,3,
[
  1, 0, 30,
  0, 1, 30,
  0, 0, 1,
]);

const deg = 270;
const rad = (deg * Math.PI) / 180;

// Rotate by 270 degrees
const m2 = new Matrix(3,3,
[
  Math.cos(rad), -Math.sin(rad), 0,
  Math.sin(rad), Math.cos(rad), 0,
  0, 0, 1,
]);

return [m1, m2];

One of these matrices describe a translation by 30px on the X and Y axis, and the other a rotation by 270 degrees.

Note: if the rotation one seems difficult to understand, refresh yourself (or learn about) the unit circle. However, you don’t have to understand how it works “under the hood” if you don’t want to.

Now we have two matrices that we’re going to multiply together. Multiplying two matrices together is not intuitive at first. What you have to do is multiply the rows of one matrix against the columns of another. So we are going to do the following:

$$ M_{1}=\begin{bmatrix} 1 & 0 & tx \\ 0 & 1 & ty \\ 0 & 0 & 1 \\ \end{bmatrix} \times \begin{bmatrix} \cos(\theta) & -\sin(\theta) & 0 \\ \sin(\theta) & \cos(\theta) & 0 \\ 0 & 0 & 1 \\ \end{bmatrix} $$

There isn’t a built-in way to do this in Javascript, and this isn’t the fastest way to do it, but here is a, hopefully, straightforward example of multiplying our two matrices together:

const [[m1, m2], me] = arguments;

const ROW = 0;
const COL = 1;

// Make an array big enough to hold our result
const out = new Array(m1.rc[ROW] * m2.rc[COL]);

function mat_mul(m1, m2, out) {
  if (m1.rc[COL] != m2.rc[ROW]) {
    throw Error("column size of m1 must match row size of m2");
  }
  const row = new Array(m1.rc[ROW]);
  const col = new Array(m2.rc[COL]);

  // Loop over each row of the first matrix
  for (let i = 0; i < m1.rc[ROW]; i++) {
    // Load a single Row
    for (let r = 0; r < m1.rc[COL]; r++) {
      row[r] = m1.d[r + i * m1.rc[COL]];
    }
    // Loop over the columns to use when multiplying 
    // against the row loaded above
    for (let j = 0; j < m2.rc[COL]; j++) {
      let v = 0;
      // Load a single column
      for (let c = 0; c < m2.rc[ROW]; c++) {
        col[c] = m2.d[j + c * m2.rc[COL]];
        v += row[c] * col[c];
      }
      out[j + i * m1.rc[ROW]] = v;
    }
  }
}

mat_mul(m1, m2, out);
K.Print(me, out);
return out;

Note: If you’re into it, trying to figure out how to quickly multiply two matrices together is a really fun project. I want to play with a quantum computer :-D.

Do Something Already

Ok now that we’ve got this matrix that will move and rotate something, what do we do with it? Let’s use it to move my profile picture up there on the left (if you’re on a desktop computer).

One problem with using a matrix in CSS is that the way CSS uses matrices is… well… special.

Our created matrix looks like this:

$$ M_{1} = \begin{bmatrix} a & c & tx \\ b & d & ty \\ 0 & 0 & 1 \\ \end{bmatrix} $$

But CSS defines the input for it’s matrix transform like this:

$$ CSS = \left[ \begin{array}{ccc} a & b & c & d & tx & ty \end{array} \right] $$

Which is neither row nor column order. I guess they decided to split the difference and just make everyone slightly irritated.

So, lets make a quick little function that will make our awesomely cool matrix into the version CSS wants, and apply it to that profile picture over there:

const [out, me] = arguments;

function mat_2d_to_css(a) {
  if(a.length < 6) {
    throw Error("Array must have at least 6 elements to be formatted");
  }
  const out3 = [a[0], a[1], a[3], a[4], a[2], a[5]];
  return `matrix(${out3.join(",")})`;
}

const transform = mat_2d_to_css(out);

K.Print(me, transform);

const b = document.querySelector("img");
b.style.transition = "all 3s ease-out";
b.style.transform = transform;

Don’t forget you need to have run all the code fragments for the image to move. Or, you can:

Hopefully that was interesting. Matrices are one of my favorite things to play with. If you’re interested in graphics programming or quantum computing, I highly recommend you dig into them and play around.

More Kale

Kale can also load 3rd party Javascript and run it. Here it is using d3 to make a simple graph:

await K.Include("https://d3js.org/d3.v4.js");

const me = arguments[1];
const svg = K.GetD3Canvas(me);

const x = d3.scaleLinear().domain([0, 100]).range([0, 400]);

svg.call(d3.axisBottom(x));

svg
  .append("circle")
  .attr("cx", x(10))
  .attr("cy", 100)
  .attr("r", 40)
  .style("fill", "blue");
svg
  .append("circle")
  .attr("cx", x(50))
  .attr("cy", 100)
  .attr("r", 40)
  .style("fill", "purple");
svg
  .append("circle")
  .attr("cx", x(100))
  .attr("cy", 100)
  .attr("r", 40)
  .style("fill", "green");

return x;