# Map Explainer — architecture reference Deep detail behind `TECHNIQUE.md`: the timing model, the river reveal + electric head, the per-country sequence, and label projection. The supplied values are examples, not a production style system. The custom-geometry example is `../assets/RiverReveal.tsx` + `../assets/CountryLabel.tsx` + `../assets/tokens.ts`. Provider-vector setup is in `../assets/MapTilerVectorElement.ts`; choose between the two modes with `data-sources.md`. ## 1. The render harness (per frame) Init the MapTiler map once (ref guard). On `load`: strip clutter (see `geo-prep.md`), add sources/layers, wait for `once('idle') → continueRender`. Per frame: ``` delayRender → setData/setPaintProperty → map.once('idle', continueRender) → triggerRepaint ``` `preserveDrawingBuffer:true` so Remotion's screenshot captures the canvas. Render `--gl=angle`. For an animated camera, read `render-stability.md`: the MapTiler renderer remains static and a CSS plate transform supplies the camera choreography. ## 2. Timing model — time-based; beat length derived from the sequences Everything keys off **seconds** (`t = frame / fps`), not reveal-units. The river draws over a window; each country **triggers when the river reaches it** and runs a fixed sequence. The beat is exactly as long as the sequences need. ```ts const RIVER_START = 0.3, RIVER_END = 8.0; // river draws over this window const BORDER_S = 2.5, FILL_S = 1.0, LABEL_S = 0.7; // per-country sequence (constant durations) const trigger = (c) => RIVER_START + META[c].stop * (RIVER_END - RIVER_START); // river-arrival time // beat length = max over c of (trigger(c) + BORDER_S + FILL_S + LABEL_S) + tail const reveal = interpolate(t, [RIVER_START, RIVER_END], [0,1], { ...clamp, easing: Easing.bezier(0.645, 0.045, 0.355, 1) }); ``` **Constant durations matter:** drive the border draw by _time since trigger_, not a slice of the reveal — otherwise complex or long borders flash by in a fraction of a second. ## 3. Provider-vector animation MapTiler Planet elements can be animated directly in place. Filter an exact `source-layer` feature, initialize its paint in the hidden or neutral state, then update paint properties from the Remotion frame. This works for line, fill, circle, and symbol layers without copying provider geometry into the project. Do not assume tiled geometry has a global order. Provider features are split at tile boundaries: opacity, colour, width, blur, radius, fill, and feature-state changes are reliable; semantic start-to-end line draws are not. Bake ordered GeoJSON when the direction of the draw carries meaning. ## 4. Custom line animation — reveal + electric draw-head The "electricity" is a **white-hot head** leading the draw — the last few % of the drawn line in its own bright + glow layers, faded out once the river completes. ```ts const riverDrawnKm = lineKm * reveal; map.getSource("river").setData(turf.lineSliceAlong(line, 0, Math.max(0.001, riverDrawnKm))); const headKm = lineKm * 0.03; map.getSource("river-head").setData(turf.lineSliceAlong(line, Math.max(0, riverDrawnKm - headKm), Math.max(0.001, riverDrawnKm))); let headFade = 0; if (reveal > 0.002 && reveal < 0.999) headFade = 1; else if (reveal >= 0.999) headFade = 1 - clamp01((t - RIVER_END) / 0.5); // fade out at the mouth map.setPaintProperty("river-headglow", "line-opacity", 0.85 * headFade); map.setPaintProperty("river-head", "line-opacity", headFade); ``` Layers, bottom→top: `river-glow` (electric blue `#49C6FF`, w11, op0.32, blur6) → `river-line` (icy core `#E8F7FF`, w3) → `river-headglow` (`rgba(120,225,255,.95)`, w16, blur9) → `river-head` (white `#FFFFFF`, w4.5). **No dark casing** — the bright icy core reads over every fill on its own. ## 5. Country animation — border draws → fill blooms → label rises Triggered by river arrival, each country runs three sequential phases. The border is a **darker shade** of the country colour (the electricity is on the river, not here). ```ts const lt = t - trigger(c); // local seconds since trigger // 1) complete source border draws on over a constant BORDER_S, multi-segment-safe const bp = interpolate(clamp01(lt / BORDER_S), [0,1], [0,1], { easing: Easing.bezier(0.645, 0.045, 0.355, 1) }); map.getSource(`trail-${c}`).setData(sliceBorder(DRAW[c], 0, DRAW[c].total * bp)); // COUNTRY_DARK line // 2) fill blooms in (opacity overshoots, then settles) after the border completes const fp = clamp01((lt - BORDER_S) / FILL_S); const fo = interpolate(fp, [0, 0.6, 1], [0, FILL_OPACITY * 1.25, FILL_OPACITY], { ...clamp, easing: Easing.bezier(0.3333333333333333, 1, 0.6666666666666666, 1) }); map.setPaintProperty(`fill-${c}`, "fill-opacity", fp <= 0 ? 0 : fo); // 3) label rises in after the fill const lp = clamp01((lt - BORDER_S - FILL_S) / LABEL_S); ``` `sliceBorder(d, fromKm, toKm)` reveals a portion of a complete (possibly multi-segment) border as a MultiLineString, slicing each segment by cumulative length — no joins across gaps and no viewport crop: ```ts const sliceBorder = (d, fromKm, toKm) => { const out = []; for (let i = 0; i < d.segLines.length; i++) { const start = d.cum[i], end = start + d.segLen[i]; const a = Math.max(fromKm, start), b = Math.min(toKm, end); if (b - a <= 0.0008) continue; out.push(turf.lineSliceAlong(d.segLines[i], a - start, b - start).geometry.coordinates); } return { type:"Feature", properties:{}, geometry:{ type:"MultiLineString", coordinates: out } }; }; ``` Choose fill, border, and river colours in the production's local token file. The bundled token values are examples only; do not carry a source project's palette into another production. ## 6. Labels — HTML overlay, projected each frame Labels are React, not map symbols (full typography control). `CountryLabel` is an example accent-rule, rise-and-fade treatment; select the typeface and final values in the production. Positioned by projecting the anchor to screen pixels **every frame**, stored in state: ```ts const p = map.project(META[c].anchor); // lngLat → screen px (respects the live camera) pos[c] = { x: p.x, y: p.y, reveal: lp }; setLabels(pos); // re-render the overlay; effect deps exclude `labels` ``` `CountryLabel` shows the mechanics: uppercase region name, short accent divider, rise/fade entrance, and `pointerEvents:none`. Select font, weight, size, spacing, contrast, and colour from the production's own type and palette system. ## 7. Camera — fixed map plate for any movement Read `render-stability.md`. Do not use per-frame `map.jumpTo()` for a moving 2D shot; it can shimmer in headless renders even on satellite imagery. Interpolate the intended camera for the CSS plate transform, while keeping the MapTiler renderer static.