How it works
Each particle samples a 3D simplex noise field n(x, y, t) and moves along its curl: the vector (dn/dy, -dn/dx). A curl field is divergence-free, so particles never pile up in sinks or empty out of sources; they swirl in coherent streams the way smoke does. The third noise coordinate is time, so the field slowly evolves.
The derivatives are estimated with central differences, four noise samples per particle per frame. Positions live in Float32Arrays rather than objects, which keeps 8,000 particles cheap enough for 60 fps on a laptop.
Trails come from never clearing the canvas: each frame paints a translucent rectangle in the background colour, so old segments fade exponentially. Lowering Trail fade gives longer ribbons.
Drawing is batched by colour. Every particle is put in one of six buckets by flow direction, and each bucket is one path with thousands of moveTo/lineTo pairs, stroked once with globalCompositeOperation = "lighter". Where streams overlap the colours add up and glow, with no shaders involved.
Move the pointer over the canvas to stir a vortex into the field: inside a radius, a tangential velocity that falls off with distance is added to the curl.