Build an Interactive Galton Board in Matter.js

This article explains how to build an interactive Galton board (quincunx) simulation using the Matter.js 2D physics engine to demonstrate the Central Limit Theorem and normal distributions. You will learn how to configure the physics engine, construct static funnels and peg lattices, partition collection bins, and emit dynamic particles whose random deflections naturally assemble into a classic bell curve.

1. Initialize the Matter.js Engine

Start by importing the required Matter.js modules and initializing the core components: the engine, world, renderer, and runner. Set up an HTML <canvas> element or let Matter.js generate one automatically within a container element.

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

const engine = Engine.create();
const world = engine.world;

const width = 600;
const height = 800;

const render = Render.create({
  element: document.body,
  engine: engine,
  options: {
    width: width,
    height: height,
    wireframes: false,
    background: '#1a1a1a'
  }
});

Render.run(render);
const runner = Runner.create();
Runner.run(runner, engine);

2. Construct the Peg Grid (Triangular Lattice)

The core mechanism of a Galton board is a staggered grid of static circular pegs. Each peg introduces an independent Bernoulli trial, deflecting falling particles left or right with roughly equal probability.

Arrange the pegs in an inverted triangle or diamond pattern using nested loops:

const pegRadius = 4;
const rows = 10;
const startY = 180;
const spacing = 35;

for (let r = 0; r < rows; r++) {
  const rowY = startY + r * spacing;
  const count = r + 1;
  const startX = width / 2 - ((count - 1) * spacing) / 2;

  for (let c = 0; c < count; c++) {
    const pegX = startX + c * spacing;
    const peg = Bodies.circle(pegX, rowY, pegRadius, {
      isStatic: true,
      render: { fillStyle: '#ffffff' }
    });
    Composite.add(world, peg);
  }
}

3. Build the Funnel and Boundary Walls

To ensure balls enter the peg matrix consistently from a central origin, add angled static barriers at the top to form a funnel. Add vertical walls on the canvas edges to contain the particles.

const wallOptions = { isStatic: true, render: { fillStyle: '#444444' } };

// Funnel guides
const leftFunnel = Bodies.rectangle(width / 2 - 50, 80, 120, 10, {
  ...wallOptions,
  angle: Math.PI / 4
});
const rightFunnel = Bodies.rectangle(width / 2 + 50, 80, 120, 10, {
  ...wallOptions,
  angle: -Math.PI / 4
});

// Outer boundary walls
const leftWall = Bodies.rectangle(0, height / 2, 20, height, wallOptions);
const rightWall = Bodies.rectangle(width, height / 2, 20, height, wallOptions);
const floor = Bodies.rectangle(width / 2, height, width, 20, wallOptions);

Composite.add(world, [leftFunnel, rightFunnel, leftWall, rightWall, floor]);

4. Create Vertical Collection Bins

Beneath the lowest row of pegs, install evenly spaced vertical dividers. As particles finish their descent, these dividers sort them into discrete slots, translating physical trajectories into a visible histogram of outcomes.

const binCount = rows + 1;
const binWidth = width / binCount;
const binHeight = 200;
const binY = height - binHeight / 2;

for (let i = 0; i <= binCount; i++) {
  const dividerX = i * binWidth;
  const divider = Bodies.rectangle(dividerX, binY, 4, binHeight, {
    isStatic: true,
    render: { fillStyle: '#888888' }
  });
  Composite.add(world, divider);
}

5. Spawn Particles and Tune Physics Parameters

Emit small circular bodies continuously above the funnel. Particle properties such as restitution (bounciness) and friction dictate how purely random the deflections are. Set restitution between 0.4 and 0.6 and reduce friction to keep the particles mobile and prevent clumping.

function dropBall() {
  // Minor random offset prevents deterministic stacking at the funnel point
  const jitter = (Math.random() - 0.5) * 4;
  const ball = Bodies.circle(width / 2 + jitter, 20, 5, {
    restitution: 0.5,
    friction: 0.05,
    render: { fillStyle: '#ff4757' }
  });

  Composite.add(world, ball);
}

// Spawn a ball every 100 milliseconds
setInterval(dropBall, 100);

6. Mathematical Convergence

Each time a ball strikes a peg, its horizontal movement represents an approximately independent binary decision (\(X_i \in \{-1, 1\}\)). By the time a ball reaches the collection bins after traversing \(n\) rows, its horizontal position corresponds to the sum \(\sum_{i=1}^{n} X_i\).

According to the Central Limit Theorem, the distribution of this sum approximates a Gaussian distribution as the number of rows increases:

\[f(x) \approx \frac{1}{\sigma \sqrt{2\pi}} e^{-\frac{(x - \mu)^2}{2\sigma^2}}\]

As hundreds of particles settle into the bins, the center bins receive the highest volume of particles while the outer bins receive exponentially fewer, producing a visible normal probability curve.