2,000 Particles, One Canvas
How a small browser physics experiment turned into an exploration of collision detection, spatial partitioning, performance bottlenecks, and the difficulty of making simulated objects behave.
The experiment
Physics-Sym began as a deliberately small browser experiment: draw circular objects on an HTML5 canvas, give them velocity and gravity, and see how they interact with the edges of the screen and with each other. The repository uses vanilla JavaScript, with separate Ball and Sym classes for individual particle behavior and the broader simulation.
What makes a physics simulation interesting is also what makes it difficult. A ball bouncing against a wall is one problem. Hundreds or thousands of balls occupying the same space, colliding, losing energy, and eventually settling is an entirely different one.
From the first bounce to an interactive system
The first recorded commit is dated June 1, 2024. That same day, a commit titled XY Wall Collision marked work on boundary interactions. On June 2, the history shows screen-orientation work and early moments of excitement, including commits titled Yay!!! and This is getting fun.
By June 4, the experiment had moved toward incorporating mass into its calculations. On June 5, mouse event listeners and an attempt at spatial partitioning appeared in the history. The focus was shifting from making a single object move to making many objects interact efficiently.
When collision detection becomes the bottleneck
The straightforward way to detect collisions is to compare particles against other particles and measure the distance between their centers. But the number of possible pairs grows rapidly with the number of objects. With 2,000 particles, a unique all-pairs check could require 1,999,000 comparisons in a single frame before filtering out distant objects.
The repository's June 5 commit describes an alternative: put particles into a Map using grid coordinates, then limit collision checks to particles within a reasonable vicinity. This is a form of spatial partitioning—an attempt to avoid expensive work that cannot produce a collision.
In the final source, initializeGrid() assigns particles to grid cells, and getBallsToCheck() gathers candidates from a cell and its eight neighbors. That reduces the search area, although the actual cost depends on how densely particles cluster and how the grid is maintained.
const gridX = Math.round(ball.x / gridWidth);
const gridY = Math.round(ball.y / gridHeight);
const key = `${gridX},${gridY}`;
if (!grid.has(key)) grid.set(key, []); grid.get(key).push(ball);
Illustrative excerpt adapted from sym.js; names shortened for readability.
The first optimization didn't solve everything
On June 18, the commit history becomes unusually candid. One commit is titled Bad Implementation, describing grid keys that overlapped incorrectly. A later commit, Refactoring Grid Logic, reports removing duplicate keys, using uniform ball sizes to avoid repeated size calculations, and rebuilding the map once per frame.
These changes are useful examples of iterative debugging: improving an algorithm often means discovering that the data structure around it is just as important as the algorithm itself.
When particles refuse to settle
The next problem wasn't just speed. On June 18, a commit asks how to stop the balls from vibrating and allow them to calm down completely. On June 21, another commit reports balls merging together and moving chaotically under high pressure. The author suspected performance issues were contributing, but the commit history does not establish a single verified cause.
The current implementation contains several practical attempts to manage these behaviors: velocity caps, small-velocity thresholds that set movement to zero, restitution and damping choices, and positional correction when circles overlap. These are common ingredients in lightweight collision solvers, but the particular combination and update order can introduce their own artifacts.
const overlap = minDistance - distance;
const correctionFactor = 0.5;
const correctionX = overlap * normalX * correctionFactor;
const correctionY = overlap * normalY * correctionFactor;
Excerpt from sym.js.
Exploring parallelism—and its limits
On June 19, the commit history turns to Web Workers. One message speculates that poor performance could be responsible for balls melding together at higher particle counts and proposes moving work into threads. Other commits refer to initializing workers alongsi de experiments with velocity-based coloring.
Importantly, the current worker.js file is empty, while worker initialization in sym.js and script.js is commented out. The repository therefore documents an exploration of parallel computation, not a completed parallel physics engine.
This is a useful distinction. Workers can move expensive calculations off the main thread, but a physics engine still needs a plan for sharing particle state, synchronizing results, and rendering consistently. Adding threads isn't a substitute for fixing the underlying algorithm.
Turning velocity into something visible
One of the more expressive additions was visual feedback based on motion. In ball.js, the current drawing method combines the absolute horizontal and vertical velocities to derive the red channel of a particle's color, while keeping the blue channel at its maximum. Faster-moving particles therefore change appearance as they move.
const velCum = Math.abs(this.velocityY) + Math.abs(this.velocityX);
const R = Math.min(255, Math.round(velCum * 50));
const G = 0;
const B = 255;
Excerpt from ball.js.
The journey in commits
First movement and boundaries. Initial commit, XY wall collisions, orientation work, and early experimentation. History ↗
More physical behavior. Mass, mouse interaction, and the first spatial-partitioning attempt.
Reworking the grid. Incorrect map keys, grid refactoring, and the challenge of persistent vibration.
Performance experiments. Velocity-based colors and investigation of Web Workers.
Stability tuning. Further adjustments to collision response, velocity bounds, and high-density behavior.
Documentation. README revisions and a final typo correction; no later simulation-code commits are visible in the retrieved history.
What the code taught me
The most valuable part of Physics-Sym is not that it became a perfect physics engine. It is the progression from a simple animation to a system where performance, numerical behavior, data structures, and user interaction all influence each other.
The repository shows an effort to separate responsibilities between the simulation controller and particle objects, replace broad collision searches with localized checks, and experiment with rendering feedback. It also preserves unresolved limitations rather than hiding them.
For me, that's the real value of small experiments: they create a safe place to test assumptions, encounter unexpected behavior, and learn which engineering decisions matter once a simple idea grows.
Try it and explore the code
The repository README links to both an early GitHub Pages version and a later spatial-partitioning deployment. These links are historical and should be tested before publication. The current source creates 2,000 small particles and animates them using requestAnimationFrame.
Editorial draft based on repository source and commit history. Verify live deployments and add personal context before publishing.
Thanks for Reading
Every project starts with curiosity.
Explore more experiments, engineering lessons, and stories from the process of building things.
Explore More Articles