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 |
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).
| drag | orbit |
| right-drag / shift-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.