Anatomy of Topographic Drift
How an 18px noise grid, a 16-case marching-squares switch, and a creeping z-offset redraw living contour lines every frame — then freeze under reduced motion.
TopographicDrift is not an SVG map you morph. Every frame it samples a
value-noise field onto a coarse grid, connects isolines with marching
squares, and nudges the noise's z-offset so the terrain slowly drifts.
The canvas clears and redraws — there is no retained path geometry.
Sample the field
Corners sit on an ~18px lattice. Each corner stores one noise value:
field[j * stride + i] = valueNoise(
i * cell * noiseScale, // default noiseScale = 0.004
j * cell * noiseScale + z,
);lineCount (default 9) is how many elevation levels you stroke afterward —
not how many grid cells you have.
Each corner is valueNoise(i·cell·scale, j·cell·scale + z). Contour count is separate — lineCount levels get stroked later.
- Grid
- cell ≈ 18px sample spacing
- Field
- noiseScale = 0.004
- Levels
- lineCount default 9
The dots in the diagram are those sample corners. Darker / lighter is just noise amplitude so you can see the field before any lines are drawn.
Marching squares → isolines
For every cell and every level l / (lineCount + 1), the four corners
become a 4-bit mask (tl|tr|br|bl). Mask 0 and 15 skip (all below /
all above). Everything else picks edge midpoints by linear interpolation and
strokes one or two segments — the classic 16-case table.
let id = 0;
if (tl > level) id |= 8;
if (tr > level) id |= 4;
if (br > level) id |= 2;
if (bl > level) id |= 1;
if (id === 0 || id === 15) continue;
const top = x + cell * ((level - tl) / (tr - tl || 1e-6));
// …right, bottom, left — then switch (id) { case 1: seg(…); … }Stroke alpha peaks on mid-levels and falls off toward the extremes:
0.1 + 0.25 * (1 - Math.abs(0.5 - level) * 2)Filled corners are above the level. The segment is the isoline — interpolated where the field crosses level on each edge.
- Cases
- 16 configs · skip 0 and 15
- Alpha
- 0.1 + 0.25×(1−|0.5−level|×2)
Drift is just z
The "living map" feeling is one line in the tick:
const tick = () => {
z += 0.0015 * speed;
draw(); // sampleField() + marching squares again
rafId = requestAnimationFrame(tick);
};Same lifecycle as the other canvas backgrounds: ResizeObserver rebuilds the
grid, IntersectionObserver and visibilitychange pause offscreen / when the
tab hides, and prefers-reduced-motion stops the loop on a still frame.
No path morphing — the tick advances z, sampleField() runs again, and marching squares redraw. Offscreen / hidden / reduced motion stop the loop.
- z-offset
- z += 0.0015 × speed
- Lifecycle
- IO · visibility · reduced freeze
Color comes from --foreground (or a color prop), re-probed when the
document theme mutates — so light and dark both stay legible without a baked
palette.
The result
Sample → march → nudge z. Contours without a mesh, motion without morphing
paths.