Home

Rendering Particles Without a Canvas

No canvas, no WebGL, no SVG. Three physics demos where the entire renderer is one CSS property on a single 1px div.

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.

Gravity & bounce
click & drag to attract
150
0
98
4000

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.

Collisions
click & drag to attract
150
9
0
60
2400

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.

Sponge
click & drag to tear
160
7.5
2700
260
97

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.