Waypoint 09 Field notes Weather · Clearing

How This Map Is Made

Field notes on the machinery: simplex noise, bent terrain, contour lines drawn by a shader, and marching squares at build time.

Every map on this site is invented. There is no satellite data and no real mountain behind the lines — just a few hundred lines of code that grow terrain from numbers. These are the field notes on how, for anyone who likes to know how the instrument works.

The ground: noise, in layers

Everything starts with simplex noise: a function that takes a position and returns a smooth, random-looking height between −1 and 1. Nearby positions return similar values, so it looks like rolling ground rather than static.

One layer of noise is too smooth — it looks like dunes. Real terrain has big hills with smaller hills on them, and rocks on those. So the noise is added to itself four times, each layer at double the frequency and half the strength. This is called fractal Brownian motion:

float fbm(vec2 p) {
  float amp = 0.5, sum = 0.0, norm = 0.0;
  for (int i = 0; i < 4; i++) {
    sum  += amp * snoise(p + vec2(17.3, -9.1) * float(i));
    norm += amp;
    amp  *= 0.5;   // each layer is half as strong…
    p    *= 2.03;  // …and about twice as detailed
  }
  return 0.5 + 0.5 * sum / norm;  // 0..1
}

The odd offsets and the 2.03 instead of exactly 2 stop the layers from lining up with each other, which would leave tell-tale grid patterns.

Bending the ground

Plain fractal noise still looks a bit too regular, like a well-raked garden. The fix is domain warping: before sampling the terrain at a point, nudge that point sideways by another noise field. Ridges get dragged into curves and valleys into meanders.

float terrain(vec2 p, float t) {
  vec2 warp = vec2(snoise(p * 0.55 + vec2(t * 0.01, 3.1)),
                   snoise(p * 0.55 + vec2(7.7, -t * 0.012)));
  return fbm(p + 0.22 * warp);
}

The t in there is time. It changes very slowly, so the land in the background is always drifting — imperceptibly, like weather over hills.

Drawing contours in a shader

The background is drawn by a fragment shader, which runs once for every pixel, every frame. Each pixel knows its own height but nothing about its neighbours, so it can’t trace a line the way you would with a pen. The trick is to ask a different question: how close am I to a contour?

Multiply the height by the number of levels. Whenever that value is close to a whole number, you’re near a line. fwidth() tells you how quickly the value changes between neighbouring pixels, which lets you turn “close” into an exact distance in pixels — so lines stay the same width on steep ground and gentle ground alike.

float v  = h * uLevels;             // height measured in contour steps
float fw = max(fwidth(v), 1e-4);    // how fast v changes per pixel
float d  = min(fract(v), 1.0 - fract(v)) / fw;  // distance to nearest line, in px
float isIndex = 1.0 - step(0.5, mod(floor(v + 0.5), 5.0));  // every 5th line
float halfW   = mix(0.5, 0.95, isIndex) * uDpr;
float line    = 1.0 - smoothstep(halfW, halfW + 1.0, d);    // anti-aliased

Every fifth line is an index contour, drawn a little heavier, exactly as on a printed map.

The lantern under your cursor

When you move a mouse, the shader receives its position and adds a soft Gaussian glow around it. Lines inside that glow get brighter and shift towards amber, so it feels like you’re holding a lamp over the sheet. The glow follows the cursor with a little lag (an exponential ease), which makes it feel carried rather than attached.

Weather

Each note sets a weather, and the background eases towards it over a couple of seconds:

  • Fog is a second, slower noise field. Where it’s high, the lines fade and the ground is washed towards a pale grey.
  • Rain is a set of thin diagonal streaks. The screen is divided into narrow columns, and a hash function decides, for each column and each moment, whether a drop is falling there.
  • Dusk and clearing change the warmth of the lantern and the strength of the lines.

Light across the ground

Contour lines alone describe the land; shading makes you feel it. The shader asks how steeply the height changes between neighbouring pixels (dFdx and dFdy again), turns that slope into a surface normal, and compares it with a light coming from the north-west — the convention on printed maps for centuries. Slopes facing the light brighten a touch, slopes facing away darken. It is very faint; it should read as relief, not as weather.

Dropping a stone

Clicking bare land adds a ripple: a ring that travels outwards and briefly raises and lowers the height itself, so the contours bend around it the way reflections bend in a pond. Up to four ripples can overlap. Where you clicked, a small survey mark appears with the height of the ground at that point — read back in JavaScript from the same noise the shader draws.

The first time you arrive, the whole map surveys itself into existence the same way: the lines spread outwards from one point, behind a faint amber front.

The folded map

Pressing M unfolds a route map drawn on a canvas. Its terrain is not random: every waypoint pulls the ground towards its own elevation (inverse-distance weighting), so the route really does run down into a basin and climb out again. The map is tinted by height and shaded by the same north-west light, and it remembers which notes you have read so the walked part of the trail is drawn solid.

Marching squares, at build time

The small maps on the cards are different: they’re drawn once, when the site is built, and shipped as plain SVG. For each one, the build samples a fresh patch of terrain on a grid, then runs marching squares for each contour level — the algorithm the visual note explains. The little segments are stitched into lines, simplified (points that sit almost on a straight line are dropped), and smoothed into curves.

Each patch is seeded from a hash of the note’s slug, so every note has its own landscape, and it’s the same landscape every time the site is built.

Being kind to devices

A full-screen shader can make a laptop fan spin up. A few things keep it quiet:

  • Resolution is capped, and lowered automatically if frames start running slow.
  • When nothing is moving but the slow drift, it draws at about 24 frames a second instead of 60.
  • It stops entirely when the tab is hidden.
  • With reduce motion switched on in your system settings, the map holds still, the rain disappears, and nothing animates in.
  • Without WebGL, a plain canvas draws static contour lines instead, using the same marching squares as the cards.

What isn’t here

No analytics, no cookies, no tracking pixels, no fonts or scripts loaded from anyone else’s servers. The only things the site remembers — your theme, your language, your weather log and which waypoints you have read — live in your own browser’s storage, and you can clear them any time. The ambient sound is synthesised on the spot, like the rain in the sound note; nothing is downloaded.