reszta poprawek
This commit is contained in:
@@ -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.
|
||||
+129
@@ -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.
|
||||
Reference in New Issue
Block a user