Particle System Simulation in Python
Build a particle system in Python with NumPy and Matplotlib. Simulate fireworks, gravity, and emitters from scratch — runnable instantly in your browser.
Try it yourself
Run this code directly in your browser. Click "Open in full editor" to experiment further.
Click Run to see output
Or press Ctrl + Enter
How it works
A particle system is one of those ideas where the implementation is short but the visual payoff is huge. Every fire effect, every smoke trail, every magic spell, every star field in every game and movie you've ever seen is some variation of "a few thousand dots, each following its own version of the same physics rules". Once you've built one, you can build all of them.
The Mental Model
A particle is just a struct with a few numbers:
A particle system is a bag of particles. Every frame, you do the same thing to all of them: apply forces, move them, age them, draw them. Then on certain frames you emit new particles to replace the ones that died.
That's the whole architecture. The art is in choosing emit patterns and force rules.
Why You Vectorize
The naive way to write this is one Python object per particle, with a for particle in particles: particle.update() loop. That works for 100 particles. It crawls at 10,000.
The NumPy way stores every particle's data in a 2D array and updates all of them in one expression:
self.vel[alive, 1] += gravity # apply gravity to every alive particle at once
self.pos[alive] += self.vel[alive] # move every alive particle at onceThis is the same trick that makes the Mandelbrot snippet fast. NumPy is ~50x faster than a Python loop here, which is the difference between a particle system that holds 500 particles and one that holds 50,000.
The Recycling Trick
Notice the snippet pre-allocates a fixed pool of n=4000 particles up front, then "emits" by finding particles whose life <= 0 and resetting their fields. It never grows the array.
This matters because:
This is exactly how production game engines do it. The pattern is called object pooling.
The Physics, In Three Lines
The step() method has three operations and that's it:
self.vel[alive, 1] += gravity # forces accelerate the particle
self.vel[alive] *= drag # drag slows it down
self.pos[alive] += self.vel[alive] # velocity moves the positionThis is the simplest possible numerical integration — Euler integration. It's not the most accurate (energy slowly creeps up over time), but for visual effects nobody cares. For a physics simulation where accuracy matters (planets, molecules), you'd use velocity Verlet or RK4 instead.
Drag Is Underrated
Adding vel *= drag (with drag = 0.99) makes the difference between particles that fly off the screen and particles that arc and float gracefully. Each frame, every particle keeps 99% of its velocity. After 60 frames, it's down to ~55%. After 200 frames, ~13%. That natural deceleration is what makes smoke look like smoke and explosions look like explosions instead of straight-line missiles.
Emit Patterns Are The Whole Personality
The step() method is identical across every particle effect you'll ever write. The only thing that changes is how new particles are spawned:
| Effect | Emit pattern |
|---|---|
| Fireworks | Big burst at one point, particles spray uniformly in a circle |
| Fountain | Continuous trickle, narrow cone aimed up |
| Smoke trail | Continuous trickle, low velocity, slow upward drift, gray fading to transparent |
| Sparks from grinding | Continuous wide cone, high speed, short life |
| Magic spell trail | Particles emit behind a moving point, curling outward |
| Rain | Particles spawn across the top of the screen, all moving down |
| Snow | Same as rain but with sideways drift and lower speed |
Every one of these uses the same step() and the same render function.
Fading And Resizing With Life
The fade = life / max_life trick is a tiny but huge detail. Each particle's transparency and size follows its remaining life:
Without this, particles pop in and out of existence — which looks terrible. With it, the system breathes.
What's Hard About This In Pyodide
The snippet above renders static snapshots at chosen frames rather than running a true animation. The reason: this code runs in a Web Worker, which doesn't have a real-time canvas to draw to. For a real animated version, you'd:
matplotlib.animation, orThe four-frame fireworks panel and the steady-state fountain shot capture the essence of what an animation would look like — particles in different stages of their life, fading, falling, dispersing.
What To Try Next
vel[hit_floor, 1] *= -0.6 (the 0.6 is energy lost on bounce).vel[alive, 0] += wind_x is a global horizontal force.Run the snippet above and you'll see fireworks captured across four moments — the bursts firing, the trails arcing, the embers fading — and a continuous blue fountain spraying upward in steady state, all driven by the same three-line physics loop.
Related examples
Mandelbrot Set Fractal in Python
Render the Mandelbrot set in Python with NumPy and Matplotlib. Vectorized fractal generation, deep zooms, and Julia sets — runnable in your browser.
Maze Generator and Solver in Python
Generate random mazes in Python and solve them with BFS. Recursive backtracker algorithm, visualized with Matplotlib — runnable instantly in your browser.
NumPy Array Operations in Python
Learn NumPy basics in Python! A fun and easy guide to super-fast arrays, matrices, and data science math without using slow for-loops.