SVG and Canvas Graphics in Angular 2

Posted on by Tero Parviainen (@teropa)

Most Angular applications are built using good old HTML and CSS. But it just so happens that HTML is only one of the four main rendering pipelines made available on the web platform, the three other ones being SVG, the 2D Canvas, and the 3D WebGL Canvas.

These alternative rendering technologies can be very useful when you are building something highly visual. Icons, charts and other data visualizations, mapping apps, and games are all examples of things that can be difficult to build using just HTML, but relatively easy to accomplish with SVG or Canvas.

In this guide I will describe how you can incorporate SVG and 2D Canvas graphics into Angular applications, and how you can avoid potential problems along the way. I will also describe how to add motion to SVG and Canvas graphics by animating them. We will look at the various options available and learn about their strengths and weaknesses.

The full source code of all the examples shown here is available on Github.

Table of Contents

SVG Graphics

When it comes to choosing a rendering technology for graphics in Angular, SVG is usually the most obvious choice. This is because SVG is a markup language just like HTML. As such it can take advantage of all the features Angular makes available in markup, such as components and directives, data and event bindings, styling, content projection, and animations.

The main difference between HTML and SVG is that whereas in HTML we write markup for things like paragraphs, divs, and text inputs, in SVG we use write markup for visual shapes, such as circles, rectangles, and polygons. SVG has a number of shape, structural, and filtering elements available, from which you can compose visual scenes of any complexity.

If you're looking for learning materials for SVG in general, check these out:

Here's a simple SVG image that displays the Angular logo:

If you go and inspect this image with your browser developer tools, you'll find that it's formed out of just three SVG elements: Two <polygon>s for the two halves of the Angular shield, and one <path> for the A character in the middle.

If we wanted to embed that logo SVG in an Angular application, we could do so simply by placing that SVG content along with its root tag into an Angular component template:

logo.component.html
<svg viewBox="0 0 250 250">
  <polygon points="125,30 125,30 125,30 31.9,63.2 46.1,186.3 125,230 125,230 125,230 203.9,186.3 218.1,63.2" />
  <polygon points="125,30 125,52.2 125,52.1 125,153.4 125,153.4 125,230 125,230 203.9,186.3 218.1,63.2 125,30" />
  <path d="M125,52.1L66.8,182.6h0h21.7h0l11.7-29.2h49.4l11.7,29.2h0h21.7h0L125,52.1L125,52.1L125,52.1L125,52.1
      L125,52.1z M142,135.4H108l17-40.9L142,135.4z"/>
</svg>

This shows how naturally SVG fits into Angular: Since SVG is markup just like HTML and browsers allow embedding <svg>s right into HTML documents, we can just drop one into a template.

But things aren't always quite as simple as this. When you start doing anything more complicated than what we've done here, you will likely run into a few issues. These issues are caused the differences in how browsers implement the respective JavaScript DOM APIs of HTML and SVG. Let's look at the three main ones and what we can do about them.

Namespacing SVG Elements

Usually when you use an SVG element in an Angular template, you will need to prefix its name with the svg: namespace prefix, as is done here:

logo.component.html
<svg viewBox="0 0 250 250">
  <svg:polygon points="125,30 125,30 125,30 31.9,63.2 46.1,186.3 125,230 125,230 125,230 203.9,186.3 218.1,63.2" />
  <svg:polygon points="125,30 125,52.2 125,52.1 125,153.4 125,153.4 125,230 125,230 203.9,186.3 218.1,63.2 125,30" />
  <svg:path d="M125,52.1L66.8,182.6h0h21.7h0l11.7-29.2h49.4l11.7,29.2h0h21.7h0L125,52.1L125,52.1L125,52.1L125,52.1
      L125,52.1z M142,135.4H108l17-40.9L142,135.4z"/>
</svg>

This will let Angular know that it's supposed to create SVG elements instead of HTML ones.

As we already saw in our very first example though, you may not always need this prefix. In particular, when the root <svg> tag is in the same component as all of the SVG content, Angular is able to infer the correct namespace automatically. But as soon as you break that content up into multiple components, this inference will no longer be possible.

For this reason, I recommend always using the namespace prefix even in cases where it's not strictly necessary. Plus, it communicates to anyone reading the template code that SVG is being used.

Under The Hood: Element Creation

