Canvas Screen Shake Without Moving Matter.js Bodies

This article explains how to create a high-impact screen shake effect in Matter.js by transforming the HTML5 Canvas context instead of altering physical body coordinates. By applying transformations directly to the 2D rendering context, you preserve the stability and accuracy of the physics simulation while dynamically translating the visual viewport.

The Problem with Altering Physics Bodies

Modifying Matter.Body positions or applying physical impulses to simulate screen shake disrupts the simulation. It causes unwanted collisions, destabilizes stacked structures, and introduces unpredictable forces. The correct approach is decoupling the physics state from the visual presentation by shaking the canvas "camera" instead of the world objects.

The Screen Shake Algorithm

A standard screen shake relies on two main variables: intensity (the maximum pixel displacement) and decay (how quickly the effect fades). On each frame, you calculate a random displacement on the X and Y axes within the bounds of the current intensity, then reduce the intensity over time until it reaches zero.

let shakeIntensity = 0;
const shakeDecay = 0.9;

function triggerShake(magnitude) {
  shakeIntensity = magnitude;
}

function updateShake() {
  if (shakeIntensity > 0.1) {
    const offsetX = (Math.random() * 2 - 1) * shakeIntensity;
    const offsetY = (Math.random() * 2 - 1) * shakeIntensity;
    shakeIntensity *= shakeDecay;
    return { x: offsetX, y: offsetY };
  }
  shakeIntensity = 0;
  return { x: 0, y: 0 };
}

Hooking into the Matter.js Render Loop

If you are using the built-in Matter.Render module, you can inject canvas transformations using the engine's render events: beforeRender and afterRender.

  1. beforeRender: Save the current canvas state and translate the context by the computed shake offset before Matter.js draws the bodies.
  2. afterRender: Restore the canvas context to its original state so that subsequent frames do not compound the translation.
const { Engine, Render, Runner, Events } = Matter;

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

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

// Hook canvas transformations into Matter.js rendering
Events.on(render, 'beforeRender', () => {
  const context = render.context;
  const offset = updateShake();

  context.save();
  context.translate(offset.x, offset.y);
});

Events.on(render, 'afterRender', () => {
  render.context.restore();
});

Implementing with a Custom Render Loop

When managing your own rendering loop using requestAnimationFrame, wrap your drawing routines between context.save(), context.translate(), and context.restore():

function draw() {
  const ctx = canvas.getContext('2d');
  const offset = updateShake();

  ctx.clearRect(0, 0, canvas.width, canvas.height);

  ctx.save();
  ctx.translate(offset.x, offset.y);

  // Render Matter.js bodies manually here
  const bodies = Matter.Composite.allBodies(engine.world);
  bodies.forEach(body => {
    // Draw vertices
  });

  ctx.restore();

  requestAnimationFrame(draw);
}

Handling Mouse and Screen Interactions

Because screen shake temporarily misaligns the visual position of bodies with their underlying coordinate space, screen-space inputs (like mouse clicks or touch coordinates) will not match physics coordinates during intense shaking. If precise interaction is needed during an impact, subtract the current frame's shake offset (offsetX, offsetY) from the screen coordinates before querying Matter.js bodies with methods like Matter.Query.point().