Every particle in the demos below is a shadow. Not
a metaphor. There is one 1-pixel div on each
page, and CSS box-shadow happens to let you
stack as many shadow offsets on a single element as you
want. So you stack hundreds, update them every frame, and
the browser composites the whole scene for you. No canvas
context, no WebGL, no SVG. Just a string.
It is the wrong way to render anything serious. It would
also work in a browser from 2010, since
box-shadow shipped in IE9, Firefox 4, and
Chrome 10. The physics is a few dozen lines of math per
demo. Drag inside any of them. Move the sliders. I will
explain the rendering trick at the end, because it is the
strangest part.
Inspired by David Gerrells' brilliant, unhinged experiments.
1. Gravity and walls
The simplest version. Balls fall under gravity, bounce off the edges, and lose a bit of energy on each bounce. Click or touch to pull them toward your cursor.
The whole update is a for loop. Add gravity to vertical velocity. If the cursor is pressed, add an attraction force. Apply damping to bleed off energy. Move. If a ball hits a wall, flip the velocity:
// Each frame, for each ball:
b.vy += gravity * dt;
// Pointer attraction
if (pointerDown && dist < 300) {
var f = (1 - dist / 300) * pull;
b.vx += (dx / dist) * f * dt;
}
// Damping bleeds energy
b.vx *= damping;
b.vy *= damping;
// Move
b.x += b.vx * dt;
// Bounce off walls
if (b.y > H) { b.y = H; b.vy *= -0.7; }
Crank gravity negative and they fall upward. Set damping to 100 and they drift forever. Pull at 10,000 turns your cursor into a black hole.
2. Collisions
Same setup, but now the balls notice each other. When two overlap, they push apart and exchange velocity along the line between them. The check is brute force (every ball against every other ball), which is fine at this scale and terrible above a few thousand.
For each pair, measure the distance. If it is less than the sum of the two radii, push them apart and reflect their velocities along the collision normal:
var dx = b.x - a.x, dy = b.y - a.y;
var dist = Math.sqrt(dx*dx + dy*dy);
var minD = a.size + b.size;
if (dist < minD) {
// Push apart
var nx = dx/dist, overlap = minD - dist;
a.x -= nx * overlap * 0.5;
b.x += nx * overlap * 0.5;
// Swap velocity along normal
var dvn = (a.vx-b.vx)*nx + (a.vy-b.vy)*ny;
a.vx -= dvn * nx * bounce;
b.vx += dvn * nx * bounce;
}
Bounce at 100 gives elastic collisions (no energy lost). Bounce at 10 makes them behave like wet clay. Crank the count and size up together to watch them pile.
3. The sponge
My favorite. Each ball remembers where it was born and wants to go back. A spring force pulls it home, and the farther it gets the stronger the pull. Your cursor overpowers the spring locally. Let go and everything snaps back.
The spring is one line per axis. Displacement from home times a constant:
b.vx += (b.homeX - b.x) * springK * dt;
b.vy += (b.homeY - b.y) * springK * dt;
Set the spring to 12 and the balls snap home instantly. Set it to 0.5 and they drift back like they are underwater. Lower the radius and you have to get very close before they react to you.
The renderer is one CSS property
Each frame: sort the balls back-to-front, build a
comma-separated string of shadow offsets, assign it to
boxShadow:
balls.sort(function(a,b) { return b.z - a.z; });
var s = [];
for (var i=0; i<balls.length; i++) {
var spread = (b.size * (1 + b.z/40) - 1) / 2;
s.push(b.x+'px '+b.y+'px 0 '+spread+'px '+b.color);
}
dot.style.boxShadow = s.join(',');
The browser does the rest. No drawing API, no transformation matrices, no buffer management. Just a string, parsed and composited by the same code path that renders the drop shadow under your card components. It is the wrong way to render anything serious, and it is glorious for this.