The underlying reason for having to use element namespacing is that when Angular compiles the template, it generates the JavaScript DOM API calls that will create those elements at runtime. And there are different DOM APIs for creating HTML elements and SVG elements. For HTML:

document.createElement('div');

And for SVG:

document.createElementNS('http://www.w3.org/2000/svg', 'path');

This means Angular needs to know what kinds of elements we're talking about. When it can't infer this from the context, we need to be explicit about it.

Using Attribute Binding Instead of Property Binding

Having static SVG content in templates is one thing, but sooner or later you'll want to construct your graphics dynamically by binding their attributes to your application state.

Here's an example of such a case. The Angular logo here can only be seen through a circular peephole that follows your mouse cursor. The position of the peephole is held in the component state and updated on mousemove events:

The way we usually set up dynamic bindings in views is by using property binding, where we surround the attribute name with square brackets: [cx]="circle[0]". But with SVG, this will generally not work. Instead it'll result in an error like this:

Can't bind to 'cx' since it isn't a known property of ':svg:circle'.

This seems a bit strange since there definitely is something called cx that you can set for <circle>s. But the problem here is that while there is a cx attribute on <circle>, there is no corresponding cx property on the DOM SVGCircleElement object. This means that we have to give Angular a hint that we want to set an attribute, not a property. We can do that by adding the attr. prefix to the binding:

logo.component.html
<svg viewBox="0 0 250 250"
     (mousemove)="setCircleLocation($event)">
  <svg:defs>
    <svg:clipPath id="clip">
      <svg:circle [attr.cx]="circle[0]" [attr.cy]="circle[1]" r="100" />
    </svg:clipPath>
  </svg:defs>
  <svg:g clip-path="url(#clip)">
    <svg:polygon points="125,30 125,30 125,30 31.9,63.2 46.1,186.3 125,230 125,230 125,230 203.9,186.3 218.1,63.2" />
    <svg:polygon points="125,30 125,52.2 125,52.1 125,153.4 125,153.4 125,230 125,230 203.9,186.3 218.1,63.2 125,30" />
    <svg:path d="M125,52.1L66.8,182.6h0h21.7h0l11.7-29.2h49.4l11.7,29.2h0h21.7h0L125,52.1L125,52.1L125,52.1L125,52.1
      L125,52.1z M142,135.4H108l17-40.9L142,135.4z"/>
  </svg:g>
</svg>

This is something we have to do most of the time in SVG, because the SVG DOM generally does not expose attributes as properties like the HTML DOM does. On modern browsers, properties like style and classList do exist, which means we can use style and class bindings just like in HTML. But for anything SVG specific, the attr. prefix is required.

On the other hand, event bindings like (click) and (mousemove) can be made without prefixes.

Under the hood: Properties vs. Attributes

This is another instance where the Angular compiler needs to produce different code for SVG and HTML. Whenever we bind a property, something like this gets emitted:

element.d = this.getPath();

Since in SVG there won't be a d property, the following needs to be emitted instead:

element.setAttribute('d', this.getPath());

This is what the attr. prefix is about.

Avoiding Element Selectors For Components

As your SVG content gets larger, at some point it will start feeling appropriate to split it out into multiple components. The reasons for this are obvious: Smaller, focused components are just easier to work with and reuse.

The usual approach for defining Angular components is to do it using custom elements. We define a component like LogoLeftComponent that attaches itself to the element <app-logo-left>, which we then expect to be able to use like this:

<svg viewBox="0 0 250 250">
  <app-logo-left></app-logo-left>
  <app-logo-right></app-logo-right>
  <app-logo-a></app-logo-a>
</svg>

Whenever we use components in this way, we end up having the custom elements in the runtime DOM, as we can see if we inspect it:

What this basically means is that we're handing the browser unknown elements, since <app-logo-left> exists in no HTML standard. Doing something like this is perfectly fine in HTML – browser have a specific way of interpreting unknown elements. But in SVG it's not OK. That's is because SVG is not HTML. It's XML. And if we throw unknown elements in it, browsers won't know what to do with them. That means this code example is not going to render.

So we need another way to attach our components to the DOM, and luckily there is a very straightforward solution available. It requires revisiting the selector property in our component metadata. In that property we usually just have the element name that we want to attach to, but, as the documentation says, it is actually more generally a "CSS selector that identifies this component in a template". This means we can replace the element selector app-logo-left with, for example, an attribute selector like [app-logo-left]:

