Transforming objects in your HTML5 Game or Experiment requires a bit of understanding regarding how they work. The behaviour and end result is not as intuitive as CSS3 transform functions like
scale(). I could quickly show you how to rotate or scale an image with its center as the origin, but it’s going to be super useful to take an indepth look at how the entire system works.
Canvas Coordinate System
You probably already know about the Cartesian coordinate system used by the 2D Rendering Context of HTML5 Canvas. Top left is the origin point with the coordinates (0,0). As you move to the right, the values increases on the X axis. Although, moving downwards increases values on the Y axis, unlike the standard cartesian plane where the values would decrease on the negative side of the number system.
Before getting into rotating and scaling, let’s first understand the concept of translating. Translating basically specifies the (x,y) position at which you want to render something.
translate method on the 2D context allows you to move the origin point of the context. Not making sense ? Basically with this method you can specify a new (x,y) coordinates on the context that’ll act as (0,0) after calling the
Let me show you some code to make things uber clear.
var canvas = document.querySelector('canvas'); var ctx = canvas.getContext('2d'); ctx.fillStyle = 'red'; ctx.fillRect(0, 0, 100, 100);
We make a call to
fillRect. Where is the rectangle going to render ? At (0,0) with a width of 100px and height of 100px. Yeh, you got that right!
Now, let’s add a bit more code that uses the
var canvas = document.querySelector('canvas'); var ctx = canvas.getContext('2d'); ctx.fillStyle = 'red'; ctx.fillRect(0, 0, 100, 100); // We "move" the origin of our coordinate system // to (100,100) of the current 2D Rendering Context ctx.translate(100, 100); ctx.fillStyle = 'blue'; ctx.fillRect(0, 0, 100, 100);
fillRect call is exactly same as the first one, so it should render a blue square atop the red square, right ? NO! It will not. Using the
translate method we shifted (0,0) – origin point – of the entire 2D rendering context to (100,100).
Finally, after hours of reading, we’re onto the key part. Rotating Objects in Canvas. I am not going to deal with simple rectangles now. Instead we’ll rotate images drawn using the
drawImage method on the canvas context. Why ? Because that’s what you’re going to use in 99% cases (games, etc.). You can always replace the
drawImage called with
fillRect or something else anytime you want.
Basically this is the code we use (refresh if you don’t see the image).
var img = new Image(); img.src = 'http://cssdeck.com/uploads/media/4/4M29duK.png'; ctx.drawImage(img, 100, 100);
First argument to
drawImage is an image object whose src is set to the Image’s source. Second and Third arguments are the start X and Y positions to draw the image on the canvas. You can pass in some extra optional arguments, but life is just fine with what we have at the moment.
So, I want to rotate the image by 30 degrees. Let’s do it then.
var TO_RADIANS = Math.PI/180; var img = new Image(); img.src = 'http://cssdeck.com/uploads/media/4/4M29duK.png'; ctx.rotate(30 * TO_RADIANS); ctx.drawImage(img, 100, 100);
Take a look at the demo now:
So, we had our rotation done. The image rotated but a part of it is cut and its positions have changed. This might not exactly be what you or me wanted. If we had a div with that background image and rotated that with CSS3 transforms, then we might have gotten something according to our desire. Basically, the rotation did not occur around the center of the original position of the image (like a clock’s hand).
That’s a smart question. The reason is, the
rotate method rotates the entire 2D Rendering Context, and NOT just the following object/figure that is drawn. See the following figure:
Clear now ? Great!
Solving the Problem
Let’s work towards the solution now. We just learnt about
translate sometime ago. Using translation with rotation will give us something like this:
Let’s use this knowledge by translating our rendering context (before rotating).
ctx.translate(100,100); ctx.rotate(30 * TO_RADIANS); ctx.drawImage(img, 0, 0);
Notice carefully. The second and third argument values to
drawImage have been used as the first and second arguments to translate and they’ve been set to 0 themselves. This little trick can save time!
So first, we translated our rendering context to
(100,100) – the old
drawImage's start x/y positions. Next we rotate the context and finally we draw our image. But, this is still not what we want. We want to rotate the image exactly at it’s center. Well, that’s quite easy.
We just need to translate a bit more by about half the width (X axis) and height (Y axis) and then only rotate. Then we need to draw by that same amount (half of width and height) as negative start (x,y) offsets (to
drawImage). Negating is very important since we are doing that extra translation.
ctx.save(); ctx.translate(100,100); ctx.translate(img.width/2, img.height/2); ctx.rotate(30 * TO_RADIANS); ctx.drawImage(img, -img.width/2, -img.height/2); ctx.restore();
Following is what we wanted, right ?
We made it! Notice those 2 extra method calls
ctx.restore() ? Using methods like
rotate, we changed our coordinate system. This means any further drawing after
drawImage is going to occur on the altered coordinate system. This is not what we want. We need to reset back to our good old healthy system instead of this screwed version.
We do that by first saving the system using
ctx.save() -> apply all transformations -> restore to our good old system by calling
ctx.restore(). So cool!
scale method scales (or resizes) the 2D Rendering Context (and not just the following drawings). See this code.
ctx.save(); ctx.scale(2,3); ctx.drawImage(img, 100, 100); ctx.restore();
We just multiplied the size of X axis by 2 and also made the Y dimension 3 times larger –
ctx.scale(2,3). Due to this, the 100 that we pass as
drawImage is now 2x the size, i.e, equal to 200. Similarly, 100 that we pass as
start_y is 3x the size, i.e., equal to 300! So, not only did all our drawing’s scale up, but also the coordinates scaled. This is an important thing to remember.
Now the thing that we’ll want to do often is, scale our object (or image) in such a way, that it’s center (origin) is same as the one of the non-scaled (original) version. The trick to use here, is same as the one we used for rotation.
ctx.save(); ctx.translate(100,100); var half_width = img.width/2; var half_height = img.height/2; ctx.translate(half_width, half_height); ctx.scale(2,3); ctx.drawImage(img, -half_width, -half_height); ctx.restore();
And here’s the last demo:
Basically, now we know how to “control” the origin point for our Canvas transformations. Hope this is gonna help you out in several cases. Let’s have further discussions in the comments.