My first blog post. The idea is entirely David Gerrells', from his brilliant, unhinged box-shadow experiments; I just rebuilt it for my own attention span.
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
Here is the whole trick: every ball is a single
shadow. Not a metaphor. One element can hold
hundreds of comma-separated box-shadow offsets,
so each frame you sort them back-to-front, build the string,
and assign it once:
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 paints all of them on the same path that draws
the shadow under a button, no drawing API, no buffers.
Changing box-shadow repaints but never lays the
page out again, so a few hundred shadows fly and a few
thousand crawl, with no GPU instancing to save them. The
wrong way to render anything serious, and glorious for this.