Fix Matter.js MouseConstraint with CSS Canvas Scaling

When styling a Matter.js canvas with responsive CSS rules such as width: 100% or dynamic flex layouts, mouse and touch interactions often drift out of alignment with the underlying physics bodies. This occurs because the physics engine calculates pointer events based on internal canvas pixel dimensions rather than the element's rendered CSS size. This guide demonstrates how to correct this misalignment by calculating the coordinate ratio and updating the Matter.js Mouse instance directly.

The Cause of Misaligned Coordinates

Matter.js binds a Mouse object to the canvas element to handle interactions via MouseConstraint. By default, it assumes a 1:1 mapping between screen pixels and canvas pixel coordinates. If the canvas element has internal rendering dimensions of 800x600 (canvas.width and canvas.height), but CSS scales it to 400x300 or stretches it across a viewport, the pointer's client coordinates will register at half the expected physics distance, preventing bodies from being dragged correctly.

The Solution: Using Mouse.setScale

To resolve the discrepancy, you must compute the ratio between the canvas's internal dimensions and its actual rendered bounding box, then supply these values to Matter.Mouse.setScale.

Here is the implementation:

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

// 1. Create engine and render
const engine = Engine.create();
const canvas = document.getElementById('world');

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

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

// 2. Add dynamic bodies to the world
const box = Bodies.rectangle(400, 200, 80, 80);
const ground = Bodies.rectangle(400, 590, 810, 60, { isStatic: true });
Composite.add(engine.world, [box, ground]);

// 3. Create mouse and MouseConstraint
const mouse = Mouse.create(render.canvas);
const mouseConstraint = MouseConstraint.create(engine, {
  mouse: mouse,
  constraint: {
    stiffness: 0.2,
    render: { visible: false }
  }
});

Composite.add(engine.world, mouseConstraint);
render.mouse = mouse;

// 4. Function to adjust mouse scale based on rendered dimensions
function updateMouseScale() {
  const rect = canvas.getBoundingClientRect();

  // Prevent division by zero if canvas is hidden
  if (rect.width === 0 || rect.height === 0) return;

  Mouse.setScale(mouse, {
    x: canvas.width / rect.width,
    y: canvas.height / rect.height
  });

  Mouse.setOffset(mouse, {
    x: 0,
    y: 0
  });
}

// Initial adjustment
updateMouseScale();

// Re-adjust whenever the window or container resizes
window.addEventListener('resize', updateMouseScale);

Addressing Margins and Offsets

In cases where the canvas is embedded inside transformed containers, scrollable elements, or elements with heavy padding, calculate the offset dynamically as well:

function updateMouseScaleAndOffset() {
  const rect = canvas.getBoundingClientRect();

  if (rect.width === 0 || rect.height === 0) return;

  // Scale factor: internal dimensions / visible CSS dimensions
  const scaleX = canvas.width / rect.width;
  const scaleY = canvas.height / rect.height;

  Mouse.setScale(mouse, { x: scaleX, y: scaleY });

  // Optional: Set offset if external bounds introduce coordinate shift
  Mouse.setOffset(mouse, {
    x: -rect.left * (scaleX - 1),
    y: -rect.top * (scaleY - 1)
  });
}

Responsive Canvases with Native Pixel Resizing

If your goal is to change the actual physics boundaries along with the window size (rather than scaling the existing physics world), resize the internal canvas dimensions directly and update the bounds:

window.addEventListener('resize', () => {
  render.canvas.width = window.innerWidth;
  render.canvas.height = window.innerHeight;
  render.options.width = window.innerWidth;
  render.options.height = window.innerHeight;

  // Reset scale back to 1:1 since the internal resolution matches the screen size
  Mouse.setScale(mouse, { x: 1, y: 1 });
  Mouse.setOffset(mouse, { x: 0, y: 0 });
});

Using Mouse.setScale ensures that user input accurately matches rendered geometry, regardless of whether CSS transforms, percentage widths, or device pixel ratios are applied to the canvas.