How to get started with Canvas animations in Angular

Post Editor

In this article, you learned how to use the HTML5 Canvas and its 2D graphics context. I showed how to draw simple shapes, and finally, we were able to animate multiple objects on the canvas.

8 min read
post

How to get started with Canvas animations in Angular

In this article, you learned how to use the HTML5 Canvas and its 2D graphics context. I showed how to draw simple shapes, and finally, we were able to animate multiple objects on the canvas.

post
post
8 min read

Or how my dreams of writing a game started by animating a square block
Link to this section

I love playing games. And I love coding too. So, one day, I got thinking, why not use those coding skills to make a game? But it sounds hard. How would one even get started?

With baby steps.

First, we need some 2D graphics. In this case, it’s moving some blocks on the screen. So, in this article, I will show how to draw and animate objects using the HTML5 Canvas and JavaScript. I will also go through some techniques to optimize performance. Who knows, it might come in handy some day.

Introduction
Link to this section

Apple introduced canvas in 2004 to power applications and the Safari browser. A few years later it was standardized by the WHATWG. It comes with finer grained control over rendering but with the cost of having to manage every detail manually. In other words, it can handle many objects, but we need to code everything in detail.

The canvas has a 2D drawing context used for drawing shapes, text, images, and other objects. First, we choose the color and brush, and then we paint. We can change the brush and color before every new drawing, or we can continue with what we have.

Canvas uses immediate rendering: When we draw, it immediately renders on the screen. But, it is a fire-and-forget system. After we paint something, the canvas forgets about the object and only knows it as pixels. So there is no object that we can move. Instead, we have to draw it again.

Doing animations on Canvas is like making a stop-motion movie. In every frame need to move the objects a little bit to animate them.

Using Canvas
Link to this section

To get started, we need to add a canvas element in our HTML. We'll also attach a reference variable to the element so that we'll be able to refer to it from the component class:

<>Copy
<canvas #canvas width="600" height="300"></canvas>

In the component class, we can then use the @ViewChild() decorator to inject a reference to the canvas.
In Angular 8, a new static flag has been introduced not to break existing applications. Read more about it here. Since I want access to the canvas in the ngOnInit hook I set it to true.

<>Copy
@ViewChild('canvas', { static: true }) canvas: ElementRef<HTMLCanvasElement>;

Once the component has initialized, we’ll have access to the Canvas DOM node, as well as its drawing context:

<>Copy
this.ctx = this.canvas.nativeElement.getContext('2d');

Here is the starting code for the component:

<>Copy
import { Component, ViewChild, ElementRef, OnInit } from '@angular/core'; @Component({ selector: 'app-root', template: ` <canvas #canvas width="600" height="300"></canvas> <button (click)="animate()">Play</button> `, styles: ['canvas { border-style: solid }'] }) export class AppComponent implements OnInit { @ViewChild('canvas', { static: true }) canvas: ElementRef<HTMLCanvasElement>; private ctx: CanvasRenderingContext2D; ngOnInit(): void { this.ctx = this.canvas.nativeElement.getContext('2d'); } animate(): void {} }

Now we have a canvas and a button to start the animation.

canvas

Let’s paint something on it!

Painting
Link to this section

Once we have a canvas context in the component class, we can draw on it using the standard canvas context API. We can use fillRect() to draw a square. It draws a filled rectangle colored by the current fill style.

<>Copy
ctx.fillRect(x, y, width, height);

So to draw a small red rectangle with a side of 5 pixels:

<>Copy
this.ctx.fillStyle = 'red'; this.ctx.fillRect(0, 0, 5, 5);

To outline the square, we could use strokeRect():

<>Copy
this.ctx.strokeRect(z * x, z * y, z, z);

Let’s create a Square class with a drawing method that draws squares:

<>Copy
export class Square { constructor(private ctx: CanvasRenderingContext2D) {} draw(x: number, y: number, z: number) { this.ctx.fillRect(z * x, z * y, z, z); } }

I’m using z here for the side lengths of the squares. 
Let’s try to use it in animate():

<>Copy
animate() { this.ctx.fillStyle = 'red'; const square = new Square(this.ctx); square.draw(5, 1, 20); }

