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:
- Matter.js Gravity: Controlled via
engine.gravity.xandengine.gravity.y. By default, gravity points downward along the Y-axis (x = 0,y = 1, with ascaleproperty). - Sensor Gamma (\(\gamma\)): Represents
left-to-right tilt around the device's longitudinal axis, returning
values from -90 to 90 degrees. This maps to the horizontal axis
(
gravity.x). - Sensor Beta (\(\beta\)): Represents front-to-back
tilt around the device's lateral axis, returning values from -180 to 180
degrees. For a phone held upright or flat on a table, tilting forward
and backward maps to the vertical axis (
gravity.y).
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
- HTTPS Requirement: Modern browsers restrict the
DeviceOrientation API entirely unless the web page is served over a
secure HTTPS connection or from
localhost. - Screen Orientation: When the user rotates their
screen between portrait and landscape modes, the coordinate axes switch.
To handle landscape mode correctly, check
window.orientationorscreen.orientation.angleand remapgammaandbetato the correspondingxandyaxes accordingly. - Sensor Smoothing: Raw sensor outputs may introduce
jitter. To smooth body movement, apply a low-pass filter to the
calculated vector before assigning it to
engine.gravity.