Javascript

HTML Canvas Manipulation With Javascript Series: Transformations and Animations

HTML Canvas Manipulation With Javascript Series Drawing 2D

In this article of the canvas series we will cover how to transform the canvas elements like applying rotations, translations, scaling then at the next part of the article i will describe how to animate canvas objects.

 

 

There are many transformation functions available in javascript canvas just like transformations in css which are:

  • Translations (moving objects).
  • Rotation
  • Scaling
  • Generic transformations using transform() function.

 

Translation

The translation works by moving the canvas origin to a new position. Originally the canvas origin located at top (0, 0) coordinates. This is provided using the translate(x, y) function and accepts two arguments the x and y coordinates to translate to. So for example if we invoke translate(20, 40) on an object, it will move the canvas grid over the x axis by 20 pixels and the y axis by 40 pixels.

 Example:

<canvas width="800" height="600" id="myCanvas" style="border: 1px solid #ccc"></canvas>

<script>
      const ctx = document.getElementById('myCanvas').getContext('2d');

      ctx.fillStyle = 'red';
      ctx.translate(200, 150);
      ctx.fillRect(0, 0, 150, 150);

  </script>

In this example we draw a filled rectangle. As notable here i called translate() before fillRect() so that the grid changes it’s position and then the fillRect() display the rectangle in the new coordinates. Also note that the first two arguments in the fillRect() function which are (0, 0) coordinates which represent the rectangle position, this position now actually (200, 150) because we moved the canvas grid entirely using translate().  

So we conclude from this that if we want to make further drawings it will be relative to the new position. To see this in action let’s make a circle using arc() function an see where it will be displayed:

ctx.fillStyle = 'red';
ctx.translate(200, 150);
ctx.fillRect(0, 0, 150, 150);

ctx.arc(250, 150, 50 ,0, 360);    // the arc will be in (250, 150) relative to the new canvas position.
ctx.stroke();

Now check this in the browser you will that the circle is rendered in (250, 150) relative to the translated canvas grid.

But how can we change this behavior and make translate() not affecting further drawings, so that we can draw each time relative to (0, 0) origin. For this to work properly javascript canvas provids two important functions when manipulating transformations which are save() and restore().

 

save() and restore()

The save() and restore() functions is about saving and restoring the canvas state respectively. The save() function saves the canvas state when making drawings and transformations. The restore() function restores the previously saved canvas state or in other words it undoing the previous saved state. 

Canvas state stored in a stack so every time you call save() the state is pushed into the stack and when calling restore() the state popped of the stack.

Thereby you can call save() and restore() multiple times, and each time you call restore() it restores the previous save() until it finds no saved state to restore.

Let’s modify the previous example using save() and restore():

ctx.save();

ctx.fillStyle = 'red';
ctx.translate(200, 150);
ctx.fillRect(0, 0, 150, 150);


ctx.restore();

ctx.arc(80, 70, 50 ,0, 360);
ctx.stroke();

As you see the first save() call is important, you should call save() at the start and before every drawing and transformation so that we can return to the first state before any transformation.

So in this example after drawing the rectangle and before drawing the circle i invoked restore(), this will restore the previous save() call and it will reset the translate() and fillStyle().

Modify the example to translate the circle:

ctx.save();
ctx.fillStyle = 'red';
ctx.translate(200, 150);
ctx.fillRect(0, 0, 150, 150);


ctx.restore();      
ctx.translate(10, 20);
ctx.arc(80, 90, 50 ,0, 360);
ctx.stroke();

Now check the result you will see that the circle translated 10 pixels horizontally and 20 pixels vertically.

Yet another example: multiple objects while applying save() and restore():

ctx.save(); 
ctx.fillStyle = 'red'; 
ctx.translate(200, 150); 
ctx.fillRect(0, 0, 150, 150); 

ctx.restore();
ctx.save(); 
ctx.translate(10, 20); 
ctx.arc(80, 90, 50 ,0, 360); 
ctx.stroke();

ctx.restore();
ctx.save();
ctx.fillStyle = 'green';
ctx.translate(400, 10);  
ctx.fillRect(0, 0, 150, 150); 

ctx.restore();
ctx.translate(400, 400);  
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(105, 25);
ctx.lineTo(25, 105);
ctx.fill();

Now after you understand save() and restore(), let’s go ahead and complete the other transformation functions.

 

Rotation

Rotation works by rotating the canvas around it’s origin. For this the rotate(angle) function is used. The function accepts just one parameter “angle” in radians.

To convert radians to degree use this formula

radians = (Math.PI/180)*degrees

Example rotating rectangle by 30 degree:

ctx.save();
ctx.fillStyle = 'red';
ctx.rotate((Math.PI / 180) * 30);
ctx.fillRect(0, 0, 150, 150);

