Spawn Impact Particles on Collision Normals in Matter.js

Matter.js provides detailed contact data during collision events, enabling developers to create realistic visual effects such as sparks, dust, or debris bursts. By listening to the collisionStart event, extracting the collision normal and contact points from collision pairs, and computing directional velocity vectors with an angular spread, you can spawn particle bursts that accurately align with the surface angle of any physical impact.

Accessing Collision Data

To spawn particles at the exact moment and location of an impact, listen to the collisionStart event emitted by the Matter.js engine. Each event payload contains a list of pairs, which represent the two colliding bodies and the mathematical details of their intersection.

Matter.Events.on(engine, 'collisionStart', (event) => {
    event.pairs.forEach((pair) => {
        // Handle collision data here
    });
});

Extracting the Normal Vector and Contact Points

The pair.collision object contains the collision normal and the contact vertices:

  1. Collision Normal (pair.collision.normal): A unit vector pointing from bodyA toward bodyB.
  2. Contact Point (pair.collision.supports): An array of vertices indicating where the contact occurred. Taking the first support point (pair.collision.supports[0]) is generally sufficient for determining the burst origin.

Because the normal always points from bodyA to bodyB, you can invert it depending on which body should emit the particles, or use the normal directly to cast particles outward from the impacted surface.

Calculating Particle Trajectory Along the Normal

To orient particles along the collision vector:

  1. Calculate the base angle using Math.atan2(normal.y, normal.x).
  2. Add a randomized offset within a desired cone angle (spread) to create a natural spray pattern.
  3. Compute the horizontal and vertical velocity components using Math.cos() and Math.sin().

Complete Implementation Example

The following self-contained implementation demonstrates how to capture collision points, compute angles from collision normals, and render bursting particles onto an HTML5 Canvas:

// Array to track active particles
const particles = [];

// Particle definition
class ImpactParticle {
    constructor(x, y, angle, speed) {
        this.x = x;
        this.y = y;
        this.vx = Math.cos(angle) * speed;
        this.vy = Math.sin(angle) * speed;
        this.life = 1.0;
        this.decay = Math.random() * 0.03 + 0.02;
        this.size = Math.random() * 3 + 2;
    }

    update() {
        this.x += this.vx;
        this.y += this.vy;
        this.life -= this.decay;
    }

    draw(ctx) {
        ctx.save();
        ctx.globalAlpha = Math.max(0, this.life);
        ctx.fillStyle = '#ffaa00';
        ctx.beginPath();
        ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
        ctx.fill();
        ctx.restore();
    }
}

// Function to trigger a burst along the normal
function createBurst(originX, originY, normal, count = 15) {
    // Base orientation from the collision normal vector
    const baseAngle = Math.atan2(normal.y, normal.x);
    const spread = Math.PI / 3; // 60-degree cone

    for (let i = 0; i < count; i++) {
        // Randomize direction within the cone
        const angle = baseAngle + (Math.random() - 0.5) * spread;
        const speed = Math.random() * 4 + 2;
        particles.push(new ImpactParticle(originX, originY, angle, speed));
    }
}

// Matter.js collision listener
Matter.Events.on(engine, 'collisionStart', (event) => {
    event.pairs.forEach((pair) => {
        const collision = pair.collision;
        
        // Ensure contact points exist
        if (collision.supports.length > 0) {
            const contactPoint = collision.supports[0];
            const normal = collision.normal;

            // Invert the normal if particles should bounce back off bodyB
            const burstNormal = {
                x: -normal.x,
                y: -normal.y
            };

            createBurst(contactPoint.x, contactPoint.y, burstNormal);
        }
    });
});

// Canvas animation loop
function renderParticles(ctx) {
    for (let i = particles.length - 1; i >= 0; i--) {
        const p = particles[i];
        p.update();
        p.draw(ctx);

        if (p.life <= 0) {
            particles.splice(i, 1);
        }
    }
}

Filtering Low-Impact Collisions

To prevent particles from spawning during slow resting contacts or sliding motions, calculate the relative velocity between the two bodies. Multiply the relative velocity by the collision normal to get the normal impact speed, and only spawn particles if this value exceeds a defined threshold:

const relativeVelocity = {
    x: pair.bodyA.velocity.x - pair.bodyB.velocity.x,
    y: pair.bodyA.velocity.y - pair.bodyB.velocity.y
};

const impactSpeed = Math.abs(
    relativeVelocity.x * collision.normal.x + 
    relativeVelocity.y * collision.normal.y
);

if (impactSpeed > 1.5) {
    createBurst(contactPoint.x, contactPoint.y, burstNormal);
}