Skip to main content
Dev ToolsBlog
HomeArticlesCategories

Dev Tools Blog

Modern development insights and cutting-edge tools for today's developers.

Quick Links

  • ArticlesView all development articles
  • CategoriesBrowse articles by category

Technologies

Built with Next.js 15, React 19, TypeScript, and Tailwind CSS.

© 2025 Dev Tools Blog. All rights reserved.

← Back to Home
frontend

JavaScript Physics Engines: The Complete Guide to Matter.js, Cannon.js, Rapier, and Interactive Web Physics

Comprehensive guide to JavaScript physics engines including Matter.js for 2D physics, Cannon.js for Three.js 3D integration, and Rapier for high-performance WASM physics. Covers implementations, optimization strategies, and real-world applications.

Published: 10/7/2025

JavaScript Physics Engines: The Complete Guide to Matter.js, Cannon.js, Rapier, and Interactive Web Physics

Executive Summary

Physics engines transform static web experiences into dynamic, interactive environments where objects respond realistically to gravity, collisions, forces, and constraints. From simple bouncing balls to complex ragdoll simulations, from product configurators to browser-based games, JavaScript physics engines enable developers to create immersive experiences that feel tangible and responsive. Three engines dominate the modern landscape: Matter.js for 2D physics with elegant APIs and rich ecosystem, Cannon.js for lightweight 3D physics integrated with Three.js, and Rapier for high-performance physics compiled from Rust to WebAssembly.

Matter.js has established itself as the gold standard for 2D web physics since its 2014 release, offering an intuitive API that makes physics accessible to developers without extensive mathematics backgrounds. The library handles rigid body dynamics, collision detection, constraint systems, and composite bodies through a clean, well-documented interface. Matter.js powers everything from creative portfolio animations to educational physics simulations to mobile games, with performance characteristics that enable hundreds of active bodies at 60 FPS on modern devices. The ecosystem includes visual debugging tools, physics editors, and extensive community examples that accelerate development from concept to deployment.

Cannon.js brings 3D physics to web applications with a focus on Three.js integration, enabling developers to add realistic motion and interactions to 3D scenes. Originally created by Stefan Hedman in 2012 and actively maintained through community forks like cannon-es, the library provides rigid body dynamics, collision detection across primitive and complex geometries, constraint systems for joints and motors, and raycast capabilities for interaction and AI. While more complex than 2D alternatives due to three-dimensional mathematics, Cannon.js abstracts the computational physics behind approachable APIs that developers can master through practical examples and patterns.

Rapier represents the cutting edge of web physics through its Rust-compiled WebAssembly foundation, delivering performance that often exceeds native JavaScript implementations by 3-10x. Released in 2021, Rapier supports both 2D and 3D physics through a unified API, handles thousands of active bodies simultaneously, and provides advanced features like continuous collision detection (CCD) for fast-moving objects, kinematic character controllers, and multibody joints. The WASM architecture enables desktop-class physics simulation in browsers, opening possibilities previously limited to native applications: real-time multiplayer physics, procedural physics-based level generation, and complex mechanical simulations.

The strategic choice between these engines depends on project requirements:

Choose Matter.js for 2D projects prioritizing developer experience, rapid prototyping, and ecosystem maturity. The gentle learning curve and comprehensive documentation enable teams to implement physics features quickly without specialized knowledge. Portfolio sites with interactive elements, educational platforms demonstrating physics concepts, and 2D browser games benefit from Matter.js's stability and community support.

Choose Cannon.js for 3D applications built with Three.js where physics adds realism to existing 3D scenes. Product configurators with object manipulation, architectural walkthroughs with interactive elements, and browser-based 3D games leverage Cannon.js's established Three.js integration patterns. The library suits projects where physics enhances experience rather than defining it—complementing visual presentation with realistic motion.

Choose Rapier for performance-critical applications requiring maximum physics fidelity or handling large numbers of simultaneous bodies. Multiplayer physics simulations, procedural content generation, vehicle simulators, and complex mechanical systems benefit from Rapier's computational efficiency. The performance headroom enables features impossible with JavaScript implementations while maintaining cross-platform web deployment.

This comprehensive guide explores practical implementation patterns, performance optimization strategies, and real-world use cases across all three engines. Whether creating subtle animations for landing pages, building interactive educational tools, or developing full-featured browser games, the techniques and code examples below provide the foundation for effective physics-driven web development.

Understanding Web Physics Fundamentals

The Physics Simulation Loop

Physics engines operate through iterative simulation loops that approximate continuous physical motion through discrete time steps. Understanding this fundamental pattern clarifies how engines work and informs optimization decisions.

Basic Simulation Cycle:

  • 1. Apply Forces: External forces (gravity, user input, wind) apply to bodiesApply Forces: External forces (gravity, user input, wind) apply to bodies
  • 2. Update Velocities: Forces modify velocities based on mass (F = ma)Update Velocities: Forces modify velocities based on mass (F = ma)
  • 3. Detect Collisions: Broad-phase and narrow-phase algorithms identify intersecting bodiesDetect Collisions: Broad-phase and narrow-phase algorithms identify intersecting bodies
  • 4. Resolve Collisions: Collision responses generate impulses that modify velocitiesResolve Collisions: Collision responses generate impulses that modify velocities
  • 5. Apply Constraints: Joints and constraints enforce relationships between bodiesApply Constraints: Joints and constraints enforce relationships between bodies
  • 6. Integrate Positions: Velocities update positions for the time stepIntegrate Positions: Velocities update positions for the time step
  • 7. Render: Visual representation syncs with physics stateRender: Visual representation syncs with physics state

This cycle executes at fixed intervals (typically 60 Hz for 16.67ms time steps) independent of rendering frame rate to ensure simulation stability and determinism.

