Image courtesy of inDepth.dev

Getting Started With Fabric JS in Angular

Jim Armstrong
ngconf
Published in
9 min readMar 8, 2021

--

Optimizing Change Detection With Canvas Interactivity

Introduction

This article discusses the basics of using the Fabric JS Canvas drawing library in an Angular application. In the past, I’ve used Pixi JS or SVG.js to illustrate dynamic drawing in an Angular application. The primary purpose of those articles was to illustrate specific algorithms and imperative drawing techniques. For those demonstrations, excess change detection cycles was not an issue. For production applications, however, it is often worthwhile to take time to fully optimize the change detection process. This is particularly true when dealing with Canvas interactivity.

This article deconstructs an EdTech application that demonstrates two popular convex hull algorithms. If you are interested, you can read up on the convex hull here. The cliff notes version is pretty simple — think of the smallest convex polygon that contains a set of points. Convex simply means that a line segment between any two points inside the polygon is entirely inside the polygon.

In terms of computing the convex hull, we resort to the tried and true methodology …

blah, blah, geometry, blah … Graham Scan … blah … Melkman … blah … API.

There! Everything you need has been done for you and conveniently provided in a Typescript library. Keep in mind that the user is a student; they are the ones who need to understand the specifics of Graham Scan and Melkman’s algorithm.

It is only necessary in the application for the user to select points in a drawing area. Then, call a library method and use the result to draw some line segments. That’s all that is required from a developer perspective.

However, this is a highly interactive application and applications tend to grow in size and complexity over time. So, we need to be aware of possible sources of performance degradation. Keep that in mind in the following sections.

Now, before we begin the deconstruction, here is the GitHub repo for the application.

Fabric JS Setup

Fabric JS setup in Angular is very simple. After scaffolding a new project (the current demo uses Angular version 11), install fabric and @types/fabric.

This demonstration uses version 4.3.0 of fabric and 4.2.1 of the typings.

There are no fabric modules to import or any other required setup. The fabric library can be imported as follows.

import { fabric } from 'fabric';

A fabric Canvas, for example, is typed as fabric.Canvas.

Application Setup in Angular

The application consists of a few Material components for selecting an algorithm and clearing the drawing area, along with the drawing area itself. Two more child components are used to display tooltip and other information. These are instances of the MessageComponent (app-message selector).

/src/app/app.component.html

<div style="width:602px">
<mat-toolbar style="width: 602px;">
<mat-toolbar-row>
<span>Interactive Convex Hull</span>

<span class="toolbar-spacer"></span>
<mat-icon (click)="onClear()" class="mr24" matTooltip="Clear All Drawings">clear_all</mat-icon>
<mat-icon (click)="onGraham()" class="mr24" matTooltip="Graham Scan">border_outer</mat-icon>
<mat-icon (click)="onMelkman()" class="mr24" matTooltip="Melkman's Algorithm">border_all</mat-icon>

</mat-toolbar-row>
</mat-toolbar>

<canvas width="600" height="600" style="border: 1px solid #cccccc;" id="fabricSurface"></canvas>
<div class="mt6">
<app-message [message]="instructions"></app-message>
<app-message [message]="summary"></app-message>
</div>
</div>

Define a fabric Canvas to manage the drawing surface, and then initialize it in the main app component’s ngOnInit() handler.

For instructional purposes, make sure the selected code segments are commented out as shown below,

/src/app/app.component.ts (first attempt)

protected _canvas?: fabric.Canvas;
.
.
.
public ngOnInit(): void
{
// this._zone.runOutsideAngular( () => {
this._canvas = new fabric.Canvas('fabricSurface', {
backgroundColor: '#ebebef',
selection: false,
preserveObjectStacking: true,
});


this._fabricService.canvas = this._canvas;

this._canvas.on('mouse:up', this._mouseUp);
// });
}

This code creates a new fabric Canvas , but the last two lines require some explanation.

The application should allow the student to click on the drawing surface to define a set of points. This requires a ‘click’ or a ‘mouse-up’ handler on the fabric Canvas. The latter is used in this demo.

The following code snippets define and then assign this handler,

/src/app/app.component.ts

