How to Build a Soft Body Jelly in Matter.js
This article explains how to simulate a soft-body jelly creature using the Matter.js 2D physics engine. By chaining together a perimeter ring of rigid circular point masses with elastic distance constraints, combined with interior cross-bracing springs to maintain volume, you can recreate convincing squishy, deformable organic physics inside a browser canvas.
Understanding the Physics Model
Matter.js is primarily a rigid-body physics engine, meaning it does not have a native deformable mesh engine. However, soft-body dynamics can be approximated using a mass-spring system composed of three elements:
- Point Masses: A circular array of small, low-radius
rigid bodies (
Matter.Bodies.circle) representing the outer skin of the creature. - Perimeter Constraints: Distance constraints
(
Matter.Constraint.create) connecting each point mass to its immediate neighbors to form the outer boundary. - Cross-Bracing Constraints: Internal distance constraints connecting opposing or diagonal point masses across the ring to act as internal pressure and prevent the shape from collapsing under gravity or impact.
Step 1: Set Up Matter.js
Initialize the foundational Matter.js modules:
const { Engine, Render, Runner, Bodies, Composite, Constraint } = Matter;
const engine = Engine.create();
const world = engine.world;
const render = Render.create({
element: document.body,
engine: engine,
options: {
width: 800,
height: 600,
wireframes: false
}
});
Render.run(render);
Runner.run(Runner.create(), engine);Step 2: Generate the Outer Ring of Masses
Place the point masses in a circle around a central coordinate \((x, y)\).
const centerX = 400;
const centerY = 200;
const creatureRadius = 80;
const particleCount = 16;
const particleRadius = 6;
const particles = [];
for (let i = 0; i < particleCount; i++) {
const angle = (i / particleCount) * Math.PI * 2;
const px = centerX + Math.cos(angle) * creatureRadius;
const py = centerY + Math.sin(angle) * creatureRadius;
const particle = Bodies.circle(px, py, particleRadius, {
friction: 0.5,
restitution: 0.2,
density: 0.002,
collisionFilter: { group: -1 } // Negative group prevents self-collision
});
particles.push(particle);
}
Composite.add(world, particles);Step 3: Link Adjacent Points with Perimeter Springs
Connect each node to the next in the array, wrapping the final connection back to the first node to close the loop.
const perimeterStiffness = 0.9;
const damping = 0.1;
const perimeterConstraints = [];
for (let i = 0; i < particleCount; i++) {
const nextIndex = (i + 1) % particleCount;
const spring = Constraint.create({
bodyA: particles[i],
bodyB: particles[nextIndex],
stiffness: perimeterStiffness,
damping: damping,
render: { strokeStyle: '#444', lineWidth: 2 }
});
perimeterConstraints.push(spring);
}
Composite.add(world, perimeterConstraints);Step 4: Add Cross-Bracing for Volume Preservation
Without internal tension, the perimeter ring will fold flat when it collides with a surface. Adding cross-braces across opposing nodes provides structural integrity while remaining flexible.
const internalStiffness = 0.2;
const crossConstraints = [];
for (let i = 0; i < particleCount; i++) {
// Connect to opposite particle across the circle
const oppositeIndex = (i + Math.floor(particleCount / 2)) % particleCount;
// Avoid duplicate reverse constraints
if (i < oppositeIndex) {
const crossSpring = Constraint.create({
bodyA: particles[i],
bodyB: particles[oppositeIndex],
stiffness: internalStiffness,
damping: damping,
render: { strokeStyle: '#888', lineWidth: 1 }
});
crossConstraints.push(crossSpring);
}
// Connect to an offset node (e.g., +4) for shear resistance
const diagonalIndex = (i + Math.floor(particleCount / 4)) % particleCount;
if (i < diagonalIndex) {
const diagonalSpring = Constraint.create({
bodyA: particles[i],
bodyB: particles[diagonalIndex],
stiffness: internalStiffness * 0.5,
damping: damping,
render: { visible: false }
});
crossConstraints.push(diagonalSpring);
}
}
Composite.add(world, crossConstraints);Tuning and Rendering
To refine the jelly-like behavior:
- Stiffness: Lower values (0.05 to 0.3) create loose, gelatinous bodies. Higher values (0.7 to 1.0) produce firm, rubbery objects.
- Damping: Controls how quickly vibrations settle. Values between 0.05 and 0.1 prevent endless jitter.
- Custom Rendering: Instead of rendering the individual bodies and springs, draw a closed path connecting the coordinates of each perimeter mass using the HTML5 Canvas 2D context, then fill the path with color to produce a solid, organic creature.