← all posts
May 29, 2026·6 min read

I wrote a physics engine for my portfolio, then deleted it

When I started this portfolio, I wanted the project cards to be real physics objects. You should be able to grab one, fling it, watch it tumble and land on the others. Not a CSS transition pretending to be physics — actual rigid bodies.

So I did the obvious thing for someone who likes writing things from scratch: I wrote a physics engine.

The Verlet phase

The first version used Verlet integration. It's a lovely technique — instead of storing velocity explicitly, you store the previous position, and velocity is implied by the difference between where a body is and where it just was. Constraints become dead simple: you just move points and the velocity updates itself for free.

It worked. Cards fell, stacked, and threw nicely. I was pleased with myself.

Then I added rotation, and everything fell apart.

The problem nobody warns you about

Here's the thing about rotated rectangles: detecting whether two of them overlap is genuinely hard. My collision detection used axis-aligned bounding boxes — the simplest possible approach, where every object is treated as an upright rectangle regardless of how it's actually oriented.

That's fine until a card rotates. Now the visual is a tilted card, but the collision shape is still an upright box that doesn't match. Cards clipped through each other. They got stuck standing on their corners. They'd tip in ways that looked subtly, unplaceably wrong.

I tried to paper over it. I added a fake "tipping torque" so off-balance cards would lean. I added a restoring spring so they'd never get stuck vertical. Each hack fixed one case and created two more. At one point a card just spun in place forever, and another time the whole field would slowly drift without anyone touching it.

The honest fix was to implement the Separating Axis Theorem — proper oriented-bounding-box collision, with contact manifolds generated by edge clipping, resolved with sequential impulses. This is what real 2D physics engines do. I started writing it. I got SAT working. Then contact generation. Then the impulse solver. Then I spent two days chasing a bug where particles — sorry, cards — would jitter at rest because my position-correction was fighting my restitution.

Deleting it

At some point I stepped back and asked the question I should have asked at the start: what am I actually trying to show here?

The answer was not "I can reimplement Box2D." Plenty of people have done that, better, and nobody hiring me is going to read 600 lines of contact-manifold code and conclude anything except that I have a lot of time. What I wanted was for the cards to feel good, so the page feels alive.

So I deleted the whole engine and dropped in matter.js. It handles SAT, manifolds, sequential impulses, sleeping, friction — all the things I was reinventing — and it does them correctly because people have been fixing its edge cases for a decade. The cards started behaving immediately.

The lesson isn't "don't write things from scratch." It's write the thing that's actually yours, and buy the thing that isn't.

What I kept

Because here's the part I didn't delete: the background.

Behind the cards is a live N-body gravitational simulation — a few hundred particles, each one pulling on every other one with Newtonian gravity, brute-force O(N²), running at 60fps in a hand-written WebGL2 program with additive blending. That I wrote from scratch, and I'd do it again, because it's the part that's genuinely mine and genuinely interesting.

And then I coupled the two systems. Each card is a gravitational point-mass in the particle field, so the particles orbit the cards and pile up around them. When two cards collide hard, the impact injects a radial velocity impulse into the nearby particles — you see the collision ripple outward through the starfield. Drag a card and you're dragging a gravity well through a simulated universe.

The matter.js bodies and the WebGL particles never know about each other directly. They talk through a tiny shared-state module that both read and write each frame, outside of React, so neither simulation pays for re-renders.

The takeaway

I spent more time on the engine I threw away than the one I shipped. I don't regret it — I understand contact resolution now in a way I never would have from reading about it. But the portfolio is better because I was willing to call it: the cards needed to work, the background needed to be mine, and those are two different problems with two different right answers.

Sometimes the most senior move is deleting your own code.