Metabubbles: Making Generative Art with Angular 2

A tutorial using Angular 2, ES2015+, Babel, and Webpack

Posted on by Tero Parviainen (@teropa)

Most people talk about Angular 2 as a platform for writing business applications. But there's no rule that says this is the only thing it should be used for. If you're in the mood for something fun, how about making some generative visual art?

You see, the same characteristics that make Angular 2 a useful framework for business app development also make it a fun platform for pure experimentation and self-expression through code. In this tutorial we're going to do just that.

So let's have some fun with circles! While we're at it, we're also going to learn a whole lot about what Angular 2 development feels like and how it works with the popular Babel + Webpack tool stack.

Table of Contents

Here's what we're going to build (you can click to stop and start the animation):

You're looking at a bunch of SVG circles, animated procedurally with a few simple rules. The concept is based on the "Emergence" chapter of Matt Pearson's wonderful Generative Art book.

Naturally, since this project is all about self-expression and there is no single correct way to construct it, I encourage you to tweak the visualization to your liking as you go through the tutorial, and to share your creation!

Prerequisites

You'll get the most out of this tutorial if you've written applications on some reasonably modern JavaScript framework like React, AngularJS, or Ember. Prior Angular 2 knowledge is not required.

In order to work through the code, you're going to need a JavaScript development environment. At bare minimum, this includes your favorite code editor and a recent version of Node.js and NPM.

The code is available on Github, but I do encourage you to build your own version of it. All the steps needed for that are described below.

You might ask "Why not TypeScript?" The answer is "no particular reason". I've got nothing against TypeScript, but I wanted to see if ES2015/2016 with Babel is also a viable option for Angular 2 development. Spoiler alert: It is.

Setting Up The Project

To get started, we're going to create a JavaScript project directory and a package.json file in it:

mkdir metabubbles
cd metabubbles
npm init -y

We're going to build the project with Webpack, so let's add it to the project as a development dependency, along with its development server package:

npm install webpack webpack-dev-server --save-dev

Webpack needs a configuration file where we tell it where to find our code and what to do with it. Here's a simple config to get us started:

webpack.config.js
module.exports = {
  entry: ['./app/main.js'],
  output: {
    path: __dirname,
    filename: 'bundle.js'
  }
};

This tells Webpack that our application entry point will be in a file called app/main.js, and that it should build the application into a file called bundle.js in the project root directory.

Let's also create that entry point file and put a little logging statement there, with which we can test that everything is working:

app/main.js
console.log('hello there');

Now we've got something we can build. If we add an NPM run script for Webpack we'll be able to do that:

package.json
{
  "name": "metabubbles",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "build": "webpack"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "webpack": "^1.12.14",
    "webpack-dev-server": "^1.14.1"
  }
}

Running this script will generate the bundle.js file with our little hello message in it:

npm run build

We should also set up the Webpack development server, which will serve the application during development and reload it whenever the code changes:

package.json
"scripts": {
  "build": "webpack",
  "start": "webpack-dev-server --inline"
},

Starting the dev server will launch a local HTTP server in port 8080:

npm run start

If you open http://localhost:8080 in a web browser now, you'll see a directory listing, but what we actually want to see is our application. We need to create the HTML host page for it. That goes to the root directory of the project:

index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="stylesheet"
        href="https://cdnjs.cloudflare.com/ajax/libs/normalize/3.0.3/normalize.min.css">

</head>
<body>
  <script src="bundle.js"></script>
</body>
</html>

We're including normalize.css to reset CSS rules to sane, consistent defaults.



If you refresh the page now and check out the dev console, you should see our hello message. You can also change the message and the page will automatically reload.

You can keep the dev server running throughout the tutorial. You will need to restart it when we make changes to webpack.config.js but everything else it will pick up without requiring restarts.

We're going to write our application code primarily in ES2015. For this to work, we'll need to transpile our code with Babel. Let's add it to the project:

npm install babel-core babel-loader babel-preset-es2015 --save-dev

Babel plugs into Webpack, so that when Webpack builds our bundle it also transpiles the code on the fly. We just need to configure our JavaScript code to be processed through the Babel Webpack loader that we just installed:

webpack.config.js
module.exports = {
  entry: ['./app/main.js'],
  module: {
    loaders: [{
      test: /\.js$/,
      exclude: /node_modules/,
      loader: 'babel',
      query: {
        presets: ['es2015']
      }
    }]
  },
  output: {
    path: __dirname,
    filename: 'bundle.js'
  }
};

This tells Webpack that all files with the .js extension - except ones under node_modules - should be processed with Babel. We also configure Babel to use the ES2015 preset, enabling ES2015 language support.

Since Angular 2 uses ES2016 decorators in TypeScript, it would be cool if we could use those in our project as well. Decorators are not part of ES2015 though, so we're going to need to use another Babel plugin for them. The babel-plugin-transform-decorators-legacy project gives us what we need here. While we're at it, let's also install the Babel stage 0 preset that enables some additional experimental features that we can use:

npm install babel-plugin-transform-decorators-legacy --save-dev
npm install babel-preset-stage-0 --save-dev

Both of these also need to be enabled in the Webpack config:

webpack.config.js
loaders: [{
  test: /\.js$/,
  exclude: /node_modules/,
  loader: 'babel',
  query: {
    presets: ['es2015', 'stage-0'],
    plugins: ['transform-decorators-legacy']
  }
}]

Setting Up Angular

We're almost done with the project set up, but we still need to bring Angular 2 into the picture. This begins by installing it and its dependencies to the project:

