Every particle asks your field for its velocity, takes a step, and leaves a trail. The field is a WGSL function:
fn get_velocity(p: vec3f) -> vec3f {
return vec3f(-p.z, 0.0, p.x); // orbit the y axis
}
Optionally colour them yourself (choose Colour by → get_color()):
fn get_color(p: vec3f, v: vec3f) -> vec3f {
return turbo(length(v) * 0.5);
}
u.time | simulation time (sum of time steps) |
u.frame | frame counter |
u.cursor | the mouse, projected onto the plane through the orbit centre that faces the camera |
u.cursorDown | 1.0 while a mouse button is held |
u.boundsMin u.boundsMax | the box particles live in |
noise(p) | gradient noise, about −1…1 |
noised(p) | vec4f: value and its gradient |
fbm(p) | four octaves of noise |
curl(p) | divergence-free noise — smoke and swirls |
hash3(p) hash1(p) | random per integer cell |
rotateX(p, a) …Y …Z | rotate a vector |
turbo(t) viridis(t) hsv2rgb(c) palette(t, k) | colour maps |
PI TAU | constants |
Under Pulse → Sync to: Aux Cord bot reads the beat map of whatever the bot is playing from its localhost endpoint (the bot must run on this machine; trim the latency with Sync offset). Shared audio analyses a tab or your screen's sound live — pick the Discord window and tick share audio. Microphone listens to the room. The knobs punch speed, glow and zoom on the beat, and your field sees the music too:
u.audio | vec4f: bass, mid, high, level — each 0…1 |
u.beat | 1.0 on a beat, decaying to 0 |
u.phase | 0…1 from one beat to the next |
u.bpm | tempo |
return v * (1.0 + 2.0 * u.beat) + normalize(p) * u.audio.x;
let is immutable, var mutable. Types are inferred: let r = length(p);. Vectors are vec3f(x, y, z) or vec3f(0.0), swizzles work (p.xz). Write floats with a point (2.0) — a bare var a = 2; is an integer. Functions may be declared in any order. Ternaries are select(ifFalse, ifTrue, cond).
💾 saves the whole state — the field, every setting and the camera — into this browser under a name you give it, and it joins the top of the picker under Saved. Saving with the same name replaces it; 🗑 forgets the one you have selected. A saved field lives in this browser only, so send one elsewhere with the 🔗 link, which carries the same state in the address.
Hold the left button over the flow and it is pulled toward the pointer; the middle button does the opposite. It is a bend rather than a shove — the force is proportional to the speed already there, so one strength reads the same in a gentle swirl and in a Lorenz attractor. Cursor → Left button switches it to push or swirl (and then the middle button pushes, or swirls the other way) or turns it off; Reach is a fraction of the box. The camera is on the right button and on shift, so orbiting never disturbs the flow.
| hold left | pull the flow to the pointer |
| hold middle | the opposite — push it away |
| right-drag / one finger | orbit |
| shift-drag / ctrl-drag / two fingers | pan |
| wheel / pinch | zoom |
| space | pause |
| R | reset particles |
| H | hide the panel |
| F | fullscreen |
| ctrl+enter | compile now |
The link button copies a URL that holds the whole state — field, settings and camera.
This page runs its particles in compute shaders, which need WebGPU. It works in Chrome and Edge 113+ on Windows, macOS and ChromeOS, Chrome 121+ on Android, and Safari 26 / Firefox 141+.
If you are on a supported browser, check that hardware acceleration is on (chrome://settings/system) and that the page is served over http://localhost or https.