Model Bell Chime Resonance Using Matter.js Springs

This article explains how to simulate the acoustic vibrations and exponential resonance decay of a bell chime by modeling it as a coupled mass-spring oscillator network in Matter.js. By discretizing a chime into a series of interconnected rigid bodies joined by tuned elastic constraints, you can reproduce high-frequency flexural standing waves and capture natural decay dynamics directly within a 2D physics engine.

The Physics of Coupled Oscillator Chimes

A tubular bell or chime vibrates through transverse flexural modes rather than purely rigid-body swing. To approximate continuous beam vibration in Matter.js:

  1. Discretization: The chime is represented as a 1D chain of small point-mass bodies.
  2. Coupling: Adjacent masses are linked using stiff linear constraints (Matter.Constraint) that act as restoring springs.
  3. Cross-bracing (Shear Resistance): Secondary springs skipping one node (connecting mass \(i\) to \(i+2\)) simulate bending stiffness and beam elasticity.
  4. Decay (Damping): Internal friction is handled by constraint damping, while acoustic radiation and air resistance are handled by body-level air friction.

Step 1: Configuring Engine and World Parameters

Matter.js defaults to iterative relaxation designed for rigid bodies, which can prematurely bleed energy from small, high-frequency oscillations. To achieve stable, sustained resonance, increase solver iterations and configure an appropriate sub-stepping rate.

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

const engine = Engine.create({
  positionIterations: 12,
  velocityIterations: 12,
  gravity: { x: 0, y: 0.5, scale: 0.001 } // Low gravity to emphasize vibrational dynamics
});

Step 2: Building the Discretized Chime Structure

Create an array of small, uniform bodies aligned vertically. Anchor the top body to a fixed point to represent the chime's mounting string, leaving the lower nodes free to vibrate.

const nodes = [];
const nodeCount = 12;
const nodeRadius = 6;
const spacing = 18;
const startX = 400;
const startY = 100;

// Create mass nodes
for (let i = 0; i < nodeCount; i++) {
  const node = Bodies.circle(startX, startY + i * spacing, nodeRadius, {
    mass: 1.0,
    frictionAir: 0.0005, // Controls global resonance decay rate
    restitution: 0.95
  });
  nodes.push(node);
}

// Fixed suspension anchor for the top node
const anchor = Constraint.create({
  pointA: { x: startX, y: startY - spacing },
  bodyB: nodes[0],
  pointB: { x: 0, y: 0 },
  stiffness: 0.9,
  damping: 0.01
});

Step 3: Coupling Nodes with Tuned Springs

To model longitudinal tension and transverse flexural resistance, apply two sets of constraints:

const springs = [];

for (let i = 0; i < nodeCount - 1; i++) {
  // Nearest-neighbor constraint (axial stiffness)
  springs.push(Constraint.create({
    bodyA: nodes[i],
    bodyB: nodes[i + 1],
    stiffness: 0.85,
    damping: 0.002 // Internal material damping
  }));

  // Next-nearest-neighbor constraint (bending stiffness)
  if (i < nodeCount - 2) {
    springs.push(Constraint.create({
      bodyA: nodes[i],
      bodyB: nodes[i + 2],
      stiffness: 0.45,
      damping: 0.005
    }));
  }
}

Composite.add(engine.world, [...nodes, anchor, ...springs]);

Step 4: Exciting the Chime (Impact)

A physical strike imparts a localized transverse impulse to the chime. Target a node near the bottom or middle to generate a combination of fundamental and harmonic frequencies.

function strikeChime(intensity = 0.05) {
  // Strike the third node from the bottom
  const targetNode = nodes[nodes.length - 3];
  Body.applyForce(targetNode, targetNode.position, { x: intensity, y: 0 });
}

Step 5: Controlling Resonance Decay

Resonance decay in this coupled model follows an exponential decay envelope determined by two primary variables:

Step 6: Extracting the Vibrational Signal

To measure the vibration or synthesize audio from the physics model, track the horizontal displacement or velocity of the bottom-most antinode over successive engine ticks:

Matter.Events.on(engine, 'afterUpdate', () => {
  const antinode = nodes[nodes.length - 1];
  const displacementX = antinode.position.x - startX;
  const velocityX = antinode.velocity.x;

  // displacementX contains the instantaneous resonant wave amplitude
});

By adjusting the stiffness ratio between axial and flexural constraints alongside node mass, you can precisely shape the natural frequencies and timbre of the chime.