So far everything we've drawn on the canvas was done without thinking much about exactly what our pixels are drawn into. Saying that our pixels are drawn on the canvas is only one part of the full picture. Under the canvas is an invisible virtual grid:
It is this invisible virtual grid that all of the various draw commands we've seen map their pixels into. By default, this grid isn't very interesting. It becomes a whole lot more interesting when you transform it. You can rotate this grid:
You can shift the starting point of this grid:
You can even scale each individual "cell" inside the grid to be larger or smaller:
Why is this interesting? It is interesting because your canvas and anything you draw inside it will get scaled, rotated, or translated as well. This sort of makes up for the lack of interesting things you can do with the draw methods we've seen. At most, you can specify the size and position. That's not a lot, so transforms provide you with a few more ways of customizing what you draw. In this tutorial, we are going to learn all about it.
To kick your canvas skills up a few notches, everything you see here and more (with all its casual clarity!) is available in both paperback and digital editions.BUY ON AMAZON
The three methods you have for transforming your canvas are translate, scale, and rotate. In the following sections, let's look at how to use these methods.
If you want to shift your canvas and everything that gets drawn, you have the the translate method:
The x and y arguments specify the number of pixels to shift your canvas horizontally and vertically by. Below is a simple example of what this looks like:
// Transform context.translate(50, 50); // Circle context.beginPath(); context.arc(200, 200, 93, 0, 2 * Math.PI, true); context.fillStyle = "#FF6A6A"; context.fill(); // Square context.fillStyle = "#00CCFF"; context.fillRect(50, 50, 100, 100);
This code draws a circle and a square to our canvas. The call to the translate method at the top shifts both of the shapes over by 50 pixels. The following diagram shows the result of this translation:
The entire canvas and the origin (0, 0) position is shifted, so all future drawing operations will have their positions offset automatically. Having a transform apply to all draw operations from here on out may be undesirable, and we'll look at how to address that in a little bit. Just ignore this minor annoyance for now.
This is probably my favorite transform, for rotating the things you draw is really hard using the drawing commands we have available today. The way you rotate is by using the rotate method, and it looks as follows:
This method takes one argument that determines the angle (in the form of radians) you wish to rotate the canvas by. Here is an example of us rotating some text that we draw by 45 degrees:
// Transform context.rotate(45 * Math.PI / 180); // Text context.font = "bold 48px Helvetica, Arial, sans-serif"; context.fillStyle = "steelblue"; context.fillText("Wheeeee!", 150, 0);
Here is what this looks like:
I chose a text example to highlight the rotate transform because text is one of the things you draw that is nearly impossible to re-create using rotated angled lines and curves. Without the rotate method, you'd be spending a lot of time trying to get a single character to look right - much less an entire word or a series of words! By comparison, rotating geometric shapes is much easier. With that said, you should still use the rotate method whenever you can instead of rotating manually...like an animal.
The last individual transform we will look at is the scale method that is responsible for scaling what you draw:
This method takes two arguments that specify the horizontal and vertical scale accordingly. You can specify the arguments in the form of decimal values with 1 representing the original scale. A number between 0 and 1 means that what you draw will be scaled down, and a number greater than 1 means that what you draw will be scaled up.
The following code highlights an example where a poor square is stretched horizontally to twice its size:
// Transform context.scale(2, 1); // Square context.fillStyle = "#FFCC00"; context.fillRect(50, 100, 100, 100);
If we had to visualize this, this would look as follows:
You can even specify negative values to flip our canvas horizontally or vertically. In the following code, we flip some text horizontally and scale it down by 50%:
// Transform context.scale(-.5, 1); // Text context.font = "bold 96px Helvetica, Arial, sans-serif"; context.fillStyle = "#CC6699"; context.fillText("Confused", -700, 100);
This would look as follows:
The negative value for the scale method's x argument flips our canvas horizontally. The value of .5 squishes things by 50%.
You aren't limited to using only a single transform to torture your canvas with. You can apply multiple transforms very easily:
context.scale(-.5, 1); context.rotate(45 * Math.PI / 180); context.translate(40, 10);
The reason this is possible has to do with how these transforms are implemented. There is a transform matrix that represents all of the transform values you can use:
These values aren't dependent on any other values, so you can independently set multiple transforms without stepping on any numerical toes. Don't worry if that doesn't make any sense. Just remember that all the translate, rotate, and scale methods end up affecting are the values stored by this matrix. You can set this matrix directly by using the setTransform method, but covering that goes beyond the scope of what you would use frequently in the real world.
This may be the part you have been eagerly waiting for. As you probably realized by now, transforming the canvas isn't an operation that resets itself with each thing you draw. It's not like a fillStyle or strokeStyle. The transformation is always there for any draw operation you perform in the future. That isn't always desirable, right?
To handle this, you need to explicitly turn the transforms off. There are several ways you can do with this. We'll look at two approaches in this section and focus on a slightly different (and heavy-handed) approach in a future tutorial where we look at how to save and restore state.
The easiest way to reset a transform is to call the resetTransform method:
// Transform context.translate(50, 50); context.scale(2, 2); // Circle context.beginPath(); context.arc(200, 200, 93, 0, 2 * Math.PI, true); context.fillStyle = '#FF6A6A'; context.fill(); // Reset the Transform context.resetTransform(); // Square context.fillStyle = '#00CCFF'; context.fillRect(50, 50, 100, 100);
The resetTransform method performs the magic needed to the transformation matrix you saw earlier to get everything back to how it was before a transform was even applied. In our example, the circle will be drawn on the transformed canvas. The square will be drawn on the untransformed canvas. Because of how drawing on the canvas works, untransforming the canvas with our circle already on it won't affect how the circle displays. Only future draw operations after resetTransform will be impacted.
TL;DR: Just use resetTransform. Skip this section. Tell your friends.
Before we go on, I should mention this upfront: I don't recommend you reset the transform with using the approach I am about to show you. The only reason I am showing you this is to give you a better understanding of how transforms affect the canvas. Plus, it inflates the length of this article and helps make all of us look really smart by learning about this.
The more tedious way to reset your canvas to its untransformed state involves setting new transforms to undo what your earlier transforms did. That seems straightforward, but as you will see in a few seconds, there are some complications here that you'll need to deal with.
Here is an example of what this madness looks like:
// Transform context.translate(50, 50); context.scale(2, 2); // Circle context.beginPath(); context.arc(200, 200, 93, 0, 2 * Math.PI, true); context.fillStyle = '#FF6A6A'; context.fill(); // Reset the Transform context.scale(.5, .5); context.translate(-50, -50); // Square context.fillStyle = '#00CCFF'; context.fillRect(50, 50, 100, 100);
Pay attention to the highlighted lines where we set the transform first and then reset the transform next. Resetting a transform in this approach isn't as simple as specifying the default transform values for translate and scale:
context.scale(1, 1); context.translate(0, 0);
That seems like the logical thing to do, but that only works in a world where the canvas can intelligently access its previous state. The moment our canvas gets transformed, it only sees the world through its transformed lenses. Setting a scale value of 1 or a translate value of 0 means that you just stay at the current transformed state. The fix is where the tediousness comes in:
You have to account for the earlier transform that has been applied and negate it.
If the original transform called for everything to be scaled by 200%, you need to reset the scale by scaling everything by 50% instead. If your translate transform shifted everything by 50 pixels horizontally and vertically, you undo this by translating back by 50 pixels in the horizontal and vertical directions.
That's what our code highlights:
context.scale(.5, .5); context.translate(-50, -50);
There is one more wrinkle. The order you perform this reset is important. Notice that we first undo the scale before resetting the position. If you didn't do this, you will have changed the position of an element that will then be repositioned again as a result of the scale operation. Getting the position right at this point will require more calculations, and that isn't particularly fun. This whole section isn't fun!
The most difficult thing about learning how to transform the canvas is how bizarre it is. If you are familiar with transforms in CSS, you know that you only affect the element or elements you are targeting. In the wacky world of the canvas, there is no concept of an element. Everything is either the canvas itself or raw pixels. If you wish to draw something rotated (or scaled or translated), you transform the canvas first and then draw whatever you were planning on drawing. The strangeness of this all goes away with practice...and a lot of therapy.
Just a final word before we wrap up. If you have a question and/or want to be part of a friendly, collaborative community of over 220k other developers like yourself, post on the forums for a quick response!