Physics engines are easy to start and hard to stop fiddling with.
Matter.js does the actual work here. Bodies, collisions, the solver, all of it. What took the second evening was everything the engine does not have an opinion about, which turns out to be most of what makes it feel like anything.
The numbers that matter are not the physics ones
Gravity is 0.25. The default is 1, and at 1 everything you drop hits the floor before you have finished looking at it. A quarter is slow enough to watch something fall and still fast enough that it does not look broken.
Restitution runs between 0.7 and 0.9 per body, randomised at spawn. Identical bounciness reads as a grid of clones; a spread reads as objects.
There are six size bands, from 20 pixels up to 120. Not a continuous random range: a continuous range gives you a lot of medium. Bands give you the occasional very small one next to the occasional enormous one, which is the bit people actually enjoy.
New bodies pop in over 300 milliseconds on a bounce-out curve, scaling from nothing. Without it they simply exist, one frame to the next, and the eye reads that as a glitch rather than an arrival.
The engine does not draw these
Every body is invisible as far as Matter is concerned. fillStyle: 'transparent', and then a second canvas pass draws a 736 by 736 sprite over each one at its position and angle. The physics simulates circles; you see faces.
That split is worth having. The simulation stays cheap and boring, and anything about how it looks is a drawing problem rather than a physics one.
It did not work
Worth saying plainly, because it is the most useful thing here. The sprite was loaded from /src/assets/images/smiley-face.jpg. That path is served by the dev server and by nothing else. In a production build there is no /src.
Every draw in this demo is behind one guard:
if (!this.smileyImageLoaded) return;
So wherever that image 404s, the physics runs perfectly, at sixty frames a second, drawing absolutely nothing. A pink box. No error in the console, because a failed image load is not an error, it is just an image that never loaded.
It is fixed here, and the sprite sits next to the page that uses it. Matter.js is vendored beside it too, rather than pulled off a CDN at load, which was the other way this demo could quietly become an empty pink box.
The body count and the frame rate under the stage are new. The original looked both elements up, tracked the numbers to put in them, and then never wrote either one out.
Have a go
Open on its own ↗Click inside the pink to drop one. They are heavier than they look.