Frictionless Ice Patches in Matter.js

Creating realistic ice patches in Matter.js that preserve momentum indefinitely requires overcoming both surface contact friction and the physics engine's default air resistance. While reducing a surface's friction to zero is intuitive, Matter.js bodies will naturally decelerate due to ambient air drag unless specifically configured. This guide demonstrates how to build zero-friction zones by combining collision filtering, zero-friction properties, and dynamic air-resistance toggling using Matter.js collision events.

The Challenge with Default Physics

Matter.js calculates contact friction between two colliding bodies by finding the minimum friction value between them using the formula Math.min(bodyA.friction, bodyB.friction). Setting the ice patch's friction to 0 successfully removes surface resistance.

However, all rigid bodies in Matter.js are assigned a default frictionAir value of 0.01. Even if surface friction is completely eliminated, this ambient drag causes moving objects to eventually slow down and stop. To preserve momentum indefinitely, you must dynamically negate frictionAir for as long as the body remains in contact with the ice.

Step 1: Create the Ice Surface

Define the ice patch with friction, frictionStatic, and restitution set to eliminate all surface drag:

const icePatch = Matter.Bodies.rectangle(400, 500, 300, 20, {
  isStatic: true,
  friction: 0,
  frictionStatic: 0,
  label: 'icePatch'
});

Matter.Composite.add(engine.world, icePatch);

Step 2: Handle Air Drag via Collision Events

To keep momentum constant, listen for collision events on the engine. When a body enters the ice patch, save its original frictionAir value and set it to 0. When the body leaves the patch, restore its original drag.

// Track initial drag and set frictionAir to 0 on contact
Matter.Events.on(engine, 'collisionStart', (event) => {
  event.pairs.forEach((pair) => {
    const { bodyA, bodyB } = pair;

    if (bodyA.label === 'icePatch' || bodyB.label === 'icePatch') {
      const dynamicBody = bodyA.label === 'icePatch' ? bodyB : bodyA;

      if (!dynamicBody.isStatic) {
        // Store the original frictionAir if not already saved
        if (dynamicBody.plugin === undefined) dynamicBody.plugin = {};
        if (dynamicBody.plugin.originalFrictionAir === undefined) {
          dynamicBody.plugin.originalFrictionAir = dynamicBody.frictionAir;
        }

        dynamicBody.frictionAir = 0;
        dynamicBody.friction = 0;
      }
    }
  });
});

// Restore original drag when exiting the ice
Matter.Events.on(engine, 'collisionEnd', (event) => {
  event.pairs.forEach((pair) => {
    const { bodyA, bodyB } = pair;

    if (bodyA.label === 'icePatch' || bodyB.label === 'icePatch') {
      const dynamicBody = bodyA.label === 'icePatch' ? bodyB : bodyA;

      if (!dynamicBody.isStatic && dynamicBody.plugin?.originalFrictionAir !== undefined) {
        dynamicBody.frictionAir = dynamicBody.plugin.originalFrictionAir;
        dynamicBody.friction = 0.1; // Restore default or intended surface friction
        delete dynamicBody.plugin.originalFrictionAir;
      }
    }
  });
});

Step 3: Prevent Angular Deceleration (Optional)

If you also want the object to spin infinitely while sliding across the ice, set its angular velocity retention by removing rotational damping. Alongside setting frictionAir to 0, you can store and clear rotational resistance by overriding angular damping in a beforeUpdate event or ensuring no torque is applied to the sliding object.

By neutralizing both surface friction and air friction during collision states, bodies entering the ice surface will maintain their exact linear velocity until they exit the patch or collide with another obstacle.