Control Matter.js Gravity with DeviceOrientation API

This article explains how to capture real-time motion data from mobile hardware using the browser's DeviceOrientation API and map it directly to the Matter.js physics engine. By linking the device's physical tilt angles (beta and gamma) to the 2D gravity vector of a Matter.js world, you can create interactive simulations where digital objects react directly to real-world device movement.

Understanding the Coordinate Systems

To connect hardware sensors to Matter.js, you must map the angles reported by the DeviceOrientation API to the 2D directional vectors used by Matter.js:

Requesting Orientation Permissions

Modern mobile browsers, particularly Safari on iOS 13+, require explicit user permission before exposing sensor events. This request must be triggered by a direct user interaction, such as tapping a button.

async function requestOrientationPermission() {
  if (typeof DeviceOrientationEvent !== 'undefined' && 
      typeof DeviceOrientationEvent.requestPermission === 'function') {
    try {
      const response = await DeviceOrientationEvent.requestPermission();
      if (response === 'granted') {
        window.addEventListener('deviceorientation', handleOrientation);
      } else {
        console.warn('Device orientation permission was denied.');
      }
    } catch (error) {
      console.error('Error requesting device orientation permission:', error);
    }
  } else {
    // Non-iOS or older devices that do not require explicit permission
    window.addEventListener('deviceorientation', handleOrientation);
  }
}

Mapping Tilt Values to Matter.js

The raw degree values must be normalized into a standard gravity range, typically between -1 and 1.

A standard mapping approach clamps the angles to prevent extreme acceleration and divides the result by 90 to normalize the scalar:

function handleOrientation(event) {
  const { beta, gamma } = event;

  if (beta === null || gamma === null) return;

  // Clamp gamma between -90 and 90 and normalize to [-1, 1]
  const clampedGamma = Math.max(-90, Math.min(90, gamma));
  const gravityX = clampedGamma / 90;

  // Clamp beta to a comfortable viewing angle (e.g., -90 to 90) and normalize
  const clampedBeta = Math.max(-90, Math.min(90, beta));
  const gravityY = clampedBeta / 90;

  // Update Matter.js engine gravity
  engine.gravity.x = gravityX;
  engine.gravity.y = gravityY;
}

Complete Implementation Example

The following script initializes an interactive Matter.js environment enclosed by static walls, applies bounds, and binds the device's orientation events to the simulation.

// 1. Initialize Matter.js modules
const { Engine, Render, Runner, Bodies, Composite } = Matter;

const engine = Engine.create();
const world = engine.world;

const render = Render.create({
  element: document.body,
  engine: engine,
  options: {
    width: window.innerWidth,
    height: window.innerHeight,
    wireframes: false
  }
});

Render.run(render);
const runner = Runner.create();
Runner.run(runner, engine);

// 2. Add boundary walls and rigid bodies
const thickness = 100;
const width = window.innerWidth;
const height = window.innerHeight;

const walls = [
  Bodies.rectangle(width / 2, -thickness / 2, width, thickness, { isStatic: true }),
  Bodies.rectangle(width / 2, height + thickness / 2, width, thickness, { isStatic: true }),
  Bodies.rectangle(-thickness / 2, height / 2, thickness, height, { isStatic: true }),
  Bodies.rectangle(width + thickness / 2, height / 2, thickness, height, { isStatic: true })
];

Composite.add(world, walls);

// Add dynamic bodies to react to gravity
for (let i = 0; i < 20; i++) {
  const box = Bodies.rectangle(
    width / 2 + (Math.random() - 0.5) * 100,
    height / 2 + (Math.random() - 0.5) * 100,
    40, 
    40, 
    { restitution: 0.6 }
  );
  Composite.add(world, box);
}

// 3. Connect the sensor event to engine gravity
function handleOrientation(event) {
  const gamma = event.gamma || 0; // Left-to-right
  const beta = event.beta || 0;   // Front-to-back

  // Normalize angles into [-1, 1] range
  engine.gravity.x = Math.max(-1, Math.min(1, gamma / 90));
  engine.gravity.y = Math.max(-1, Math.min(1, beta / 90));
}

// 4. Set up an activation trigger for modern mobile browsers
const enableButton = document.createElement('button');
enableButton.textContent = 'Enable Motion Controls';
enableButton.style.position = 'absolute';
enableButton.style.top = '20px';
enableButton.style.left = '20px';
enableButton.style.zIndex = '1000';
document.body.appendChild(enableButton);

enableButton.addEventListener('click', () => {
  if (typeof DeviceOrientationEvent !== 'undefined' && 
      typeof DeviceOrientationEvent.requestPermission === 'function') {
    DeviceOrientationEvent.requestPermission()
      .then(permissionState => {
        if (permissionState === 'granted') {
          window.addEventListener('deviceorientation', handleOrientation);
          enableButton.remove();
        }
      })
      .catch(console.error);
  } else {
    window.addEventListener('deviceorientation', handleOrientation);
    enableButton.remove();
  }
});

Considerations for Deployment