protected _mouseUp: (evt: fabric.IEvent) => void;
.
.
.
constructor(protected _fabricService: FabricService, protected _zone: NgZone)
{
this._algorithm = HULL_METHOD.GRAHAM;
this._points = new Array<POINT>();
this.summary = this.__setSummary();

this._mouseUp = (evt: fabric.IEvent) => this.__onMouseUp(evt);
}

A direct reference to the mouse handler is maintained so that it can be added and removed with the fabric Canvas ‘on’ and ‘off’ methods.

Now, what about this line?

this._fabricService.canvas = this._canvas;

In past articles where I’ve covered dynamic drawing, an Angular directive was used to select the drawing surface and then handle drawing specifics. As there is often more than one way to accomplish a task, I wanted to show you another approach in this example.

Instead of a directive, a service is employed to handle all drawing tasks. The fabric Canvas reference is passed to the service after injection. So, the same drawing methods can be applied to multiple fabric Canvas instances across different Angular components.

/src/app/shared/services/fabric-service.ts

This service will be deconstructed in more detail in a later section.

Now, return to the main app component and comment out the following line in the ngOnInit() handler.

this._canvas.on('mouse:up', this._mouseUp);

Now, there is no Canvas interactivity. Build and run the application. Move your mouse over the drawing area while the console is open.

Nothing. No console logs and of course nothing appears on the Canvas.

Now, uncomment setting of the mouse-up handler and repeat the exercise. You should notice multiple console logs — even in the MessageComponent. Note that none of that Component’s Input values change as a result of mouse-over of the Canvas. Yet, we can see that ngDoCheck() is called for the main app component and its two child components.

The DoCheck lifecycle method is always called, even with OnPush change detection, which is used in the MessageComponent. Beginning Angular developers often think that OnPush change detection means that no CD cycles are ever generated unless the Inputs change or there is an http request or mouse/timing event in the component to trigger CD. The ngDoCheck() method, however, is always called, even when component Inputs do not change. As the two MessageComponent instances are children of the main app component, ngDoCheck() is called on them any time CD is triggered in the main app component.

Optimizing Change Detection

It’s easy to get in the trap of thinking that change-detection optimization means OnPush and that is the edge of the envelope. This is a misconception that is often ‘pushed’ onto new Angular devs :)

Angular monkey-patches mouse events and does not discriminate between mouse events on DOM elements. So, mouse interaction with a DIV and mouse interaction with an HTML Canvas are all treated equally in Angular. However, mouse interaction with the HTML Canvas in this application never affects anything outside the Canvas. Just like Las Vegas, what happens in the Canvas stays in the Canvas.

So, what we really want to do is tell Angular to ignore change detection on mouse interactions involving the Canvas.

Return to the main app component’s ngOnInit() handler and remove the comments that were added above. The complete handler should look like

public ngOnInit(): void
{
this._zone.runOutsideAngular( () => {
this._canvas = new fabric.Canvas('fabricSurface', {
backgroundColor: '#ebebef',
selection: false,
preserveObjectStacking: true,
});

this._fabricService.canvas = this._canvas;

this._canvas.on('mouse:up', this._mouseUp);
});
}

Now, we have separated the creation of and interaction with the fabric Canvas from Angular’s zone.

If the concept of zones is new, here are a few references that may help.

Build and re-run the demo again. Notice that mousing over the Canvas causes no new calls to the main app component’s ngDoCheck() method. Since no Input to either instance of the MessageComponent changes, that component’s ngOnChanges() lifecycle method is not called.

In terms of actual performance, Angular is so good about running CD cycles that you could develop a Canvas-centric application and never notice what is happening in the background. However, as the application grows in complexity and more components are added, optimizing CD could benefit the user experience. This is particularly true in games or the gamification of business applications. Benefits include smoother UI display, higher frame rates, and more available cycles for game-loop items such as AI.

Fabric JS Drawing

This section is provided for those wishing more detail in the specifics of Fabric JS drawing. Most Canvas-based libraries either use or are heavily influenced by the Flash drawing API. The core of this API is a graphic context that can be drawn into using a sequence of move-to, line-to, and quadratic-bezier-to commands. These actions move the pen, and then draw a line segment from the current pen point to a specified point or a quadratic bezier curve from the current pen point to the specified point (using a specified control point).