Fixed Time Step vs. Variable Time Step:

Fixed time steps maintain simulation accuracy and determinism—critical for multiplayer games or recorded playback. Variable time steps tie physics updates to rendering frame rate, creating inconsistent behavior across devices with different performance characteristics.

// Fixed time step pattern (recommended)
const fixedTimeStep = 1000 / 60; // 60 FPS = 16.67ms per step
let accumulator = 0;
let lastTime = performance.now();

function update() { const currentTime = performance.now(); const deltaTime = currentTime - lastTime; lastTime = currentTime;

accumulator += deltaTime;

// Execute fixed time steps while (accumulator >= fixedTimeStep) { physicsEngine.step(fixedTimeStep / 1000); // Convert ms to seconds accumulator -= fixedTimeStep; }

// Render with interpolation for smooth visuals const interpolation = accumulator / fixedTimeStep; render(interpolation);

requestAnimationFrame(update); }

update();

This pattern maintains stable physics at 60 Hz while rendering as fast as possible, interpolating visual positions between physics states for smooth motion.

Collision Detection: Broad Phase and Narrow Phase

Collision detection represents the most computationally expensive physics operation. Naive approaches checking every body against every other body scale at O(n²)—with 100 bodies requiring 10,000 checks per frame. Production engines employ two-phase detection strategies.

Broad Phase: Quickly eliminate body pairs that definitely aren't colliding using spatial partitioning (grid, quadtree, bounding volume hierarchy) or sweep-and-prune algorithms. This reduces potential collisions from O(n²) to O(n log n) or better.

Narrow Phase: Precisely calculate collision points, normals, and penetration depths for body pairs identified by broad phase using geometry-specific algorithms (SAT for polygons, GJK for convex shapes).

Most engines handle this optimization transparently, but understanding the concept informs decisions about body counts and scene structure.

Matter.js: 2D Physics with Elegant APIs

Getting Started with Matter.js

Matter.js excels at making physics approachable through clean APIs that abstract mathematical complexity without sacrificing control.

Installation and Basic Setup:

npm install matter-js
import Matter from 'matter-js';

// Create engine const engine = Matter.Engine.create(); const world = engine.world;

// Create renderer const render = Matter.Render.create({ element: document.body, engine: engine, options: { width: 800, height: 600, wireframes: false, // Show styled bodies instead of wireframes background: '#1a1a2e' } });

// Create ground const ground = Matter.Bodies.rectangle(400, 580, 810, 60, { isStatic: true, render: { fillStyle: '#16213e' } });

// Create falling boxes const boxA = Matter.Bodies.rectangle(400, 200, 80, 80, { restitution: 0.5, // Bounciness render: { fillStyle: '#0f3460' } });

const boxB = Matter.Bodies.rectangle(450, 50, 80, 80, { restitution: 0.8, render: { fillStyle: '#e94560' } });

// Add all bodies to world Matter.World.add(world, [ground, boxA, boxB]);

// Run the engine Matter.Engine.run(engine);

// Run the renderer Matter.Render.run(render);

This minimal example creates a physics world with gravity, ground, and falling boxes that bounce realistically—demonstrating Matter.js's approachable API.

Creating Interactive Physics Objects

Real applications require mouse interaction, dynamic body creation, and event handling:

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

Matter.World.add(world, mouseConstraint);

// Keep mouse in sync with rendering render.mouse = mouse;

// Create variety of shapes function createRandomBody(x, y) { const shapeType = Math.random();

let body;

if (shapeType < 0.33) { // Circle const radius = 20 + Math.random() * 40; body = Matter.Bodies.circle(x, y, radius, { restitution: 0.6, friction: 0.01, render: { fillStyle: hsl(${Math.random() * 360}, 70%, 50%) } }); } else if (shapeType < 0.66) { // Rectangle const width = 40 + Math.random() * 60; const height = 40 + Math.random() * 60; body = Matter.Bodies.rectangle(x, y, width, height, { restitution: 0.4, friction: 0.1, render: { fillStyle: hsl(${Math.random() * 360}, 70%, 50%) } }); } else { // Polygon const sides = Math.floor(Math.random() * 4) + 5; // 5-8 sides const radius = 30 + Math.random() * 30; body = Matter.Bodies.polygon(x, y, sides, radius, { restitution: 0.5, friction: 0.05, render: { fillStyle: hsl(${Math.random() * 360}, 70%, 50%) } }); }

return body; }

// Click to spawn bodies render.canvas.addEventListener('click', (event) => { const rect = render.canvas.getBoundingClientRect(); const x = event.clientX - rect.left; const y = event.clientY - rect.top;

const body = createRandomBody(x, y); Matter.World.add(world, body); });

// Collision events Matter.Events.on(engine, 'collisionStart', (event) => { const pairs = event.pairs;

pairs.forEach(pair => { const { bodyA, bodyB } = pair;

// Flash bodies on collision bodyA.render.fillStyle = '#ffffff'; bodyB.render.fillStyle = '#ffffff';

setTimeout(() => { bodyA.render.fillStyle = bodyA.render.originalColor || '#0f3460'; bodyB.render.fillStyle = bodyB.render.originalColor || '#e94560'; }, 100);

// Play collision sound based on impact velocity const impactVelocity = Matter.Vector.magnitude( Matter.Vector.sub(bodyA.velocity, bodyB.velocity) );

if (impactVelocity > 2) { playCollisionSound(impactVelocity); } }); });

Building Composite Bodies and Constraints

Complex objects require multiple bodies connected through constraints:

// Ragdoll character
function createRagdoll(x, y, scale = 1) {
  const headRadius = 25 * scale;
  const limbWidth = 15 * scale;
  const limbHeight = 40 * scale;

// Create body parts const head = Matter.Bodies.circle(x, y, headRadius, { density: 0.001, render: { fillStyle: '#ffa07a' } });

const torso = Matter.Bodies.rectangle(x, y + 50 * scale, 30 * scale, 50 * scale, { density: 0.001, render: { fillStyle: '#4a90e2' } });

const leftArm = Matter.Bodies.rectangle(x - 30 * scale, y + 40 * scale, limbWidth, limbHeight, { density: 0.001, render: { fillStyle: '#ffa07a' } });

const rightArm = Matter.Bodies.rectangle(x + 30 * scale, y + 40 * scale, limbWidth, limbHeight, { density: 0.001, render: { fillStyle: '#ffa07a' } });

const leftLeg = Matter.Bodies.rectangle(x - 10 * scale, y + 95 * scale, limbWidth, limbHeight * 1.2, { density: 0.001, render: { fillStyle: '#4a90e2' } });

const rightLeg = Matter.Bodies.rectangle(x + 10 * scale, y + 95 * scale, limbWidth, limbHeight * 1.2, { density: 0.001, render: { fillStyle: '#4a90e2' } });

// Create constraints (joints) const neckConstraint = Matter.Constraint.create({ bodyA: head, bodyB: torso, pointA: { x: 0, y: headRadius }, pointB: { x: 0, y: -25 * scale }, stiffness: 0.6, length: 5 * scale });

const leftShoulderConstraint = Matter.Constraint.create({ bodyA: torso, bodyB: leftArm, pointA: { x: -15 * scale, y: -20 * scale }, pointB: { x: 0, y: -limbHeight / 2 }, stiffness: 0.4 });

const rightShoulderConstraint = Matter.Constraint.create({ bodyA: torso, bodyB: rightArm, pointA: { x: 15 * scale, y: -20 * scale }, pointB: { x: 0, y: -limbHeight / 2 }, stiffness: 0.4 });

const leftHipConstraint = Matter.Constraint.create({ bodyA: torso, bodyB: leftLeg, pointA: { x: -10 * scale, y: 25 * scale }, pointB: { x: 0, y: -(limbHeight * 1.2) / 2 }, stiffness: 0.5 });

const rightHipConstraint = Matter.Constraint.create({ bodyA: torso, bodyB: rightLeg, pointA: { x: 10 * scale, y: 25 * scale }, pointB: { x: 0, y: -(limbHeight * 1.2) / 2 }, stiffness: 0.5 });

// Combine into composite const ragdoll = Matter.Composite.create();

Matter.Composite.add(ragdoll, [ head, torso, leftArm, rightArm, leftLeg, rightLeg, neckConstraint, leftShoulderConstraint, rightShoulderConstraint, leftHipConstraint, rightHipConstraint ]);

return ragdoll; }

// Add ragdoll to world const ragdoll = createRagdoll(400, 100); Matter.World.add(world, ragdoll);

// Apply force to ragdoll Matter.Body.applyForce( ragdoll.bodies[0], // Apply to head ragdoll.bodies[0].position, { x: 0.05, y: -0.1 } // Force vector );

Advanced Matter.js Techniques

Soft Bodies with Spring Networks:

function createSoftBody(x, y, columns, rows, columnGap, rowGap, crossBrace) {
  const particleOptions = {
    inertia: Infinity,
    friction: 0.00001,
    collisionFilter: { group: -1 }, // Particles don't collide with each other
    render: { visible: true, radius: 5 }
  };

const constraintOptions = { stiffness: 0.06, render: { type: 'line', anchors: false } };

const softBody = Matter.Composites.softBody( x, y, columns, rows, columnGap, rowGap, crossBrace, particleOptions, constraintOptions );

return softBody; }

// Create cloth-like soft body const cloth = createSoftBody(200, 100, 10, 10, 10, 10, true);

// Pin top corners to make it hang const topLeftParticle = cloth.bodies[0]; const topRightParticle = cloth.bodies[9];

topLeftParticle.isStatic = true; topRightParticle.isStatic = true;

Matter.World.add(world, cloth);

Chain and Rope Simulation:

function createChain(x, y, length, linkSize) {
  const chain = Matter.Composites.stack(
    x, y,
    1, length, // 1 column, multiple rows
    0, 0,
    (x, y) => {
      return Matter.Bodies.rectangle(x, y, linkSize, linkSize * 3, {
        density: 0.005,
        friction: 0.8,
        render: { fillStyle: '#8b4513' }
      });
    }
  );

// Connect links with constraints Matter.Composites.chain(chain, 0.5, 0, -0.5, 0, { stiffness: 0.9, length: 2 });

// Pin top link Matter.Composite.add(chain, Matter.Constraint.create({ bodyB: chain.bodies[0], pointB: { x: 0, y: -linkSize * 1.5 }, pointA: { x: chain.bodies[0].position.x, y: chain.bodies[0].position.y - linkSize * 1.5 }, stiffness: 0.9 }));

return chain; }

// Create swinging chain const chain = createChain(400, 100, 15, 10); Matter.World.add(world, chain);

// Attach object to bottom of chain const ball = Matter.Bodies.circle( chain.bodies[chain.bodies.length - 1].position.x, chain.bodies[chain.bodies.length - 1].position.y + 50, 30, { density: 0.01, render: { fillStyle: '#ff6b6b' } } );

Matter.Composite.add(chain, Matter.Constraint.create({ bodyA: chain.bodies[chain.bodies.length - 1], bodyB: ball, length: 30, stiffness: 0.9 }));

Matter.World.add(world, ball);

Cannon.js: 3D Physics for Three.js

Setting Up Cannon.js with Three.js

Cannon.js integrates with Three.js to add physics to 3D scenes:

