This lesson is for PRO members.

Unlock this lesson NOW!
Already subscribed? sign in

Canvas Transformations

8:44 JavaScript lesson by

Transformations can change the default canvas coordinate system and create complex drawings with much less code.

Get the Code Now
click to level up

egghead.io comment guidelines

Avatar
egghead.io

Transformations can change the default canvas coordinate system and create complex drawings with much less code.

Let's talk about the canvas coordinate system. By default, the origin, or (0, 0) point, is up here in the top-left corner. It extends positively on the x-axis from left to right and positively on the y-axis from top to bottom. One unit along either axis is equal to one CSS pixel in the browser.

As for angles, an angle of zero points directly to the right. Angles increase positively as they move around clockwise. All of that is the default and can all be changed with canvas transformations.

Canvas transformations transform that coordinate system in three main ways. Translation moves the origin point along one or both axes. Scaling grows or shrinks the system so that one unit in the canvas space can be equal to more or less than a single CSS pixel.

Note that you can also apply negative scaling, which will flip the coordinate system on one or both axes. Rotation rotates the canvas around the origin point. Using combinations of these three types of transformations has a lot of potential benefits, including drawing more complex graphics with much less code.

First, let's draw a square at position (0, 0). As expected, the square is drawn at the top-left corner of the canvas. Just before drawing the square, say "context.translate(100, 100)." Now the square is drawn 100 pixels away from each edge. Note that we didn't change the drawing code. The square is still being drawn at position (0, 0), but (0, 0) is no longer in the corner.

Also keep in mind that like most other canvas operations, all of these transformations only affect the rendering of future content and don't have any effect on graphics that have already been drawn. We can use this to draw a whole row of squares without any math at all.

I'll set up a loop here that will run 10 iterations. In that, I'll draw a 30-pixel square at (0, 0). Then I'll translate 40 pixels on the x-axis. Now we have a row of 10 evenly spaced squares. No math involved.

Say we wanted to turn this into a grid. We can just throw another for loop inside of this one and after drawing one row, translate 40 pixels on the y-axis. That didn't work out very well because the x translation just keeps on building up.

You need to set it back to zero at the end of each inner loop. We could do this manually, figuring out that 40 pixels * 10 iterations is 400. Translating by -400 on x will work, but that's messy. Instead, we can save the state of the context at any time and then restore it to that state whenever we want to.

This acts like a stack. You can push multiple save states onto the stack and restore them one by one. Here, I'll save the state of the context before that inner loop. After drawing one row of squares, I'll restore it. That did the trick.

Realize that we're still accumulating a y offset. If we wanted to draw something after drawing this grid, like a 100-pixel red square, it would wind up way down here. We might want to save the state before we did any transforming at all and then restore it when we're done. Now the origin is back to where it was originally. You can see how that stack works.

Next, let's look at scaling. If I call "context.scale(2, 2)," everything is drawn at twice the normal size. Note that the spaces between the squares have also increased, meaning that the scaling affected the amount of translation as well as the size of what is drawn. If I scale (0.5, 0.5) instead, everything is half-sized.

Of course, I could pass in a different value for scaling on the x and y-axes, say 2 and 0.5. Now we have horizontal bars instead of squares. Note that in each of these above cases, the red square is unchanged, as that is drawn after the final call to context.restore, which puts the context back into the state it was initially saved as.

context.rotate takes a single value which is an angle in radians. The coordinate system will be rotated around the origin point to that angle. I'll say, "context.rotate(Math.PI / 4)," which should be 45 degrees clockwise, and then I'll draw a rectangle at (0, 0). Sure enough, the rectangle is rotated, but now half of it is off the left side of the canvas.

Maybe we we want to move it over to the right a bit so we can see it all. I'll just draw it at (300, 0) instead of (0, 0). That may not have been what you expected. It's moved on both the x and y-axes. Not really though. It only moved on the x-axis, but that whole x-axis is rotated now.

We could do the same thing by setting this back to zero and then calling "translate(300, 0)" before drawing the rectangle. The lesson here is that when you're applying multiple transformations, the order is very important. There's no right order, but different orders will give you very different results.

To explain, here's our canvas with the coordinate system centered in the top-left corner. If we rotate it 45 degrees, it looks like this. If we translate it 300 pixels on the x-axis, we're actually translating it along that rotated axis. The square we draw winds up here.

On the other hand, if we go back to the initial state and this time we translate 300 pixels first, now our origin is here. Now if we rotate the canvas, it will rotate around this translated origin. When we draw the square, it will be here. Back in the code, I'll just swap these two lines. There we go. Just as predicted.

Say you wanted to make some kind of scientific graphing calculator. You might want the coordinate system with the origin centered on the screen. You can easily do that with "translate(300, 300)," knowing that the canvas is 600 pixels square.

Now I'll draw a horizontal line from -300 to +300 and another vertical line from -300 to +300. Now we have our x and y-axes. I can plot a point at (100, 100) by drawing a circle there using the arc command.

That works, but if you were doing a scientific calculator with Cartesian coordinates, you'd expect (100, 100) to be up here, in quadrant one, not down here, in quadrant four. A quick fix for this is to apply a y scale of -1 by saying, "context.scale(1, -1)." This effectively flips the whole coordinate system around the origin point. Now our plotted point is in the right quadrant.

This will also reverse angles so that they increase counterclockwise, as you would expect in a Cartesian system. The main drawback to this method is if you wanted to draw any text to the screen, it will also be flipped upside down. You'll have to get creative about a way to fix that. By the end of this video, you should be able to do that.

Translation and rotation can be combined to create some nice structured designs that would normally require a whole lot of trigonometry and math. For example, we can arrange a number of objects around in a circle.

This would normally require keeping track of an increasing angle and getting the sine and cosine of that angle and multiplying by radius to get the position of each object, but with transformations, it becomes far easier.

First, I'll create a variable called "num" and set it to 20. This will be the number of objects we'll draw. Then I'll translate the context to the center of the canvas and start a for loop that runs num number of times.

Inside that, I'll rotate by Math.PI * 2 / num. Remember that 2pi radians is 360 degrees. Dividing that by 20 means we're rotating 18 degrees on each iteration. Then I'll simply begin a path, draw an arc at (100, 0), and fill it.

Although I'm drawing the arc to the same position each time, because the context is continually rotating, the circle I'm drawing winds up drawn in different locations on the canvas. There you have it. No math other than figuring out the amount to rotate.

Now I can change num to 10 or 5 or 30. It always works out just right. Just remember that if you're going to do any other drawing after this, you'd want to save your context and restore it after you were done here.

HEY, QUICK QUESTION!
Joel's Head
Why are we asking?