If you run this example and check the output you will see that the red rectangle is rotated around the canvas origin which is in this case (0,0). But you can change the rotation center by using translate() first to translate the canvas.

Example using translate() and rotate()

ctx.save();
ctx.fillStyle = 'red';
ctx.translate(200, 200);
ctx.rotate((Math.PI / 180) * 30);
ctx.fillRect(0, 0, 150, 150);

Now the red rectangle is moved and rotated in the same time. Note that we use save() again to save the canvas state like we saw above.

 

Scaling

The scale(x, y) function used to scale the canvas grid which increase the units in the canvas grid. This allows to draw enlarged or scaled down shapes.

scale(x, y) accepts two arguments, the x argument allows to scale the canvas horizontally while the y argument allows to scale vertically. The default value for both argument is 1.0. Values that are smaller than 1.0 reduce the unit size and values that are are larger than 1.0 increase the unit size. 

Example scaling text:

ctx.save();
ctx.scale(3, 10);
ctx.fillStyle = 'green';
ctx.font = '20px arial';
ctx.fillText('Canvas', 10, 30);

Example scaling text and rectangle:

ctx.save();
ctx.scale(3, 10);
ctx.fillStyle = 'green';
ctx.font = '20px arial';
ctx.fillText('Canvas', 10, 30);

ctx.restore();
ctx.fillStyle = 'red';
ctx.translate(200, 200);
ctx.rotate((Math.PI / 180) * 30);
ctx.scale(3, 10);
ctx.fillRect(0, 0, 150, 150);

 

Using transform()

The last function is the transform() function. This function can do multiple transformations at once using a transformation matrix.

The terminology of this function as follows:

transform(a, b, c, d, e, f)

a: Horizontal scaling.
b: Horizontal skewing.
c: Vertical skewing.
d: Vertical scaling.
e: Horizontal moving.
f: Vertical moving.

Example applying transform to rectangle:

ctx.save();
ctx.restore();
ctx.fillStyle = 'red';
ctx.transform(3, 0, 5, 10, 200, 140);
ctx.fillRect(0, 0, 150, 150);

In this example i make multiple transformations so i scaled the rectangle horizontally by 5 units, and 5 values for vertical skewing, and scaled vertically by 10 units and translated horizontally by 200 pixels, and translated vertically by 140 pixels. 

When transforming multiple objects it’s better to use another function which is the resetTransform() function to reset the transformation matrix, otherwise all objects will be transformed according to the first object as in this example:

ctx.save(); 
ctx.restore(); 
ctx.fillStyle = 'red'; 
ctx.transform(3, 0, 5, 10, 200, 140); 
ctx.fillRect(0, 0, 150, 150);

ctx.arc(80, 90, 50 ,0, 360);
ctx.stroke();

So to avoid this behavior we can use resetTransform():

ctx.save(); 
ctx.restore(); 
ctx.fillStyle = 'red'; 
ctx.transform(3, 0, 5, 10, 200, 140); 
ctx.fillRect(0, 0, 150, 150); 

ctx.resetTransform();
ctx.arc(80, 90, 50 ,0, 360); 
ctx.stroke();

There is also another function which is setTransform(), this function accept the same arguments as the transform() function. Internally this function calls both resetTransform() and transform() which is handy in case you need to transform multiple objects and you don’t need to call resetTransform() every time.

Updating the previous example to transform the circle:

ctx.save(); 
ctx.restore(); 
ctx.fillStyle = 'red'; 
ctx.transform(3, 0, 5, 10, 200, 140); 
ctx.fillRect(0, 0, 150, 150); 

ctx.setTransform(3, 0, 0, 2, 20, 30);
ctx.arc(80, 90, 50 ,0, 360); 
ctx.stroke();

Now let’s see in the next section how to manipulate animations in canvas

Animation In Canvas

The most important topic when using canvas is animations. For this there are two important functions we can use which are the requestAnimationFrame() and setInterval(). Both functions accept a callback and execute it periodically. The most optimized function is requestAnimationFrame() which i will be using in the below examples.

However there are certain rules when dealing with animations in canvas:

  • Always clear the canvas before drawing using clearRect().
  • Use save() and restore() functions to save and restore state.
  • Invoke requestAnimationFrame() on the start of the animation and end of the animation.

The number of callbacks in the requestAnimationFrame() function is 60 times per second.

Example: Rotating rectangle

<canvas width="600px" height="600px" style="border: 1px solid #ccc;" id="myCanvas"></canvas>

