# CanvasContext2D Transform Math for Beginners

It’s easy to go to find a canvas tutorial, and often it will show a developer how to move the canvas context around to draw images using the specialized transform functions on the **CanvasContext2DPrototype**. These tutorials are absolutely fine for beginners and often, development teams will look no further to optimize their code. However, these tutorials overlook some very simple math that could make the canvas operations faster.

For example, typical canvas transforms *normally *look like this for sprites:

//draw a sprite and skip the math

ctx.save();

ctx.translate(x, y); //move to canvas coordinates

ctx.rotate(rotation); //rotate in radians

ctx.scale(sx, sy); //scale in size

ctx.translate(-cx, -cy); //center the image relative to itself

ctx.drawImage(img, 0, 0, img.width, img.height);

ctx.restore();

However, this isn’t the fastest way to draw an image on canvas. In the prior example, the **ctx.save()** and **ctx.restore()** functions have quite a bit of overhead, especially when combined with 4 extra function calls. It’s probably better to just do a bit of the math yourself.

Doing matrix math can be complicated, but it also enables us to use the **ctx.setTransform()** function instead of the save and restore functions, which does turn out to be less cpu intensive. The following code snippet is equivalent to the sprite example above:

//don't skip the math!

let cos = Math.cos(rotation), sin = Math.sin(rotation);

//Transform the canvas context

ctx.setTransform(

sx * cos,

sy * sin,

sx * -sin,

sy * cos,

x,

y

);

//draw an image

ctx.drawImage(img, -cx, -cy, img.width, img.height);

//restore the canvas state

ctx.setTransform(1, 0, 0, 1, 0, 0);

Wow! The amount of function calls have been reduced, and we no longer have to save or restore the canvas context. Scaling it inside a for loop can even look like this:

let cos, sin;

for (let box of this.boxes) {

cos = Math.cos(box.rotation); //calculate the cosine ratio

sin = Math.sin(box.rotation); //calculate the sine ratio

ctx.setTransform(

box.size[0] * cos, //box.size[0] is the width

box.size[1] * sin, //box.size[1] is the height

box.size[0] * -sin,

box.size[1] * cos,

box.position[0], //box.position[0] is the x position

box.position[1] //box.position[1] is the y position

);

ctx.beginPath();

ctx.rect(-0.5, -0.5, 1, 1); //set the rectangle path

ctx.setTransform(1, 0, 0, 1, 0, 0); //restore the transform stack

ctx.stroke(); //then draw the rectangle

}

Note: Remember to set the transform back to[1, 0, 0, 1, 0, 0]after defining the position of the box object to make sure the line widths are consistent, when they are stroked with the API.

So what’s happening under the hood? Why does this work?

### Enter The Matrix

Beware! Advanced math is ahead!

So to begin, let’s describe what a matrix is. The initial matrix is a (3x3) “identity matrix” that looks like this:

This matrix is very important, because it’s the exact starting point for every canvas operation.

*Note: the order of the letters and positions in the matrix are row first: i.e. **[y, x]**. This means item b is at **[2, 1]**.*

One of the magical rules about Canvas operations is that all the stretching, rotation, and translating happens in two dimensional space. As a result, the bottom row, because of mathematical reasons is always **[0, 0, 1]**.

### Starting Off Basic — Save and Restore

Now that we have a starting point, we want to be able to save and restore canvas state in a way that makes sense.

For the sake of example, follow along in code and derive your own canvas functions.

//set currentMatrix to the identity matrix

var currentMatrix = [1, 0, 0, 1, 0, 0];

var cache = new Float64Array(6); //reuse this variable a lot!

Using a **Float64Array** for number caching is a wise choice, especially for number arrays that get re-used a lot. When creating new **TypedArrays**, it uses more memory and more CPU cycles *up front* to make read and write operations faster later, so allocate what you can up front, and reuse a lot later.

Next, we are going to treat the state of those six numbers as a stackable value. Knowing that canvas transforms are ** stackable** allows you to create an array of transforms, then

