How to Create an Hourglass in Matter.js
This guide explains how to construct a functional hourglass simulation in Matter.js where granular sand particles reliably flow through a narrow throat without jamming or tunneling. You will learn how to configure the physics engine settings, construct the static funnel geometry using angled bodies, generate hundreds of particle bodies with optimal physical properties, and fine-tune friction and collision parameters to ensure a smooth, realistic granular flow.
1. Engine and World Configuration
Granular simulations involve high-density collisions. To prevent particles from tunneling through the funnel walls or each other, the physics engine requires higher collision solver iterations than the default settings.
const { Engine, Render, Runner, Bodies, Composite, Body } = Matter;
const engine = Engine.create({
positionIterations: 10,
velocityIterations: 10
});
const render = Render.create({
element: document.body,
engine: engine,
options: {
width: 600,
height: 700,
wireframes: false
}
});
Render.run(render);
const runner = Runner.create();
Runner.run(runner, engine);2. Constructing the Funnel Geometry
An hourglass requires an upper chamber, a lower chamber, and a narrow throat. You can build the funnel using angled static rectangular bodies.
The neck width must be carefully calibrated against the particle diameter. In granular physics, if an opening is less than 3 to 4 times the diameter of the grains, spontaneous physical arches form, permanently blocking the flow.
const wallOptions = { isStatic: true, friction: 0.1, render: { fillStyle: '#333' } };
const throatWidth = 24; // Gap size between left and right choke points
const centerX = 300;
const centerY = 350;
const walls = [
// Upper Funnel (V-Shape)
Bodies.rectangle(centerX - 80, centerY - 90, 180, 14, {
...wallOptions,
angle: Math.PI / 4
}),
Bodies.rectangle(centerX + 80, centerY - 90, 180, 14, {
...wallOptions,
angle: -Math.PI / 4
}),
// Lower Chamber (Inverted V-Shape)
Bodies.rectangle(centerX - 80, centerY + 90, 180, 14, {
...wallOptions,
angle: -Math.PI / 4
}),
Bodies.rectangle(centerX + 80, centerY + 90, 180, 14, {
...wallOptions,
angle: Math.PI / 4
}),
// Outer Boundary Enclosure
Bodies.rectangle(centerX, centerY + 220, 300, 20, wallOptions), // Floor
Bodies.rectangle(centerX - 150, centerY, 20, 440, wallOptions), // Left wall
Bodies.rectangle(centerX + 150, centerY, 20, 440, wallOptions), // Right wall
];
Composite.add(engine.world, walls);3. Generating Sand Particles
To mimic sand, use small circular rigid bodies. Introduce slight variations in particle radius to avoid crystallization (ordered stacking), which leads to artificial structural locking above the neck.
Keep restitution (bounciness) at zero and keep friction relatively low to facilitate smooth movement through the bottleneck.
const particles = [];
const particleRadius = 3.5;
const rows = 25;
const cols = 20;
for (let i = 0; i < rows; i++) {
for (let j = 0; j < cols; j++) {
// Slight random variation in radius disrupts rigid grid formation
const r = particleRadius + (Math.random() * 0.8 - 0.4);
// Stagger positions to randomize initial contacts
const x = centerX - 60 + j * 6 + (Math.random() * 2);
const y = centerY - 260 + i * 6 + (Math.random() * 2);
const sandGrain = Bodies.circle(x, y, r, {
friction: 0.05,
frictionAir: 0.001,
restitution: 0,
density: 0.002,
render: { fillStyle: '#e0c068' }
});
particles.push(sandGrain);
}
}
Composite.add(engine.world, particles);4. Tuning to Prevent Jamming
If particles clog at the narrow throat, apply the following adjustments:
- Increase the Funnel Angle: Steep wall angles (around 45° to 60° relative to horizontal) channel downward kinetic energy more efficiently than flatter angles.
- Reduce Inter-Particle Friction: High static
friction causes particles to bind along the shear plane. Reducing
frictionon the particles from0.1down to0.02allows grains to slide past each other. - Widen the Throat: Ensure the neck clearance is at least \(4 \times r_{\text{particle}}\). For particles with a radius of \(3.5\text{px}\) (diameter \(7\text{px}\)), the throat width should be no less than \(24\text{px}\) to \(28\text{px}\).
- Active Agitation (Optional): If a very small throat
is required, apply a minute periodic force or jitter to particles
directly above the neck via
Matter.Events.on(engine, 'beforeUpdate', ...)to break structural force chains before they solidify into jams.