red square

We have painted our first element on the canvas!

Check out some more advanced examples on MDN.

Animation
Link to this section

“Animation is not the art of drawings that move but the art of movements that are drawn.” — Norman McLaren

Now, let’s see if we can get our block to move on the canvas. We can create a function for this. With a loop, we can move the x value on the canvas and draw the object over and over again. Here I move the block to the right until it reaches the canvas end. We send in y for vertical position and z for size.

<>Copy
move(y: number, z: number) { const max = this.ctx.canvas.width / z; for (let x = 0; x < max; x++) { this.draw(x, y, z); } }

Let’s change from drawing the square to moving it:

<>Copy
square.move(1, 30);

move red square

OK, we were able to draw the square as we wanted. But we have two issues:

  1. We are not cleaning up after us.
  2. It’s too fast to see the animation.

We need to clear away the old block. What we can do is to erase the pixels in a rectangular area with clearRect(). By using the width and height of the canvas, we can clean it between paints.

<>Copy
move(y: number, z: number) { const max = this.ctx.canvas.width / z; const canvas = this.ctx.canvas; for (let x = 0; x < max; x++) { this.ctx.clearRect(0, 0, canvas.width, canvas.height); this.draw(x, y, z); } }

fast red square

Great! We fixed the first problem. Now let’s try to slow down the painting so we can see the animation.

You might be familiar with setInterval(function, delay). It starts repeatedly executing the specified function every delay milliseconds. 
I set the interval to 200 ms, which means the code will run five times a second.

<>Copy
move(y: number, z: number) { const max = this.ctx.canvas.width / z; const canvas = this.ctx.canvas; let x = 0; const i = setInterval(() => { this.ctx.clearRect(0, 0, canvas.width, canvas.height); this.draw(x, y, z); x++; if (x >= max) { clearInterval(i); } }, 200); }

To stop a timer created by setInterval, you need to call clearInterval and give it the identifier for the interval you want to cancel. The id to use is the one that is returned by setInterval, and this is why we need to store it.

We can now see that if we press the button, we get a square that moves from left to right.

But, if we press the play button several times, we can see that there is a problem when we try to animate multiple squares at the same time. It’s not working when every block has an interval that clears the board and paints on its own.

It’s all over the place! Let’s see how we can fix this.

Multiple objects
Link to this section

To be able to run the animations for several blocks, we need to rethink the logic. We only need one interval that paints all the objects simultaneously. Every time we press the play button, we add a new Square to a squares-array. The interval then clears the screen and paints all the objects in the array. This way, we avoid the flicker we had before.

<>Copy
setInterval(() => { this.ctx.clearRect(0, 0, this.width, this.height); this.squares.forEach((square: Square) => { square.moveRight(); }); }, 200);

We can move the coordinates to the square model since we don’t need to know about its internals to move it. Instead of a general move command we could call it with commands to move in different directions.

<>Copy
export class Square { private color = 'red'; private x = 0; private y = 0; private z = 30; constructor(private ctx: CanvasRenderingContext2D) {} moveRight() { this.x++; this.draw(); } private draw() { this.ctx.fillStyle = this.color; this.ctx.fillRect(this.z * this.x, this.z * this.y, this.z, this.z); } }

And now we get better animations.

What we did here is the first step of making a game loop. This loop is the heart of every game. It’s a controlled infinite loop that keeps your game running — it’s the place where all your little pieces will be updated and drawn on the screen.

Optimize animations
Link to this section

Another option for animating is to use requestAnimationFrame. It tells the browser that you wish to perform an animation and requests the browser to call a function to update an animation before the next repaint. In other words, we tell the browser: “Next time you paint on the screen, also run this function because I want to paint something too.”

The way to animate with requestAnimationFrame is to create a function that paints a frame and then schedules itself to invoke again. With this, we get an asynchronous loop that will execute when we draw on the canvas. We will be invoking the animate method over and over again until we decide to stop.

