An Exploration in Software
2,000 Particles, One Canvas | Physics-Sym
Engineering Experiment · JavaScript · Canvas

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.

Physics-Sym · Development history: June–October 2024 · By Ryan Large

Explore the source on GitHub ↗

Hero

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.

It was after watching Coding Adventures on you tube I realized I could reenact real life physics on the computer. This opened up a very powerful urge to test this out with a bunch of bouncy balls!

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.

Comparison of Spacial Partitioning vs Not

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.

[GRID VISUALIZATION]Overlay a 2D grid on the canvas and highlight one particle's neighboring cells. A GIF showing candidate selection would be especially effective.

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.

Engineering lesson: Reducing the theoretical number of collision candidates does not automatically guarantee a stable or fast simulation. Correct cell assignment, repeated work, particle density, and the physics update order all matter.

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.

Behold the Jitter and Strange Floating Effects

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.

I felt as though after rendering a successful simulation with a spacial partitioning algorith that worked, I did not need to continue on down a more optimal path in this project. Maybe in a future project to come I could advance to the next level of optimizations.

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.

This project was a massive win for me. It showed me that with research time and even just a mild amount of interest I can accomplish some really interesting things with a computer and learn from it on the way. The engineering behind so many software tasks that involve splitting and distributing tasks appropriately durring runtime is unimaginably useful. I beleive this is a fundamental learning exprience to grasp as an up and coming Software Engineer.

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.

View Physics-Sym on GitHub ↗


VANILLA JAVASCRIPT · HTML5 CANVAS · COLLISION DETECTION · SPATIAL PARTITIONING · EXPERIMENTAL PHYSICS

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