Catalog / Technique
Gpu particles
Points as the image, on the GPU, with no library and no vertex data. When a word, a mark or a decorative picture may be made of points, which route to take, how a point is computed, what it costs, and where it must never go.
- Loads as
technique-gpu-particles- Written for
- An agent, to follow
GPU particles
When to use
Points are the image only where the image is a word, a mark or a decorative picture. A heading that assembles from dust on arrival. A mark that lifts off as the reader scrolls on. A band of points that flow for a few seconds and rest.
Never on evidence: a screenshot, a diagram, a chart. Never on a thumbnail: a dissolved picture carries nothing while dissolved, and a thumbnail is the thing the reader came to see. Never as perpetual motion beside text. Never as ambient dust behind content: it is a common utility effect and a canvas that never leaves.
Rules
- Prefer the stateless route. A point is a pure function of its index, the progress and the time, computed in the vertex shader. Reach for state (a texture of positions, or transform feedback) only for a field, where a point responds to where it is.
- Draw with
gl_VertexIDand no vertex buffers.drawArrays(POINTS, 0, n)runs the vertex shader n times; the index is the point. - Hide a point by moving it out of clip space,
vec4(2, 2, 2, 1). A zero size is not a reliable hide: the minimum size is 1. - Per-point randomness from chained hashes, one stream per use: hash the index, then hash again with a large odd constant per stream. Never neighbouring integers through one hash.
- A wander’s phase comes from the point’s own hash, never from its position. A drift that is a function of position is a map from start to drawn position; where amplitude times spatial frequency passes about one, the map folds and the cloud becomes a lattice of bright lines and empty cells.
- Arrive exactly.
mixto the target with weight 1, and multiply every offset by the remaining distance, so at the end the point is at its target to the float. - Register a text mask with the half-leading baseline, after the
fonts have loaded: the line box’s top, plus half of the line height
minus ascent plus descent, plus the ascent, from
measureText’s font bounding box. NevertextBaseline = 'middle': it sits about 0.2 em too high. One line only, measured through a Range over the mark’s contents: a block mark’s own rect is its column, not its letters, and is one rect however many lines it wraps to. Draw the text the page renders, whitespace collapsed andtext-transformapplied in the page’s language. Expect descenders and a cedilla to overflow a line box whose line-height is under the font’s content height; that is where the DOM draws them too. - A float texture of targets takes nearest filtering on both
filters. With the default linear magnification a 32-bit float
texture is incomplete without an extension and reads as zero, even
through
texelFetch, and every point lands in one corner with nothing logged. - One precision per shared uniform. A uniform both shaders declare
must have the same precision in each, or the program does not link.
Declare
precision highp floatin both. - Cost is overdraw, not count. Pixels covered, not points drawn. A point’s size in CSS px is multiplied by the pixel ratio; keep the largest drawn point under 64 buffer px, and cap the ratio at 1.5. A hundred thousand small points is a phone effect; a million is a desktop scene.
- Premultiplied output. Write
vec4(colour * a, a). Over (ONE, ONE_MINUS_SRC_ALPHA) on a light ground; add (ONE, ONE) only on a dark one, at a lower strength. - Never hide the real word or picture during the run. The effect only adds. Hiding the word moves the load metric by the whole run and makes the heading unreadable until the points land.
- A point field is legible only with trails. Points advected through a field read as uniform noise in a still; forty frames of trail read as flow at once.
- Keep big travelling points inside the box. Drivers disagree on drawing a point whose centre is off screen; a large point near the edge can pop out early.
- The mask’s read-back is the main-thread cost. Cap the mask width and the count; measured 14 to 26 ms for 40,000 points from a 1,200 px mask.
- Rest is the page as served. Under reduced motion nothing mounts; after the run nothing remains; the first served still is the heading as text.
- Square points of a whole cell centred on a grid tile a picture exactly. One point per cell, size equal to the cell in buffer pixels, each sampling the picture at its own fragment, reproduces it with no differing pixel: that is the exact handover a lid needs. A partial cell at an edge is clamped inside the buffer, because a point centred past the edge may be dropped by a driver.
- A round dot blends to the square over the last tenth of its run, and its colour from its cell’s centre to the picture under it; only the picture under the fragment is exact at the end. A switch in one frame pops.
- A field’s positions live in
RGBA32F, one texel per point, read bytexelFetch, never filtered and never blended. Half float is too coarse: a slow point’s move a frame is two or three steps of its precision, so it jitters or stalls. Test the 32-bit target’s completeness yourself; the harness probes the 16-bit one. - A texel carries age and life, and respawns by a hash of its index and the time once past its life or outside the box, with the seed already part way through a life so the first frames are not a burst. The curl of one value noise, with the field’s aspect in it, is divergence-free, so points neither pile up nor thin out.
- A point field is drawn into a fading trail and shown from the trail. The points themselves are drawn additively into the trail target each frame; the trail fades with the floor subtracted so rest is exactly zero. The reader sees the trail.
- A field has a budget of a few seconds by default and never sits behind text. It ends on the budget, or with the last trail kept as a still; with the pointer it listens and rests between touches.
Decisions
| Decision | Chosen | Why |
|---|---|---|
| First point recipe | point-gather, stateless |
Fits the one-shot harness with one hook; measured; leaves nothing |
| A picture as points | point-lift, built in Lab 10, limited to hero, decorative figure and page-load |
Mid-run the picture’s detail is lost: never on evidence |
| A field | point-field, built in Lab 11, on the stateful harness extended with a point draw and a 32-bit float target |
A transform-feedback form would be a fifth harness for one recipe |
| Over or add | Over by default; add a toggle for a dark ground | Add on a light ground only makes the colour more opaque |
| The word during a gather | Visible throughout | A contract line |
| Count or density | A count, capped by the inked area | The obvious dial |
| Start on load or on view | A toggle through the harness; on view, nothing is made until the block is seen | A section heading below the fold would play unseen, a context held there counts against the page’s few, and the mask must be sampled against the final fonts and layout |
| Where the font wait lives | In the harness, as a promise from prepare |
A mask redrawn mid-run moves every target under points in flight |
Tuning
Ranges are the research’s estimates from stills unless marked measured.
| Dial | Start | Range | Low end | High end |
|---|---|---|---|---|
count |
30,000 | 5,000 to 100,000, capped by the inked area | A dotted outline, airy | A solid fill; prepare time grows (26 ms at 40,000, measured) |
size (CSS px at rest) |
2 | 1 to 6 | Dust; shimmer below 1.5 buffer px | Blobs; overdraw |
travelSize (growth in flight) |
1.5 | 0 to 3 | The same size throughout | Big soft motes; keep size times (1 + travelSize) times the ratio under 64 |
stagger (share of the run) |
0.45 | 0 to 0.8 | A rigid morph | A trickle; the last points arrive late and alone |
wander (share of the box) |
0.04 | 0 to 0.1 | Straight paths | A swarm; above about 0.1 the word reads late |
strength |
1 | 0.2 to 1; add 0.2 to 0.4 | Faint | Clipped in add |
duration (ms) |
1400 | 900 to 2400 | A snap; the stagger cannot be seen | A loading screen |
Rest state
The page as served: the heading as real text, the picture as an
img. Under reduced motion and forced colours nothing mounts. After
the run the canvas is gone and the context released; nothing of the
effect remains, and the host’s style is as it was. A field at rest
is either nothing or its last trail frame kept as a plain 2D canvas.
Cost
- Bytes:
point-gathermeasured 5,379 gzipped, of which the one-shot region is about 2,770. - Main thread: the mask’s read-back and sampling, 14 to 26 ms for 40,000 points from a 1,200 px mask (measured). Cap the mask width and the count.
- GPU: overdraw. 100,000 points at 2 px took 1.52 ms a frame, at 32 px additive 26 ms (measured on an integrated GPU). Cost grows with the square of the pixel ratio: 100,000 points at 16 px, 7.6 ms at ratio 1 and 15.5 ms at 1.5 (measured).
- Memory: the targets texture, 8 bytes a point; a transient mask the size of the element at up to 1,200 px wide.
- Battery: nothing after the run; the context is released.
Checklist
- Stills at progress 0, 0.35, 0.6, 0.85 and 0.99.
- The word in Turkish with İ, ı, ş, ğ and ç at the smallest size it will be used; every glyph has points.
- A frame time at pixel ratio 1.5 with the largest version.
- A reduced-motion load showing the page untouched.
- The canvas gone after the run, the host’s style as it was.
- The largest drawn point under 64 buffer px for every version.
- The program links on a fresh context, with both shaders at one precision, and the targets texture complete.
Recipes
point-gather: points gather onto a heading and leave; the stateless route on the one-shot harness.point-lift: a picture arrives as its own points on the lid harness, and the lid is the picture at the end.point-field: points on a drifting curl-noise field with trails, on the stateful harness; the pointer can stir it.
Sources
webgl2fundamentals-drawing-without-data: drawing points fromgl_VertexIDwith no buffers, and the edge-clipping disagreement.webgl2fundamentals-gpgpu: transform feedback and texture state, the two stateful routes.mdn-webgl-best-practices: the point size range, the pixel ratio trade, texture limits.codrops-untillabs-particles-2025: a production system that moves 60,000 points by a function of time.apple-forums-point-size-64: the one report of a 64 px point cap on Apple silicon.mdn-getcontext: the canvas is premultiplied by default.wcag-pause-stop-hide: a field that runs past five seconds needs a pause.wcag-three-flashes: additive shimmer must stay slow and small.mdn-prefers-reduced-motion: the media feature that stops it starting.figma-implement-motion-skill: the public skill that says to use a library for particles, which this one disagrees with.hermes-p5js-skill: counts without an overdraw budget.