Simulating Heart Valve Leaflets in Matter.js

This guide explains how to simulate the mechanical opening and closing of heart valve leaflets under cyclic pressure using Matter.js. You will learn how to construct the anatomical geometry with rigid bodies, attach rotational constraints to mimic natural hinge points, apply angular limits to prevent prolapse, and drive the motion using a periodic force function that mimics physiological hemodynamics.

1. Architectural Overview

A typical bi-leaflet or tri-leaflet heart valve (such as the aortic or mitral valve) operates passively in response to pressure gradients across the valve orifice. In a 2D physics engine like Matter.js, this system consists of:

2. Initializing the Physics Environment

Set up the Matter.js engine, world, and rendering canvas:

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

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

// Disable default gravity to simulate localized fluid pressure directly
world.gravity.y = 0;
world.gravity.x = 0;

const render = Render.create({
    element: document.body,
    engine: engine,
    options: {
        width: 800,
        height: 600,
        wireframes: false
    }
});

Render.run(render);
Runner.run(Runner.create(), engine);

3. Constructing Leaflets and Hinge Anchors

To model a bi-leaflet valve, create two angled leaflets anchored symmetrically along a central orifice:

const leafletWidth = 120;
const leafletHeight = 12;

// Left Leaflet
const leftLeaflet = Bodies.rectangle(340, 300, leafletWidth, leafletHeight, {
    chamfer: { radius: 4 },
    density: 0.002,
    frictionAir: 0.05 // Air friction mimics fluid drag/damping
});

// Right Leaflet
const rightLeaflet = Bodies.rectangle(460, 300, leafletWidth, leafletHeight, {
    chamfer: { radius: 4 },
    density: 0.002,
    frictionAir: 0.05
});

// Anchor left leaflet at its outer edge
const leftHinge = Constraint.create({
    pointA: { x: 280, y: 300 },
    bodyB: leftLeaflet,
    pointB: { x: -leafletWidth / 2, y: 0 },
    stiffness: 1,
    length: 0
});

// Anchor right leaflet at its outer edge
const rightHinge = Constraint.create({
    pointA: { x: 520, y: 300 },
    bodyB: rightLeaflet,
    pointB: { x: leafletWidth / 2, y: 0 },
    stiffness: 1,
    length: 0
});

Composite.add(world, [leftLeaflet, rightLeaflet, leftHinge, rightHinge]);

4. Implementing Coaptation and Motion Limits

Real leaflets cannot rotate backward through the annular plane (prolapse) and are mechanically limited when fully opened. While static invisible collision bodies can serve as mechanical stops, constraining leaflet angles programmatically inside the engine update loop ensures numerical stability:

const MIN_ANGLE_LEFT = -Math.PI / 6;  // Fully closed (slight forward incline)
const MAX_ANGLE_LEFT = Math.PI / 3;   // Fully open

const MIN_ANGLE_RIGHT = -Math.PI / 3; // Fully open
const MAX_ANGLE_RIGHT = Math.PI / 6;  // Fully closed

Events.on(engine, 'beforeUpdate', () => {
    // Clamp Left Leaflet
    if (leftLeaflet.angle < MIN_ANGLE_LEFT) {
        Body.setAngle(leftLeaflet, MIN_ANGLE_LEFT);
        Body.setAngularVelocity(leftLeaflet, 0);
    } else if (leftLeaflet.angle > MAX_ANGLE_LEFT) {
        Body.setAngle(leftLeaflet, MAX_ANGLE_LEFT);
        Body.setAngularVelocity(leftLeaflet, 0);
    }

    // Clamp Right Leaflet
    if (rightLeaflet.angle < MIN_ANGLE_RIGHT) {
        Body.setAngle(rightLeaflet, MIN_ANGLE_RIGHT);
        Body.setAngularVelocity(rightRightLeaflet = rightLeaflet, 0);
    } else if (rightLeaflet.angle > MAX_ANGLE_RIGHT) {
        Body.setAngle(rightLeaflet, MAX_ANGLE_RIGHT);
        Body.setAngularVelocity(rightLeaflet, 0);
    }
});

5. Applying Cyclic Pressure Forces

The driving force across the valve alternates between a forward pressure gradient (systole) and an adverse pressure gradient (diastole). This can be modeled using a sinusoidal or half-wave rectified pressure function:

\[\Delta P(t) = P_{\text{peak}} \cdot \sin(\omega t)\]

Apply this differential pressure as a normal vector force directly to the center of each leaflet:

const frequency = 1.2; // Cardiac cycles per second (~72 BPM)
const peakPressure = 0.015; // Force magnitude

Events.on(engine, 'beforeUpdate', (event) => {
    const time = event.timestamp / 1000;
    
    // Waveform: Positive during systole (forward flow), negative during diastole (reverse flow)
    const pressure = Math.sin(2 * Math.PI * frequency * time) * peakPressure;

    const leaflets = [leftLeaflet, rightLeaflet];

    leaflets.forEach(leaflet => {
        // Calculate the normal vector perpendicular to leaflet surface
        const normalAngle = leaflet.angle - Math.PI / 2;
        const forceMagnitude = pressure;

        const force = {
            x: Math.cos(normalAngle) * forceMagnitude,
            y: Math.sin(normalAngle) * forceMagnitude
        };

        Body.applyForce(leaflet, leaflet.position, force);
    });
});

6. Tuning for Biological Realism

To achieve physiologically accurate movement:

  1. Damping: Increase frictionAir (between 0.04 and 0.1) on both leaflets. Blood is viscous; leaflets do not oscillate freely like rigid boards in a vacuum.
  2. Asymmetric Cycles: Replace the pure sine wave with an asymmetric piecewise curve (e.g., 35% systole duration, 65% diastole duration) to replicate true ventricular pressure curves.
  3. Tip Coaptation: Add a small restitution buffer or elastic collision body between the tips to prevent numerical interpenetration during the closed phase.