npm install es6-promise@3.1.2 es6-shim@0.33.13 --save
npm install reflect-metadata@0.1.2 rxjs@5.0.0-beta.2 zone.js@0.5.15 --save
npm install angular2 --save

Angular 2 is in beta and the exact versions of its peer dependencies are changing frequently. You'll find the most up-to-date information on the versions in the official quickstart documentation.

Before Angular will run, it needs the polyfills that we've just installed to be present on the page. Let's create a file that loads them:

js/polyfills.js
import 'es6-shim';
import 'es6-promise';
import 'reflect-metadata';
import 'zone.js/dist/zone-microtask';

The file loads several things that Angular needs:

We can now instruct Webpack to load in the polyfills file before loading our application code:

webpack.config.js
entry: ['./app/polyfills.js', './app/main.js'],

Now, let's get an Angular component rendered on the page. This will be the component that eventually renders our visualization. Let's call it CanvasComponent.

js/canvas.component.js
import {Component} from 'angular2/core';

@Component({
  selector: 'mb-canvas',
  template: '<h1>Ohai!</h1>'
})
export class CanvasComponent {

}

This is a regular ES2015 class, to which we've applied the Angular 2 Component decorator. That decorator makes the class an Angular 2 component. There are two things we've configured for the component:

  1. It should be matched to the mb-canvas CSS element selector. The mb- prefix is short for "metabubbles". It is customary to use such prefixes for Angular 2 components so that the selectors don't conflict with standard HTML.
  2. It has an inline HTML template with an <h1> tag.

We're going to be using the feature.type.js file naming scheme, recommended in John Papa's Angular 2 style guide draft.

One might argue that "canvas" isn't the best name for this component since it may be confused with the HTML5 <canvas>, which we won't be using. But this is art, damn it, and for art you need a canvas.

CanvasComponent will be our application's root component. We can bootstrap our Angular 2 application with it from our application entry point:

app/main.js
import {bootstrap} from 'angular2/platform/browser';
import {CanvasComponent} from './canvas.component';

bootstrap(CanvasComponent);

If you try to load the app now, you'll see an error about the mb-canvas element not being present. Since we asked Angular to bootstrap with this component, it's trying to find an element for it on the page. We need to add that element, which we can do inside the <body> tag:

index.html
<body>
  <mb-canvas></mb-canvas>
  <script src="bundle.js"></script>
</body>

Now you should be able to see the <h1> element on the page. We're running an Angular 2 app!

Drawing SVG Circles

A single <h1> tag doesn't make for a very exciting art project. We want to draw circles, and we're going to do it using SVG. The first thing we should do to support that is to initialize an SVG element in our canvas component:

js/canvas.component.js
import {Component} from 'angular2/core';

@Component({
  selector: 'mb-canvas',
  template: `
    <svg viewBox="0 0 900 500"
         preserveAspectRatio="xMidYMid meet">
    </svg>
  `
})
export class CanvasComponent {

}

We're using the viewBox attribute to set the internal coordinate system of the element to a 900x500 rectangle. We're also using the preserveAspectRatio attribute to make the visualization robust to different screen sizes.

Note that this does not mean that the visualization is physically 900x500 pixels in size. It just sets the internal coordinate system of the SVG, while the actual size will be dynamic and based on the browser window size.

If you want to learn more about how the SVG coordinate system works, Sara Soueidan has a great article about it.

Let's see if we can add some circles to the SVG:

js/canvas.component.js
template: `
  <svg viewBox="0 0 900 500"
       preserveAspectRatio="xMidYMid meet">
    <svg:circle cx="50" cy="50" r="10" />
    <svg:circle cx="75" cy="75" r="20" />
    <svg:circle cx="115" cy="115" r="30" />
  </svg>
`

Here's how this should look like. We've got three circles at different points, with radii of 10, 20, and 30:

The svg: prefix in the elements does not end up on the page at runtime. It's just Angular 2 template syntax that lets it know that it should create these elements using the SVG namespace.

Right now we're hardcoding these circles in the template. Since our visualization will be highly dynamic, we should instead render the circles dynamically, based on JavaScript objects. Let's create a few such objects in our component:

js/canvas.component.js
export class CanvasComponent {
  circles = [
    {x: 50, y: 50, radius: 10},
    {x: 75, y: 75, radius: 20},
    {x: 115, y: 115, radius: 30}
  ];
}

We're initializing the property right in the class body using ES class property syntax, enabled by the Babel stage-0 preset that we installed earlier.

We can render these circles in the template by looping over them using ngFor directive, and binding each circle's attributes using the square bracket binding syntax:

js/canvas.component.js
@Component({
  selector: 'mb-canvas',
  template: `
    <svg viewBox="0 0 900 500"
         preserveAspectRatio="xMidYMid meet">
      <svg:circle *ngFor="#circle of circles"
                  [attr.cx]="circle.x"
                  [attr.cy]="circle.y"
                  [attr.r]="circle.radius" />
    </svg>
  `
})
export class CanvasComponent {
  circles = [
    {x: 50, y: 50, radius: 10},
    {x: 75, y: 75, radius: 20},
    {x: 115, y: 115, radius: 30}
  ];
}

This renders the same circles as before, but now they're driven by JavaScript objects instead of being hardcoded into the template.

The attr. prefix in the bindings lets Angular know it should make the binding for an attribute instead of a property, which is what it usually does. (You can read more about the difference here.) This is something we need to do for SVG attributes, since the DOM does not expose property setters for them.

Introducing The Circle Component