@Component({
  selector: '[app-logo-left]',
  templateUrl: './app-logo-left.component.html'
})
export class LogoLeftComponent {
}

After this, the component will attach itself to any element that has the attribute app-logo-left. Crucially, we can now use it with standard SVG elements, such as a <g>:

<svg viewBox="0 0 250 250">
  <svg:g app-logo-left />
  <svg:g app-logo-right />
  <svg:g app-logo-a />
</svg>

In the future, as the Web Components standard becomes more widely supported, I'd say there's a distinct possibility that Angular will begin using actual custom elements for components instead of relying on unknown elements. To some extent that will happen automatically when custom element support is rolled out by browser vendors.

But we're not there yet. It is also unclear to me whether the custom element spec applies to HTML only or also to SVG. I am not holding my breath waiting for browsers to add full-on SVG support for them.

Animating SVG Graphics

Once you have some SVG graphics on the screen, sooner or later you'll start wanting to move them around. Motion is an important part of most web applications these days, and that's certainly true for the kinds of highly visual apps that have SVG in them!

There are a few different options for adding motion to SVG, and they include both standards-based and library-based approaches. Let's look at the most important ones.

SMIL

Of all the standards-based options of animating SVG graphics, SMIL (or "smile") is the most versatile and powerful. SMIL is a W3C standard for "describing multimedia presentations" and it basically allows embedding animation definitions as XML tags right into SVG:

<svg>
  <animate
    xlink:href="#blue-rectangle"
    attributeName="x"
    from="50"
    to="425"
    dur="5s"
    begin="circ-anim.begin + 1s"
    fill="freeze"
    id="rect-anim" />
  <!-- ... -->
</svg>

SMIL is capable of a lot of things: On top of simple size, position, and color changes you can animate object movement along motion paths, you can morph shapes, you can synchronize different animations with each other, and you can even define additive animations that compose together well.

Here's a great article by Sara Soueidan that describes these capabilities in detail.

But there's a problem with SMIL: Browser support. First of all, not all browsers implement SMIL at all. It is notably absent in both Internet Explorer and Edge. But what's even more damning is that both Chrome and Opera have announced that they will deprecate SMIL. This makes SMIL a nonstarter for Angular applications, which is kind of a pity because of all the power it has.

CSS Transitions And Animations

Since SMIL has been written off by browser vendors, we're pretty much left with one widely supported standard method for animating SVG: CSS transitions and keyframe animations. These are tools you may be familiar with through having used them with HTML. Luckily they are also supported for SVG content.

With CSS you can attach keyframe animations to individual SVG elements to make them move about, as I've done here:

CSS transitions and animations can be applied to SVG in the exact same way they're applied to HTML, which is by defining them in component stylesheets.

logo.component.css
@keyframes oscillate-enlarge {
  from { transform: scale(1) }
  to { transform: scale(8)}
}

@keyframes oscillate-reduce {
  from { transform: scale(1) }
  to { transform: scale(0.125)}
}

.group {
  animation: oscillate-enlarge 3s ease-in-out 0s infinite alternate;
  transform-origin: 125px 115px;
}
.shield {
  animation: oscillate-reduce 3.2s ease-in-out 0s infinite alternate-reverse;
  transform-origin: 125px 115px;
}
.a {
  animation: oscillate-reduce 3.1s ease-in-out 0s infinite alternate;
  transform-origin: 125px 115px;
}

We're using CSS classes here, so to make this work we also need to define classes for the SVG elements in the template:

<svg viewBox="0 0 250 250">
  <svg:g class="group">
    <svg:polygon class="shield" points="125,30 125,30 125,30 31.9,63.2 46.1,186.3 125,230 125,230 125,230 203.9,186.3 218.1,63.2" />
    <svg:path class="a" d="M125,52.1L66.8,182.6h0h21.7h0l11.7-29.2h49.4l11.7,29.2h0h21.7h0L125,52.1L125,52.1L125,52.1L125,52.1
      L125,52.1z M142,135.4H108l17-40.9L142,135.4z"/>
  </svg:g>
</svg>

