Matter.js Mouse Interactions in a Web Worker
Running Matter.js inside a Web Worker prevents heavy physics
calculations from degrading UI rendering performance, but it isolates
the physics engine from the browser's DOM. Because Web Workers lack
direct access to DOM events, standard modules like
Matter.Mouse and Matter.MouseConstraint cannot
listen for pointer inputs directly. Achieving smooth, responsive mouse
interactions requires creating a lightweight input-proxy system on the
main thread, streaming serialized input states to the worker thread, and
updating virtual constraints within the physics loop without introducing
latency or frame drops.
The Decoupled Architecture
By default, Matter.js binds DOM listeners directly to an
HTMLCanvasElement to update mouse coordinates and resolve
dragging constraints. In a multi-threaded architecture, you must split
this responsibility into two parts:
- The Main Thread (Input Capture): Listens to native pointer events, calculates canvas-relative coordinates, and sends minimal payloads to the worker.
- The Worker Thread (Physics Simulation): Maintains a
virtual mouse representation and applies forces or updates a
MouseConstraintbased on the received data.
Capturing and Throttling Main Thread Events
Do not spam worker.postMessage on every raw
mousemove event, as excessive messaging overhead can
saturate the thread channel and cause noticeable input lag. Instead,
capture the latest pointer coordinates and synchronize them using
requestAnimationFrame or immediate messaging only when the
pointer state changes.
// main.js
const canvas = document.getElementById('physics-canvas');
const worker = new Worker('physics.worker.js');
let mouseState = { x: 0, y: 0, isDown: 0, dirty: false };
function getCanvasCoordinates(e) {
const rect = canvas.getBoundingClientRect();
return {
x: (e.clientX - rect.left) * (canvas.width / rect.width),
y: (e.clientY - rect.top) * (canvas.height / rect.height)
};
}
canvas.addEventListener('mousedown', (e) => {
const coords = getCanvasCoordinates(e);
mouseState.x = coords.x;
mouseState.y = coords.y;
mouseState.isDown = e.buttons;
worker.postMessage({ type: 'pointerdown', ...coords, buttons: e.buttons });
});
canvas.addEventListener('mousemove', (e) => {
const coords = getCanvasCoordinates(e);
mouseState.x = coords.x;
mouseState.y = coords.y;
mouseState.dirty = true;
});
window.addEventListener('mouseup', (e) => {
mouseState.isDown = 0;
worker.postMessage({ type: 'pointerup' });
});
// Flush movement updates aligned with frame rendering
function syncInput() {
if (mouseState.dirty) {
worker.postMessage({
type: 'pointermove',
x: mouseState.x,
y: mouseState.y
});
mouseState.dirty = false;
}
requestAnimationFrame(syncInput);
}
requestAnimationFrame(syncInput);Emulating Matter.Mouse in the Worker
Inside the worker, instantiate Matter.Mouse with a dummy
object instead of a canvas element. This prevents Matter.js from
throwing errors when attempting to run
element.addEventListener. You then update the internal
coordinates manually whenever an event arrives from the main thread.
// physics.worker.js
importScripts('matter.min.js');
const { Engine, World, Bodies, Mouse, MouseConstraint } = Matter;
const engine = Engine.create();
const world = engine.world;
// Create a mock element to satisfy Matter.Mouse requirements
const dummyElement = {
addEventListener: () => {},
removeEventListener: () => {},
getBoundingClientRect: () => ({ left: 0, top: 0, width: 800, height: 600 })
};
const mouse = Mouse.create(dummyElement);
const mouseConstraint = MouseConstraint.create(engine, {
mouse: mouse,
constraint: {
stiffness: 0.2,
render: { visible: false }
}
});
World.add(world, mouseConstraint);
self.onmessage = (event) => {
const { type, x, y, buttons } = event.data;
switch (type) {
case 'pointerdown':
mouse.position.x = x;
mouse.position.y = y;
mouse.button = buttons === 1 ? 0 : 1; // Map left click to 0
mouse.buttonIndices[mouse.button] = true;
break;
case 'pointermove':
mouse.position.x = x;
mouse.position.y = y;
break;
case 'pointerup':
mouse.button = -1;
mouse.buttonIndices = {};
break;
}
};Optimizing for Zero Input Jitter
- Avoid Stale Dragging: Always bind
mouseupto thewindowrather than the canvas. If a user drags outside the canvas boundary and releases the mouse, the physics engine must immediately release the constrained body to prevent physics artifacts. - Coordinate Interpolation: When displaying the dragged body on the main thread, interpolate body positions between physics ticks. If the worker runs at 60Hz and the display runs at 120Hz, linear interpolation (lerp) between the two latest state snapshots ensures the object tracks smoothly beneath the cursor.
- Use SharedArrayBuffer for High-Frequency Sync: If
postMessageserialization causes micro-stutters under heavy simulation loads, use aSharedArrayBuffercontaining a fixed-sizeFloat32Arrayfor the mouse position and button state. The worker reads directly from shared memory at the start of every physics tick, eliminating message deserialization latency entirely.