Our application currently consists of just one component - the CanvasComponent. Since our circles are going to have a bit of logic of their own, it would make sense to extract them into their own component so that the canvas code doesn't get too large. Let's create such a component:

js/circle.component.js
import {Component} from 'angular2/core';

@Component({
  selector: '[mb-circle]',
  inputs: ['circle'],
  template: `
    <svg:circle [attr.cx]="circle.x"
                [attr.cy]="circle.y"
                [attr.r]="circle.radius" />
  `
})
export class CircleComponent {
}

This component is pretty similar to what we did before with CanvasComponent. There are a few interesting things in it though:

In our canvas, we'll now use the new CircleComponent instead of rendering the circles directly:

js/canvas.component.js
import {Component} from 'angular2/core';
import {CircleComponent} from './circle.component';

@Component({
  selector: 'mb-canvas',
  template: `
    <svg viewBox="0 0 900 500"
         preserveAspectRatio="xMidYMid meet">
      <svg:g mb-circle
             *ngFor="#circle of circles"
             [circle]="circle" />
    </svg>
  `,
  directives: [CircleComponent]
})
export class CanvasComponent {
  // ...
}

We're still looping over the circle data, but this time with the CircleComponent. Notice that we also need to mention the component in a directives attribute, so that Angular knows to make it available in the template.

Here we see why we needed to use an attribute selector: If we had an element selector like 'mb-circle', an <mb-circle> element would be attached to the DOM. SVG does not play well with custom elements (nor does it support Shadow DOM), so we need to only use elements that are in the SVG standard. Here we're using a <g> element.

Styling The Components with CSS

Our components could use some visual improvements. That's something we can do with CSS.

While we could just stick a stylesheet to the page and be done with it, what we'll do instead is use Angular's component stylesheet support combined with Webpack's CSS loader. Together these will let us define component-local styles.

Let's install the CSS loader first:

npm install css-loader --save-dev

Then we'll create a stylesheet for the canvas component:

js/canvas.component.css
svg {
  position: fixed;
  top: 0;
  left: 0;
  bottom: 0;
  right: 0;

  background-color: #011;
}

This makes the SVG element take up the whole browser viewport and gives it an off-black background color.

We can then load the stylesheet into the component using require. We'll load it through the CSS loader, and then turn it into a string. The net result is that Angular sees a string of the CSS code that we put in canvas.component.css:

js/canvas.component.js
@Component({
  selector: 'mb-canvas',
  template: `
    <svg viewBox="0 0 900 500"
         preserveAspectRatio="xMidYMid meet">
      <svg:g mb-circle
             *ngFor="#circle of circles"
             [circle]="circle" />
    </svg>
  `,
  styles: [require('css!./canvas.component.css').toString()],
  directives: [CircleComponent]
})
export class CanvasComponent {
  // ...
}

While we're at it, let's repeat the same trick with the circle component. For now, let's just make the circles white and translucent. We'll be returning to their visual layout later:

js/circle.component.css
circle {
  fill: rgba(255, 255, 255, 0.5);
}

In the component we again require the CSS file and attach it to the component's styles:

js/circle.component.js
@Component({
  selector: '[mb-circle]',
  inputs: ['circle'],
  template: `
    <svg:circle [attr.cx]="circle.x"
                [attr.cy]="circle.y"
                [attr.r]="circle.radius" />
  `,
  styles: [require('css!./circle.component.css').toString()]
})
export class CircleComponent {
}

Here's roughly what we end up with:

If you open up your brower's dev tools and inspect the styles that actually get generated, you'll see that they're not exactly what we wrote in the CSS files.

What's happening here is that Angular is generating unique selectors for the styles so that they are encapsulated to the component element. This is a neat feature, since it means we don't have to come up with globally unique selectors in component styles. We can just use whatever makes sense for the component in isolation.

Extracting A Circles Service

We're about to start making much more interesting circles, but first let's do a little bit of maintenance work that'll make our life easier in the long run: Instead of creating the circle objects locally in the UI components, let's do it in a service, where we will also later add the logic of moving them around:

js/circles.service.js
import {Injectable} from 'angular2/core';

@Injectable()
export class Circles {
  circles = [
    {x: 50, y: 50, radius: 10},
    {x: 75, y: 75, radius: 20},
    {x: 115, y: 115, radius: 30}
  ];
}

This is another plain ES2015 class. It has one property called circles. We decorate it with Angular's @Injectable decorator, which enables Angular dependency injection support for it.

The decoration isn't strictly necessary since the class has no dependencies, but it's a good habit to do it in any case. It also clarifies to whoever's reading the code that this is an Angular service.

In CanvasComponent we'll now inject this service and get the circles from it:

js/circle.component.js
import {Component} from 'angular2/core';
import {CircleComponent} from './circle.component';
import {Circles} from './circles.service';

@Component({
  selector: 'mb-canvas',
  template: `
    <svg viewBox="0 0 900 500"
         preserveAspectRatio="xMidYMid meet">
      <svg:g mb-circle
             *ngFor="#circle of circles.circles"
             [circle]="circle" />
    </svg>
  `,
  styles: [require('css!./canvas.component.css').toString()],
  directives: [CircleComponent],
  providers: [Circles]
})
export class CanvasComponent {

  static parameters = [Circles];
  constructor(circles) {
    this.circles = circles;
  }

}

We actually have several references to the service here, each with a different purpose:

What we've gained with this is separation of concerns: In UI components we merely render the circles. The work that goes into defining those circles is elsewhere - in a service.

Generating Random Circles

