Simulating Electrostatic Repulsion in Matter.js

This article explains how to simulate electrostatic repulsion between identically charged bodies using Matter.js. While Matter.js is a rigid-body 2D physics engine that lacks built-in electromagnetic mechanics, you can model electrostatic behavior by calculating repulsive forces via Coulomb's Law and applying them continuously to bodies before each physics engine update.

The Underlying Physics: Coulomb's Law

Electrostatic repulsion between two bodies carrying identical charges is governed by Coulomb's Law:

\[F = k \cdot \frac{q_1 \cdot q_2}{r^2}\]

Because identical charges repel, the resulting force vector points directly away from the other body along the line connecting their centers.

Implementation Steps in Matter.js

To implement this custom force:

  1. Assign a Charge Property: Add a custom charge property to each body during creation.
  2. Listen to the beforeUpdate Event: Use Matter.Events.on(engine, 'beforeUpdate', callback) to compute and apply forces on every frame prior to collision and position resolution.
  3. Iterate Through Body Pairs: Loop through unique pairs of charged bodies to avoid duplicate calculations.
  4. Calculate and Apply Force: Compute the distance, direction, and magnitude, then use Matter.Body.applyForce() to push the bodies apart.

Code Example

Below is a complete implementation using Matter.js:

const { Engine, Render, Runner, Bodies, Composite, Events, Vector, Body } = Matter;

// 1. Initialize engine and world
const engine = Engine.create({ gravity: { x: 0, y: 0 } }); // Zero gravity for clear observation
const world = engine.world;

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

// 2. Create charged bodies
const chargedBodies = [];
for (let i = 0; i < 15; i++) {
  const body = Bodies.circle(
    200 + Math.random() * 400,
    150 + Math.random() * 300,
    15,
    {
      restitution: 0.8,
      frictionAir: 0.05,
      render: { fillStyle: '#ff4757' }
    }
  );
  
  // Custom charge property (identical positive charge)
  body.charge = 1.0;
  
  chargedBodies.push(body);
}

Composite.add(world, chargedBodies);

// 3. Apply electrostatic repulsion before each update
const COULOMB_CONSTANT = 50; // Adjust to scale force strength
const MIN_DISTANCE = 30;     // Prevent infinite force at close proximity

Events.on(engine, 'beforeUpdate', () => {
  for (let i = 0; i < chargedBodies.length; i++) {
    for (let j = i + 1; j < chargedBodies.length; j++) {
      const bodyA = chargedBodies[i];
      const bodyB = chargedBodies[j];

      // Vector from bodyA to bodyB
      const delta = Vector.sub(bodyB.position, bodyA.position);
      const distance = Vector.magnitude(delta);

      // Clamp distance to avoid division by zero or explosive forces
      const effectiveDistance = Math.max(distance, MIN_DISTANCE);

      // Calculate force magnitude: F = k * (q1 * q2) / r^2
      const forceMagnitude = (COULOMB_CONSTANT * bodyA.charge * bodyB.charge) / (effectiveDistance * effectiveDistance);

      // Normalized direction vector
      const normal = Vector.normalise(delta);

      // Force applied to bodyB (repels away from bodyA)
      const forceOnB = Vector.mult(normal, forceMagnitude);
      // Force applied to bodyA (Newton's third law: equal and opposite)
      const forceOnA = Vector.neg(forceOnB);

      Body.applyForce(bodyA, bodyA.position, forceOnA);
      Body.applyForce(bodyB, bodyB.position, forceOnB);
    }
  }
});

Critical Considerations