npm install three cannon-es
import * as THREE from 'three';
import * as CANNON from 'cannon-es';

// Three.js setup const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); camera.position.z = 15; camera.position.y = 5; camera.lookAt(0, 0, 0);

const renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(window.innerWidth, window.innerHeight); renderer.shadowMap.enabled = true; document.body.appendChild(renderer.domElement);

// Lighting const ambientLight = new THREE.AmbientLight(0xffffff, 0.5); scene.add(ambientLight);

const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8); directionalLight.position.set(10, 20, 10); directionalLight.castShadow = true; scene.add(directionalLight);

// Cannon.js physics world const world = new CANNON.World({ gravity: new CANNON.Vec3(0, -9.82, 0) // m/s² });

// Ground plane (physics) const groundBody = new CANNON.Body({ type: CANNON.Body.STATIC, shape: new CANNON.Plane() }); groundBody.quaternion.setFromEuler(-Math.PI / 2, 0, 0); // Rotate to horizontal world.addBody(groundBody);

// Ground plane (visual) const groundGeometry = new THREE.PlaneGeometry(30, 30); const groundMaterial = new THREE.MeshStandardMaterial({ color: 0x808080 }); const groundMesh = new THREE.Mesh(groundGeometry, groundMaterial); groundMesh.rotation.x = -Math.PI / 2; groundMesh.receiveShadow = true; scene.add(groundMesh);

// Helper function to sync Three.js mesh with Cannon.js body function createPhysicsBox(x, y, z, width, height, depth, mass) { // Physics body const shape = new CANNON.Box(new CANNON.Vec3(width / 2, height / 2, depth / 2)); const body = new CANNON.Body({ mass, shape }); body.position.set(x, y, z); world.addBody(body);

// Visual mesh const geometry = new THREE.BoxGeometry(width, height, depth); const material = new THREE.MeshStandardMaterial({ color: Math.random() * 0xffffff }); const mesh = new THREE.Mesh(geometry, material); mesh.castShadow = true; mesh.receiveShadow = true; scene.add(mesh);

return { body, mesh }; }

// Create falling boxes const boxes = []; for (let i = 0; i < 10; i++) { const box = createPhysicsBox( (Math.random() - 0.5) * 10, 5 + i * 2, (Math.random() - 0.5) * 10, 1, 1, 1, 1 // mass ); boxes.push(box); }

// Animation loop const timeStep = 1 / 60; // 60 FPS

function animate() { requestAnimationFrame(animate);

// Step physics simulation world.step(timeStep);

// Sync visual meshes with physics bodies boxes.forEach(({ body, mesh }) => { mesh.position.copy(body.position); mesh.quaternion.copy(body.quaternion); });

renderer.render(scene, camera); }

animate();

Interactive 3D Physics

Mouse Picking and Dragging:

import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';

// Add orbit controls const controls = new OrbitControls(camera, renderer.domElement);

// Raycasting for mouse interaction const raycaster = new THREE.Raycaster(); const mouse = new THREE.Vector2();

let selectedBody = null; let jointConstraint = null;