Let's get into the "generative" part of generative art. Right now we have three hard-coded circles on our canvas. What would be a lot more interesting is a number of randomly generated circles. Let's make that happen.

There are three random things we need to generate for our circles: Their x coordinate, their y coordinate, and their radius. These are all integer numbers. Unfortunately, JavaScript lacks a built-in function for making random integers so we'll need to introduce one. We can put it in the Circles service and define it in terms of Math.random().

js/circles.service.js
import {Injectable} from 'angular2/core';

@Injectable()
export class Circles {
  circles = [
    {x: 50, y: 50, radius: 10},
    {x: 75, y: 75, radius: 20},
    {x: 115, y: 115, radius: 30}
  ];

  randInt(max) {
    return Math.floor(Math.random() * max);
  }

}

The method returns a random integer between zero and max. Let's use that function now to generate 100 random circles:

js/circles.service.js
import {Injectable} from 'angular2/core';

@Injectable()
export class Circles {

  constructor() {
    this.circles = [];
    for (let i=0 ; i<100 ; i++) {
      this.circles.push({
        x: this.randInt(900), // 0..900
        y: this.randInt(500), // 0..500
        radius: this.randInt(100) + 10 // 10..110
      });
    }
  }

  randInt(max) {
    return Math.floor(Math.random() * max);
  }

}

Things just got a lot more interesting:

There's an unfortunate bit of duplication in our code at the moment: We've defined our canvas size to be 900x500 and we now have that information in two separate places: In CanvasComponent where we set up the SVG viewport, and in Circles where we generate the random coordinates.

Let's eliminate this duplication by plugging values for the canvas width and height to Angular's root dependency injector. We can do this when we bootstrap the application:

app/main.js
import {provide} from 'angular2/core';
import {bootstrap} from 'angular2/platform/browser';
import {CanvasComponent} from './canvas.component';

bootstrap(CanvasComponent, [
  provide('canvasWidth', {useValue: 900}),
  provide('canvasHeight', {useValue: 500})
]);

We can now inject these values to Circles:

js/circles.service.js
import {Injectable} from 'angular2/core';

@Injectable()
export class Circles {

  static parameters = ['canvasWidth', 'canvasHeight'];
  constructor(canvasWidth, canvasHeight) {
    this.circles = [];
    for (let i=0 ; i<100 ; i++) {
      this.circles.push({
        x: this.randInt(canvasWidth),
        y: this.randInt(canvasHeight),
        radius: this.randInt(100) + 10
      });
    }
  }

  randInt(max) {
    return Math.floor(Math.random() * max);
  }

}

And we can do the same in CanvasComponent, where we then create the SVG view box dynamically using a component method:

js/canvas.component.js
@Component({
  selector: 'mb-canvas',
  template: `
    <svg [attr.viewBox]="getViewBox()"
         preserveAspectRatio="xMidYMid meet">
      <svg:g mb-circle
             *ngFor="#circle of circles"
             [circle]="circle" />
    </svg>
  `,
  styles: [require('css!./canvas.component.css').toString()],
  directives: [CircleComponent],
  providers: [Circles]
})
export class CanvasComponent {

  static parameters = [Circles, 'canvasWidth', 'canvasHeight'];
  constructor(circles, canvasWidth, canvasHeight) {
    this.circles = circles.circles;
    this.width = canvasWidth;
    this.height = canvasHeight;
  }

  getViewBox() {
    return `0 0 ${this.width} ${this.height}`;
  }

}

Circle Movement Logic

Static images like the one we have now are kind of cool, but what we really want to do is make things move. Let's do that next.

We'll be creating a procedural animation, which basically means that instead of using things like CSS transitions or ngAnimate, we'll write code that actually calculates the positions and sizes of our objects on every frame. To that effect, our Circles service will have a method called update(), which will be called on every frame, and which should calculate and set new position and radius of each circle:

js/circles.service.js
@Injectable()
export class Circles {

  static parameters = ['canvasWidth', 'canvasHeight'];
  constructor(canvasWidth, canvasHeight) {
    this.circles = [];
    for (let i=0 ; i<100 ; i++) {
      this.circles.push({
        x: this.randInt(canvasWidth),
        y: this.randInt(canvasHeight),
        radius: this.randInt(100) + 10
      });
    }
  }

  update() {

  }

  randInt(max) {
    return Math.floor(Math.random() * max);
  }

}

So, we want our circles to move around. Let's give each of them a random speed, expressed in terms of the number of x and y coordinates they should move on each frame:

js/circles.service.js
constructor(canvasWidth, canvasHeight) {
  this.circles = [];
  for (let i=0 ; i<100 ; i++) {
    this.circles.push({
      x: this.randInt(canvasWidth),
      y: this.randInt(canvasHeight),
      radius: this.randInt(100) + 10,
      xMove: this.randInt(5) - 2, // -2..2
      yMove: this.randInt(5) - 2  // -2..2
    });
  }
}

In update() we'll then move each circle based on its speed:

js/circles.service.js
update() {
  for (const circle of this.circles) {
    circle.x += circle.xMove;
    circle.y += circle.yMove;
  }
}

Animating Circle Movement

Our circles now have the potential to move, but they don't actually move yet. That's because nothing is calling update(). We want that method to be called on each frame of the animation. Our preferred tool for that purpose is the window.requestAnimationFrame API.

We are going to drive the animation from the CanvasComponent component, so that when that component mounts the animation is started, and when it is destroyed the animation stops. We can use component lifecycle hooks to react to those two events:

js/canvas.component.js
export class CanvasComponent {

