Catalog / Technique
Shader on a page
Use when putting a WebGL shader on a page people read, such as a load effect, a hero field or a background. Covers when it starts, how it sizes and ends, and what it does when anything is missing, so that it never costs the page.
- Loads as
technique-shader-on-a-page- Written for
- An agent, to follow
A shader on a content page
How a hand-written WebGL2 effect behaves as a guest on a page whose job is to be read. This is about being a good citizen. What the shader draws belongs to the skill for that effect.
When to use
- A load effect, a hero field, a background, a transition between images: anything drawn by a fragment shader on a page with content.
Do not use it for:
- A page where the canvas is the content (a game, a tool). Its trade-offs are different.
- Anything a CSS animation can do. A context costs tens of milliseconds and a share of sixteen.
Rules
- Never ask a shader’s status before it has finished. Link, then poll the parallel-compile extension. One early question moved first paint by 1.3 s in testing.
- Create the context after a paint. It is a blocking call of 4 to 70 ms.
- Attach the canvas only when there is a frame to draw, and read tokens only after attaching.
- Draw inside the size callback. Setting a canvas’s size clears it.
- Cap pixels twice: per CSS pixel and in total. A canvas too heavy for the GPU delays every click on the page with no script running.
- Remove the canvas, then lose the context. The other order shows a broken mark.
- Sixteen contexts, shared with every frame and tab of the site. Release on finish.
- A load effect starts and ends transparent. Never cover the page, from the script or from the markup.
- Play on arrival only: not on a reload, not from another page of the same site. That also keeps it out of page transitions.
- End a one-shot when the page is hidden or swapped.
- Under reduced motion or forced colours, do not start.
- Text near a field: keep the field out, or prove the contrast for the weakest text colour.
- Time from a clock, never a count of frames. Many phones run at 30 frames a second for part of the day.
- Refuse a software renderer. It burns a weak machine’s cores for decoration.
- Every option explains itself above its declaration. A
/** description */on each member ofOptions; the bench shows those words on hover, an editor shows them to whoever copied the file, and the contract check fails a recipe without one. - A closed set is a
choice, taste is atoggle, a quantity is arange. Choices are drawn as picks, never as a free number or a dropdown. The bench’s range is the demo’s, not the recipe’s limit. - Named versions are
PRESETS. The first is empty, which is the defaults. Every option a preset sets has a control indemo.ts. demo.tssays how the bench drives it. The fixture the effect is for, the target,updatefor anything live andremountfor a one-shot, andmountOptionsfor what the stage must always pass.- What a recipe needs from the page comes from a
data-child of its host, never from an option. Options cross the stage boundary as numbers and booleans. - Four harness forms. One-shot, driven by
progressand released when done. Stateful, driven by a step with targets, resting when it has nothing to draw and released by a budget. The lid, a canvas over an image: pooled contexts, a bitmap at the lid’s size, a rescue on the compositor. The lid driven by input, the pointer or the page’s speed, with a rest test in device pixels in place of a clock. Each is one region, copied, and held to its copy. A one-shot recipe declares above its region, besides its fragment shader and tokens, its vertex shader, ageometrythat says the draw mode and vertex count, whether it needs a depth buffer, any style of its own on the canvas, apreparethat runs after the program is in use and before the canvas is attached (it may return a promise the start waits on, for a font or a picture) and astartsOnViewunder which nothing is made until the element enters the viewport; the region clears before every draw, and positions a static host that is not the page for the length of the effect. - One arrival per page. A page entered by a view transition
carries
data-arrival="transition"onhtml, set by the transition’s own head script; a load effect reads it and does not play a second arrival.shader-washalready declines on any same-site navigation (rule 9), which covers the same case.
Decisions
| Question | Choose | Because |
|---|---|---|
| Does it end? | One-shot if it can | A loop owes the page a pause control after five seconds |
| What is left when it ends? | Nothing, or a still copied to a 2D canvas | A live context held for a still picture is a context nobody else can have |
| Over or under the content? | Over for a passage, under for a field | Under needs the host to become a stacking context |
| Which colours? | The page’s own tokens, read once | A shader with its own palette is a second design system |
| What when it cannot run? | The page, unchanged | It never hid anything, so there is nothing to restore |
Tuning
| Dial | Start | Range | Low end | High end |
|---|---|---|---|---|
| Duration | 900 ms | 600 to 1600 | A flick; under 400 it is over before the eye lands | Past 2 s it reads as a loading screen |
| Pixels per CSS pixel | 1.5 | 0.25 to 2 | Soft and cheap; right for a blurry field | Four times the work of 1 |
| Total pixels | 2.5 million | 1 to 4 million | Soft on a large monitor | A 4K screen is 8 million a frame |
These ranges are estimates from stills. Tune duration by watching.
Rest state
Nothing. A one-shot is transparent before its first frame and after its last, and absent under reduced motion, forced colours, no WebGL2 or a software renderer. Where a still is kept it is a plain 2D canvas.
Cost
- To build: medium for the first recipe, small after, because the harness is the same region in each.
- To run: about 2.3 kB gzipped for the harness. Context creation 4 to 70 ms after first paint. 8 bytes of GPU memory per buffer pixel at rest, 12 while drawing.
- To the reader: a moment in which something moves over what they came to read. Keep it under about a second, and off anything they have begun.
Checklist
- The budget check passes.
-
npm run check:browser: after a full run there is no canvas, no animation, and the host’s style is as it was. -
npm run check:browser: thirty mounts in a row log nothing. -
npm run check:browser: under reduced motion no context is requested. - Looked at once on the stage at phone size, and once with the 30 fps preview.
- Run once in Safari and once in Firefox before it is marked passed.
Recipes
shader-wash: a band of token colour crosses the page once.contour-field: a contour map that reveals itself and leaves.feedback-trails: the stateful form, carrying its harness.reaction-diffusion: Gray-Scott on the stateful form.automaton-fill: a Life-like automaton on the stateful form.shader-lift: the lid form, carrying its harness.plane-arrive: an arrival on the lid form.plane-press: the driven lid form, carrying its region; a hover.plane-bend: a scroll response on the driven lid.plane-settle: the map on a tilted plane that swings flat.terrain-settle: a displaced mesh, with its own vertex shader and a depth buffer, on the one-shot form.palette-resolve: a colour field that ends as the ground token.point-gather: points that gather onto a heading; the first point recipe, with apreparethat samples the word and a start on view.
Sources
mdn-prefers-reduced-motion: the media feature that stops it startingwcag-animation-from-interactions: why motion must be possible to switch off