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:
- Engine: Manages the physics world and state updates.
- Render: Draws the simulation to a canvas inside your referenced container.
- 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:
beforeUpdate: Fired immediately before the engine updates physics calculations.afterUpdate: Fired right after physics calculations, ideal for syncing coordinates to Vue's reactive state.collisionStart/collisionEnd: Fired when bodies begin or cease intersecting.
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>