Matter.js HTML Elements with CSS matrix3d
This guide explains how to synchronize 2D physics bodies from
Matter.js with standard HTML DOM elements using CSS
matrix3d transforms. By mapping the position and rotation
data of a physics body directly into a 4x4 transformation matrix, you
can achieve high-performance, GPU-accelerated rendering of HTML elements
without triggering expensive layout recalculations in the browser.
Why Use matrix3d
Standard CSS properties like top and left
trigger browser layout and repaint passes on every frame, leading to
stuttering physics simulations. Using CSS transforms bypasses layout
updates and leverages GPU compositing. While a 2D matrix()
or combination of translate() and rotate()
works, matrix3d() explicitly promotes the element to its
own hardware-accelerated compositing layer, reducing latency and
subpixel jitter during fast-paced simulations.
Matrix Structure for 2D Physics
A CSS matrix3d expects 16 values in column-major order
representing an affine 4x4 transformation matrix:
matrix3d(
m11, m12, m13, m14,
m21, m22, m23, m24,
m31, m32, m33, m34,
m41, m42, m43, m44
)
To map a 2D rotation (angle \(\theta\) around the Z-axis) and 2D translation (\(X, Y\)) from Matter.js, configure the matrix as follows:
- \(m11 = \cos(\theta)\)
- \(m12 = \sin(\theta)\)
- \(m21 = -\sin(\theta)\)
- \(m22 = \cos(\theta)\)
- \(m33 = 1\)
- \(m41 = X\)
- \(m42 = Y\)
- \(m44 = 1\)
- All other indices remain \(0\).
DOM and CSS Setup
Matter.js defines a body's coordinates at its center of mass. To align an HTML element properly, ensure its transform origin is at its center and its initial layout coordinates start at the origin (0, 0):
.physics-element {
position: absolute;
top: 0;
left: 0;
transform-origin: 50% 50%;
will-change: transform;
pointer-events: auto;
}If your element has dimensions \(W\)
and \(H\), offsetting the origin is
automatically handled if the element has explicit dimensions matching
the physics body and uses transform-origin: 50% 50%. The
translation will move the center of the element directly to the physics
body coordinates \((x, y)\).
The Update Loop
Hook into the Matter.js update cycle using
Events.on(engine, 'afterUpdate', ...) or request a standard
requestAnimationFrame loop to update the DOM elements:
import { Engine, Events, Bodies, Composite } from 'matter-js';
// Setup engine and world
const engine = Engine.create();
const domElement = document.querySelector('.physics-element');
// Create a body with matching dimensions (e.g., 100x100)
const body = Bodies.rectangle(400, 200, 100, 100);
Composite.add(engine.world, body);
// Synchronize on every physics tick
Events.on(engine, 'afterUpdate', () => {
const { x, y } = body.position;
const angle = body.angle;
const cos = Math.cos(angle);
const sin = Math.sin(angle);
// If the element's width/height offsets are required when not using 50% transform-origin:
// const tx = x - width / 2;
// const ty = y - height / 2;
// Directly mapping center-to-center:
const tx = x;
const ty = y;
// Construct column-major matrix3d string
domElement.style.transform = `matrix3d(
${cos}, ${sin}, 0, 0,
${-sin}, ${cos}, 0, 0,
0, 0, 1, 0,
${tx}, ${ty}, 0, 1
)`;
});Performance Considerations
- String Allocation: Constructing matrix strings every frame can cause minor garbage collection overhead. Pre-allocating template arrays or keeping string templates concise mitigates this.
- Precision: Rounding values using
toFixed()is generally unnecessary and adds CPU overhead; passing standard JavaScript floats directly into the transform string yields the best performance. - Layer Management: Avoid applying
matrix3dto thousands of elements simultaneously, as excessive compositing layers can exhaust GPU memory. For large numbers of bodies, combine HTML syncing only for interactive elements while rendering static or decorative bodies on a Canvas.