<script>

        init();

        var angle = 1;
        var speed = 2;
        function init()
        {
            window.requestAnimationFrame(draw);
        }

        function draw()
        {
            const ctx = document.getElementById('myCanvas').getContext('2d');

            ctx.clearRect(0, 0, 600, 600);  // clear canvas

            ctx.fillStyle = 'red';

            ctx.save();

            ctx.translate(200, 150);

            if(angle >= 360) {
                angle = 1;
            } else {
                angle+= speed;
            }

            ctx.rotate((Math.PI / 180) * angle);

            ctx.fillRect(-70, -70, 150, 150);

            ctx.restore();



            window.requestAnimationFrame(draw);
        }

</script>

 Let’s break this code into pieces. At first i declared two variables angle for rotation angle and speed for animation speed respectively. Next in the init() function i invoked window.requestAnimationFrame() and passed the draw() callback.

The draw() callback responsible for drawing and rotating the rectangle. So i cleared the canvas first using clearRect() function. Then i called fillStyle property so that set the rectangle color. Then i called the save() function to save the canvas state.

To make the rectangle to be rotated from the center i translated the canvas using translate() function, Then i called the rotate() function passing in the angle variable which will be incremented in each callback iteration.Then i called the fillRect() function to display the rectangle and invoked the restore() function.

At the end of the draw() function i called requestAnimationFrame() again. So why we invoke the requestAnimationFrame() twice. The answer is that in the first time we call in the init() function we actually started the animation only which means that the rectangle will be rotated with specific value and stopped. So to make the animation continues we must call the requestAnimationFrame() again. It’s much like a recursive function that execute indefinitely.

Another point to note if you don’t save and restore the state, the result is unexpected behavior.

 

Example: Moving Pendulum

<canvas width="600px" height="600px" style="border: 1px solid #ccc;" id="myCanvas"></canvas>

    <script>

        init();

        var x = 100;                          // the start x position
        var y = 200;                          // the start y position
        var speed = 5;                        // speed of the moving ball
        var reverse = false;                  // indicates whether to reverse the ball movement

        var angle = 20;                       // bar rotation angle
        var reverse_rotate = false;           // indicates to reverse rotate the bar
        var rotate_direction = 'ltr';         // the rotation direction fro the bar rtl or ltr


        function init()
        {
            window.requestAnimationFrame(draw);
        }

        function draw()
        {
            const ctx = document.getElementById('myCanvas').getContext('2d');


            ctx.clearRect(0, 0, 600, 600);  // clear canvas

            ctx.fillStyle = 'blue';
            ctx.strokeStyle = 'green';
            ctx.lineWidth = 3;

            ctx.save();
            
            if(!reverse_rotate) {
                if(rotate_direction == 'ltr') {
                    if(angle >= 0) {
                       if(angle == 0) {
                          rotate_direction = 'rtl';
                       } else {
                          angle += -1;
                       }
                    }
                }
                
                if(rotate_direction == 'rtl') {
                    if(angle == -20) {
                        reverse_rotate = true;
                        rotate_direction = 'ltr';
                    } else {
                        angle += -1;
                    }
                }
            } else {
               if(rotate_direction == 'ltr') {
                   if(angle == 0) {
                       rotate_direction = 'rtl';
                   } else {
                        angle += 1;
                   }
               }
               
               if(rotate_direction == 'rtl') {
               
                  if(angle == 20) {
                    reverse_rotate = false;
                    rotate_direction = 'ltr';
                  } else {
                    angle += 1;
                  }
               }
            }
            
            ctx.translate(280, 0);
            ctx.rotate((Math.PI/180)*angle);

            ctx.moveTo(0, 0);
    		ctx.lineTo(0, 350);
    		ctx.stroke();

    		ctx.restore();

    		ctx.save();

            ctx.beginPath();

            ctx.translate(x, y);

            ctx.arc(80, 90, 50 ,0, 360);

            ctx.fill();

            ctx.restore();

            if(!reverse) {
               if(x >= 200) {
               	   if(y == 200) {
               	   	  
               	   	  reverse = true;
               	   } else {
               		  x+= speed;
               		  y-= speed;
               	   }
               } else {
               	 x+= speed;
               	 y+= speed;
               }
            } else {

                if(x <= 200) {
                    if(x == 100) {
                        reverse = false;
                    } else {
                        x -= speed;
                        y -= speed;
                    }
                } else {
                    x -= speed;
                    y += speed;
                }

                
            }


            window.requestAnimationFrame(draw);
        }

        
        
    </script>

HTML Canvas animation - moving pendulum

Here i this code is used some flags and x and y coordinates to set the bar and ball coordinates. To learn more about animation in javascript you have to read more about easing functions, and how to apply them. Read my article here which i talk about animation.

 

0 0 votes
Article Rating

What's your reaction?

Excited
0
Happy
0
Not Sure
0
Confused
0

You may also like

Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments