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,70 @@
import React from 'react';
import {Easing, interpolate} from 'remotion';
// Reusable country label. Supply typography and final values from the consuming project; the CSS custom
// properties below provide neutral fallbacks. Positioned by its centre (x,y in screen px).
export const CountryLabel: React.FC<{
name: string;
color: string;
reveal: number;
x: number;
y: number;
}> = ({name, color, reveal, x, y}) => {
const e = interpolate(reveal, [0, 1], [0, 1], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
easing: Easing.bezier(0.3333333333333333, 1, 0.6666666666666666, 1),
});
return (
<div
style={{
position: 'absolute',
left: x,
top: y,
transform: 'translate(-50%, -50%)',
pointerEvents: 'none',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
opacity: e,
}}
>
<div
style={{
transform: `translateY(${(1 - e) * 16}px)`,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}
>
{/* accent rule — draws out from the centre in the country's colour */}
<div
style={{
width: 64,
height: 3,
borderRadius: 2,
background: color,
transform: `scaleX(${e})`,
boxShadow: `0 0 10px ${color}`,
}}
/>
<div
style={{
fontFamily: 'var(--map-label-font, system-ui, sans-serif)',
fontWeight: 'var(--map-label-weight, 600)',
fontSize: 'var(--map-label-size, 34px)',
letterSpacing: 'var(--map-label-tracking, 0.16em)',
textTransform: 'uppercase',
color: 'var(--map-label-color, #ffffff)',
textShadow: 'var(--map-label-shadow, 0 2px 18px rgba(0,0,0,0.9))',
marginTop: 13,
paddingLeft: 'var(--map-label-tracking, 0.16em)',
whiteSpace: 'nowrap',
}}
>
{name}
</div>
</div>
</div>
);
};
@@ -0,0 +1,80 @@
// Surface an existing MapTiler Planet feature without maintaining duplicate GeoJSON.
// Paint animation is deterministic; geometry slicing is not. For a source-to-end line draw,
// bake the selected feature to ordered GeoJSON and use RiverReveal.tsx instead.
type VectorLayerType = 'fill' | 'line' | 'circle' | 'symbol';
export type MapTilerVectorElement = {
id: string;
sourceLayer: string;
type: VectorLayerType;
filter?: unknown[];
minzoom?: number;
maxzoom?: number;
layout?: Record<string, unknown>;
paint: Record<string, unknown>;
};
const SOURCE_ID = 'maptiler-planet';
export const addMapTilerVectorElement = (
map: any,
apiKey: string,
element: MapTilerVectorElement,
beforeId?: string,
) => {
if (!map.getSource(SOURCE_ID)) {
map.addSource(SOURCE_ID, {
type: 'vector',
url: `https://api.maptiler.com/tiles/v3/tiles.json?key=${apiKey}`,
});
}
if (map.getLayer(element.id)) return;
map.addLayer(
{
id: element.id,
type: element.type,
source: SOURCE_ID,
'source-layer': element.sourceLayer,
...(element.filter ? {filter: element.filter} : {}),
...(element.minzoom === undefined ? {} : {minzoom: element.minzoom}),
...(element.maxzoom === undefined ? {} : {maxzoom: element.maxzoom}),
...(element.layout ? {layout: element.layout} : {}),
paint: element.paint,
},
beforeId,
);
};
export const setVectorElementPaint = (
map: any,
layerId: string,
paint: Record<string, unknown>,
) => {
for (const [property, value] of Object.entries(paint)) {
map.setPaintProperty(layerId, property, value);
}
};
// Example:
//
// 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},
// });
//
// Per Remotion frame:
// setVectorElementPaint(map, "story-river", {
// "line-opacity": reveal,
// "line-width": 2 + reveal * 2,
// });
@@ -0,0 +1,363 @@
import * as maptilersdk from '@maptiler/sdk';
import * as turf from '@turf/turf';
import React, {useEffect, useRef, useState} from 'react';
import '@maptiler/sdk/dist/maptiler-sdk.css';
import {
AbsoluteFill,
Easing,
continueRender,
delayRender,
interpolate,
useCurrentFrame,
useVideoConfig,
} from 'remotion';
import {CountryLabel} from './CountryLabel';
import countryMeta from './sample-data/country-meta.json';
import flowCoords from './sample-data/yarlung-flow.json';
import {COLORS, COUNTRY, COUNTRY_DARK, FILL_OPACITY} from './tokens';
// Sample route reveal. Replace the imported sample geometry, names, timing, and visual tokens in the
// consuming production. The renderer stays static; approved centre/zoom motion is a CSS plate transform.
maptilersdk.config.apiKey = process.env.REMOTION_MAPTILER_KEY as string;
const line = turf.lineString(flowCoords as [number, number][]);
const lineKm = turf.length(line);
const START = {
center: [89.6, 27.7] as [number, number],
zoom: 4.75,
pitch: 0,
bearing: 0,
};
const END = {
center: [90.2, 27.0] as [number, number],
zoom: 5.05,
pitch: 0,
bearing: 0,
};
const lerp = (a: number, b: number, t: number) => a + (b - a) * t;
const clamp01 = (v: number) => Math.max(0, Math.min(1, v));
const ORDER = ['china', 'india', 'bangladesh'] as const;
type Country = (typeof ORDER)[number];
const META = countryMeta as Record<
Country,
{stop: number; anchor: [number, number]; border: [number, number][][]}
>;
const countryPolygons = {
type: 'FeatureCollection' as const,
features: ORDER.map((country) => ({
type: 'Feature' as const,
properties: {country},
geometry: {
type: 'MultiPolygon' as const,
coordinates: META[country].border.map((ring) => [ring]),
},
})),
};
// Pre-build each country's border as ordered segments with cumulative lengths (for the multi-segment draw).
const DRAW = Object.fromEntries(
ORDER.map((c) => {
const segLines = META[c].border.map((s) => turf.lineString(s));
const segLen = segLines.map((l) => turf.length(l));
const cum: number[] = [];
let acc = 0;
for (const L of segLen) {
cum.push(acc);
acc += L;
}
return [c, {segLines, segLen, cum, total: acc}];
}),
) as Record<
Country,
{segLines: any[]; segLen: number[]; cum: number[]; total: number}
>;
// Reveal the portion of the border between fromKm and toKm as a MultiLineString (no joins across gaps).
const sliceBorder = (
d: (typeof DRAW)[Country],
fromKm: number,
toKm: number,
) => {
const out: number[][][] = [];
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' as const,
properties: {},
geometry: {type: 'MultiLineString' as const, coordinates: out},
};
};
const EMPTY = {
type: 'Feature' as const,
properties: {},
geometry: {type: 'MultiLineString' as const, coordinates: [] as number[][][]},
};
// --- Timing (seconds). River draws over [RIVER_START, RIVER_END]; each country triggers when the river
// reaches it (stop · span), then runs border → fill → label. Beat length is derived from these. ---
const RIVER_START = 0.3;
const RIVER_END = 8.0;
const BORDER_S = 2.5;
const FILL_S = 1.0;
const LABEL_S = 0.7;
const trigger = (c: Country) =>
RIVER_START + META[c].stop * (RIVER_END - RIVER_START);
export const RiverReveal: React.FC = () => {
const ref = useRef<HTMLDivElement>(null);
const started = useRef(false);
const frame = useCurrentFrame();
const {fps, durationInFrames, width, height} = useVideoConfig();
const [map, setMap] = useState<any>(null);
const [labels, setLabels] = useState<
Record<string, {x: number; y: number; reveal: number}>
>({});
const [plate, setPlate] = useState({x: 0, y: 0, scale: 1});
const [handle] = useState(() => delayRender('maptiler init A'));
useEffect(() => {
if (!ref.current || started.current) return;
started.current = true;
const m = new maptilersdk.Map({
container: ref.current,
style: maptilersdk.MapStyle.BASIC,
center: END.center,
zoom: Math.max(START.zoom, END.zoom),
pitch: END.pitch,
bearing: END.bearing,
interactive: false,
attributionControl: true,
navigationControl: false,
geolocateControl: false,
maptilerLogo: true,
fadeDuration: 0,
canvasContextAttributes: {preserveDrawingBuffer: true},
} as any);
m.on('load', () => {
// Strip basemap labels (symbols) AND the inner admin-1 borders ('Other border[ dash]',
// admin_level 310) to cut basemap clutter. Keep country + disputed borders.
for (const l of m.getStyle().layers as any[])
if (l.type === 'symbol' || /other border/i.test(l.id))
m.removeLayer(l.id);
m.addSource('countries', {type: 'geojson', data: countryPolygons});
for (const c of ORDER) {
m.addLayer({
id: `fill-${c}`,
type: 'fill',
source: 'countries',
filter: ['==', ['get', 'country'], c],
paint: {'fill-color': COUNTRY[c], 'fill-opacity': 0},
});
}
// Per country: just the border that draws on, settled to a darker shade of the country colour
// (the electricity now lives on the river, not the borders).
for (const c of ORDER) {
m.addSource(`trail-${c}`, {type: 'geojson', data: EMPTY});
m.addLayer({
id: `trail-${c}`,
type: 'line',
source: `trail-${c}`,
layout: {'line-cap': 'round', 'line-join': 'round'},
paint: {
'line-color': COUNTRY_DARK[c],
'line-width': 2,
'line-opacity': 0.95,
},
});
}
const seed = turf.lineSliceAlong(
line,
0,
Math.max(0.001, lineKm * 0.001),
);
m.addSource('river', {type: 'geojson', data: seed});
m.addSource('river-head', {type: 'geojson', data: seed});
// Electric water: soft blue glow → icy core → white-hot leading head with its own glow.
m.addLayer({
id: 'river-glow',
type: 'line',
source: 'river',
layout: {'line-cap': 'round', 'line-join': 'round'},
paint: {
'line-color': '#49C6FF',
'line-width': 11,
'line-opacity': 0.32,
'line-blur': 6,
},
});
m.addLayer({
id: 'river-line',
type: 'line',
source: 'river',
layout: {'line-cap': 'round', 'line-join': 'round'},
paint: {'line-color': COLORS.river, 'line-width': 3},
});
m.addLayer({
id: 'river-headglow',
type: 'line',
source: 'river-head',
layout: {'line-cap': 'round', 'line-join': 'round'},
paint: {
'line-color': COLORS.riverHeadGlow,
'line-width': 16,
'line-opacity': 0,
'line-blur': 9,
},
});
m.addLayer({
id: 'river-head',
type: 'line',
source: 'river-head',
layout: {'line-cap': 'round', 'line-join': 'round'},
paint: {
'line-color': COLORS.riverHead,
'line-width': 4.5,
'line-opacity': 0,
},
});
m.once('idle', () => {
setMap(m);
continueRender(handle);
});
});
}, [handle]);
useEffect(() => {
if (!map) return;
const h = delayRender(`frame A ${frame}`);
const t = frame / fps; // seconds
const tt = interpolate(frame, [0, durationInFrames - 1], [0, 1], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
// River draw
const reveal = interpolate(t, [RIVER_START, RIVER_END], [0, 1], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
easing: Easing.bezier(0.645, 0.045, 0.355, 1),
});
const riverDrawnKm = lineKm * reveal;
(map.getSource('river') as any)?.setData(
turf.lineSliceAlong(line, 0, Math.max(0.001, riverDrawnKm)),
);
// Electric draw-head leading the river: white-hot core + glow, fading out once the river completes.
const riverHeadKm = lineKm * 0.03;
(map.getSource('river-head') as any)?.setData(
turf.lineSliceAlong(
line,
Math.max(0, riverDrawnKm - riverHeadKm),
Math.max(0.001, riverDrawnKm),
),
);
let riverHeadFade = 0;
if (reveal > 0.002 && reveal < 0.999) riverHeadFade = 1;
else if (reveal >= 0.999)
riverHeadFade = 1 - clamp01((t - RIVER_END) / 0.5);
map.setPaintProperty(
'river-headglow',
'line-opacity',
0.85 * riverHeadFade,
);
map.setPaintProperty('river-head', 'line-opacity', riverHeadFade);
const camera = {
center: [
lerp(START.center[0], END.center[0], tt),
lerp(START.center[1], END.center[1], tt),
] as [number, number],
zoom: lerp(START.zoom, END.zoom, tt),
};
const cameraPoint = map.project(camera.center);
const plateScale = 2 ** (camera.zoom - Math.max(START.zoom, END.zoom));
const plateX = width / 2 - cameraPoint.x * plateScale;
const plateY = height / 2 - cameraPoint.y * plateScale;
const pos: Record<string, {x: number; y: number; reveal: number}> = {};
for (const c of ORDER) {
const d = DRAW[c];
const lt = t - trigger(c); // local seconds since this country triggered
// 1) border draws on (constant duration), settling to a darker shade — no electric head
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}`) as any)?.setData(
bp <= 0 ? EMPTY : sliceBorder(d, 0, d.total * bp),
);
// 2) fill blooms in (overshoot then settle) 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],
{
extrapolateLeft: 'clamp',
extrapolateRight: '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);
const p = map.project(META[c].anchor);
pos[c] = {
x: p.x * plateScale + plateX,
y: p.y * plateScale + plateY,
reveal: lp,
};
}
setLabels(pos);
setPlate({x: plateX, y: plateY, scale: plateScale});
map.once('idle', () => continueRender(h));
map.triggerRepaint();
}, [map, frame, fps, durationInFrames, width, height]);
return (
<AbsoluteFill style={{backgroundColor: COLORS.bg}}>
<div
ref={ref}
style={{
width: width * 2,
height: height * 2,
position: 'absolute',
transform: `translate(${plate.x}px, ${plate.y}px) scale(${plate.scale})`,
transformOrigin: '0 0',
}}
/>
<AbsoluteFill style={{pointerEvents: 'none'}}>
{ORDER.map((c) =>
labels[c] ? (
<CountryLabel
key={c}
name={c.toUpperCase()}
color={COUNTRY[c]}
reveal={labels[c].reveal}
x={labels[c].x}
y={labels[c].y}
/>
) : null,
)}
</AbsoluteFill>
</AbsoluteFill>
);
};
@@ -0,0 +1,22 @@
// Minimal Remotion scaffold for the map explainer. In a Remotion project (`bunx create-video@latest`,
// blank template), this is `src/Root.tsx`; `src/index.ts` is just `registerRoot(RemotionRoot)`.
//
// The Composition sets DURATION (the beat) and dimensions. The component reads durationInFrames / width /
// height from useVideoConfig(). The beat must be long enough for the last country's full sequence after
// the river reaches it. See references/map-explainer-architecture.md §2.
// Render with: bunx remotion render src/index.ts MapExplainer out.mp4 --gl=angle --concurrency=1 --timeout=120000
import React from 'react';
import {Composition} from 'remotion';
import {RiverReveal} from './RiverReveal'; // → src/components/RiverReveal.tsx in your project
export const RemotionRoot: React.FC = () => (
<Composition
id="MapExplainer"
component={RiverReveal}
durationInFrames={12 * 30} // 12 s @ 30 fps — raise if a later country needs more room after the river arrives
fps={30}
width={1920}
height={1080}
/>
);
@@ -0,0 +1,605 @@
[
[84.144, 29.565],
[84.171, 29.577],
[84.22500000000001, 29.559],
[84.246, 29.541],
[84.297, 29.544],
[84.321, 29.532],
[84.411, 29.544],
[84.426, 29.529],
[84.45, 29.535],
[84.468, 29.523],
[84.492, 29.523],
[84.531, 29.490000000000002],
[84.507, 29.466],
[84.501, 29.445],
[84.513, 29.385],
[84.495, 29.379],
[84.483, 29.361],
[84.486, 29.34],
[84.519, 29.316],
[84.51, 29.292],
[84.522, 29.286],
[84.525, 29.265],
[84.57600000000001, 29.253],
[84.58800000000001, 29.259],
[84.60300000000001, 29.247],
[84.615, 29.265],
[84.63, 29.268],
[84.624, 29.283],
[84.651, 29.286],
[84.669, 29.259],
[84.732, 29.25],
[84.741, 29.235],
[84.771, 29.25],
[84.777, 29.241],
[84.843, 29.223],
[84.864, 29.232],
[84.879, 29.202],
[84.9, 29.199],
[84.909, 29.181],
[84.933, 29.184],
[84.96900000000001, 29.163],
[85.023, 29.187],
[85.071, 29.259],
[85.122, 29.274],
[85.167, 29.322],
[85.185, 29.310000000000002],
[85.26, 29.319],
[85.284, 29.301000000000002],
[85.34400000000001, 29.289],
[85.35600000000001, 29.265],
[85.413, 29.262],
[85.431, 29.271],
[85.449, 29.247],
[85.464, 29.244],
[85.521, 29.25],
[85.551, 29.265],
[85.611, 29.235],
[85.617, 29.193],
[85.662, 29.172],
[85.686, 29.172],
[85.69500000000001, 29.184],
[85.71600000000001, 29.172],
[85.72800000000001, 29.187],
[85.764, 29.181],
[85.761, 29.193],
[85.776, 29.202],
[85.788, 29.196],
[85.797, 29.22],
[85.809, 29.211000000000002],
[85.848, 29.211000000000002],
[85.863, 29.193],
[85.881, 29.193],
[85.917, 29.172],
[85.968, 29.175],
[86.004, 29.163],
[86.09400000000001, 29.175],
[86.10000000000001, 29.166],
[86.133, 29.172],
[86.148, 29.166],
[86.193, 29.178],
[86.223, 29.202],
[86.268, 29.193],
[86.295, 29.208000000000002],
[86.34, 29.205000000000002],
[86.412, 29.217000000000002],
[86.427, 29.211000000000002],
[86.43, 29.199],
[86.47800000000001, 29.196],
[86.496, 29.22],
[86.514, 29.211000000000002],
[86.52, 29.235],
[86.529, 29.238],
[86.535, 29.217000000000002],
[86.559, 29.196],
[86.607, 29.202],
[86.616, 29.184],
[86.658, 29.193],
[86.67, 29.205000000000002],
[86.682, 29.199],
[86.7, 29.208000000000002],
[86.736, 29.205000000000002],
[86.778, 29.187],
[86.805, 29.196],
[86.82300000000001, 29.184],
[86.84100000000001, 29.19],
[86.901, 29.181],
[86.949, 29.163],
[87.027, 29.172],
[87.039, 29.148],
[87.063, 29.136],
[87.084, 29.142],
[87.12, 29.175],
[87.162, 29.142],
[87.21000000000001, 29.142],
[87.237, 29.157],
[87.249, 29.136],
[87.297, 29.127],
[87.303, 29.115000000000002],
[87.366, 29.121000000000002],
[87.459, 29.097],
[87.486, 29.115000000000002],
[87.546, 29.109],
[87.56700000000001, 29.121000000000002],
[87.59400000000001, 29.121000000000002],
[87.60600000000001, 29.133],
[87.654, 29.139],
[87.666, 29.13],
[87.684, 29.133],
[87.687, 29.157],
[87.669, 29.181],
[87.675, 29.205000000000002],
[87.705, 29.217000000000002],
[87.714, 29.235],
[87.732, 29.238],
[87.732, 29.253],
[87.756, 29.283],
[87.753, 29.301000000000002],
[87.789, 29.304000000000002],
[87.795, 29.328],
[87.81, 29.337],
[87.849, 29.331],
[87.87, 29.349],
[87.906, 29.343],
[87.924, 29.349],
[87.97800000000001, 29.388],
[87.996, 29.376],
[88.014, 29.379],
[88.017, 29.37],
[88.125, 29.367],
[88.137, 29.337],
[88.161, 29.331],
[88.176, 29.337],
[88.188, 29.325],
[88.2, 29.328],
[88.221, 29.367],
[88.245, 29.367],
[88.287, 29.349],
[88.299, 29.358],
[88.365, 29.316],
[88.41, 29.322],
[88.422, 29.316],
[88.443, 29.325],
[88.458, 29.349],
[88.503, 29.361],
[88.536, 29.334],
[88.563, 29.328],
[88.596, 29.349],
[88.629, 29.346],
[88.641, 29.334],
[88.671, 29.34],
[88.71000000000001, 29.331],
[88.72800000000001, 29.343],
[88.791, 29.337],
[88.818, 29.352],
[88.86, 29.319],
[88.884, 29.334],
[88.908, 29.316],
[88.923, 29.331],
[88.962, 29.328],
[88.971, 29.349],
[89.007, 29.361],
[89.019, 29.352],
[89.034, 29.358],
[89.08500000000001, 29.322],
[89.136, 29.316],
[89.16, 29.343],
[89.181, 29.337],
[89.211, 29.349],
[89.235, 29.379],
[89.253, 29.373],
[89.265, 29.385],
[89.289, 29.385],
[89.307, 29.376],
[89.349, 29.379],
[89.397, 29.358],
[89.45100000000001, 29.355],
[89.46000000000001, 29.334],
[89.58, 29.358],
[89.595, 29.346],
[89.61, 29.355],
[89.631, 29.346],
[89.661, 29.349],
[89.679, 29.364],
[89.757, 29.295],
[89.787, 29.292],
[89.808, 29.310000000000002],
[89.85300000000001, 29.322],
[89.931, 29.319],
[89.985, 29.343],
[90.015, 29.337],
[90.072, 29.352],
[90.156, 29.355],
[90.168, 29.346],
[90.20100000000001, 29.349],
[90.22500000000001, 29.331],
[90.273, 29.337],
[90.276, 29.328],
[90.342, 29.313],
[90.378, 29.295],
[90.435, 29.241],
[90.477, 29.256],
[90.492, 29.25],
[90.51, 29.256],
[90.522, 29.247],
[90.54, 29.262],
[90.621, 29.277],
[90.642, 29.301000000000002],
[90.663, 29.298000000000002],
[90.681, 29.313],
[90.684, 29.328],
[90.705, 29.34],
[90.729, 29.328],
[90.747, 29.337],
[90.765, 29.328],
[90.768, 29.310000000000002],
[90.756, 29.295],
[90.771, 29.277],
[90.855, 29.283],
[90.897, 29.319],
[90.933, 29.310000000000002],
[90.95100000000001, 29.295],
[90.993, 29.322],
[91.035, 29.295],
[91.065, 29.316],
[91.116, 29.325],
[91.131, 29.313],
[91.173, 29.325],
[91.194, 29.286],
[91.233, 29.274],
[91.296, 29.289],
[91.308, 29.277],
[91.34700000000001, 29.28],
[91.374, 29.292],
[91.395, 29.28],
[91.449, 29.286],
[91.479, 29.268],
[91.512, 29.28],
[91.533, 29.271],
[91.554, 29.289],
[91.587, 29.292],
[91.602, 29.271],
[91.62, 29.265],
[91.659, 29.271],
[91.674, 29.259],
[91.71000000000001, 29.268],
[91.74, 29.259],
[91.782, 29.274],
[91.839, 29.268],
[91.869, 29.283],
[91.98, 29.262],
[92.007, 29.232],
[92.07000000000001, 29.289],
[92.115, 29.283],
[92.154, 29.289],
[92.196, 29.244],
[92.22, 29.253],
[92.235, 29.244],
[92.277, 29.244],
[92.295, 29.229],
[92.319, 29.226],
[92.376, 29.226],
[92.397, 29.241],
[92.406, 29.262],
[92.433, 29.25],
[92.46600000000001, 29.253],
[92.529, 29.172],
[92.529, 29.133],
[92.553, 29.148],
[92.598, 29.145],
[92.589, 29.121000000000002],
[92.61, 29.097],
[92.619, 29.115000000000002],
[92.658, 29.121000000000002],
[92.685, 29.139],
[92.697, 29.127],
[92.676, 29.112000000000002],
[92.679, 29.103],
[92.709, 29.109],
[92.739, 29.067],
[92.772, 29.091],
[92.775, 29.073],
[92.796, 29.082],
[92.808, 29.061],
[92.82000000000001, 29.067],
[92.82300000000001, 29.082],
[92.82900000000001, 29.076],
[92.85300000000001, 29.082],
[92.85600000000001, 29.073],
[92.88, 29.073],
[92.904, 29.058],
[92.934, 29.067],
[92.952, 29.049],
[92.973, 29.061],
[92.988, 29.043],
[92.997, 29.049],
[93.066, 29.043],
[93.081, 29.094],
[93.117, 29.115000000000002],
[93.123, 29.136],
[93.138, 29.139],
[93.15, 29.127],
[93.147, 29.094],
[93.165, 29.064],
[93.165, 29.043],
[93.153, 29.037],
[93.153, 29.025000000000002],
[93.162, 29.016000000000002],
[93.21300000000001, 29.025000000000002],
[93.22800000000001, 29.019000000000002],
[93.23100000000001, 29.001],
[93.261, 28.992],
[93.285, 29.001],
[93.312, 28.998],
[93.315, 29.016000000000002],
[93.348, 29.046],
[93.375, 29.052],
[93.393, 29.094],
[93.429, 29.109],
[93.447, 29.106],
[93.438, 29.13],
[93.48, 29.175],
[93.492, 29.163],
[93.54, 29.178],
[93.57000000000001, 29.166],
[93.627, 29.172],
[93.63, 29.151],
[93.645, 29.145],
[93.675, 29.151],
[93.681, 29.163],
[93.699, 29.16],
[93.702, 29.142],
[93.735, 29.127],
[93.75, 29.136],
[93.75, 29.148],
[93.78, 29.154],
[93.789, 29.124000000000002],
[93.825, 29.118000000000002],
[93.834, 29.124000000000002],
[93.831, 29.142],
[93.894, 29.13],
[93.912, 29.145],
[93.903, 29.166],
[93.909, 29.178],
[93.94500000000001, 29.184],
[93.95400000000001, 29.196],
[94.017, 29.193],
[94.035, 29.205000000000002],
[94.176, 29.196],
[94.251, 29.262],
[94.305, 29.271],
[94.302, 29.292],
[94.33200000000001, 29.316],
[94.35000000000001, 29.316],
[94.389, 29.337],
[94.401, 29.361],
[94.419, 29.37],
[94.434, 29.406000000000002],
[94.542, 29.448],
[94.581, 29.481],
[94.656, 29.490000000000002],
[94.70700000000001, 29.463],
[94.818, 29.493000000000002],
[94.875, 29.541],
[94.923, 29.61],
[94.926, 29.622],
[94.887, 29.634],
[94.917, 29.67],
[94.893, 29.697],
[94.905, 29.715],
[94.956, 29.757],
[95.124, 29.763],
[95.124, 29.781000000000002],
[95.09100000000001, 29.814],
[95.11200000000001, 29.868000000000002],
[95.13, 29.88],
[95.175, 29.895],
[95.196, 29.892],
[95.223, 29.868000000000002],
[95.286, 29.868000000000002],
[95.304, 29.856],
[95.307, 29.838],
[95.283, 29.82],
[95.289, 29.811],
[95.385, 29.772000000000002],
[95.379, 29.718],
[95.397, 29.688000000000002],
[95.388, 29.595],
[95.403, 29.562],
[95.427, 29.538],
[95.43900000000001, 29.478],
[95.43, 29.451],
[95.313, 29.331],
[95.256, 29.289],
[95.202, 29.28],
[95.04, 29.175],
[95.001, 29.169],
[95.004, 29.139],
[94.908, 29.052],
[94.902, 29.016000000000002],
[94.869, 28.998],
[94.839, 28.956],
[94.788, 28.935000000000002],
[94.773, 28.854],
[94.794, 28.824],
[94.914, 28.821],
[94.923, 28.812],
[94.911, 28.794],
[94.92, 28.752],
[94.977, 28.713],
[94.983, 28.674],
[94.998, 28.683],
[95.031, 28.617],
[95.09700000000001, 28.539],
[95.10300000000001, 28.506],
[95.09100000000001, 28.455000000000002],
[95.09700000000001, 28.419],
[95.013, 28.326],
[94.992, 28.287],
[94.992, 28.242],
[95.019, 28.215],
[95.031, 28.173000000000002],
[95.06700000000001, 28.155],
[95.145, 28.149],
[95.211, 28.179000000000002],
[95.277, 28.155],
[95.292, 28.116],
[95.316, 28.089000000000002],
[95.355, 28.071],
[95.379, 28.071],
[95.385, 28.059],
[95.382, 27.951],
[95.4, 27.936],
[95.412, 27.882],
[95.379, 27.843],
[95.349, 27.837],
[95.325, 27.810000000000002],
[95.325, 27.78],
[95.298, 27.762],
[95.289, 27.717000000000002],
[95.253, 27.657],
[95.211, 27.666],
[95.151, 27.624000000000002],
[95.139, 27.63],
[95.10300000000001, 27.609],
[94.992, 27.6],
[94.944, 27.57],
[94.917, 27.573],
[94.869, 27.504],
[94.803, 27.495],
[94.803, 27.48],
[94.785, 27.471],
[94.794, 27.456],
[94.767, 27.435000000000002],
[94.767, 27.414],
[94.73100000000001, 27.402],
[94.71900000000001, 27.372],
[94.69800000000001, 27.36],
[94.69800000000001, 27.333000000000002],
[94.683, 27.324],
[94.677, 27.294],
[94.635, 27.291],
[94.629, 27.273],
[94.602, 27.255],
[94.596, 27.231],
[94.584, 27.228],
[94.587, 27.177],
[94.596, 27.162],
[94.584, 27.147000000000002],
[94.566, 27.150000000000002],
[94.554, 27.102],
[94.518, 27.105],
[94.449, 27.060000000000002],
[94.44, 27.018],
[94.407, 26.985],
[94.35900000000001, 26.97],
[94.287, 26.919],
[94.197, 26.925],
[94.164, 26.898],
[94.146, 26.865000000000002],
[94.125, 26.856],
[94.113, 26.868000000000002],
[94.134, 26.907],
[94.113, 26.913],
[94.077, 26.886],
[93.927, 26.832],
[93.864, 26.766000000000002],
[93.753, 26.733],
[93.657, 26.715],
[93.618, 26.724],
[93.56700000000001, 26.751],
[93.441, 26.769000000000002],
[93.411, 26.76],
[93.369, 26.718],
[93.288, 26.742],
[93.249, 26.724],
[93.183, 26.673000000000002],
[93.12, 26.646],
[93.087, 26.655],
[93.018, 26.646],
[92.898, 26.655],
[92.883, 26.652],
[92.874, 26.625],
[92.83500000000001, 26.607],
[92.733, 26.613],
[92.676, 26.601],
[92.658, 26.592000000000002],
[92.616, 26.523],
[92.595, 26.517],
[92.508, 26.517],
[92.43, 26.535],
[92.391, 26.532],
[92.244, 26.472],
[92.229, 26.442],
[92.202, 26.454],
[92.172, 26.442],
[92.124, 26.403000000000002],
[92.06700000000001, 26.373],
[92.07000000000001, 26.325],
[92.058, 26.304000000000002],
[91.971, 26.271],
[91.932, 26.271],
[91.899, 26.241],
[91.878, 26.235],
[91.854, 26.241],
[91.665, 26.166],
[91.608, 26.172],
[91.542, 26.145],
[91.497, 26.139],
[91.377, 26.172],
[91.287, 26.166],
[91.197, 26.202],
[91.134, 26.214000000000002],
[91.062, 26.187],
[90.96300000000001, 26.175],
[90.888, 26.136],
[90.849, 26.13],
[90.807, 26.154],
[90.741, 26.163],
[90.681, 26.19],
[90.58200000000001, 26.205000000000002],
[90.507, 26.229],
[90.474, 26.226],
[90.435, 26.178],
[90.402, 26.16],
[90.345, 26.148],
[90.309, 26.124],
[90.22500000000001, 26.106],
[90.177, 26.076],
[89.955, 26.01],
[89.904, 25.941],
[89.84700000000001, 25.89],
[89.81400000000001, 25.815],
[89.736, 25.692],
[89.724, 25.632],
[89.697, 25.572],
[89.709, 25.482],
[89.697, 25.398],
[89.67, 25.323],
[89.7, 25.266000000000002],
[89.703, 25.242],
[89.685, 25.2],
[89.661, 25.173000000000002],
[89.625, 25.074],
[89.613, 24.96],
[89.613, 24.936],
[89.658, 24.87],
[89.673, 24.792],
[89.706, 24.75],
[89.769, 24.549],
[89.748, 24.372],
[89.754, 24.282],
[89.733, 24.225],
[89.742, 24.201],
[89.739, 24.123],
[89.697, 24.015],
[89.7, 23.958000000000002],
[89.727, 23.883],
[89.787, 23.796],
[89.85600000000001, 23.748],
[89.919, 23.664],
[89.985, 23.643],
[90.144, 23.544],
[90.249, 23.463]
]
@@ -0,0 +1,34 @@
// Example tokens only. Replace every visual value with production-local tokens.
export const COLORS = {
bg: '#101315',
// Electric water — a near-white icy core with a blue glow and a white-hot draw-head (the "electricity"
// travels along the river as it draws on). No dark casing.
river: '#E8F7FF', // bright icy core
riverGlow: 'rgba(73,198,255,0.5)', // electric-blue glow
riverHead: '#FFFFFF', // white-hot leading head
riverHeadGlow: 'rgba(120,225,255,0.95)',
border: '#f5f2ed', // neutral cream country borders/labels over the colored fills
cream: '#f5f0eb',
} as const;
// Example progressive fill tokens. Rename these keys and replace values for each production.
export const COUNTRY = {
china: '#D4A853',
india: '#5B8A8A',
bangladesh: '#C07B57',
} as const;
// Darker shade of each country colour — the settled border line (the bright COUNTRY colour is the
// glowing draw-head that leads the animation).
export const COUNTRY_DARK = {
china: '#9A7530',
india: '#3C5C5C',
bangladesh: '#855239',
} as const;
export const FILL_OPACITY = 0.5;
export const VIDEO = {width: 1920, height: 1080, fps: 30} as const;
// Beat durations (seconds → frames at VIDEO.fps)
export const DUR = {
mapExplainer: 12 * VIDEO.fps,
} as const;