Integrate Matter.js Physics in Angular Lifecycle

This article provides a concise guide on integrating the Matter.js 2D physics engine within an Angular component. You will learn which lifecycle hooks to target for initializing the physics world, how to bind the simulation to a template reference using ngAfterViewInit, how to optimize performance using NgZone, and how to properly clean up physics loops in ngOnDestroy to prevent memory leaks.

Installation

Install Matter.js and its TypeScript definitions into your Angular project:

npm install matter-js
npm install --save-dev @types/matter-js

Core Lifecycle Strategy

Integrating a canvas-based library like Matter.js into Angular requires managing DOM availability and browser animation frames:

  1. ngAfterViewInit: The simulation must initialize here. Matter.js needs direct access to a rendered HTML element (usually a container <div> or a <canvas>), which is only guaranteed to exist after the view initializes.
  2. NgZone.runOutsideAngular: Matter.js uses requestAnimationFrame to update its engine and renderer. Running this loop inside Angular's default zone triggers continuous Change Detection across the application, degrading performance. Running the runner outside the zone ensures high frame rates.
  3. ngOnDestroy: When the component unmounts, the Matter.js Render and Runner instances must be explicitly stopped, and the engine cleared to free memory.

Implementation

1. Template Setup

Define a container element using a template reference variable in your component template:

<div #matterContainer class="physics-container"></div>

2. Component Logic

Handle the initialization, performance isolation, and disposal inside the component class:

import {
  Component,
  ElementRef,
  ViewChild,
  AfterViewInit,
  OnDestroy,
  NgZone
} from '@angular/core';
import * as Matter from 'matter-js';

@Component({
  selector: 'app-physics-world',
  templateUrl: './physics-world.component.html',
  styleUrls: ['./physics-world.component.css']
})
export class PhysicsWorldComponent implements AfterViewInit, OnDestroy {
  @ViewChild('matterContainer', { static: true })
  matterContainer!: ElementRef<HTMLDivElement>;

  private engine!: Matter.Engine;
  private render!: Matter.Render;
  private runner!: Matter.Runner;

  constructor(private ngZone: NgZone) {}

  ngAfterViewInit(): void {
    this.initPhysics();
  }

  private initPhysics(): void {
    const { Engine, Render, Runner, Bodies, Composite } = Matter;

    // 1. Create the engine
    this.engine = Engine.create();

    // 2. Create the renderer attached to the ViewChild container
    this.render = Render.create({
      element: this.matterContainer.nativeElement,
      engine: this.engine,
      options: {
        width: 800,
        height: 600,
        wireframes: false
      }
    });

    // 3. Add bodies to the world
    const boxA = Bodies.rectangle(400, 200, 80, 80);
    const ballA = Bodies.circle(380, 50, 40, { restitution: 0.9 });
    const ground = Bodies.rectangle(400, 590, 810, 60, { isStatic: true });

    Composite.add(this.engine.world, [boxA, ballA, ground]);

    // 4. Run renderer and runner outside Angular's zone
    this.ngZone.runOutsideAngular(() => {
      Render.run(this.render);
      this.runner = Runner.create();
      Runner.run(this.runner, this.engine);
    });
  }

  ngOnDestroy(): void {
    // Stop the simulation loop
    if (this.runner) {
      Matter.Runner.stop(this.runner);
    }

    // Stop the renderer and clear the canvas
    if (this.render) {
      Matter.Render.stop(this.render);
      this.render.canvas.remove();
      this.render.textures = {};
    }

    // Clear world bodies and engine state
    if (this.engine) {
      Matter.World.clear(this.engine.world, false);
      Matter.Engine.clear(this.engine);
    }
  }
}

Best Practices