renderer.domElement.addEventListener('mousedown', (event) => { // Update mouse coordinates mouse.x = (event.clientX / window.innerWidth) * 2 - 1; mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;

// Raycast to find clicked object raycaster.setFromCamera(mouse, camera); const intersects = raycaster.intersectObjects(scene.children);

if (intersects.length > 0) { const clickedMesh = intersects[0].object;

// Find corresponding physics body const physicsPair = boxes.find(({ mesh }) => mesh === clickedMesh);

if (physicsPair) { selectedBody = physicsPair.body;

// Create joint at click point const intersectPoint = intersects[0].point;

const jointBody = new CANNON.Body({ mass: 0 }); jointBody.position.copy(intersectPoint); world.addBody(jointBody);

jointConstraint = new CANNON.PointToPointConstraint( selectedBody, new CANNON.Vec3(0, 0, 0), jointBody, new CANNON.Vec3(0, 0, 0) );

world.addConstraint(jointConstraint);

controls.enabled = false; // Disable orbit during drag } } });

renderer.domElement.addEventListener('mousemove', (event) => { if (selectedBody && jointConstraint) { // Update mouse position mouse.x = (event.clientX / window.innerWidth) * 2 - 1; mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;

// Raycast to world space point raycaster.setFromCamera(mouse, camera);

const intersectPoint = new THREE.Vector3(); raycaster.ray.at(10, intersectPoint); // Project ray to distance

// Move joint constraint jointConstraint.bodyB.position.copy(intersectPoint); } });

renderer.domElement.addEventListener('mouseup', () => { if (jointConstraint) { world.removeConstraint(jointConstraint); world.removeBody(jointConstraint.bodyB); jointConstraint = null; selectedBody = null; controls.enabled = true; } });

Vehicle Physics with Cannon.js

function createVehicle(chassisX, chassisY, chassisZ) {
  // Chassis
  const chassisShape = new CANNON.Box(new CANNON.Vec3(2, 0.5, 4));
  const chassisBody = new CANNON.Body({ mass: 150 });
  chassisBody.addShape(chassisShape);
  chassisBody.position.set(chassisX, chassisY, chassisZ);

// Chassis mesh const chassisGeometry = new THREE.BoxGeometry(4, 1, 8); const chassisMaterial = new THREE.MeshStandardMaterial({ color: 0xff0000 }); const chassisMesh = new THREE.Mesh(chassisGeometry, chassisMaterial); chassisMesh.castShadow = true; scene.add(chassisMesh);

// Create vehicle const vehicle = new CANNON.RigidVehicle({ chassisBody });

// Wheel configuration const wheelOptions = { radius: 0.5, directionLocal: new CANNON.Vec3(0, -1, 0), suspensionStiffness: 30, suspensionRestLength: 0.3, frictionSlip: 1.4, dampingRelaxation: 2.3, dampingCompression: 4.4, maxSuspensionForce: 100000, rollInfluence: 0.01, axleLocal: new CANNON.Vec3(-1, 0, 0), chassisConnectionPointLocal: new CANNON.Vec3(-1, 0, 1), maxSuspensionTravel: 0.3, customSlidingRotationalSpeed: -30, useCustomSlidingRotationalSpeed: true };

// Add wheels const wheelPositions = [ new CANNON.Vec3(-1, 0, 2), // Front left new CANNON.Vec3(-1, 0, -2), // Rear left new CANNON.Vec3(1, 0, 2), // Front right new CANNON.Vec3(1, 0, -2) // Rear right ];

const wheelBodies = []; const wheelMeshes = [];

wheelPositions.forEach((position) => { const wheelBody = new CANNON.Body({ mass: 10, shape: new CANNON.Sphere(wheelOptions.radius) }); wheelBodies.push(wheelBody);

const wheelGeometry = new THREE.CylinderGeometry( wheelOptions.radius, wheelOptions.radius, 0.4, 32 ); wheelGeometry.rotateZ(Math.PI / 2); const wheelMaterial = new THREE.MeshStandardMaterial({ color: 0x333333 }); const wheelMesh = new THREE.Mesh(wheelGeometry, wheelMaterial); wheelMesh.castShadow = true; scene.add(wheelMesh); wheelMeshes.push(wheelMesh);

vehicle.addWheel({ ...wheelOptions, chassisConnectionPointLocal: position }); });

vehicle.addToWorld(world);

// Controls const controls = { forward: false, backward: false, left: false, right: false };

document.addEventListener('keydown', (event) => { switch(event.key) { case 'w': controls.forward = true; break; case 's': controls.backward = true; break; case 'a': controls.left = true; break; case 'd': controls.right = true; break; } });

document.addEventListener('keyup', (event) => { switch(event.key) { case 'w': controls.forward = false; break; case 's': controls.backward = false; break; case 'a': controls.left = false; break; case 'd': controls.right = false; break; } });

return { vehicle, chassisMesh, wheelMeshes, controls }; }

// Create and drive vehicle const car = createVehicle(0, 3, 0);

// In animation loop, apply vehicle forces function updateVehicle() { const maxSteerVal = 0.5; const maxForce = 1000;

if (car.controls.forward) { car.vehicle.setWheelForce(maxForce, 0); car.vehicle.setWheelForce(maxForce, 1); }

if (car.controls.backward) { car.vehicle.setWheelForce(-maxForce / 2, 0); car.vehicle.setWheelForce(-maxForce / 2, 1); }

if (car.controls.left) { car.vehicle.setSteeringValue(maxSteerVal, 0); car.vehicle.setSteeringValue(maxSteerVal, 2); }

if (car.controls.right) { car.vehicle.setSteeringValue(-maxSteerVal, 0); car.vehicle.setSteeringValue(-maxSteerVal, 2); }

// Sync meshes car.chassisMesh.position.copy(car.vehicle.chassisBody.position); car.chassisMesh.quaternion.copy(car.vehicle.chassisBody.quaternion);

car.vehicle.wheelInfos.forEach((wheel, i) => { car.vehicle.updateWheelTransform(i); car.wheelMeshes[i].position.copy(wheel.worldTransform.position); car.wheelMeshes[i].quaternion.copy(wheel.worldTransform.quaternion); }); }

// Add to animation loop function animate() { requestAnimationFrame(animate);

world.step(timeStep); updateVehicle();

renderer.render(scene, camera); }

Rapier: High-Performance WASM Physics

Installing and Initializing Rapier

npm install @dimforge/rapier3d-compat
import('@dimforge/rapier3d-compat').then(RAPIER => {
  // Initialize physics world
  const gravity = { x: 0.0, y: -9.81, z: 0.0 };
  const world = new RAPIER.World(gravity);

// Create ground const groundColliderDesc = RAPIER.ColliderDesc.cuboid(10.0, 0.1, 10.0); world.createCollider(groundColliderDesc);

// Create dynamic rigid body const rigidBodyDesc = RAPIER.RigidBodyDesc.dynamic() .setTranslation(0.0, 5.0, 0.0); const rigidBody = world.createRigidBody(rigidBodyDesc);

const colliderDesc = RAPIER.ColliderDesc.cuboid(0.5, 0.5, 0.5); world.createCollider(colliderDesc, rigidBody);

// Simulation loop function step() { world.step();

const position = rigidBody.translation(); console.log(Body position: x=${position.x}, y=${position.y}, z=${position.z});

requestAnimationFrame(step); }

step(); });

Rapier with Three.js Integration

import * as THREE from 'three';
import('@dimforge/rapier3d-compat').then(RAPIER => {
  // Three.js setup
  const scene = new THREE.Scene();
  const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
  camera.position.set(10, 10, 10);
  camera.lookAt(0, 0, 0);

const renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(window.innerWidth, window.innerHeight); document.body.appendChild(renderer.domElement);

// Physics world const world = new RAPIER.World({ x: 0.0, y: -9.81, z: 0.0 });

// Ground const groundCollider = world.createCollider( RAPIER.ColliderDesc.cuboid(20.0, 0.1, 20.0) );

const groundGeometry = new THREE.BoxGeometry(40, 0.2, 40); const groundMaterial = new THREE.MeshStandardMaterial({ color: 0x808080 }); const groundMesh = new THREE.Mesh(groundGeometry, groundMaterial); scene.add(groundMesh);

// Helper to create physics-enabled boxes function createBox(x, y, z, width, height, depth) { // Physics const rigidBodyDesc = RAPIER.RigidBodyDesc.dynamic() .setTranslation(x, y, z); const rigidBody = world.createRigidBody(rigidBodyDesc);

const colliderDesc = RAPIER.ColliderDesc.cuboid(width / 2, height / 2, depth / 2); world.createCollider(colliderDesc, rigidBody);

// Visual const geometry = new THREE.BoxGeometry(width, height, depth); const material = new THREE.MeshStandardMaterial({ color: Math.random() * 0xffffff }); const mesh = new THREE.Mesh(geometry, material); scene.add(mesh);

return { rigidBody, mesh }; }

// Create stack of boxes const boxes = []; for (let i = 0; i < 5; i++) { for (let j = 0; j < 5; j++) { const box = createBox( i - 2, j * 1.1 + 0.5, 0, 1, 1, 1 ); boxes.push(box); } }

// Animation loop function animate() { requestAnimationFrame(animate);

// Step physics world.step();

// Sync meshes boxes.forEach(({ rigidBody, mesh }) => { const position = rigidBody.translation(); const rotation = rigidBody.rotation();

mesh.position.set(position.x, position.y, position.z); mesh.quaternion.set(rotation.x, rotation.y, rotation.z, rotation.w); });

renderer.render(scene, camera); }

animate(); });

