Integrate Matter.js in Vue 3 Composition API

Integrating Matter.js lifecycle events into Vue 3 using the Composition API allows you to build high-performance 2D physics simulations synchronized with Vue's reactive state. This guide demonstrates how to instantiate the Matter.js engine, bind simulation events (such as beforeUpdate and collision triggers) to Vue lifecycle hooks, and properly clean up resources to prevent memory leaks.

Setup and Initialization in onMounted

Matter.js requires a direct reference to a DOM element to mount its canvas renderer. In the Composition API, use a ref combined with the onMounted hook to guarantee the DOM node is ready before starting the physics simulation.

import { ref, onMounted, onUnmounted } from 'vue';
import Matter from 'matter-js';

const canvasContainer = ref(null);

const { Engine, Render, Runner, Bodies, Composite, Events } = Matter;

let engine;
let render;
let runner;

Inside onMounted, initialize the primary modules:

  1. Engine: Manages the physics world and state updates.
  2. Render: Draws the simulation to a canvas inside your referenced container.
  3. Runner: Orchestrates the game loop and frame updates.
onMounted(() => {
  engine = Engine.create();

  render = Render.create({
    element: canvasContainer.value,
    engine: engine,
    options: {
      width: 800,
      height: 600,
      wireframes: false
    }
  });

  Render.run(render);
  runner = Runner.create();
  Runner.run(runner, engine);

  // Add bodies
  const box = Bodies.rectangle(400, 200, 80, 80);
  const ground = Bodies.rectangle(400, 580, 810, 40, { isStatic: true });
  Composite.add(engine.world, [box, ground]);
});

Listening to Matter.js Lifecycle Events

Matter.js provides the Events module to register callbacks for its internal lifecycle. The most common events include:

To map physics state to Vue reactivity, register event listeners inside onMounted:

const boxPosition = ref({ x: 0, y: 0 });

const handleAfterUpdate = () => {
  // Syncing a specific physics body's position to reactive state
  boxPosition.value = {
    x: box.position.x,
    y: box.position.y
  };
};

const handleCollision = (event) => {
  const pairs = event.pairs;
  for (let i = 0; i < pairs.length; i++) {
    const pair = pairs[i];
    // Handle collision logic
  }
};

// Hooking into Matter.js events
Events.on(engine, 'afterUpdate', handleAfterUpdate);
Events.on(engine, 'collisionStart', handleCollision);

Cleaning Up in onUnmounted

Failing to tear down the physics engine when a component unmounts causes memory leaks, orphaned requestAnimationFrame loops, and lingering event listeners.

Use Vue's onUnmounted hook to reverse the initialization process:

onUnmounted(() => {
  // 1. Remove all custom event listeners
  Events.off(engine, 'afterUpdate', handleAfterUpdate);
  Events.off(engine, 'collisionStart', handleCollision);

  // 2. Stop the loop and the renderer
  Runner.stop(runner);
  Render.stop(render);

  // 3. Clear the physics world
  Composite.clear(engine.world, false);
  Engine.clear(engine);

  // 4. Remove the canvas element from DOM
  if (render.canvas) {
    render.canvas.remove();
  }
  render.canvas = null;
  render.context = null;
  render.textures = {};
});

Complete Component Implementation

Here is a full Single File Component demonstrating the integration:

<template>
  <div ref="canvasContainer" class="physics-container"></div>
</template>

<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import Matter from 'matter-js';

const canvasContainer = ref(null);

const { Engine, Render, Runner, Bodies, Composite, Events } = Matter;

let engine;
let render;
let runner;

const handleBeforeUpdate = () => {
  // Run custom logic before physics step
};

onMounted(() => {
  engine = Engine.create();

  render = Render.create({
    element: canvasContainer.value,
    engine: engine,
    options: {
      width: 600,
      height: 400,
      wireframes: false
    }
  });

  const ground = Bodies.rectangle(300, 390, 600, 20, { isStatic: true });
  const circle = Bodies.circle(300, 100, 30, { restitution: 0.8 });

  Composite.add(engine.world, [ground, circle]);

  Events.on(engine, 'beforeUpdate', handleBeforeUpdate);

  Render.run(render);
  runner = Runner.create();
  Runner.run(runner, engine);
});

onUnmounted(() => {
  Events.off(engine, 'beforeUpdate', handleBeforeUpdate);
  Runner.stop(runner);
  Render.stop(render);
  Composite.clear(engine.world, false);
  Engine.clear(engine);

  if (render.canvas) {
    render.canvas.remove();
  }
});
</script>