The reading room · Build notes
How the atlas was drawn
Meridian is an entire cartographic animation built from SVG and CSS. There is no map library, no animation library, and no runtime JavaScript at all. This is the working method, specific enough to rebuild.
- Paths generated
- 367
- Runtime JS
- 0 bytes
- Raster images
- 0
- Dependencies (runtime)
- 0
1. The flow field
The map is grown from a seeded value-noise field. A small deterministic PRNG (mulberry32) fills a grid of random values; a smoothstep-interpolated lookup turns any point into a smooth scalar, summed over two octaves. That scalar becomes an angle: at every coordinate, the field points somewhere. Because it is seeded, the same map is reproduced byte-for-byte on every build.
function makeNoise(rand, cells = 8) {
const vals = Array.from({ length: (cells + 1) ** 2 }, rand);
// bilinear + smoothstep lookup -> smooth [0,1) scalar
}
// angle at a point = (noise - 0.5) * TAU, plus a prevailing drift2. Streamlines
From a jittered grid of seed points, each streamline is integrated through the field with a second-order (midpoint) step, forward and backward, until it leaves the plate. The point list is decimated to about twenty nodes and smoothed into a cubic Bezier path with Catmull-Rom tangents, which keeps the SVG small while staying fluid. Chart II, currents, folds four rotational gyres into the angle field; Chart III, migrations, abandons the field for sparse routes bowed between generated stations.
The generator (scripts/gen-map.mjs) runs at build time and writes a typed data module. The browser never computes any of this. Present count: 184 wind lines, 154 current lines, 18 routes across 14 stations.
3. Line-drawing on scroll
Every path ships with pathLength="1", which normalises its length regardless of shape. In CSS, stroke-dasharray: 1; stroke-dashoffset: 1 hides it; a keyframe retracts the offset to 0 to draw it. The trick is binding that keyframe to the scroll rather than to a clock, with animation-timeline. No JavaScript observes the scroll.
.flow path { stroke-dasharray: 1; stroke-dashoffset: 1;
animation: draw both; animation-timeline: --atlas; }
@keyframes draw { to { stroke-dashoffset: 0; } }A named view-timeline: --atlas block on a tall track element gives a progress value as that element passes the viewport. Each path draws over a slice of that progress (animation-range: contain X% Y%), and a six-bucket diagonal cascade (adata-d attribute set by the generator) makes the field resolve as a wave rather than all at once. Attributes and classes only: no inline styles, so the strict Content Security Policy holds.
4. The reconfigure
The three charts are three complete maps stacked in one position: sticky stage. As the track scrolls, opacity keyframes on the same --atlas timeline cross-fade winds into currents into migrations, each thirded across the pinned window, while the incoming chart draws itself. The variable-font legend animates its weight and optical-size axes over the identical range, so the numbers thicken into focus as their chart comes true.
5. The fallback strategy
Scroll-driven animation is not everywhere yet (Firefox ships it behind a flag), so every animated rule lives inside@supports (animation-timeline: view()) and (prefers-reduced-motion: no-preference). Outside that branch the map is not broken, only less animated: the sticky stage un-pins, the three charts un-stack into separately legible figures, and every line defaults to fully drawn (stroke-dashoffset: 0). Firefox, reduced-motion, and no-JavaScript all receive a finished atlas. That is the whole discipline: the enhancement is additive, never load-bearing.
6. Type and delivery
Two self-hosted variable fonts do the talking: Fraunces (display, with the optical-size axis the legend animates) and Hanken Grotesk (data, with tabular numerals so coordinates never jitter). Both load from this domain, so no request leaves it and there is nothing to consent to. The whole site is a handful of static HTML files with inlined, hashed CSS. The single largest asset is a font.
To reproduce the geometry: npm run gen:map. To build: npm run build(the generator runs first). Change the SEED constant and the entire world is redrawn.