Wind Drift Field
Still air you stir with the pointer, running on a real fluid solver rather than a painted force field. Fourteen thousand tracers ride the velocity field and leave trails that fade back into the paper, and every trail is coloured by the speed of the air carrying it, so a slow eddy stays green and a hard sweep burns red. The air is a Stable Fluids grid and the three steps in its loop are what make it read as wind rather than as blobs. Self-advection carries the velocity field along itself, so momentum keeps travelling after your hand stops and a stroke moves downstream and stretches instead of sitting where you drew it. Pressure projection solves a Poisson equation each frame and subtracts the gradient to remove divergence, which is the step that makes air behave like air: without it velocity is created and destroyed in place, which is exactly what a stamped gust looks like, and with it the air pushed out of one place has to go somewhere, so a stroke rolls up into vortices at its ends and the flow curves around itself. Vorticity confinement puts back the small swirls the solver own interpolation smears away, and its gain is capped at half the energy viscosity removes, so a wake stays turbulent without the field ever churning forever. Nothing moves on its own: with the air at rest a tracer draws a zero-length segment and the buffer keeps fading, so the surface sits empty until it is touched, and once the kinetic energy decays the loop parks itself on a cleanly cleared buffer rather than on the residue an eight bit fade can never round away. The trail is a ping-pong pair of framebuffers, so it is never stored as history, only as what is left of the last frame. One WebGL canvas, no library and no asset required.
npx shadcn@latest add https://ui.aryank.space/r/wind-drift-field.jsonInstalls from ui.aryank.space. To add it by hand, copy the files in Usage below, or register the @compronents namespace via the docs.
| Prop | Type | Default | Description |
|---|---|---|---|
| palette | string[] | eight-stop green to red | Trail colours from stillest air to fastest, as hex. Sampled continuously, so any length works and intermediate speeds blend between neighbours. |
| background | string | "#0a121b" | Field colour. Trails fade toward it every frame, so it is the fade target as much as the backdrop. |
| particleCount | number | 14000 | Tracers on the field. Each contributes one line segment per frame, and the whole loop runs on a 30fps gate. |
| turbulence | number | 0.6 | Vorticity confinement, 0 to 1, expressed as a fraction of the energy viscosity is removing rather than as a raw epsilon. It cannot exceed the damping, so no setting makes the air churn forever. Zero lets the solver smooth a wake into a smear. |
| viscosity | number | 0.96 | Velocity kept per frame. Lower brings the air to rest sooner and, because turbulence is scaled against it, also shortens how long a wake stays lively. |
| gustStrength | number | 1 | Multiplier on the pointer's push. Zero leaves the air permanently still, which is the right setting behind dense content. |
| flowSpeed | number | 2.9 | How far a tracer travels per unit of air speed. Raising it lengthens every streak without changing the shape of the flow. |
| drift | [number, number] | [0, 0] | Even background breeze in field units per second, added at sample time since a uniform flow is already divergence-free. Zero, the default, is what lets the field sit empty and the loop idle; any non-zero value means it never parks. |
| showGrid | boolean | false | A faint survey grid over the field. Off by default so the airflow reads on its own. |
| children | ReactNode | none | Content rendered above the field, full height. |
turbulencetextSmall-swirl recovery, 0 to 1, as a fraction of what viscosity removes. Default 0.6.
viscositytextVelocity kept per frame. Lower settles sooner. Default 0.96.
gustStrengthtextHow hard a sweep pushes the air. Default 1.
backgroundcolorField colour. Trails fade into this, so it is the fade target as much as the backdrop.