The main problem with CSS transitions and animations, particularly when compared to SMIL, is that they can only be applied to a subset of SVG attributes. With CSS you can only animate things that you can style using CSS in the first place: Strokes, fills, line widths, opacities, as well as CSS transforms. (Together these are called presentation attributes). But you cannot animate other things, like the actual coordinates and points of SVG shapes. This is because these things cannot be styled using CSS at all. They can only be set from the SVG XML content.

A couple of extensions to web standards may improve this situation in the future:

Firstly, the upcoming CSS motion-path property will allow you to make shapes move around along path geometries, which is something you can already do in SMIL but not yet in CSS. Browser support still remains patchy but this will hopefully improve.

Secondly, there is a W3C proposal that would add more SVG attributes to the list of styleable attributes, which would also make them animatable. However, it is unclear when this will happen, if at all.

For more information on animating SVG with CSS, see this guide by Chris Coyier. Everything said in the guide applies to Angular apps as well.

Web Animations and Angular Animations

On top of SMIL and CSS, the third standard way of animating SVG content is the emerging Web Animations standard. This standard introduces the Element.animate method, which allows creating the same kinds of animations as CSS but with much better programmatic control. This example from the Google Dev blog illustrates how you can use dynamically calculated values in animations as well as control them after they have been launched:

var player = snowFlake.animate([
  {transform: 'translate(' + snowLeft + 'px, -100%)'},
  {transform: 'translate(' + snowLeft + 'px, ' + window.innerHeight + 'px)'}
], 1500);
// less than 1500ms later...changed my mind
player.cancel();

Web Animations have already been partially implemented by some browsers, and can be polyfilled for others, making them an attractive option for animating SVG. But what makes this API even more interesting is that it also happens to be the underlying engine for Angular Animations.

What this means is that we can indeed use ngAnimate with SVG. And this is great because ngAnimate brings many of the programmatic control benefits of Web Animations to Angular by tying your animations more closely to the rest of your application. Compared to CSS, it is much easier to do enter and leave animations, and in general to synchronize animated transitions with logical component states.

Here's an example of an animation done with Angular Animations. The logo has a click handler on the "A", which toggles a component attribute between two values. That attribute is bound to an animation trigger:

Angular Animations can be used in SVG exactly like they're used in HTML. We define our animation triggers in the component metadata in as usual:

logo.component.ts
@Component({
  selector: 'app-logo',
  templateUrl: './app-logo.component.html',
  styleUrls: ['./app-logo.component.scss'],
  animations: [
    trigger('aState', [
      state('small', style({transform: 'scale(1)'})),
      state('large', style({transform: 'scale(4.2)'})),
      transition('small => large', animate('1s ease', keyframes([
        style({transform: 'scale(1)', offset: 0}),
        style({transform: 'scale(0.7) rotate(15deg)', offset: 0.15}),
        style({transform: 'scale(1)', offset: 0.3}),
        style({transform: 'scale(4.2)', offset: 1})
      ]))),
      transition('large => small', animate('1s ease', keyframes([
        style({transform: 'scale(4.2)', offset: 0}),
        style({transform: 'scale(5) rotate(-15deg)', offset: 0.15}),
        style({transform: 'scale(4.2)', offset: 0.3}),
        style({transform: 'scale(1)', offset: 1})
      ])))
    ])
  ]
})

And then we can bind those triggers to SVG elements by using the animation binding syntax in our template:

<svg viewBox="0 0 250 250">
  <svg:polygon class="left" points="125,30 125,30 125,30 31.9,63.2 46.1,186.3 125,230 125,230 125,230 203.9,186.3 218.1,63.2" />
  <svg:polygon class="right" points="125,30 125,52.2 125,52.1 125,153.4 125,153.4 125,230 125,230 203.9,186.3 218.1,63.2 125,30" />
  <svg:path class="a"
        (click)="toggleAState()"
        [@aState]="aState"
        d="M125,52.1L66.8,182.6h0h21.7h0l11.7-29.2h49.4l11.7,29.2h0h21.7h0L125,52.1L125,52.1L125,52.1L125,52.1
    L125,52.1z M142,135.4H108l17-40.9L142,135.4z"/>
</svg>

Although the Web Animations API is often talked about as a replacement for SMIL, unfortunately it still doesn't quite match it in power. Web Animations have the same limitations as CSS animations in what exactly you can animate: SVG presentation attributes only. The improvement we get here over CSS transitions and animations is in the programmatic control and debuggability, not so much in what you can animate.