  static parameters = [Circles, 'canvasWidth', 'canvasHeight'];
  constructor(circles, canvasWidth, canvasHeight) {
    this.circles = circles;
    this.width = canvasWidth;
    this.height = canvasHeight;
  }

  ngOnInit() {
    this.running = true;
  }

  ngOnDestroy() {
    this.running = false;
  }

  getViewBox() {
    return `0 0 ${this.width} ${this.height}`;
  }

}

In practice the CanvasComponent is never actually destroyed in our application, but it's a good habit to write code that assumes some day it will be.

From ngOnInit we can then also call a method that will keep invoking circles.update() on every animation frame until the component is destroyed:

js/canvas.component.js
ngOnInit() {
  this.running = true;
  this.animationFrame();
}

ngOnDestroy() {
  this.running = false;
}

animationFrame() {
  this.circles.update();
  if (this.running) {
    requestAnimationFrame(() => this.animationFrame());
  }
}

We can also let the user start and stop the animation by clicking on it. It can be done by attaching a click event handler on the <svg> element. The handler toggles the running flag and starts the animation loop if required:

js/canvas.component.js
@Component({
  selector: 'mb-canvas',
  template: `
    <svg [attr.viewBox]="getViewBox()"
         preserveAspectRatio="xMidYMid meet"
         (click)="toggleRunning()">
      <svg:g mb-circle
             *ngFor="#circle of circles"
             [circle]="circle" />
    </svg>
  `,
  styles: [require('css!./canvas.component.css').toString()],
  directives: [CircleComponent],
  providers: [Circles]
})
export class CanvasComponent {

  static parameters = [Circles, 'canvasWidth', 'canvasHeight'];
  constructor(circles, canvasWidth, canvasHeight) {
    this.circles = circles;
    this.width = canvasWidth;
    this.height = canvasHeight;
  }

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

  ngOnDestroy() {
    this.running = false;
  }

  toggleRunning() {
    this.running = !this.running;
    if (this.running) {
      this.animationFrame();
    }
  }

  animationFrame() {
    this.circles.update();
    if (this.running) {
      requestAnimationFrame(() => this.animationFrame());
    }
  }

I originally added this feature just so you can spare your laptop battery while you're reading this article. But turns out sometimes you also just get appealing circle combinations that you want to pause and look at!

Coordinate Wrap-around

Looking at the animation right now, it's a bit sad. You see the circles move but pretty soon they disappear from view, forever moving in their predestined direction in the coordinate space.

We could keep this as a visual metaphor for the ultimate pointlessness of existence and call it a day, but let's not. Let's help our circles out instead, by having them wrap around the edges of the visualization.

The trick is to detect when a circle is fully outside of the 900x500 view box, and when that occurs, to move it to the opposite edge of the box. This is done separately for both x and y:

js/circles.service.js
@Injectable()
export class Circles {

  static parameters = ['canvasWidth', 'canvasHeight'];
  constructor(canvasWidth, canvasHeight) {
    this.canvasWidth = canvasWidth;
    this.canvasHeight = canvasHeight;
    this.circles = [];
    for (let i=0 ; i<100 ; i++) {
      this.circles.push({
        x: this.randInt(canvasWidth),
        y: this.randInt(canvasHeight),
        radius: this.randInt(100) + 10,
        xMove: this.randInt(5) - 2,
        yMove: this.randInt(5) - 2
      });
    }
  }

  update() {
    for (const circle of this.circles) {
      this.moveCircle(circle);
    }
  }

  moveCircle(circle) {
    circle.x += circle.xMove;
    circle.y += circle.yMove;
    if (circle.x > (this.canvasWidth + circle.radius)) {
      circle.x = 0 - circle.radius;
    }
    if (circle.x < (0 - circle.radius)) {
      circle.x = this.canvasWidth + circle.radius;
    }
    if (circle.y > (this.canvasHeight + circle.radius)) {
      circle.y = 0 - circle.radius;
    }
    if (circle.y < (0 - circle.radius)) {
      circle.y = this.canvasHeight + circle.radius;
    }
  }