Fabric JS does not follow this API, but it does support SVG-style paths. In a way, this is a benefit since the same concepts of move-to, line-to, and quad-to are present in paths. Once a path specification is available, it can be rendered to a Canvas using Fabric JS or into an SVG container.

This demonstration interactively updates the result of either Graham Scan or Melkman’s algorithm. Graham Scan computes the convex hull of a point set. Melkman’s algorithm computes the hull of a polyline (which should have NO intersecting segments).

So, all we need to do (beyond calling a library method) is plot points wherever the user clicks in the drawing area and then draw line segments. This is quite easy to accomplish with an SVG path.

It is also possible to draw the polygon line segments with Fabric’s built-in PolyLine class. This is the approach used in the FabricService class shown above.

There may be multiple polylines to display, so each is given a name and they are stored internally in the FabricService class. This facility is used when clearing the fabric Canvas (which happens when switching from Graham Scan to Melman’s algorithm and back).

The polyline may also be cleared (removed from the fabric Canvas) before drawing as shown below

/src/app/shared/services/fabric-service.ts

public addPolyline(name: string, points: Array<POINT>, clear: boolean = true): void
{
const polyLine: fabric.Polyline = new fabric.Polyline(points,
{
strokeWidth: this.strokeWidth,
stroke: this.strokeColor,
fill: 'transparent',
});

if (this._canvas)
{
if (clear && this._polylines[name] !== undefined) {
this._canvas.remove(this._polylines[name]);
}

this._canvas.add(polyLine);
this._canvas.renderAll();
}

this._polylines[name] = polyLine;
}

The service’s addPoint() method is used to render a small circle to visually indicate a point as the user clicks inside the drawing area. The clear() method clears the entire drawing surface and prepares it for new drawing.

The __onMouseUp() handler in the main app component illustrates usage of the FabricService drawing methods,

/src/app/app.component.ts

protected __onMouseUp(evt: fabric.IEvent): void
{
if (evt.pointer)
{
const p: POINT = {x: evt.pointer.x, y: evt.pointer.y};
this._points.push(p);

this._fabricService.addPoint(p);

let hull: Array<POINT>;

// update hull and drawing
if (this._points.length > 2)
{
switch (this._algorithm)
{
case HULL_METHOD.GRAHAM:
hull = grahamScan(this._points);

this._fabricService.strokeColor = 'black';
this._fabricService.addPolyline('hull', hull);
break;

case HULL_METHOD.MELKMAN:
hull = melkman(this._points);

// draw polyline, then hull
this._fabricService.strokeColor = 'black';
this._fabricService.addPolyline('polyline', this._points);

this._fabricService.strokeColor = 'red';
this._fabricService.addPolyline('hull', hull);
break;
}
}
}
}

The two library methods, grahamScan() and melkman() perform the computations required by each algorithm. These are found in the /src/app/shared/libs/convex-hull.ts file.

Summary

Working with Fabric JS in Angular is fairly simple, but Canvas drawing libraries all share the same performance issues. At the minimum, I hope this stimulates some desire on your part to study Angular change detection in more detail. Such a study will serve you well, even if you never delve into dynamic drawing.

If you should ever need to draw something imperatively in a future Angular application, then performance should be the last of your concerns :)

Good luck with your Angular efforts!

Now that you’ve read this article and learned a thing or two (or ten!), let’s kick things up another notch!
Take your skills to a whole new level by joining us in person for the world’s first MAJOR Angular conference in over 2 years! Not only will You be hearing from some of the industry’s foremost experts in Angular (including the Angular team themselves!), but you’ll also get access to:

  • Expert panels and Q&A sessions with the speakers
  • A friendly Hallway Track where you can network with 1,500 of your fellow Angular developers, sponsors, and speakers alike.
  • Hands-on workshops
  • Games, prizes, live entertainment, and be able to engage with them and a party you’ll never forget

We’ll see you there this August 29th-Sept 2nd, 2022. Online-only tickets are available as well.
https://2022.ng-conf.org/

--

--

Jim Armstrong
ngconf
Editor for

Jim Armstrong is an applied mathematician who began his career writing assembly-language math libraries for supercomputers. He now works on FE apps in Angular.