If you have ever spent forty minutes watching an indie platformer character's cape flutter against a pixel-art cliffside instead of actually playing the level, you already understand the hypnotic power of cloth physics. Dynamic fabric turns a sterile browser game into something tangible, tactile, and delightfully responsive.
Many developers assume convincing fabric physics requires dragging a 400KB WebAssembly physics engine into their lightweight project. But with the ongoing revival of hyper-lean web games on itch.io and dev threads buzzing over canvas micro-optimisations, you can achieve gorgeous results with roughly a hundred lines of clean, vanilla JavaScript.
The secret weapon? Verlet integration.
[Pinned Anchor] ---- [Particle] ---- [Particle]
| | |
(Distance) (Distance) (Distance)
| | |
[Particle] <====== [Particle] ---- [Particle]
(Wind Force)
What Is Verlet Integration in Game Physics?
Direct Definition: Verlet integration is a numerical method used to calculate trajectories of particles by tracking their current and previous positions rather than storing an explicit velocity vector. In 2D cloth simulations, velocity is implicitly derived from
position - previous_position, making constraint solving vastly more stable and resistant to explosive mathematical errors than standard Euler integration.
In standard Euler physics (the type we all write when making our first Pong clone), you add velocity to position, and acceleration to velocity:
$\text{Velocity} = \text{Velocity} + \text{Acceleration}$
$\text{Position} = \text{Position} + \text{Velocity}$
If your frame rate hitches or an external collision pushes an object too hard, Euler calculations shoot off into infinity, flinging your ragdoll into the digital stratosphere. Verlet integration avoids this entirely. Because it looks backwards at where the particle just was, damping and resting states resolve naturally.
Step 1: The Verlet Particle
Every scrap of cloth begins life as a point mass. Here is the minimum viable Particle in JavaScript:
class Particle {
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;
// Step forward
this.x += vx;
this.y += vy + gravity;
}
}
The friction variable (usually something like 0.99) mimics air drag. Without it, your cloth will behave like a perpetually shivering jelly mould.
Step 2: Distance Constraints (The Fabric Threads)
A field of floating dots is just a snowstorm. To turn it into cloth, you connect adjacent particles with structural constraints.
A constraint enforces a fixed distance between two points. If two points drift too far apart or cram too close together, we calculate the error and push each particle halfway back to the target length.
class Constraint {
constructor(p1, p2, length) {
this.p1 = p1;
this.p2 = p2;
this.length = length;
}
resolve() {
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 offsetX = dx * difference * 0.5;
const offsetY = dy * difference * 0.5;
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 Relaxation Loop
Because adjusting p1 might pull it out of alignment with another neighbour, you must run your constraint solver several times per frame. Game developers refer to this as relaxation iterations:
- 1–2 Iterations: Feels like stretchy, low-budget spandex.
- 4–6 Iterations: The sweet spot for browser performance and crisp fabric tension.
- 15+ Iterations: Rigid sheet metal (and an uninvited cooling fan concert from your laptop).
Step 3: Unleashing Dynamic Wind Physics
Gravity makes your cloth dangle, but wind gives it personality. Recent creative coding experiments across developer social channels reveal that you do not need complex fluid dynamics to simulate convincing gusts. A layered sine wave generator applied to point columns produces immediate, believable flutter.
function applyWind(particles, time) {
for (let p of particles) {
if (p.pinned) continue;
// Combine sine waves of differing frequencies for natural turbulence
const gust = Math.sin(time * 0.003 + p.y * 0.05) *
Math.cos(time * 0.001 + p.x * 0.02);
const windForce = Math.max(0, gust) * 1.8;
p.x += windForce;
}
}
By grounding the wave's phase in the particle's spatial coordinates (p.y and p.x), the gust moves across the sheet in ripples rather than pushing every thread in unison.
Physics Engine Comparison for 2D Web Games
Choosing the right approach depends on whether you are building a full physics puzzle game or simply adding cosmetic flair to a retro sprite.
| Engine / Approach | Bundle Cost | Setup Complexity | CPU Overhead | Best Use Case |
|---|---|---|---|---|
| Custom Verlet (Vanilla) | < 2 KB | Very Low | Minimal | Capes, banners, ropes, casual puzzles |
| Matter.js | ~80 KB | Moderate | Medium | Rigid body platforms, pinball games |
| Rapier (WASM) | ~1.5 MB | High | Low (Multi-threaded) | Complex, multi-thousand-body physics |
| Euler Hand-Rolled | < 1 KB | Very Low | Minimal | Basic bullet paths (unstable for cloth) |
60 FPS Optimisation Tactics
If your cloth starts lagging, keep these three hard rules in mind:
1. Beware Object Allocations in the Tick: Do not create temporary vector objects inside your render or update loops. JavaScript’s garbage collector will pause your game every few seconds to bin them. Reuse existing variables.
2. Batch Canvas Draws: Avoid running ctx.stroke() for every single thread. Create a single master Path2D, build all the segments using moveTo and lineTo, and call ctx.stroke() once per frame.
3. Pin the Anchors: Ensure at least two top-corner points have pinned = true. A cloth without anchors is just laundry falling through an endless void.
Key Takeaways
- Verlet integration computes motion from past positions, making it immune to the explosive instability of Euler physics.
- Fabric stiffness is managed by the number of solver iterations per frame, not complex elastic formulas.
- Dynamic wind feels natural when driven by superimposed sine waves tied to world space.