Implement Matter.js Canvas Zoom with Renderer Bounds
Controlling the camera zoom in a Matter.js simulation is best
accomplished by manipulating the bounds object of the
built-in renderer rather than applying manual 2D canvas context
transformations. By enabling renderer bounds, Matter.js automatically
maps a virtual rectangular viewport to the canvas element's dimensions,
allowing you to zoom in and out by shrinking or expanding this bounding
box. This guide covers how to activate this feature, calculate zoom
scaling relative to the cursor position, and implement mouse wheel zoom
controls.
Enable Bounds on the Renderer
By default, the Matter.js renderer uses the canvas width and height
directly without tracking custom view boundaries. You must enable
hasBounds when instantiating the renderer:
const render = Matter.Render.create({
element: document.body,
engine: engine,
options: {
width: 800,
height: 600,
wireframes: false,
hasBounds: true // Required for custom bounds and zooming
}
});When hasBounds is set to true, Matter.js
uses render.bounds.min and render.bounds.max
to determine which region of the physics world is drawn onto the
canvas.
Understanding the Bounds Property
The render.bounds object defines the visible rectangular
area in world coordinates:
render.bounds.min.xandrender.bounds.min.y: The top-left corner of the view.render.bounds.max.xandrender.bounds.max.y: The bottom-right corner of the view.
To zoom in, decrease the distance between
min and max (making the rendered area
smaller). To zoom out, increase the distance between
min and max.
Implementing Mouse Wheel Zoom Controls
To provide an intuitive user experience, zoom operations should scale around the user's cursor position rather than the top-left corner or canvas center.
Here is the complete implementation attaching to the
wheel event:
const canvas = render.canvas;
// Configure zoom boundaries and speed
const ZOOM_FACTOR = 0.1;
const MIN_WIDTH = 200; // Maximum zoom in
const MAX_WIDTH = 4000; // Maximum zoom out
canvas.addEventListener('wheel', (event) => {
event.preventDefault();
// 1. Get current mouse position relative to canvas
const rect = canvas.getBoundingClientRect();
const mouseCanvasX = event.clientX - rect.left;
const mouseCanvasY = event.clientY - rect.top;
// 2. Normalize mouse position (0.0 to 1.0)
const normalizedX = mouseCanvasX / canvas.width;
const normalizedY = mouseCanvasY / canvas.height;
// 3. Get current world dimensions
const currentBounds = render.bounds;
const currentWidth = currentBounds.max.x - currentBounds.min.x;
const currentHeight = currentBounds.max.y - currentBounds.min.y;
// 4. Calculate scale factor based on wheel direction
const zoomDirection = event.deltaY < 0 ? 1 : -1;
const scale = 1 - (zoomDirection * ZOOM_FACTOR);
// 5. Calculate new width and apply constraints
let newWidth = currentWidth * scale;
if (newWidth < MIN_WIDTH) newWidth = MIN_WIDTH;
if (newWidth > MAX_WIDTH) newWidth = MAX_WIDTH;
// Maintain canvas aspect ratio
const aspectRatio = canvas.height / canvas.width;
const newHeight = newWidth * aspectRatio;
// 6. Calculate new bounds centered on the cursor position
const mouseWorldX = currentBounds.min.x + (currentWidth * normalizedX);
const mouseWorldY = currentBounds.min.y + (currentHeight * normalizedY);
render.bounds.min.x = mouseWorldX - (newWidth * normalizedX);
render.bounds.min.y = mouseWorldY - (newHeight * normalizedY);
render.bounds.max.x = render.bounds.min.x + newWidth;
render.bounds.max.y = render.bounds.min.y + newHeight;
});Handling Mouse Constraints with Zoom
If your simulation utilizes Matter.MouseConstraint for
object dragging, updating render.bounds without syncing the
mouse module will cause a mismatch between cursor clicks and object
interactions.
Ensure the mouse offset and scale are updated whenever the bounds change:
const mouse = Matter.Mouse.create(render.canvas);
const mouseConstraint = Matter.MouseConstraint.create(engine, {
mouse: mouse
});
// Update the mouse scale and offset during the render loop or directly after zooming
Matter.Events.on(render, 'beforeRender', () => {
Matter.Mouse.setScale(mouse, {
x: (render.bounds.max.x - render.bounds.min.x) / render.canvas.width,
y: (render.bounds.max.y - render.bounds.min.y) / render.canvas.height
});
Matter.Mouse.setOffset(mouse, render.bounds.min);
});This synchronization ensures physics interactions remain accurate regardless of the current zoom level.