Catalog / Technique
Contour field
Use when drawing contour lines, isolines or a topographic map from a noise or height field in a fragment shader. Covers the line technique, the reveals, the tuning, and where such a map may and may not sit on a page.
- Loads as
technique-contour-field- Written for
- An agent, to follow
Contour field
A height field that is never shown, read as the lines of a map. Drawn entirely in the fragment shader: no geometry, no texture.
When to use
- A load effect or a hero band that should read as craft and still belong to the page: the lines are the page’s own ink.
- A background mark for a region with no text in it.
Do not use it for:
- Behind body text, at any strength.
- Anywhere near real numbers, charts or maps. It reads as part of them.
- Listings and card grids.
Rules
- Differentiate the continuous height, not the wrapped value. The wrap has a jump in it, and the derivative at the jump is a false line.
- Use the length of the derivative vector, not
fwidth, where line evenness matters.fwidththickens diagonals. - Multiply CSS widths by the canvas’s real pixel ratio. Below one device pixel, fade the line instead of thinning it.
- Drop intermediate contours on steep ground and keep the index lines. Otherwise a slope is a smear.
- Normalise the field and set the interval as a number of levels. Otherwise the line count changes when the octave count does.
- Spread the levels over the height the field really has. Layered gradient noise stays within about a third of its nominal range. Left unscaled, eleven levels draw four, and a reveal that sweeps the range is over in a fifth of its time.
- Size features in CSS pixels, never as a share of the element.
- Hash with integers. A sine hash differs between GPUs.
- Write the noise from the published algorithm. Do not copy a shader snippet whose licence you have not read.
- Output premultiplied ink on a transparent canvas, so the ground is the page’s and one shader serves both themes.
- Explain in comments above the shader string, not inside it. A comment in the string ships.
Decisions
| Question | Choose | Because |
|---|---|---|
| Which reveal? | Tide by default | It is the one where the map visibly arrives |
| Can a line draw itself on? | No | An implicit line has no start, end or length. Tide is the honest substitute |
| Which noise? | Gradient noise | Value noise gives lattice-shaped contours; simplex gains nothing in 2D |
| Relief in perspective? | A real mesh, not a fragment-shader fake | The fake breaks above about 30 degrees of tilt and costs 3.6 times as much |
| Ends on? | Nothing for a page load, a still for a hero band | A still behind text must be far weaker than the reveal was |
| Which colour? | The ink by default; the accent, or ink rising to accent by height, as a named version | Colour comes from the page’s two tokens and nowhere else, so it is right on every skin. A third colour would be the effect’s own |
| One recipe or several looks? | One recipe with named versions in PRESETS |
A look is a few options that belong together. A second file for it would be a second harness to keep the same |
Tuning
| Dial | Start | Range | Low end | High end |
|---|---|---|---|---|
| Feature size (CSS px) | 180 | 90 to 400 | Busy, many small islands | A few broad sweeps |
| Octaves | 3 | 1 to 5 | Smooth rings, very designed | Ragged coastlines; cost is linear |
| Gain | 0.5 | 0.35 to 0.7 | The base shape dominates | Detail as strong as the hills |
| Warp | 0 | 0 to 0.8 | Plain terrain | Folded, geological; at 1.5 lines break up |
| Levels | 11 | 6 to 22 | Sparse and airy | Dense; steep ground drops lines |
| Line width (CSS px) | 1.25 | 0.75 to 2 | Hairline | Heavy, diagram-like |
| Ink while playing | 0.6 | 0.3 to 1 | A watermark passing | Full ink; text under it is hard to read |
| Ink in a still behind a headline | 0.2 | 0.12 to 0.3 | Barely there | Above 0.3 it costs reading |
| Drift (rad/s) | 0.25 | 0 to 1 | A frozen map | Visibly churning |
Ranges are from stills. Motion has not been tuned by eye.
Rest state
Nothing by default. With a still: the last frame as a 2D canvas, at the still’s ink strength, behind the content. Under reduced motion, without WebGL2 or on a software renderer there is no map at all.
Cost
- To build: small on a finished harness. The line function is the whole craft.
- To run: about 1.3 kB of shader on a 2.3 kB harness. Cost per pixel is linear in octaves, and doubles with warp on.
- To the reader: lines over the page for under a second. A still costs nothing to run and something to read past.
Checklist
-
npm run stills, and the frames look like the reveal’s description. - Lines are even on diagonals and upright runs alike.
- On a 2x screen a 1.25 px line is 2.5 device pixels, not 1.25.
- Steep ground shows index lines, not a black mass.
- The same settings read on the light theme and the dark.
- No text sits under lines at more than a watermark.
- Looked at once at phone size and once at 30 fps.
Recipes
contour-field: the flat form, with four reveals.plane-settle: the same map on a tilted plane that swings flat and lands on the flat form.terrain-settle: the same field as real relief on a mesh, with its contours re-evaluated per fragment.
Sources
mdn-prefers-reduced-motion: the media feature that stops it starting