  randInt(max) {
    return Math.floor(Math.random() * max);
  }

}

Now, when a circle drifts off the screen it pops back in on the other side.

Going Meta: Collision Circles

If you go back to the top of this page and look at the visualization we're supposed to be building, you'll notice it looks quite a bit different from what we have right now. The circles change in size and they come and go in interesting clusters. What are we missing?

The key to this is to start looking into collisions between our circles. We need to detect when any two circles overlap. We're going to need to do this on each frame, so let's first make that a bit easier by pregenerating an array of all possible pairs of circles. We can do this in the Circles constructor:

js/circles.service.js
static parameters = ['canvasWidth', 'canvasHeight'];
constructor(canvasWidth, canvasHeight) {
  this.canvasWidth = canvasWidth;
  this.canvasHeight = canvasHeight;
  this.circles = [];
  for (let i=0 ; i<100 ; i++) {
    this.circles.push({
      x: this.randInt(canvasWidth),
      y: this.randInt(canvasHeight),
      radius: this.randInt(100) + 10,
      xMove: this.randInt(5) - 2,
      yMove: this.randInt(5) - 2
    });
  }
  this.pairs = [];
  for (let i = 0 ; i < this.circles.length - 1 ; i++) {
    for (let j = i ; j < this.circles.length - 1 ; j++) {
      this.pairs.push([this.circles[i], this.circles[j + 1]]);
    }
  }
}

After the constructor has run, pairs will be an array of arrays. For our 100 circles, it will have 4950 circle pairs in it.

4950 is quite a big number for something we need to be iterating on every frame. Collision detection is in fact one of the bigger performance bottlenecks of this project.

A more sophisticated implementation might use some kind of spatial index or other optimization tricks to do collision detection with less effort.

We then need a method that calculates the distance between the centerpoints of two circles. This is an application of the Pythagorean theorem, which you might remember from school:

js/circles.service.js
distance(circle1, circle2) {
  return Math.sqrt(
    (circle2.x - circle1.x) ** 2 +
    (circle2.y - circle1.y) ** 2
  );
}

The ** here is the ES7 exponentiation operator in action.

Now we can also do collision detection on every frame. For each circle pair, we calculate the distance between their centerpoints. We then subtract from that distance the radius of each circle. If the resulting number is negative, the circles overlap:

js/circles.service.js
update() {
  for (const circle of this.circles) {
    this.moveCircle(circle);
  }
  for (const [left, right] of this.pairs) {
    const dist = this.distance(left, right);
    const overlap = dist - left.radius - right.radius;
    if (overlap < 0) {
      // Overlap!
    }
  }
}

But what do we do with this information? This is the most important trick that makes our little art project look the way it does, and also where the name of the project comes from: What if we created a third circle based on the collision of two circles? Its centerpoint could be the midpoint between the two circles, and its radius based on the amount that they currently overlap:

js/circles.service.js
for (const [left, right] of this.pairs) {
  const dist = this.distance(left, right);
  const overlap = dist - left.radius - right.radius;
  if (overlap < 0) {
    // midpoint = average of the two coordinates
    const midX = (left.x + right.x) / 2;
    const midY = (left.y + right.y) / 2;
    const collisionCircle = {x: midX, y: midY, radius: -overlap / 2};
  }
}

Then what if these are the circles that we actually show? We can make the original circles an internal implementation detail of the Circles service, and actually just expose the collision circles through the circles collection that we're visualizing in our UI. If we do that, the visualization changes radically.

js/circles.service.js
@Injectable()
export class Circles {

  static parameters = ['canvasWidth', 'canvasHeight'];
  constructor(canvasWidth, canvasHeight) {
    this.canvasWidth = canvasWidth;
    this.canvasHeight = canvasHeight;
    this.sourceCircles = [];
    for (let i=0 ; i<100 ; i++) {
      this.sourceCircles.push({
        x: this.randInt(canvasWidth),
        y: this.randInt(canvasHeight),
        radius: this.randInt(100) + 10,
        xMove: this.randInt(5) - 2,
        yMove: this.randInt(5) - 2
      });
    }
    this.pairs = [];
    for (let i = 0 ; i < this.sourceCircles.length - 1 ; i++) {
      for (let j = i ; j < this.sourceCircles.length - 1 ; j++) {
        this.pairs.push([this.sourceCircles[i], this.sourceCircles[j + 1]]);
      }
    }
  }

  update() {
    for (const sourceCircle of this.sourceCircles) {
      this.moveCircle(sourceCircle);
    }
    this.circles = [];
    for (const [left, right] of this.pairs) {
      const dist = this.distance(left, right);
      const overlap = dist - left.radius - right.radius;
      if (overlap < 0) {
        const midX = (left.x + right.x) / 2;
        const midY = (left.y + right.y) / 2;
        const collisionCircle = {x: midX, y: midY, radius: -overlap / 2};
        this.circles.push(collisionCircle);
      }
    }
  }

  // ...

}

Now we're talking!

Caching The Collision Circles

We're now very close to the visualization we want, but in the previous step we've also made it slow as molasses. We are nowhere near 60fps. (At least not on my 2015 Macbook Pro, that is. Your mileage may vary.)

The biggest problem is relatively clear to see: For each frame, we're reinitializing the circles array with newly generated collision circles. This is wasteful not only because of the circle objects themselves, but because it leads Angular to destroy and regenerate the SVG circle elements on each frame. Because we're creating new objects for each frame, the ngFor directive we have in CanvasComponent has no way of knowing some of them are actually logically the same ones as before, and should just be moved around.

We can fix this by keeping the same collision circle objects around from one frame to the next and only updating their coordinates x, y, and radius. This means we need to check whether we already have a collision circle for a given pair before we create one. Let's introduce a Map into which we put the collision circles so that we can do this efficiently. The keys in the maps are the source circle pairs, and the values are the collision circles:

js/circles.service.js
@Injectable()
export class Circles {
  this.circleMap = new Map();

  // ...

  update() {
    for (const sourceCircle of this.sourceCircles) {
      this.moveCircle(sourceCircle);
    }
    this.circles = [];
    for (const pair of this.pairs) {
      const [left, right] = pair;
      const dist = this.distance(left, right);
      const overlap = dist - left.radius - right.radius;
      if (overlap < 0) {
        const midX = (left.x + right.x) / 2;
        const midY = (left.y + right.y) / 2;
        const collisionCircle = {x: midX, y: midY, radius: -overlap / 2};
        this.circles.push(collisionCircle);
        this.circleMap.set(pair, collisionCircle);
      }
    }
  }

  // ...

}

We're still also keeping the circles array around, so that we can iterate over the circles efficiently with the ngFor. Both the map and the array hold references to the same collision circle objects.

Now, using this Map, we can check if we already have a collision circle for a given pair. If we do, we just update its attributes. Then we don't need to regenerate the whole circles array on each call to update().

js/circles.service.js
@Injectable()
export class Circles {
  circleMap = new Map();
  circles = [];

  // ...

