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-jsCore Lifecycle Strategy
Integrating a canvas-based library like Matter.js into Angular requires managing DOM availability and browser animation frames:
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.NgZone.runOutsideAngular: Matter.js usesrequestAnimationFrameto 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.ngOnDestroy: When the component unmounts, the Matter.jsRenderandRunnerinstances 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
- Handle Window Resizing: If the canvas needs to be
responsive, listen to the window resize event using
@HostListener('window:resize')and update therender.boundsandrender.canvas.width/heightattributes accordingly. - Component Encapsulation: Always clean up generated
DOM elements. Calling
this.render.canvas.remove()inngOnDestroyensures no orphaned<canvas>elements persist if the route changes or the component is conditionally destroyed via*ngIf.