atelier

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

  1. 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.
  2. Create the context after a paint. It is a blocking call of 4 to 70 ms.
  3. Attach the canvas only when there is a frame to draw, and read tokens only after attaching.
  4. Draw inside the size callback. Setting a canvas’s size clears it.
  5. 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.
  6. Remove the canvas, then lose the context. The other order shows a broken mark.
  7. Sixteen contexts, shared with every frame and tab of the site. Release on finish.
  8. A load effect starts and ends transparent. Never cover the page, from the script or from the markup.
  9. Play on arrival only: not on a reload, not from another page of the same site. That also keeps it out of page transitions.
  10. End a one-shot when the page is hidden or swapped.
  11. Under reduced motion or forced colours, do not start.
  12. Text near a field: keep the field out, or prove the contrast for the weakest text colour.
  13. Time from a clock, never a count of frames. Many phones run at 30 frames a second for part of the day.
  14. Refuse a software renderer. It burns a weak machine’s cores for decoration.
  15. Every option explains itself above its declaration. A /** description */ on each member of Options; 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.
  16. A closed set is a choice, taste is a toggle, a quantity is a range. 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.
  17. Named versions are PRESETS. The first is empty, which is the defaults. Every option a preset sets has a control in demo.ts.
  18. demo.ts says how the bench drives it. The fixture the effect is for, the target, update for anything live and remount for a one-shot, and mountOptions for what the stage must always pass.
  19. 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.
  20. Four harness forms. One-shot, driven by progress and 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, a geometry that says the draw mode and vertex count, whether it needs a depth buffer, any style of its own on the canvas, a prepare that 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 a startsOnView under 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.
  21. One arrival per page. A page entered by a view transition carries data-arrival="transition" on html, set by the transition’s own head script; a load effect reads it and does not play a second arrival. shader-wash already 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 a prepare that samples the word and a start on view.

Sources

  • mdn-prefers-reduced-motion: the media feature that stops it starting
  • wcag-animation-from-interactions: why motion must be possible to switch off