  update() {
    for (const sourceCircle of this.sourceCircles) {
      this.moveCircle(sourceCircle);
    }
    for (const pair of this.pairs) {
      const [left, right] = pair;
      const dist = this.distance(left, right);
      const overlap = dist - left.radius - right.radius;
      if (overlap < 0) {
        const midX = (left.x + right.x) / 2;
        const midY = (left.y + right.y) / 2;
        const radius = -overlap / 2;
        let collisionCircle = this.circleMap.get(pair);
        if (collisionCircle) {
          collisionCircle.x = midX;
          collisionCircle.y = midY;
          collisionCircle.radius = radius;
        } else {
          collisionCircle = {x: midX, y: midY, radius};
          this.circles.push(collisionCircle);
          this.circleMap.set(pair, collisionCircle);
        }
      }
    }
  }

  // ...
}

That's much better. What happens with this approach though is that circles that are once displayed never go away, because we never remove collision circles. This causes an interesting effect, where lots of tiny collision circles are left in the UI:

If you like the effect, you can leave it there. I'd rather not have it though, so what we'll do is add a new visible attribute to the collision circles, which is set to true when there actually is a collision/overlap at the moment, and false otherwise:

js/circles.service.js
for (const pair of this.pairs) {
  const [left, right] = pair;
  const dist = this.distance(left, right);
  const overlap = dist - left.radius - right.radius;
  if (overlap < 0) {
    const midX = (left.x + right.x) / 2;
    const midY = (left.y + right.y) / 2;
    const radius = -overlap / 2;
    let collisionCircle = this.circleMap.get(pair);
    if (collisionCircle) {
      collisionCircle.x = midX;
      collisionCircle.y = midY;
      collisionCircle.radius = radius;
    } else {
      collisionCircle = {x: midX, y: midY, radius};
      this.circles.push(collisionCircle);
      this.circleMap.set(pair, collisionCircle);
    }
    collisionCircle.visible = true;
  } else if (this.circleMap.has(pair)) {
    this.circleMap.get(pair).visible = false;
  }
}

This attribute can be used in CircleComponent to make the circle invisible when visible is currently false. We can just bind a value to the style property:

js/circle.component.js
@Component({
  selector: '[mb-circle]',
  inputs: ['circle'],
  template: `
    <svg:circle [attr.cx]="circle.x"
                [attr.cy]="circle.y"
                [attr.r]="circle.radius"
                [style]="getStyle()" />
  `,
  styles: [require('css!./circle.component.css').toString()]
})
export class CircleComponent {

  getStyle() {
    return this.circle.visible ?
      '' :
      'display: none';
  }

}

Changing Colors

We're almost done with our visualization. There's just one thing missing from it, which is circle color generation.

What we would like is a situation where circles are gradually changing colors based on when they become visible. There will be many different colors displayed at any given time, but they will generally tend to move toward similar values, causing a pleasingly harmonious effect.

In order to vary colors over time, the Circles service is going to need to know "what time it is". In the little universe in which this visualization lives, we can model time as the amount of update cycles that have been made so far. This information can be maintained by the Circles service as an incrementing counter:

js/circles.service.js
@Injectable()
export class Circles {
  circleMap = new Map();
  circles = [];
  timeStep = 0;

  // ...

  update() {
    this.timeStep++;
    // ...
  }

  // ...

}

Based on the time step we can then generate colors for our circles. Note that in the CSS rgba syntax colors are defined in terms of four values:

It might look interesting if we varied the red, green, and blue values over time by taking the remainder of the current timestep when divided by 256. This would make the color values vary between 0 and 255, cycling every 256 frames. We can keep the alpha value a constant 0.5:

js/circles.service.js
update() {
  this.timeStep++;
  for (const sourceCircle of this.sourceCircles) {
    this.moveCircle(sourceCircle);
  }
  for (const pair of this.pairs) {
    const [left, right] = pair;
    const dist = this.distance(left, right);
    const overlap = dist - left.radius - right.radius;
    if (overlap < 0) {
      const midX = (left.x + right.x) / 2;
      const midY = (left.y + right.y) / 2;
      const radius = -overlap / 2;
      let collisionCircle = this.circleMap.get(pair);
      if (collisionCircle) {
        collisionCircle.x = midX;
        collisionCircle.y = midY;
        collisionCircle.radius = radius;
      } else {
        collisionCircle = {x: midX, y: midY, radius};
        this.circles.push(collisionCircle);
        this.circleMap.set(pair, collisionCircle);
      }
      if (!collisionCircle.visible) {
        collisionCircle.visible = true;
        const red = this.timeStep % 256;
        const green = this.timeStep % 256;
        const blue = this.timeStep % 256;
        collisionCircle.color = `rgba(${red}, ${green}, ${blue}, 0.5)`;
      }
    } else if (this.circleMap.has(pair)) {
      this.circleMap.get(pair).visible = false;
    }
  }
}

We can see how this looks like by first removing the fill style from circle.component.css and then setting the fill attribute in the component instead:

js/circle.component.js
@Component({
  selector: '[mb-circle]',
  inputs: ['circle'],
  template: `
    <svg:circle [attr.cx]="circle.x"
                [attr.cy]="circle.y"
                [attr.r]="circle.radius"
                [attr.fill]="circle.color"
                [style]="getStyle()" />
  `,
  styles: [require('css!./circle.component.css').toString()]
})
export class CircleComponent {