Greensock

We've seen how SVG can be animated with either CSS animations or Web Animations (and, by association, Angular Animations). But we still can't do something like this with either of those approaches:

This is a path morphing animation, which actually changes the points of the two SVG polygons in the Angular shield. It is something you can do with SMIL but not with the other standards-based approaches.

What you need to do when you want something like this is to bring in a third-party library, for which I recommend Greensock. To use it, you must first add the package to your project along with its TypeScript definitions:

npm install -S gsap @types/greensock

Greensock does not expose any ES6 or CommonJS modules that you could import. Instead it uses a more traditional way of installing itself: As a global on window. So you won't need to import any Greensock classes or functions, but you do need to import the whole package into your application so that it gets installed. This you can do in main.ts, for example:

main.ts
import 'gsap';

After this, Greensock will be available to be used in components.

Greensock operates on raw DOM elements, which means we need to gain access to the ones we have in our templates so that we can hand them over. We can do this by first tagging reference variables on them:

logo.component.html
<svg viewBox="0 0 250 250">
  <svg:polygon #left points="125,30 125,30 125,30 31.9,63.2 46.1,186.3 125,230 125,230 125,230 203.9,186.3 218.1,63.2" />
  <svg:polygon #right points="125,30 125,52.2 125,52.1 125,153.4 125,153.4 125,230 125,230 203.9,186.3 218.1,63.2 125,30" />
  <svg:path  d="M125,52.1L66.8,182.6h0h21.7h0l11.7-29.2h49.4l11.7,29.2h0h21.7h0L125,52.1L125,52.1L125,52.1L125,52.1
    L125,52.1z M142,135.4H108l17-40.9L142,135.4z"/>
</svg>

Then we can inject these elements into the component class using the @ViewChild() decorator. Once we have them, we can give them to Greensock to animate. In this example, I'm doing it from OnInit but it could also be from some other lifecycle hook or event handler:

logo.component.ts
export class LogoComponent implements OnInit {
  @ViewChild('left') left: ElementRef;
  @ViewChild('right') right: ElementRef;