<>Copy
animate() { const id = requestAnimationFrame(this.animate); // Do stuff }

There is no recursion here, since the function is not calling itself. It is requestAnimationFrame that is asking for animate to be called.

The requestAnimationFrame method returns an id that we use for canceling the scheduled animation frame. To cancel a scheduled animation frame, you can use the cancelAnimationFrame method. This method should be passed the id for the frame you wish to cancel.

<>Copy
cancelAnimationFrame(id);

So, in conclusion, why should we use requestAnimationFrame instead of setInterval or setTimeout?

  • It enables browser optimizations.
  • It handles the frame rate.
  • Animations only run when visible.

What about Change Detection?
Link to this section

Angular applications execute inside an “Angular Zone”, which makes change detection automatically run without us having to do anything. When events like a mouse click or an HTTP response occur, we enter the zone and run the event handling code. Then, we exit and Angular performs change detection for the application. In most cases, you don’t need to worry about this. But when we fire timers frequently like like setInterval, setTimeout or requestAnimationFrame, we are also firing off change detection.

By default, every requestAnimationFrame runs inside NgZone and triggers change detection. This could mean that we end up running change detection 60 times a second. Since our application is small and Angular’s change detection is fast, we will most likely not notice it. But it could become a problem, so how do we solve it?

To run animations outside the zone, we use the ngZone.runOutsideAngular function. This function accepts a callback where we can execute the animate function.

<>Copy
constructor(private ngZone: NgZone) {} ... this.ngZone.runOutsideAngular(() => this.animate());

This code runs the first frame outside the NgZone, and it will also run all the subsequent frames outside the zone. With this small change, we've achieved a potentially significant performance improvement to the animation loop.

Max Koretskyi aka Wizard, has more about change detection:

We don’t need all this optimization in our simple animation but to see some example code, I have prepared a Stackblitz.

Conclusion
Link to this section

In this article, you learned how to use the HTML5 Canvas and its 2D graphics context. I showed how to draw simple shapes, and finally, we were able to animate multiple objects on the canvas. We learned how to use setInterval to create an animation loop that keeps track of the objects on the screen.

We also learned how to optimize animations with requestAnimationFrame and how we can run outside the “Angular Zone”.

With this intro to canvas animations, we have taken our first steps into game development. I think we are ready to start on a real game next time!

Resources
Link to this section

Share

About the author

author_image

Angular by day. React by night. Full-stack when needed. I like blogging about web stuff. Co-organizer ngVikings.

author_image

About the author

Michael Karén

Angular by day. React by night. Full-stack when needed. I like blogging about web stuff. Co-organizer ngVikings.

About the author

author_image

Angular by day. React by night. Full-stack when needed. I like blogging about web stuff. Co-organizer ngVikings.

Looking for a JS job?
Job logo
Senior Full Stack Developer | ASP.NET | Angular

Triskelle Solutions

Worldwide
Remote
$90k - $150k
Job logo
UI/Angular Developer

DXC Technology

Worldwide
Remote
$93k - $93k
Job logo
PDQ team| JavaScript developer (Angular/Node)

SD Solutions

Ukraine
Remote
$42k - $84k
Job logo
Full Stack AngularJS / Laravel Developer

The Kotter Group

Worldwide
Remote
$85k - $90k
More jobs
NxAngularCli
NxAngularCli
NxAngularCli

Featured articles

JavaScriptpost
27 September 202130 min read
An in-depth perspective on webpack's bundling process

Webpack is a very powerful and interesting tool that can be considered a fundamental component in many of today's technologies that web developers use to build their applications. However, many people would argue it is quite a challenge to work with it, mostly due to its complexity.

JavaScriptpost
27 September 202130 min read
An in-depth perspective on webpack's bundling process

Webpack is a very powerful and interesting tool that can be considered a fundamental component in many of today's technologies that web developers use to build their applications. However, many people would argue it is quite a challenge to work with it, mostly due to its complexity.

Read more
JavaScriptpostAn in-depth perspective on webpack's bundling process

27 September 2021

30 min read

Webpack is a very powerful and interesting tool that can be considered a fundamental component in many of today's technologies that web developers use to build their applications. However, many people would argue it is quite a challenge to work with it, mostly due to its complexity.

Read more