Advanced Rapier Features

Kinematic Character Controller:

// Create character controller
const characterControllerDesc = RAPIER.CharacterControllerDesc.create(0.1);
characterControllerDesc.enableSnapToGround(0.2);
characterControllerDesc.enableAutostep(0.3, 0.1);

const characterController = world.createCharacterController(characterControllerDesc);

// Character physics body const characterBody = world.createRigidBody( RAPIER.RigidBodyDesc.kinematicPositionBased() .setTranslation(0, 5, 0) );

const characterCollider = world.createCollider( RAPIER.ColliderDesc.capsule(0.5, 0.3), characterBody );

// Movement input const movement = { x: 0, z: 0 };

document.addEventListener('keydown', (e) => { switch(e.key) { case 'w': movement.z = -0.1; break; case 's': movement.z = 0.1; break; case 'a': movement.x = -0.1; break; case 'd': movement.x = 0.1; break; } });

document.addEventListener('keyup', (e) => { switch(e.key) { case 'w': case 's': movement.z = 0; break; case 'a': case 'd': movement.x = 0; break; } });

// In animation loop function updateCharacter() { const desiredMovement = { x: movement.x, y: -0.1, z: movement.z };

characterController.computeColliderMovement( characterCollider, desiredMovement );

const correctedMovement = characterController.computedMovement(); const currentPos = characterBody.translation();

characterBody.setNextKinematicTranslation({ x: currentPos.x + correctedMovement.x, y: currentPos.y + correctedMovement.y, z: currentPos.z + correctedMovement.z }); }

Advanced Use Cases and Real-World Applications

Interactive Product Configurators

Physics-enhanced product configurators enable customers to interact naturally with 3D models, rotating, stacking, and testing products:

// Furniture arrangement tool
function createFurnitureConfigurator() {
  const world = new CANNON.World({ gravity: new CANNON.Vec3(0, -9.82, 0) });

// Room bounds (walls) const wallMaterial = new CANNON.Material('wall'); const furnitureMaterial = new CANNON.Material('furniture');

// Wall contact behavior const wallFurnitureContact = new CANNON.ContactMaterial( wallMaterial, furnitureMaterial, { friction: 0.9, restitution: 0.1 } ); world.addContactMaterial(wallFurnitureContact);

// Create walls const roomWidth = 10; const roomDepth = 10;

[ { pos: [0, 0, -roomDepth/2], size: [roomWidth, 3, 0.1] }, // Back wall { pos: [0, 0, roomDepth/2], size: [roomWidth, 3, 0.1] }, // Front wall { pos: [-roomWidth/2, 0, 0], size: [0.1, 3, roomDepth] }, // Left wall { pos: [roomWidth/2, 0, 0], size: [0.1, 3, roomDepth] } // Right wall ].forEach(({ pos, size }) => { const shape = new CANNON.Box(new CANNON.Vec3(...size.map(s => s/2))); const body = new CANNON.Body({ mass: 0, material: wallMaterial }); body.addShape(shape); body.position.set(...pos); world.addBody(body); });

// Draggable furniture pieces function addFurniture(type, x, z) { const dimensions = { sofa: { width: 2, height: 0.8, depth: 1 }, chair: { width: 0.6, height: 0.8, depth: 0.6 }, table: { width: 1.5, height: 0.7, depth: 0.8 } };

const dim = dimensions[type];

const shape = new CANNON.Box( new CANNON.Vec3(dim.width/2, dim.height/2, dim.depth/2) );

const body = new CANNON.Body({ mass: 10, material: furnitureMaterial, linearDamping: 0.9, angularDamping: 0.9 });

body.addShape(shape); body.position.set(x, dim.height/2, z);

// Lock Y rotation for upright furniture body.angularFactor.set(0, 1, 0);

world.addBody(body);

return { body, type, dimensions: dim }; }

return { world, addFurniture }; }

Educational Physics Simulations

Interactive physics demonstrations make abstract concepts tangible:

// Pendulum simulation with damping controls
function createPendulumLab() {
  const world = Matter.Engine.create();

// Pendulum parameters (controllable) const params = { length: 200, mass: 1, damping: 0.01, gravity: 1 };

world.gravity.y = params.gravity;

// Create pendulum function createPendulum(x, y) { const pivot = Matter.Bodies.circle(x, y, 5, { isStatic: true, render: { fillStyle: '#333' } });

const bob = Matter.Bodies.circle( x, y + params.length, 20, { density: params.mass, render: { fillStyle: '#e74c3c' } } );

const constraint = Matter.Constraint.create({ bodyA: pivot, bodyB: bob, length: params.length, stiffness: 1, damping: params.damping });

return { pivot, bob, constraint }; }