  ngOnInit() {
    TweenMax.to(this.left.nativeElement, 1, {
      attr: {
        points: '125,30 125,30 125,30 31.9,30 31.9,230 125,230 125,230 125,230 203.9,186.3 218.1,63.2'
      },
      repeat: -1,
      yoyo: true,
      ease: Cubic.easeInOut
    });
    TweenMax.to(this.right.nativeElement, 1, {
      attr: {
        points: '125,30 125,52.2 125,52.1 125,153.4 125,153.4 125,230 125,230 218.1,230 218.1,30 125,30'
      },
      repeat: -1,
      yoyo: true,
      ease: Cubic.easeInOut
    });
  }

While Greensock is very powerful, here we can see how it isn't quite as nice to use with Angular as ngAnimate. We can't do things in a purely declarative way, and we also need to drop the abstraction level to where we are operating with raw DOM elements. As far as tradeoffs go, it could be a lot worse, but I still usually try to do things with CSS or ngAnimate first and only reach for Greensock when that doesn't cut it:

JavaScript & requestAnimationFrame

If all else fails, you can also just animate SVGs manually from JavaScript, by creating animation loops using requestAnimationFrame.

We will discuss the use of requestAnimationFrame with Canvas graphics below, and you can create similar loops for SVG as well. Instead of drawing on a canvas, you just update your SVG attributes separately for each animation frame. This is generally both more complex and less performant than anything discussed above, but may still be appropriate in some cases.

Canvas Graphics

Whereas SVG is a markup language that drops naturally into Angular components, Canvas is an entirely different beast. In Canvas there is no markup or templates. There is just an imperative JavaScript API. We draw shapes by calling methods on a canvas context, which resembles moving a pen on a 2D surface:

ctx.beginPath();
ctx.moveTo(125, 30);
ctx.lineTo(31.9, 63.2);
ctx.lineTo(46.1, 186.3);
ctx.closePath();
ctx.stroke();

The major difference between how things are done in SVG and Canvas is the one between immediate and retained rendering modes:

So canvas is a more low-level, restrictive API than SVG. But there's a flipside to that, which is that with canvas you can do more with the same amount of resources. Because the browser does not have to create and maintain the in-memory object graph of all the things we have drawn, it needs less memory and computation resources to draw the same visual scene. If you have a very large and complex visualization to draw, Canvas may be your ticket.

There is also more work involved in making canvas content accessible than there is with SVG.

Making An Angular Canvas Component

When you want to use a Canvas in Angular, what you can do is create one inside a component and then draw on it from lifecycle hooks and event handlers on that component.

Say we wanted to draw the Angular logo, but instead of using three simple SVG shapes for it, we wanted to draw it using about 50,000 tiny circles. We can do something like that with a Canvas:

Arguably you could do this in SVG too, but drawing logos using 50,000 DOM elements doesn't sound like a great idea to me. Canvas, on the other hand, has no problem with this kind of thing.

To draw this logo, we first need to introduce a <canvas> element, which we can do in the component's template. We'll also attach a reference variable to the element, so that we'll be able to refer to it from the component class:

logo.component.html
<canvas #myCanvas width="500" height="500">
</canvas>

In the component class we can then use the @ViewChild() decorator to inject a reference to the canvas. Once the component has initialized, we'll have access to the Canvas DOM node, as well as its drawing context:

logo.component.ts
export class LogoComponent implements OnInit {
  @ViewChild('myCanvas') canvasRef: ElementRef;

Drawing On The Canvas from Lifecycle Hooks

Once we have a canvas context in the component class, we can draw on it using the standard canvas context API. One simple way to do this is from the ngOnInit component lifecycle hook that gets called when the component has initialized:

logo.component.ts
ngOnInit() {
  let ctx: CanvasRenderingContext2D =
    this.canvasRef.nativeElement.getContext('2d');

  // Draw the clip path that will mask everything else
  // that we'll draw later.
  ctx.beginPath();
  ctx.moveTo(250, 60);
  ctx.lineTo(63.8, 126.4);
  ctx.lineTo(92.2, 372.6);
  ctx.lineTo(250, 460);
  ctx.lineTo(407.8, 372.6);
  ctx.lineTo(436.2, 126.4);
  ctx.moveTo(250, 104.2);
  ctx.lineTo(133.6, 365.2);
  ctx.lineTo(177, 365.2);
  ctx.lineTo(200.4, 306.8);
  ctx.lineTo(299.2, 306.8);
  ctx.lineTo(325.2, 365.2);
  ctx.lineTo(362.6, 365.2);
  ctx.lineTo(250, 104.2);
  ctx.moveTo(304, 270.8);
  ctx.lineTo(216, 270.8);
  ctx.lineTo(250, 189);
  ctx.lineTo(284, 270.8);
  ctx.clip('evenodd');

  // Draw 50,000 circles at random points
  ctx.beginPath();
  ctx.fillStyle = '#DD0031';
  for (let i=0 ; i < 50000 ; i++) {
    let x = Math.random() * 500;
    let y = Math.random() * 500;
    ctx.moveTo(x, y);
    ctx.arc(x, y, 1, 0, Math.PI * 2);
  }
  ctx.fill();
}

However, this is seldom enough. It's quite rare to have a canvas on which you just draw once, when it initializes. It's more common to draw dynamic visuals that are based on the application state, which may change over time. In SVG graphics, Angular's data binding system takes care of applying changes over time. In Canvas graphics we need to do the updates ourselves.

For example, our circle-based Angular logo could work so that it renders a changing set of points. Here's a version that regenerates the points ten times every second:

Usually we get this kind of data into our components using Angular's data binding, as component @Inputs. That is indeed the case with this component: It gets the points to render as an input.

logo.component.ts
export class CanvasLogoComponent implements OnChanges {
  @Input() particles: Particle[];
  @ViewChild('myCanvas') canvasRef: ElementRef;

This is something I usually like to do with canvas components: They take their data as inputs just like regular Angular components, even though internally they may be doing a lot of work to actually re-render that data. The user of the component does not necessarily need to know that there's immediate mode canvas rendering going on.

Inside the component we now need to re-render the logo whenever this input changes. We can get that done by implementing the ngOnChanges lifecycle hook. It gets called whenever any of the inputs of the components has changed. What we can do in the hook is clear the canvas and then redraw the scene with the latest data:

logo.component.ts
ngOnChanges() {
  let ctx: CanvasRenderingContext2D =
    this.canvasRef.nativeElement.getContext('2d');

  // Clear any previous content.
  ctx.clearRect(0, 0, 500, 500);

  // Draw the clip path that will mask everything else
  // that we'll draw later.
  ctx.beginPath();
  ctx.moveTo(250, 60);
  ctx.lineTo(63.8, 126.4);
  ctx.lineTo(92.2, 372.6);
  ctx.lineTo(250, 460);
  ctx.lineTo(407.8, 372.6);
  ctx.lineTo(436.2, 126.4);
  ctx.moveTo(250, 104.2);
  ctx.lineTo(133.6, 365.2);
  ctx.lineTo(177, 365.2);
  ctx.lineTo(200.4, 306.8);
  ctx.lineTo(299.2, 306.8);
  ctx.lineTo(325.2, 365.2);
  ctx.lineTo(362.6, 365.2);
  ctx.lineTo(250, 104.2);
  ctx.moveTo(304, 270.8);
  ctx.lineTo(216, 270.8);
  ctx.lineTo(250, 189);
  ctx.lineTo(284, 270.8);
  ctx.clip('evenodd');

  // Draw the points given as input
  ctx.beginPath();
  ctx.fillStyle = '#DD0031';
  for (let {x, y} of this.particles) {
    ctx.moveTo(x, y);
    ctx.rect(x, y, 1, 1);
  }
  ctx.fill();
}

Here we clear the canvas completely for every change, but this is just one way to solve the re-rendering problem. It has the benefit of being really simple to implement, but for performance reasons you may sometimes want to just redraw those sections of the canvas that actually update. Should you need to, you could also get more information about what exactly changed by looking at the arguments received by the ngOnChanges hook.

Note that ngOnChanges() will not be called when there are internal mutations in the input data, and that means it's mostly appropriate for use with immutable inputs, typically combined with the OnPush change detection strategy. This is my preferred approach.

As an alternative, you could implement ngDoCheck(). But do note that this hook will get called very often – in every change detection turn regardless of whether there actually are changes or not. So with a naive implementation of that hook you will end up redrawing very often. So you may need to combine it with some of your own custom change detection to determine whether you actually need to redraw.

Animating Canvas Graphics

We've seen how to paint still images on a Canvas, and now we'll turn to our final topic: How to make animated Canvas imagery.

Unfortunately, none of the animation options we discussed in the context of SVG apply to canvas, and that's because the immediate rendering mode of the Canvas is fundamentally different from the retained rendering mode of SVG. There simply are no objects around for SMIL, CSS animations, Web Animations, or Greensock to animate.

Instead, we have to drop to a lower abstraction level where we draw each animation frame individually. If you want to make a shape move around on a Canvas, you'll need to draw that shape over and over again, making a different image for each animation frame, with infinitesimal changes from one frame to the next.

With Canvas animations we draw all movement manually frame by frame.

The most important API we have for this kind of work is the window.requestAnimationFrame function that all modern browsers support. This function allows us to attach a callback to the browser's rendering pipeline. We can basically tell the browser: "Next time you're about to paint on the screen, also run this function because I want to paint something too".

requestAnimationFrame(function() {
  ctx.fillRect(getX(), getY(), 10, 10);
});

The way we can then implement an animation on top of requestAnimationFrame is to construct a function that paints a frame and then schedules itself to be invoked again. With this we get an asynchronous loop that will execute as fast as the browser is able to paint on the screen – which is going to be up to 60 times per second:

function paintLoop() {

  // Paint current frame
  ctx.fillRect(getNextX(), getNextY(), 10, 10);

  // Schedule next frame
  requestAnimationFrame(paintLoop);
}

// Start loop
paintLoop();

Of course, the complexity of this kind of animation code is bound to grow a lot larger than that of CSS or Web Animations. Since we have to draw each animation frame manually, we end up having to calculate where all our objects should be in each frame, based on the time elapsed from the previous frame as well as other parameters.

So, say we wanted to create an animation like this in Angular - a "flock" of Angular logos flying around:

This is running Hugh Kennedy's implementation of the Boids flocking algorithm.

This is the kind of animation for which using Canvas and requestAnimationFrame is appropriate: There are a lot of objects moving around, which in SVG would mean having a lot of DOM nodes on the page. Furthermore, it is a real time simulation, where we don't actually know the positions of these objects beforehand. We couldn't use CSS or Element.animate because these objects don't fly along straight paths. The paths change all the time as the dynamics of the system affect them. For something like this, requestAnimationFrame is exactly the right tool.

To set up a requestAnimationFrame loop for an Angular component, I usually implement the OnInit and OnDestroy lifecycle hooks, and have them maintain a "running" flag that will be true after the component is initialized and false after it's been destroyed. This signals that the component is "alive" and mounted on the screen. I then also launch the paint loop from ngOnInit() by calling a private paint() method:

logo-flock.component.ts
export class LogoFlockComponent implements OnInit, OnDestroy {

