# Particle Life (WebGL2)

A single-file "particle life" simulation: 6 colour species, 8,000 particles by default (up to 20,000), and a 6×6 attraction/repulsion matrix that produces cells, worms, orbiting blobs, chasers and pinwheels.

Open `index.html` directly, or serve the folder (for example `python3 -m http.server`). There is no build step, and no external assets or accounts.

## Idle auto-play
The simulation starts as soon as the page loads. Every 8 seconds it moves on to the next preset, crossfading the matrix smoothly over about 2 seconds so the existing structures morph instead of resetting. Choosing a preset, randomising or editing the matrix by hand turns auto-play off. Tick **Auto-cycle** or press `A` to turn it back on.

## Controls
| Control | Action |
|---|---|
| **1–5 / preset buttons** | Cells, Worms, Orbits, Predators, Pinwheels |
| **🎲 Randomise matrix** / `R` | Crossfade to a random matrix |
| **Particles** slider | 1,000 – 20,000 particles; changes take effect live |
| **Friction** slider | 0 = slippery and energetic, 1 = thick and sluggish |
| **Auto-cycle** / `A` | Turn the 8-second preset cycle on or off |
| **Trails** / `T` | Turn motion trails on or off |
| **Matrix grid** | Click a cell to add +0.25, Shift-click or right-click to subtract 0.25. A row is the species that feels the force; a column is the species it reacts to. Green means attract, red means repel. |
| `Space` | Pause / resume |
| `H` / **–** button | Hide or show the panel |

The FPS readout in the panel header shows rendered frames per second. The label beside the matrix shows `WebGL2 · software` when a software rasteriser is detected.

## How it works
- **Physics:** each pair of particles closer than `R_MAX` pushes apart at short range, then attracts or repels in proportion to the matrix entry (the classic piecewise-linear particle-life force). Velocity decays according to the friction setting, and the world wraps around at the edges (a torus).
- **Speed:** particles are counting-sorted into a uniform grid every step. The force pass is split by grid rows across a pool of Web Workers, created from a Blob URL so they also work from `file://`. If workers are unavailable, it falls back to the main thread.
- **Rendering:** plain WebGL2 using point sprites with additive blending, drawn into a pair of RGBA8 framebuffers that alternate frames to create fading trails. No float textures or extensions are needed. Software renderers (SwiftShader/llvmpipe) automatically render at a lower internal resolution to keep the frame rate up.
- WebGPU is not used, so the WebGL2 path is the only one and runs everywhere WebGL2 does.
