reszta poprawek

This commit is contained in:
2026-08-09 09:47:34 +02:00
parent 966dd3f209
commit a0c9d7947b
439 changed files with 112891 additions and 0 deletions
@@ -0,0 +1,91 @@
# Map element data sources
Choose the source independently for every story element. A single map can—and often should—mix
provider vectors with custom geodata.
## Decision rule
Use a MapTiler Planet vector layer when the feature already exists there, its attributes support an
editorially precise filter, and provider geometry is acceptable for the claim. Use custom GeoJSON when
the feature is absent, proposed, historical, disputed, corrected, privately sourced, or needs ordered
geometry for a deterministic draw.
| Requirement | MapTiler vector layer | Custom GeoJSON |
| --------------------------------------------------------------------------- | --------------------- | ------------------------------------------- |
| Roads, waterways, water, boundaries, land cover, or other standard context | Prefer | Use only when provider data is insufficient |
| Basemap-consistent geometry without a duplicate local dataset | Prefer | No |
| Proposed, historical, classified, corrected, or production-specific element | No | Prefer |
| Fade, colour, width, radius, blur, or fill-opacity animation | Yes | Yes |
| Feature-state highlight when a stable feature ID exists | Yes | Yes |
| Deterministic source-to-end line draw or perimeter draw | Bake first | Prefer |
| Geometry editing, morphing, clipping, or exact sequencing | No | Prefer |
Provider alignment is not proof of correctness. Inspect the attributes and geometry against the
editorial source before presenting a provider feature as evidence.
## MapTiler vector mode
MapTiler Planet is a vector tile source. Add it once, then build story layers with a
`source-layer` and an exact attribute filter. Read the current MapTiler Planet schema before choosing
layer names or fields.
Common layer categories include `waterway`, `water`, `transportation`, `boundary`, `landcover`, and
`poi`; availability, fields, and zoom ranges vary by schema version.
```ts
import {addMapTilerVectorElement, setVectorElementPaint} from "./MapTilerVectorElement";
addMapTilerVectorElement(map, process.env.REMOTION_MAPTILER_KEY!, {
id: "story-river",
sourceLayer: "waterway",
type: "line",
filter: [
"all",
["==", ["get", "class"], "river"],
["==", ["coalesce", ["get", "name_en"], ["get", "name"]], "Yarlung Tsangpo"],
],
layout: {"line-cap": "round", "line-join": "round"},
paint: {
"line-color": "#E8F7FF",
"line-width": 3,
"line-opacity": 0,
},
});
setVectorElementPaint(map, "story-river", {
"line-opacity": reveal,
"line-width": 2 + reveal * 2,
});
```
Animate provider features by changing paint properties from the Remotion frame: opacity, colour,
width, blur, fill opacity, circle radius, or symbol opacity. Use feature state only when the source
provides stable IDs and the selection remains deterministic across tiles.
Do not treat a tiled line as one ordered path. Vector tiles split features at tile boundaries, so a
source-to-mouth or start-to-end draw has no reliable global order. If that motion carries meaning,
extract and verify the complete feature, order it once, save it as GeoJSON, and use the custom mode.
## Custom geodata mode
Use the bundled `../assets/RiverReveal.tsx` and `../scripts/prep-geo.mjs` pattern for custom GeoJSON. This mode owns
the exact geometry and can slice it by distance, calculate entry triggers, draw complete borders, and
produce deterministic sequences.
Custom mode is mandatory when:
- the feature is not in the provider dataset;
- the story uses a proposed route, planned tunnel, historical boundary, disputed interpretation, or
non-public dataset;
- provider geometry was editorially corrected;
- motion must travel through the geometry in a verified order;
- a complete off-screen boundary matters and a viewport query would silently crop it.
## Hybrid mode
Use provider vectors for ordinary contextual features and custom GeoJSON for the specific claim. For
example: MapTiler waterways and roads as aligned context; a custom proposed tunnel, dam site, disputed
boundary, or verified evacuation area as the highlighted evidence.
Keep provider and custom layers visually distinct when they carry different evidentiary weight. Record
the source and effective date of every custom layer in the production notes.
@@ -0,0 +1,129 @@
# 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.
@@ -0,0 +1,76 @@
# Map Explainer — basemap & geo prep
How the basemap is cleaned and how `../scripts/prep-geo.mjs` bakes the per-country data the component reads.
## Basemap styling — strip the clutter
On `load`, remove the basemap's labels and inner admin borders so only your geography reads:
```ts
for (const l of m.getStyle().layers as any[])
if (l.type === "symbol" || /other border/i.test(l.id)) m.removeLayer(l.id);
```
- `type === "symbol"` → every place/water/road **label** (the "MapTiler labels"). Gone.
- Inner admin-border layer IDs vary by style. Inspect the loaded style, remove state/province/district
layers as needed, and retain only the context borders the production requires.
- Logo/attribution: `maptilerLogo:false` + `attributionControl:false` aren't always enough — also hide
via CSS in the component:
```tsx
<style>{`.maplibregl-ctrl-bottom-left,.maplibregl-ctrl-bottom-right,.maplibregl-ctrl-attrib,.maptiler-logo{display:none!important}`}</style>
```
## `../scripts/prep-geo.mjs` → outputs
Reads a routed river GeoJSON + country polygon GeoJSONs; writes:
- **River line** — simplified for a smooth draw. For a braided river, route one source→mouth path through
the network first: greedy endpoint-chaining bounces between parallel channels. → `src/geo/river-flow.json`.
- **`public/geo/borders.geojson`** — each country's polygon tagged `{country: name}` (one source,
filtered per country for the fills).
- **`src/geo/country-meta.json`** — per country `{ stop, anchor, border }`.
### `stop` — when a country lights up
Walk the river points; first point inside a country (`turf.booleanPointInPolygon`) = the arc-length
fraction where the river **enters** it. Drives the trigger time. The headwaters country = 0.
### `anchor` — label centre via pole of inaccessibility
The most-interior point of the country (clipped to a per-country **story bbox** so a big country
centres in the relevant region, not its far bulge), then a small operator **nudge**. Pole = grid-sample
inside the polygon, keep the point with max distance to the boundary. **Centroids get pulled to edges —
don't use them.**
```js
const pole = (poly) => {
const bb = turf.bbox(poly), edge = turf.polygonToLine(poly), N = 46;
let best = null, bestD = -1;
for (let i = 0; i <= N; i++) for (let j = 0; j <= N; j++) {
const p = turf.point([bb[0]+(bb[2]-bb[0])*i/N, bb[1]+(bb[3]-bb[1])*j/N]);
if (!turf.booleanPointInPolygon(p, poly)) continue;
const d = turf.pointToLineDistance(p, edge);
if (d > bestD) { bestD = d; best = p.geometry.coordinates; }
}
return best;
};
const ANCHOR_BBOX = { china:[82,27,96,32], india:[76,14,99,31], bangladesh:[86,20,93,27] }; // story regions
const NUDGE = { china:[0,0.6], india:[-1.0,0], bangladesh:[0,-0.6] }; // operator-directed
```
### `border` — complete source geometry
Preserve every exterior ring from the named country source. Never clip a country or bilateral border to
the framed bbox and never discard an off-screen segment: the geometry may leave the frame naturally.
The renderer handles a MultiLineString by cumulative length, so it remains one timed reveal without
inventing joins across gaps.
## Tuning the geo prep for a new scenario
| Want | Knob |
| --------------------------------- | -------------------------------------------------------------------- |
| Which countries | the country list in `prep-geo.mjs` (+ supply their polygon GeoJSONs) |
| Label centred in the right region | `ANCHOR_BBOX[country]` (the story bbox) |
| Nudge a label | `NUDGE[country]` (lng, lat offset) |
| What border is drawn | the complete named source geometry; never the visible extent |
| When each lights up | derived from `stop` — depends on the river geometry |
@@ -0,0 +1,64 @@
# Moving Map Render Stability
Read this reference before building a moving 2D MapTiler scene or diagnosing a wavering Remotion render.
## Symptom and cause
If basemap detail shimmers or jitters during a pan/zoom, the likely cause is per-frame `map.jumpTo()`.
Headless MapTiler capture can resample both vector hillshade and satellite imagery differently frame to
frame. Tile retries, easing changes, and label changes do not solve that renderer effect.
## Required pattern: fixed map plate
For any 2D pan/zoom:
1. Render the MapTiler canvas once at the largest required zoom in an oversized container. Size it from the camera route and keep each dimension below the browser's reliable WebGL render-buffer limit (commonly 4096 px). Do **not** blindly use 3×: a 1920×1080 composition becomes 5760 px wide and Chromium may silently downsample it, causing visible pixelation during the CSS zoom.
2. Keep the map's camera static.
3. For each frame, calculate the approved target centre/zoom, then move the canvas with CSS `translate` + `scale`.
4. Apply the same transform to every projected HTML overlay.
5. Continue to animate GeoJSON data and paint properties imperatively; only the renderer camera is frozen.
Keep pitch and bearing constant. Use a 3D engine such as Cesium for genuine changing pitch/bearing or a terrain flythrough.
```ts
const baseZoom = Math.max(start.zoom, end.zoom);
const map = new maptilersdk.Map({
container,
style,
center: end.center,
zoom: baseZoom,
pitch: end.pitch ?? 0,
bearing: end.bearing ?? 0,
interactive: false,
fadeDuration: 0,
canvasContextAttributes: {preserveDrawingBuffer: true},
});
// Per Remotion frame. `camera` is the approved centre/zoom interpolation.
const projected = map.project(camera.center);
const scale = 2 ** (camera.zoom - baseZoom);
const plate = {
transform: `translate(${width / 2 - projected.x * scale}px, ${height / 2 - projected.y * scale}px) scale(${scale})`,
transformOrigin: "0 0",
};
// Convert label projection with exactly the same plate transform.
const labelX = labelPoint.x * scale + width / 2 - projected.x * scale;
const labelY = labelPoint.y * scale + height / 2 - projected.y * scale;
```
### Plate sizing and sharpness
- Render at the maximum zoom reached by **any** camera waypoint, including intermediate or hold cameras. The CSS scale should never exceed `1`; otherwise the plate is being enlarged.
- Centre the frozen map on the midpoint of the camera route's geographic extent, not automatically on the final camera. This minimizes required overscan.
- Keep the largest canvas dimension at or below 4096 px unless the actual render environment has been tested with a larger `MAX_RENDERBUFFER_SIZE`.
- For 1920×1080, a 3840×2160 plate is a safe default. For 1080×1920, use approximately 2700×3840 when the route needs extra horizontal pan room.
- If the route cannot fit within that plate at the required zoom, split the shot into two fixed plates with a deliberate editorial transition. Do not trade sharpness for one enormous canvas.
- Distinguish failure modes: repeating shimmer means the live renderer is moving; steadily soft tiles during a CSS push means the fixed plate is underspecified, internally downsampled, or being scaled above `1`.
## Verification
- Render a short MP4, not only a Studio preview.
- Inspect static terrain texture and satellite detail while the camera moves.
- If any underlying map detail wavers, use the fixed map plate. Do not approve it as a minor preview artefact.
- Render WebGL with `--gl=angle`, `preserveDrawingBuffer:true`, and conservative concurrency (`1`) while validating.