  getStyle() {
    return this.circle.visible ?
      '' :
      'display: none';
  }
}

It does look interesting, but it's not quite the technicolor extravaganza that we're after:

Since we set each of the three color components to the same value every time, what we get is different shades of gray.

We can get different colors instead if we vary the three color components in different phases. If green is 85 frames ahead of red, and blue is another 85 frames ahead of green, they'll all reach their peaks at different times:

js/circles.service.js
if (!collisionCircle.visible) {
  collisionCircle.visible = true;
  const red = this.timeStep % 256;
  const green = (this.timeStep + 85) % 256;
  const blue = (this.timeStep + 85 + 85) % 256;
  collisionCircle.color = `rgba(${red}, ${green}, ${blue}, 0.5)`;
}

There we go!

Tweaking Change Detection Performance

Our little generative art piece is feature complete, but it is still a little bit slow. While there are probably many ways we could make it faster, there's some low-hanging fruit that we should pay particular attention to.

At least on my Chrome browser, when I run the application with a profiler, the the biggest bottleneck ends up being Angular's change detection:

This kind of makes sense: There's a lot of stuff to check and we're doing it very rapidly, on every animation frame. Certainly this is applying much more pressure on the change detection system than most typical business applications would.

One trick we can apply is to try different change detection strategies. What's currently happening is that Angular is re-testing the values of all our property bindings on every frame. This is the default strategy.

We could try changing this to the OnPush strategy for CircleComponent. What this means is that instead of re-checking all the property bindings inside CircleComponent, Angular will only check if any of the inputs of the component have changed:

js/circle.component.js
import {Component, ChangeDetectionStrategy} from 'angular2/core';

@Component({
  selector: '[mb-circle]',
  inputs: ['circle'],
  template: `
    <svg:circle [attr.cx]="circle.x"
                [attr.cy]="circle.y"
                [attr.r]="circle.radius"
                [attr.fill]="circle.color"
                [style]="getStyle()" />
  `,
  styles: [require('css!./circle.component.css').toString()],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class CircleComponent {

  getStyle() {
    return this.circle.visible ?
      '' :
      'display: none';
  }
}

If we do this, our circles stop moving! Why does that happen?

The key to understanding this is to think about the "inputs" of CircleComponent. It has one input, which is a circle object. Since we're now caching these objects, they do in fact not change after they've first been constructed. The attributes inside the circles - x, y, radius, visible, and color - do change, but the OnPush strategy only does a shallow reference check of the circle object. This makes it fast, but it also misses deep changes to object attributes, the symptoms of which we are witnessing right now.

If you're into React, this may sound familiar. It is the exact same problem you get when you combine PureRenderMixin with mutable data.

We can still use the OnPush strategy if we pick apart the circle object in CanvasComponent and give CircleComponent each attribute as a separate binding:

js/canvas.component.js
@Component({
  selector: 'mb-canvas',
  template: `
    <svg [attr.viewBox]="getViewBox()"
         preserveAspectRatio="xMidYMid meet"
         (click)="toggleRunning()">
      <svg:g mb-circle
             *ngFor="#circle of circles.circles"
             [x]="circle.x"
             [y]="circle.y"
             [radius]="circle.radius"
             [visible]="circle.visible"
             [color]="circle.color" />
    </svg>
  `,
  styles: [require('css!./canvas.component.css').toString()],
  directives: [CircleComponent],
  providers: [Circles]
})
export class CanvasComponent {
  // ...
}
js/circle.component.js
@Component({
  selector: '[mb-circle]',
  inputs: ['x', 'y', 'radius', 'visible', 'color'],
  template: `
    <svg:circle [attr.cx]="x"
                [attr.cy]="y"
                [attr.r]="radius"
                [attr.fill]="color"
                [style]="getStyle()" />
  `,
  styles: [require('css!./circle.component.css').toString()],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class CircleComponent {

  getStyle() {
    return this.visible ?
      '' :
      'display: none';
  }
}

These separate inputs will be detected by the OnPush strategy, and using it does give us a bit of a performance boost.

If you'd like to learn more about how change detection works in Angular 2, Pascal's got you covered.

Building for Production

There's one more effective and easy trick we can do to boost performance. There's a hint about it in the console output of the application:

One of the things that happens when Angular runs in development mode is that there's a lot more change detection going on. Angular is being helpful and making sure we don't have side effects in our data binding expressions, and it is doing it by running change detection twice on every turn.

This obviously adds some overhead, and it is why it should be disabled when the app is built for production. Let's add a little JavaScript file that turns on production mode:

js/prod.js
import {enableProdMode} from 'angular2/core';

enableProdMode();

Let's then add another Webpack configuration file, this one meant for production use. It has one big difference when compared with the development config file, which is that it includes prod.js in the application entry points, causing production mode to be enabled in the bundle:

webpack.prod.config.js
module.exports = {
  entry: ['./app/polyfills.js', './app/prod.js', './app/main.js'],
  module: {
    loaders: [{
      test: /\.js$/,
      exclude: /node_modules/,
      loader: 'babel',
      query: {
        presets: ['es2015', 'stage-0'],
        plugins: ['transform-decorators-legacy']
      }
    }]
  },
  output: {
    path: __dirname,
    filename: 'bundle.js'
  }
};

We can reference this config file in our run script for building the project:

package.json
"scripts": {
  "build": "webpack --config webpack.prod.config.js",
  "start": "webpack-dev-server --inline"
},

When we now build the project with npm run build, it'll do it in prod mode, resulting in a faster application.

There are other things one might do when building the app for production, the most important being uglification/minification of the code. If you do do that for an Angular 2 app right now, be aware that there's an open bug you'll need to work around.

comments powered by Disqus