If you have spent more than ten minutes idling on indie gaming portals, you have almost certainly poked, dragged, and sliced a wobbling sheet of digital fabric with your cursor. There is something endlessly hypnotic about tearable cloth simulators running right in a browser tab.
Building one does not require a bulky physics engine or a degree in advanced mechanics. In fact, you can create a remarkably stable, tactile cloth simulator in vanilla JavaScript using Verlet integration.
What Is Verlet Integration in Game Development?
Direct Answer: Verlet integration is a numerical method used to calculate trajectories and positions of moving objects. Instead of calculating velocity explicitly (like standard Euler integration), Verlet computes an object’s current velocity implicitly using its current position and its previous position:
next_position = current_position + (current_position - previous_position) + acceleration * (delta_time^2)
In browser-based game development, Verlet physics provides rock-solid stability for soft-body dynamics, ropes, chains, and woven fabric without the explosive jitter common to naive Euler calculations.
Why Euler Fails (and Verlet Saves the Day)
Most fledgling developers start with standard Euler physics: add velocity to position, apply acceleration to velocity, and hope nothing explodes. When you connect twenty moving particles together with rigid constraints using Euler maths, slight rounding errors accumulate rapidly. Within seconds, your digital cape violently vibrates into deep space.
Verlet integration bypasses this headache entirely. Because the velocity is baked into the delta between the current and previous frames, constraints do not fight against stored momentum vectors. If you manually yank a point back to satisfy a distance constraint, you automatically update its velocity for the next tick. It is delightfully forgiving, self-correcting, and easy on the CPU.
| Physics Method | Mathematical Complexity | Constraint Stability | Best Used For |
|---|---|---|---|
| Explicit Euler | Ultra-low | Terrible (blows up easily) | Basic arcade ball bouncing |
| Verlet Integration | Low | High (self-correcting) | Cloth, ropes, ragdolls, soft bodies |
| Runge-Kutta (RK4) | High | Moderate-High | Space flight simulators, orbital physics |
Building the Cloth: Points and Sticks
A cloth simulation is essentially a grid of point masses linked together by structural constraints (often nicknamed "sticks").
1. The Point Entity
Every point in the grid needs a current position, an old position, and optional pinning to hold it in place (like a washing line).
class Point {
constructor(x, y, pinned = false) {
this.x = x;
this.y = y;
this.oldX = x;
this.oldY = y;
this.pinned = pinned;
}
update(gravity, friction) {
if (this.pinned) return;
// Derive velocity implicitly
const vx = (this.x - this.oldX) * friction;
const vy = (this.y - this.oldY) * friction;
this.oldX = this.x;
this.oldY = this.y;
// Verlet integration step
this.x += vx;
this.y += vy + gravity;
}
}
2. The Constraint (Distance Stick)
Constraints maintain a fixed resting distance between two adjacent points. If the points drift too far apart or squash too close together, we push or pull them back to their intended separation.
class Constraint {
constructor(p1, p2, length) {
this.p1 = p1;
this.p2 = p2;
this.length = length;
this.torn = false;
}
resolve() {
if (this.torn) return;
const dx = this.p2.x - this.p1.x;
const dy = this.p2.y - this.p1.y;
const distance = Math.hypot(dx, dy);
const difference = (this.length - distance) / distance;
const percent = difference * 0.5;
const offsetX = dx * percent;
const offsetY = dy * percent;
if (!this.p1.pinned) {
this.p1.x -= offsetX;
this.p1.y -= offsetY;
}
if (!this.p2.pinned) {
this.p2.x += offsetX;
this.p2.y += offsetY;
}
}
}
The Secret Sauce: Constraint Relaxation
If you only resolve your constraints once per frame, your cloth will behave like stretchy rubber rather than woven linen. The community consensus across GitHub experiments and developer tutorials is simple: run your constraint resolution loop multiple times per frame.
function updatePhysics(points, constraints, iterations = 5) {
// Update all points first
for (const point of points) {
point.update(0.25, 0.99); // gravity, air drag
}
// Relax the constraints iteratively
for (let i = 0; i < iterations; i++) {
for (const constraint of constraints) {
constraint.resolve();
}
}
}
Running between 3 to 8 iterations produces a taut, responsive fabric without burning out your player's laptop fan.
Adding Slicing and Interaction
What makes these tech demos viral on social feeds is interactivity. To add mouse tearing:
1. Track the Pointer: Keep track of the mouse's current and previous screen coordinates.
2. Intersection Check: Check if the line formed by the mouse movement intersects any constraint line segment.
3. Snip the Thread: If intersected (or if the distance between two points stretches past a breaking threshold), set constraint.torn = true.
When a constraint tears, the points immediately react to their remaining neighbours, naturally parting like cut fabric.
Rendering Cleanly on HTML5 Canvas
When drawing the mesh, avoid rendering hundreds of individual circles. Instead, clear the canvas and batch draw lines between points whose constraints remain intact:
function render(ctx, constraints) {
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.beginPath();
ctx.strokeStyle = "#4ea8de";
ctx.lineWidth = 1.5;
for (const c of constraints) {
if (c.torn) continue;
ctx.moveTo(c.p1.x, c.p1.y);
ctx.lineTo(c.p2.x, c.p2.y);
}
ctx.stroke();
}
Key Takeaways
- Velocity without Vectors: Verlet calculates movement from the difference between current and old positions, eliminating complex velocity tracking.
- Inherent Stability: If a constraint forcibly moves a point, the velocity automatically adapts on the subsequent frame, avoiding numerical instability.
- Iterative Relaxation: Run constraint resolution passes between 3 and 8 times per frame to control fabric stiffness.
- Tearing Mechanics: Simply flag severed links as inactive when distance thresholds or mouse slicing lines cross their paths.