**push**and

**pop**them as we need to.

In this example, we create a stack, and then describe a **push** operation.

var stack = [currentMatrix.slice()];//slice copies the currentMatrix

//This is a pushing operation

stack.push(currentMatrix.slice());

ctx.setTransform(

currentMatrix[0],

currentMatrix[1],

currentMatrix[2],

currentMatrix[3],

currentMatrix[4],

currentMatrix[5]

);

Great! Now let’s describe a **pop**ping operation.

//remove the top value

stack.pop();

//restore the next value

currentMatrix = stack[stack.length — 1]; //restore previous value

ctx.setTransform(

currentMatrix[0],

currentMatrix[1],

currentMatrix[2],

currentMatrix[3],

currentMatrix[4],

currentMatrix[5]

);

Excelent! We have **push **and **pop **defined very well! Let’s start manipulating that stack with some example math.

### Lost In Translation!

In order to describe certain kinds of canvas transforms, we need to give them a matrix representation, and indeed, those representations are very simple! A translate matrix looks like this:

The following example uses tx and ty in the calculations required for translation.

currentMatrix[4] += currentMatrix[0] * tx + currentMatrix[2] * ty;

currentMatrix[5] += currentMatrix[1] * tx + currentMatrix[3] * ty;

push();

Note: Translation only moves theeandfvalues of the current matrix. This is because a translation does not change how a drawn image is rotated or stretched.

In the side example, the green square is translated from the location of the blue square.

### Scaling Your Operation

Next we investigate how scaling operations work. This is what a scale matrix looks like:

The following example uses sx and sy in the calculations required for scaling.

currentMatrix[0] *= sx;

currentMatrix[1] *= sx;

currentMatrix[2] *= sy;

currentMatrix[3] *= sy;

push();

Note: Since scaling doesn't affect the current[x,y]coordinates of the transform, scaling is applied to the first four items in the matrix.

As the name scaling implies, scaling makes your images and path coordinates bigger, smaller, or reflects them over a given axis.

In the side example, the blue square is reflected over the x and y axis, then made bigger by the scaling operation.

### Rotating The Point Of View

Investigating rotation is a bit harder, because it involves trigonometry functions.

The following example uses the cosine and sine* *functions* *to calculate a few transform values. (Note that the *a* variable is rotation in *radians* and not degrees)

var cosa = Math.cos(a), sinr = Math.sin(a);

cache[0] = currentMatrix[0];

cache[1] = currentMatrix[1];

cache[2] = currentMatrix[2];

cache[3] = currentMatrix[3];

currentMatrix[0] = cache[0] * cosa + cache[2] * sina;

currentMatrix[1] = cache[1] * cosa + cache[3] * sina;

currentMatrix[2] = cache[0] * -sina + cache[2] * cosa;

currentMatrix[3] = cache[1] * -sina + cache[3] * cosa;

push();

In conventional math, rotations happen counter-clockwise, but because the y axis is flipped, they happen clockwise on canvas operations.

Do not forget to convert degrees to radians for **Math.cos()**, and **Math.sin()**.

const rads = (degrees) => Math.PI * 2 * degrees / 360;

### Transformers! Mathematics in disguise!(Extra Credit!)

Last but not least, the full matrix transform is in the example below. Every other transform is derived from this formula, only simplified.

cache.set(currentMatrix);

currentMatrix[0] = cache[0] * t[0] + cache[2] * t[1]; //a...

currentMatrix[1] = cache[1] * t[0] + cache[3] * t[1]; //b...

currentMatrix[2] = cache[0] * t[2] + cache[2] * t[3]; //c...

currentMatrix[3] = cache[1] * t[2] + cache[3] * t[3]; //d...

currentMatrix[4] = cache[0] * t[4] + cache[2] * t[5] + cache[4]; currentMatrix[5] = cache[1] * t[4] + cache[3] * t[5] + cache[5];

Good luck,

-Josh