  @ViewChild('myCanvas') canvasRef: ElementRef;
  private running: boolean;

  ngOnInit() {
    this.running = true;
    this.paint();
  }

  ngOnDestroy() {
    this.running = false;
  }

The paint() method implements the paint loop by doing three things:

  1. It checks that the running flag is still true, because if it isn't, we are not going to do anything else.
  2. It paints a single animation frame on the screen.
  3. It schedules itself to be executed again in the next animation frame.

This implements the basic asynchronous requestAnimationFrame() loop described earlier, but in an Angular context. The paint() method will keep getting called up to 60 times every second until the component is destroyed.

logo-flock.component.ts
private paint() {
  // Check that we're still running.
  if (!this.running) {
    return;
  }

  // Paint current frame
  let ctx: CanvasRenderingContext2D =
    this.canvasRef.nativeElement.getContext('2d');

  // Draw background (which also effectively clears any previous drawing)
  ctx.fillStyle = 'rgb(221, 0, 49)';
  ctx.fillRect(0, 0, 800, 500);

  // Advance flock. This updates the positions of all objects.
  this.flock.tick();

  // Draw flock
  ctx.beginPath();
  ctx.fillStyle = `rgb(255,255,255)`;
  for (const [x, y, speedX, speedY] of this.flock.boids) {
    const angle = Math.atan2(speedY, speedX) + 0.5 * Math.PI;
    ctx.save();
    ctx.translate(x, y);
    ctx.rotate(angle);
    ctx.scale(0.4, 0.4);
    this.paintA(ctx);
    ctx.restore();
  };

  // Schedule next
  requestAnimationFrame(() => this.paint());
}

Avoiding Excessive Change Detection By Escaping The Angular Zone

There is still one problem with the animation setup we have above, which has to do with how Angular performs change detection using the zone.js library.

Angular applications are always executed inside an "Angular Zone". The most important effect this has is that change detection gets automatically executed without us having to do anything: Whenever any browser event (a click event, an HTTP response, a setTimeout, etc.) occurs, we enter the Angular Zone and run the event handling code. After that's done we exit the zone and Angular performs change detection for the application.

For anyone familiar with AngularJS 1.x, this is kind of the same as an automated call to $scope.$apply() integrated to every browser event.

For 99.9% of the time, this is exactly what we want. But with paint loops we have a problem, which is that requestAnimationFrames are also executed within the Angular Zone. And that means when we have an animation running we end up running up to 60 change detections every second.

By default, every requestAnimationFrame enters the NgZone and triggers change detection.

You may not notice this at first because Angular's change detection is very fast and does not cause much overhead in small applications. But it is highly likely to become a problem at some point. Furthermore, there is almost never any need to run change detection from the paint loop, because typically we don't change anything in the loop. There will simply never be any changes to detect.

Luckily, there's a very easy way out of this, which is to take our paint loop and run it outside the Angular zone. We can do this by injecting the NgZone object into our canvas component, and then scheduling the first paint to run outside the zone by using the runOutsideAngular method:

logo-flock.component.ts
constructor(private ngZone: NgZone) {
}

ngOnInit() {
  this.running = true;
  this.ngZone.runOutsideAngular(() => this.paint());
}

This will run the first frame outside the NgZone, and because of the way zones work, 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 paint loop.

To learn more about what Zones in Angular are all about, see this Thoughtram article.

In Summary: Recommendations For SVG and Canvas In Angular

Know Your AngularJS Inside Out

Build Your Own AngularJS

Build Your Own AngularJS helps you understand everything there is to understand about AngularJS (1.x). By creating your very own implementation of AngularJS piece by piece, you gain deep insight into what makes this framework tick. Say goodbye to fixing problems by trial and error and hello to reasoning your way through them

eBook Available Now

comments powered by Disqus