Attach Particle Emitters to Matter.js Bodies

This article explains how to attach dynamic particle emitters to Matter.js physics bodies to create realistic smoke trail effects. You will learn how to decouple physics simulation from visual rendering, calculate dynamic emission offsets based on body rotation, update particle states across animation ticks, and draw fading smoke trails using the HTML5 Canvas API.

Understanding the Architecture

Matter.js is primarily a 2D physics engine, not a full visual framework. While it includes a built-in canvas renderer (Matter.Render), custom visual effects like smoke, fire, or sparks are best implemented by overlaying a secondary rendering loop or hooking directly into the engine's update cycle via Matter.Events.

To create a smoke trail:

  1. Track the physical body’s current position (body.position) and angle (body.angle).
  2. Calculate the local offset where the smoke should originate (such as the rear of a rocket or vehicle).
  3. Spawn particles at that calculated global coordinate during each physics update.
  4. Render and update the particles independently so they drift, expand, and fade away in the world space.

Calculating the Emission Point

If you attach a thruster or exhaust to a body, the emission point must rotate alongside the body. You can translate a local offset \((x_{offset}, y_{offset})\) relative to the body's center into world coordinates using basic trigonometry:

function getEmitterPosition(body, localOffsetX, localOffsetY) {
  const cos = Math.cos(body.angle);
  const sin = Math.sin(body.angle);

  return {
    x: body.position.x + (localOffsetX * cos - localOffsetY * sin),
    y: body.position.y + (localOffsetX * sin + localOffsetY * cos)
  };
}

Implementing the Smoke Particle System

A basic particle system requires a collection to hold active particles, a spawn function, and an update/render step.

class SmokeParticle {
  constructor(x, y, baseAngle) {
    this.x = x;
    this.y = y;
    
    // Slight spread backwards relative to body direction
    const spread = (Math.random() - 0.5) * 0.5;
    const speed = Math.random() * 1.5 + 0.5;
    const travelAngle = baseAngle + Math.PI + spread;

    this.vx = Math.cos(travelAngle) * speed;
    this.vy = Math.sin(travelAngle) * speed;
    
    this.radius = Math.random() * 4 + 4;
    this.maxLife = Math.random() * 30 + 40;
    this.life = this.maxLife;
  }

  update() {
    this.x += this.vx;
    this.y += this.vy;
    this.radius += 0.3; // Smoke expands as it cools
    this.life -= 1;
  }

  draw(ctx) {
    const alpha = Math.max(this.life / this.maxLife, 0) * 0.4;
    ctx.save();
    ctx.beginPath();
    ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
    ctx.fillStyle = `rgba(180, 180, 180, ${alpha})`;
    ctx.fill();
    ctx.restore();
  }

  isDead() {
    return this.life <= 0;
  }
}

Hooking into the Matter.js Event Loop

Use Matter.Events.on(engine, 'afterUpdate', ...) to bind particle emission directly to the physics simulation step. This ensures that the smoke trails stay synchronized with your physics rate even during frame drops.

const particles = [];
const exhaustOffset = { x: -30, y: 0 }; // Behind the center of mass

// Hook into the physics engine update
Matter.Events.on(engine, 'afterUpdate', () => {
  // Only emit smoke if the body is moving significantly or actively accelerating
  const speed = body.speed;
  if (speed > 0.5) {
    const origin = getEmitterPosition(body, exhaustOffset.x, exhaustOffset.y);
    // Spawn 1-2 particles per update
    particles.push(new SmokeParticle(origin.x, origin.y, body.angle));
  }

  // Update existing particles
  for (let i = particles.length - 1; i >= 0; i--) {
    particles[i].update();
    if (particles[i].isDead()) {
      particles.splice(i, 1);
    }
  }
});

Rendering the Effect

Render the particles underneath or directly on top of the Matter.js bodies. If you use Matter.Render, you can hook into its render event:

Matter.Events.on(render, 'afterRender', () => {
  const ctx = render.context;
  for (let i = 0; i < particles.length; i++) {
    particles[i].draw(ctx);
  }
});

If you are using an external renderer such as PixiJS or a separate HTML5 Canvas, call particle.draw(ctx) in your main requestAnimationFrame render loop right before or after you draw your physics sprites.