const pendulum = createPendulum(400, 100);

Matter.World.add(world.world, [ pendulum.pivot, pendulum.bob, pendulum.constraint ]);

// Interactive controls const controls = { length: document.getElementById('length-slider'), damping: document.getElementById('damping-slider'), gravity: document.getElementById('gravity-slider') };

controls.length.addEventListener('input', (e) => { params.length = parseFloat(e.target.value); pendulum.constraint.length = params.length; });

controls.damping.addEventListener('input', (e) => { params.damping = parseFloat(e.target.value); pendulum.constraint.damping = params.damping; });

controls.gravity.addEventListener('input', (e) => { params.gravity = parseFloat(e.target.value); world.gravity.y = params.gravity; });

// Measure period and amplitude let previousAngle = 0; let peakDetection = [];

Matter.Events.on(world, 'afterUpdate', () => { const angle = Math.atan2( pendulum.bob.position.y - pendulum.pivot.position.y, pendulum.bob.position.x - pendulum.pivot.position.x );

// Detect peaks for period calculation if (previousAngle > 0 && angle < 0) { const time = world.timing.timestamp; if (peakDetection.length > 0) { const period = time - peakDetection[peakDetection.length - 1]; console.log(Period: ${period.toFixed(2)}ms); } peakDetection.push(time); if (peakDetection.length > 10) peakDetection.shift(); }

previousAngle = angle; });

return { world, pendulum, params }; }

Game Physics: Platformer Example

// 2D platformer with Matter.js
function createPlatformerGame() {
  const engine = Matter.Engine.create();
  const world = engine.world;

// Player character const player = Matter.Bodies.rectangle(100, 100, 40, 60, { inertia: Infinity, // Prevent rotation friction: 0.001, frictionAir: 0.01, render: { fillStyle: '#3498db' } });

// Platforms const platforms = [ Matter.Bodies.rectangle(200, 400, 400, 30, { isStatic: true, render: { fillStyle: '#2c3e50' } }), Matter.Bodies.rectangle(500, 300, 200, 30, { isStatic: true, render: { fillStyle: '#2c3e50' } }), Matter.Bodies.rectangle(100, 200, 150, 30, { isStatic: true, render: { fillStyle: '#2c3e50' } }) ];

Matter.World.add(world, [player, ...platforms]);

// Player controls const keys = { left: false, right: false, jump: false }; let isGrounded = false;

document.addEventListener('keydown', (e) => { if (e.key === 'a' || e.key === 'ArrowLeft') keys.left = true; if (e.key === 'd' || e.key === 'ArrowRight') keys.right = true; if (e.key === ' ') keys.jump = true; });

document.addEventListener('keyup', (e) => { if (e.key === 'a' || e.key === 'ArrowLeft') keys.left = false; if (e.key === 'd' || e.key === 'ArrowRight') keys.right = false; if (e.key === ' ') keys.jump = false; });

// Ground detection Matter.Events.on(engine, 'collisionStart', (event) => { event.pairs.forEach(pair => { if (pair.bodyA === player || pair.bodyB === player) { isGrounded = true; } }); });

Matter.Events.on(engine, 'collisionEnd', (event) => { event.pairs.forEach(pair => { if (pair.bodyA === player || pair.bodyB === player) { isGrounded = false; } }); });

// Game loop Matter.Events.on(engine, 'beforeUpdate', () => { const moveForce = 0.001; const jumpForce = 0.08;

if (keys.left) { Matter.Body.applyForce(player, player.position, { x: -moveForce, y: 0 }); }

if (keys.right) { Matter.Body.applyForce(player, player.position, { x: moveForce, y: 0 }); }

if (keys.jump && isGrounded) { Matter.Body.setVelocity(player, { x: player.velocity.x, y: -jumpForce }); }

// Cap horizontal velocity if (Math.abs(player.velocity.x) > 5) { Matter.Body.setVelocity(player, { x: Math.sign(player.velocity.x) * 5, y: player.velocity.y }); } });

return { engine, player }; }

Performance Optimization Strategies

Reducing Active Bodies

Sleeping Bodies: Physics engines automatically deactivate (sleep) bodies that haven't moved recently:

// Matter.js sleeping configuration
const engine = Matter.Engine.create({
  enableSleeping: true
});

// Adjust sleep thresholds Matter.Sleeping.set(body, { sleepThreshold: 60, // Frames of inactivity before sleeping sleepMinTimeout: 500 // Minimum time before sleeping eligible });

Object Pooling: Reuse physics bodies instead of creating/destroying:

class BulletPool {
  constructor(world, size) {
    this.world = world;
    this.pool = [];
    this.active = [];

// Pre-create bullets for (let i = 0; i < size; i++) { const bullet = Matter.Bodies.circle(0, 0, 5, { isSleeping: true, render: { fillStyle: '#f39c12' } }); this.pool.push(bullet); Matter.World.add(this.world, bullet); } }

spawn(x, y, velocity) { const bullet = this.pool.pop();

if (!bullet) return null; // Pool exhausted

// Activate and position Matter.Sleeping.set(bullet, false); Matter.Body.setPosition(bullet, { x, y }); Matter.Body.setVelocity(bullet, velocity);

this.active.push(bullet);

return bullet; }

recycle(bullet) { // Deactivate Matter.Sleeping.set(bullet, true); Matter.Body.setPosition(bullet, { x: -1000, y: -1000 }); // Off-screen Matter.Body.setVelocity(bullet, { x: 0, y: 0 });

// Return to pool this.active = this.active.filter(b => b !== bullet); this.pool.push(bullet); }

update() { // Recycle bullets that left screen this.active.forEach(bullet => { if (bullet.position.y > 1000 || bullet.position.x > 1000) { this.recycle(bullet); } }); } }

Collision Filtering

Reduce collision checks by filtering which objects can collide:

// Matter.js collision groups
const Category = {
  PLAYER: 0x0001,
  ENEMY: 0x0002,
  BULLET: 0x0004,
  WALL: 0x0008,
  PICKUP: 0x0016
};

// Player collides with enemies, walls, and pickups (not bullets) const player = Matter.Bodies.circle(100, 100, 20, { collisionFilter: { category: Category.PLAYER, mask: Category.ENEMY | Category.WALL | Category.PICKUP } });

// Enemy bullet collides with player and walls only const enemyBullet = Matter.Bodies.circle(200, 200, 5, { collisionFilter: { category: Category.BULLET, mask: Category.PLAYER | Category.WALL } });

Spatial Partitioning

Some engines automatically use spatial partitioning, but understanding helps optimization:

// Cannon.js broadphase configuration
const world = new CANNON.World({
  broadphase: new CANNON.SAPBroadphase(world) // Sweep and Prune
});

// Or grid-based world.broadphase = new CANNON.GridBroadphase();

Comparison with Alternatives

Feature Comparison Matrix

| Feature | Matter.js | Cannon.js | Rapier | Ammo.js | Oimo.js | |---------|-----------|-----------|--------|---------|---------| | Dimensions | 2D | 3D | 2D/3D | 3D | 3D | | Language | JavaScript | JavaScript | Rust/WASM | C++/WASM | JavaScript | | Performance | Good | Good | Excellent | Excellent | Good | | Learning Curve | Easy | Moderate | Moderate | Hard | Moderate | | Documentation | Excellent | Good | Good | Limited | Limited | | Ecosystem | Large | Moderate | Growing | Small | Small | | File Size | ~100KB | ~200KB | ~500KB | ~1MB | ~150KB | | Soft Bodies | Yes | Limited | No | Yes | No | | Vehicle Physics | No | Yes | Yes | Yes | Yes | | Character Controllers | No | No | Yes | Yes | No | | Multiplayer/Determinism | Good | Good | Excellent | Good | Good | | License | MIT | MIT | Apache 2.0 | zlib | MIT |

When to Choose Each Engine

Matter.js:

  • •2D games, animations, or interactive experiences
  • •Rapid prototyping and quick iterations
  • •Teams without deep physics knowledge
  • •Projects prioritizing ecosystem and community support
  • •Creative coding and generative art

Cannon.js:

  • •3D projects already using Three.js
  • •Moderate physics requirements (not extreme performance needs)
  • •Vehicle simulations and mechanical systems
  • •Projects requiring established Three.js integration patterns

Rapier:

  • •Performance-critical applications
  • •Large-scale physics simulations (1000+ bodies)
  • •Multiplayer games requiring determinism
  • •Projects needing both 2D and 3D physics
  • •Advanced features like character controllers

Ammo.js:

  • •Existing Bullet Physics C++ expertise on team
  • •Complex soft-body physics requirements
  • •Projects needing Bullet compatibility

Oimo.js:

  • •Lightweight 3D physics needs
  • •Projects prioritizing small bundle size
  • •Simple 3D object interactions

Conclusion

JavaScript physics engines have matured from experimental curiosities into production-ready tools that power everything from marketing websites to browser-based games to educational simulations. Matter.js democratizes 2D physics through approachable APIs and comprehensive documentation, enabling developers without physics backgrounds to create compelling interactive experiences. Cannon.js brings realistic motion to Three.js scenes, opening possibilities for product configurators, architectural visualizations, and 3D web games. Rapier pushes performance boundaries through WebAssembly, delivering desktop-class physics simulation in browsers.

The practical applications extend across industries: marketing teams create memorable interactive brand experiences, educators build intuitive demonstrations of physics concepts, game developers ship browser-based titles rivaling native games, and product teams enable customers to interact naturally with 3D models. The barrier to entry has never been lower—comprehensive documentation, visual debugging tools, and extensive examples accelerate development from concept to deployment.

Looking forward, WebAssembly-based engines like Rapier signal the future: native-level performance, advanced features previously limited to desktop applications, and expanding capabilities that blur lines between web and native development. Multiplayer physics games with hundreds of simultaneous players, real-time architectural simulations, and procedurally generated physics-based worlds become feasible as performance headroom increases.

For developers exploring physics integration, the path forward is clear: start with Matter.js for 2D needs or Cannon.js for 3D projects, understanding that both provide production-ready foundations with active communities. As requirements grow or performance becomes critical, migrate to Rapier's high-performance WASM implementation. The JavaScript physics ecosystem offers solutions spanning simple animations to complex simulations—all deployable through standard web browsers without plugins or native installations.

The web has evolved from static documents to dynamic, physics-driven experiences that feel tangible and responsive. JavaScript physics engines are the foundation enabling this transformation, and mastering them opens creative possibilities limited only by imagination.

Key Features

  • ▸Matter.js Accessibility

    Elegant 2D physics with intuitive API, rich ecosystem, and visual debugging tools

  • ▸Cannon.js 3D Integration

    Lightweight 3D physics optimized for Three.js with vehicle and constraint systems

  • ▸Rapier Performance

    Rust/WASM foundation delivering 3-10x performance with advanced features like CCD

  • ▸Production Use Cases

    Interactive product configurators, educational simulations, and browser-based games

Related Links

  • Matter.js ↗
  • Cannon.js ↗
  • Rapier ↗
  • Three.js ↗