This was my first post. I rebuilt three demos from David Gerrells' How not to use box shadows, with live controls.
1. Gravity and walls
Raise Gravity to pull the balls down. They bounce off the edges, and Damping slows them between bounces. Press and drag inside the frame to attract nearby balls.
Each frame updates velocity and position. A wall collision
reverses the velocity on that axis and reduces it.
dt is the simulation step in seconds:
// Each frame, for each ball:
b.vy += gravity * dt;
// Pointer attraction
if (pointerDown && dist > 1 && dist < 300) {
var f = (1 - dist / 300) * pull;
b.vx += (dx / dist) * f * dt;
b.vy += (dy / dist) * f * dt;
}
// Reduce speed
b.vx *= damping;
b.vy *= damping;
// Move
b.x += b.vx * dt;
b.y += b.vy * dt;
// Bounce off the bottom edge
var r = (b.size * (1 + b.z/40)) / 2;
if (b.y > H - r) {
b.y = H - r;
b.vy = -Math.abs(b.vy) * 0.7;
}
Negative gravity pulls upward. Damping at 100 stops the slowdown between bounces; wall collisions still reduce speed. Raise Pull to increase the attraction.
2. Collisions
This version checks every pair of balls. When their centers are too close, it separates them and changes their velocities along the line joining them. Doubling the count roughly quadruples the number of checks.
The test uses each ball's size as its collision
radius. Move overlapping balls half the overlap distance
each. Then adjust their velocities if they're moving
toward each other:
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 && dist > 0.1) {
// Push apart
var nx = dx/dist, ny = dy/dist;
var overlap = minD - dist;
a.x -= nx * overlap * 0.5;
a.y -= ny * overlap * 0.5;
b.x += nx * overlap * 0.5;
b.y += ny * overlap * 0.5;
// Adjust velocity along the collision axis
var dvn = (a.vx-b.vx)*nx + (a.vy-b.vy)*ny;
if (dvn > 0) {
a.vx -= dvn * nx * bounce;
a.vy -= dvn * ny * bounce;
b.vx += dvn * nx * bounce;
b.vy += dvn * ny * bounce;
}
}
At Bounce 100, ball-to-ball collisions preserve the pair's energy of motion. A separate slowdown still applies each frame. Raise Gravity, Count, and Size to build a pile.
3. The sponge
Each ball has a fixed starting position. A spring force pulls it back, with a stronger pull the farther it moves away. Drag to displace nearby balls, then release to let them return.
On each axis, the spring changes velocity in proportion to the distance from the starting position:
b.vx += (b.homeX - b.x) * springK * dt;
b.vy += (b.homeY - b.y) * springK * dt;
Raise Spring for a quicker return. Lower it for a slower one. Radius sets how far your pointer's pull reaches.
Drawing with box-shadow
Each ball is an entry in a comma-separated
box-shadow list. Its entry sets the position,
size, and color, with zero blur. Every frame builds a new
list and assigns it to the div:
balls.sort(function(a,b) { return b.z - a.z; });
var s = [];
for (var i=0; i<balls.length; i++) {
var b = balls[i];
var zScale = (mode === 'collide') ? 1 : 1 + b.z/40;
var spread = (b.size * zScale - 1) / 2;
s.push(b.x+'px '+b.y+'px 0 '+spread+'px '+b.color);
}
dot.style.boxShadow = s.join(',');
The z value sets the drawing order. In the
gravity and spring demos, it also scales the size to suggest
depth. CSS paints the
first shadow in front,
so higher z values come first in the list.
Shadows don't change the element's layout, but the browser still has to process and paint the updated list. The collision demo also has to check every pair. The full script contains all three simulations and the shared renderer.