map-animation
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseMap Animation
地图动画
Build editorial, "Vox-style" animated maps: a camera that zooms, orbits, and pans over a base map while location pins drop, routes draw on, regions highlight, and labels call out — all snapped to a deliberate 12fps stutter so motion reads as graphic, not photographic. Two production paths: photoreal base motion from Google Earth Studio composited in After Effects, and code-renderable vector maps (GeoJSON/SVG) driven from a coordinates array in Remotion or D3.
制作具有编辑风格的“Vox式”动画地图:相机在底图上进行缩放、环绕和平移,同时标记点落下、路线绘制完成、区域高亮显示、标签弹出——所有动效都刻意设置为12fps的卡顿节奏,让动效呈现出图形感而非摄影感。有两种制作路径:一是使用Google Earth Studio生成写实底图动效,再在After Effects中合成;二是通过Remotion或D3中的坐标数组驱动可代码渲染的矢量地图(GeoJSON/SVG)。
When to use
适用场景
- A documentary/explainer beat that establishes place: zoom from globe to city, orbit a landmark, pan along a border.
- Show movement across geography: a route drawing between two points, a supply chain, a migration, a flight path.
- Highlight a country, state, or region and label it; build a step-by-step geographic walkthrough.
- Turn a list of coordinates/events into a repeatable, data-driven map sequence for video.
- 纪录片/解说视频中用于交代场景:从地球缩放至城市、环绕地标、沿边界平移。
- 展示地理范围内的移动:两点间的路线绘制、供应链、迁徙路径、飞行航线。
- 高亮并标注国家、州或地区;制作分步地理演示。
- 将坐标/事件列表转换为可重复的、基于数据的地图视频序列。
Pick the path
路径选择
| You need | Path | Output |
|---|---|---|
| Photoreal terrain, real satellite/3D buildings, cinematic camera | Google Earth Studio → After Effects | Image sequence + camera |
| Clean editorial vector look, full control, data-driven | Vector (GeoJSON/SVG) in Remotion or D3 | Code-rendered MP4/GIF or web SVG |
| Pins / routes / highlights / labels on top of either | Overlay layer | AE shape/null layers OR React/SVG components |
Most "Vox-style" pieces combine them: Earth Studio for the base camera move, vector overlays for the pins, routes, and labels on top.
| 需求 | 路径 | 输出 |
|---|---|---|
| 写实地形、真实卫星/3D建筑、电影级相机 | Google Earth Studio → After Effects | 图像序列 + 相机 |
| 简洁的编辑风格矢量效果、完全可控、基于数据驱动 | Remotion或D3中的矢量地图(GeoJSON/SVG) | 代码渲染的MP4/GIF或网页SVG |
| 在任意一种底图上添加标记点/路线/高亮/标签 | 叠加层 | AE形状/空对象图层 或 React/SVG组件 |
大多数“Vox风格”作品会结合两种路径:用Earth Studio制作基础相机动效,在上方叠加矢量图层实现标记点、路线和标签效果。
The signature look: 12fps stutter
标志性风格:12fps卡顿节奏
The single most recognizable trait of this style is animating the map graphics at 12fps inside a 24fps edit. The base footage and audio run at 24 (smooth); the graphic layer — camera-driven pins, route draw-on, label pops — steps at 12. The result reads as intentional, hand-built, "infographic" motion rather than soft video.
- Math: at 24fps, hold each graphic-layer value for 2 frames (update on even frames only). At 30fps, hold ~2–3 frames (≈12.5/10fps).
- After Effects: apply to the layer/comp, OR nest the graphics in a 12fps precomp placed inside the 24fps master.
posterizeTime(12) - Remotion / code: snap the frame before computing any animated value:
js
// Quantize the timeline to 12fps "steps" while the comp renders at 24fps.
const STEP = Math.round(fps / 12); // 2 at 24fps
const f = Math.floor(frame / STEP) * STEP; // use `f`, not `frame`, for graphic motion
const progress = interpolate(f, [0, 48], [0, 1], { extrapolateRight: "clamp" });Keep the base footage/camera smooth (24fps) and stutter only the graphic overlay — stuttering everything looks like dropped frames, not style.
该风格最显著的特征是在24fps的剪辑中,将地图图形的动画帧率设置为12fps。底图素材和音频保持24fps(流畅);而图形层——相机驱动的标记点、路线绘制、标签弹出——以12fps步进。最终效果会呈现出刻意设计的、手工制作的“信息图”动效,而非柔和的视频感。
- 计算方式:24fps时,每个图形层的数值保持2帧(仅在偶数帧更新)。30fps时,保持约2-3帧(≈12.5/10fps)。
- After Effects操作:对图层/合成应用,或将图形嵌套在12fps的预合成中,再放入24fps的主合成。
posterizeTime(12) - Remotion/代码实现:在计算任何动画值前,先对齐帧:
js
// 在合成以24fps渲染时,将时间轴量化为12fps的“步进”。
const STEP = Math.round(fps / 12); // 24fps时为2
const f = Math.floor(frame / STEP) * STEP; // 图形动效使用`f`而非`frame`
const progress = interpolate(f, [0, 48], [0, 1], { extrapolateRight: "clamp" });保持底图素材/相机流畅(24fps),仅让图形叠加层产生卡顿——如果所有元素都卡顿,会看起来像丢帧,而非风格设计。
Path A — Google Earth Studio → After Effects
路径A — Google Earth Studio → After Effects
Earth Studio is a browser tool that keyframes a camera over Google's 3D globe and exports two things you import into AE: an image sequence (the rendered frames) and a camera-track script that rebuilds the exact 3D camera in After Effects.
.jsx- Keyframe in Earth Studio (browser): set keyframes for camera position (lat/long/altitude), tilt, heading, and field of view. Use the auto-ease or set custom ease per keyframe. Keep moves slow and deliberate — establish, don't whip.
- Export → render: choose the image sequence (JPG/PNG), set resolution and fps, and enable the "3D Camera Export" / After Effects checkbox so it ships the and track-point files alongside the frames.
.jsx - In After Effects: import the image sequence as footage, then File → Scripts → Run Script File… and run the exported . It creates a comp with a 3D camera and null layers whose keyframes match the Earth Studio move frame-for-frame.
.jsx - Parent overlays to the track: parent your pin/label null layers to the imported 3D camera/track nulls so they stick to the right map location as the camera moves (the includes track points for any coordinates you marked in Earth Studio).
.jsx - Apply the stutter to the overlay precomp only (), color-grade the base footage, then render.
posterizeTime(12)
See for the exact keyframe/export settings, the import step, and how track points map screen-space to lat/long.
references/earth-studio-to-ae.md.jsxEarth Studio是一款浏览器工具,可在谷歌3D地球上为相机设置关键帧,并导出两种可导入AE的内容:图像序列(渲染后的帧)和相机跟踪脚本,用于在After Effects中重建完全一致的3D相机。
.jsx- 在Earth Studio(浏览器)中设置关键帧:为相机位置(纬度/经度/高度)、倾斜角度、朝向和视野设置关键帧。使用自动缓动或为每个关键帧设置自定义缓动。保持动效缓慢且刻意——重在交代场景,而非快速切换。
- 导出→渲染:选择图像序列(JPG/PNG),设置分辨率和帧率,并启用“3D相机导出”/After Effects复选框,以便在导出帧的同时附带和跟踪点文件。
.jsx - 在After Effects中操作:将图像序列作为素材导入,然后通过**文件→脚本→运行脚本文件…**运行导出的脚本。它会创建一个包含3D相机和空对象图层的合成,其关键帧与Earth Studio中的动效逐帧匹配。
.jsx - 将叠加层绑定到跟踪点:将标记点/标签空对象图层父级链接到导入的3D相机/跟踪空对象,以便在相机移动时,它们能固定在正确的地图位置(包含你在Earth Studio中标记的所有坐标的跟踪点)。
.jsx - 仅对叠加预合成应用卡顿效果(),对底图素材进行调色,然后渲染。
posterizeTime(12)
详见,其中包含精确的关键帧/导出设置、导入步骤,以及跟踪点如何将屏幕空间映射到经纬度。
references/earth-studio-to-ae.md.jsxPath B — Vector maps (GeoJSON / SVG)
路径B — 矢量地图(GeoJSON / SVG)
For the clean editorial look, project GeoJSON to SVG paths and animate them. Use a real projection so coordinates land correctly.
js
import { geoMercator, geoPath } from "d3-geo";
const projection = geoMercator().fitSize([1920, 1080], geojson); // fit map to frame
const toPath = geoPath(projection);
const [x, y] = projection([lng, lat]); // any lat/long → screen pixelRender as . The same converts every pin coordinate to a screen position, so pins, routes, and labels all share one coordinate space as the camera (a / transform) moves.
geojson.features<path d={toPath(f)} />projection([lng,lat])scaletranslateCamera move in vector land is a CSS/transform zoom-and-pan toward a target lat/long:
js
const [tx, ty] = projection([targetLng, targetLat]);
const scale = interpolate(f, [0, 36], [1, 4], { extrapolateRight: "clamp" });
// transform-origin at the target so the zoom homes in on it
const transform = `translate(${960 - tx*scale}px, ${540 - ty*scale}px) scale(${scale})`;要实现简洁的编辑风格,可将GeoJSON投影为SVG路径并制作动画。使用真实投影确保坐标位置准确。
js
import { geoMercator, geoPath } from "d3-geo";
const projection = geoMercator().fitSize([1920, 1080], geojson); // 将地图适配到帧尺寸
const toPath = geoPath(projection);
const [x, y] = projection([lng, lat]); // 将任意经纬度转换为屏幕像素将渲染为。同一个可将所有标记点坐标转换为屏幕位置,因此当相机(通过/变换)移动时,标记点、路线和标签都共享同一坐标空间。
geojson.features<path d={toPath(f)} />projection([lng,lat])scaletranslate矢量地图中的相机动效是通过CSS/变换朝向目标经纬度进行缩放和平移:
js
const [tx, ty] = projection([targetLng, targetLat]);
const scale = interpolate(f, [0, 36], [1, 4], { extrapolateRight: "clamp" });
// 变换原点设为目标点,以便缩放时聚焦于该点
const transform = `translate(${960 - tx*scale}px, ${540 - ty*scale}px) scale(${scale})`;Overlays: pins, routes, highlights, labels
叠加层:标记点、路线、高亮、标签
Pin drop (drops in, settles, optional pulse) — stagger across a coordinates array:
jsx
const pins = [{ name:"Kyiv", lng:30.52, lat:50.45 }, { name:"Lviv", lng:24.03, lat:49.84 }];
pins.map((p, i) => {
const local = f - i * STEP * 4; // stagger, quantized to the 12fps step
const drop = spring({ frame: local, fps, config:{ damping: 12 } }); // overshoot = settle
const [x, y] = projection([p.lng, p.lat]);
return <Pin key={p.name} x={x} y={y - 40*(1-drop)} opacity={local > 0 ? 1 : 0} />;
});Route draw-on — build the path with the projection, then stroke it on with :
stroke-dashoffsetjs
// route = [[lng,lat], ...]; project each point, join into an SVG path
const d = route.map((c,i) => (i ? "L" : "M") + projection(c).join(" ")).join(" ");
// in the component: pathLength=1, strokeDashoffset = 1 - drawProgress
const draw = interpolate(f, [12, 60], [0, 1], { extrapolateLeft:"clamp", extrapolateRight:"clamp" });pathLength="1"offset-distanced3.geoInterpolate(a, b)Region highlight — fill/stroke the matched GeoJSON feature, dim the rest (focus + context):
js
const isTarget = (f) => f.properties.name === "France";
// target: fill accent, opacity 1, stroke up; others: opacity .35Animate the highlight in by interpolating fill opacity, or wipe it with a clip-path mask. Keep color = meaning and never reassign it mid-piece.
Label callout — pin label or leader-line box; fade/slide in after its pin lands, anchored to so it tracks the camera. Keep labels horizontal and inside the safe area; never let them rotate with the map.
projection([lng,lat])标记点落下(落下、稳定、可选脉冲效果)——在坐标数组中错开时间:
jsx
const pins = [{ name:"Kyiv", lng:30.52, lat:50.45 }, { name:"Lviv", lng:24.03, lat:49.84 }];
pins.map((p, i) => {
const local = f - i * STEP * 4; // 错开时间,量化到12fps的步进
const drop = spring({ frame: local, fps, config:{ damping: 12 } }); // 过冲后稳定
const [x, y] = projection([p.lng, p.lat]);
return <Pin key={p.name} x={x} y={y - 40*(1-drop)} opacity={local > 0 ? 1 : 0} />;
});路线绘制——通过投影构建路径,然后用实现描边动画:
stroke-dashoffsetjs
// route = [[lng,lat], ...]; 投影每个点,拼接成SVG路径
const d = route.map((c,i) => (i ? "L" : "M") + projection(c).join(" ")).join(" ");
// 在组件中:pathLength=1,strokeDashoffset = 1 - drawProgress
const draw = interpolate(f, [12, 60], [0, 1], { extrapolateLeft:"clamp", extrapolateRight:"clamp" });pathLength="1"offset-distanced3.geoInterpolate(a, b)区域高亮——填充/描边匹配的GeoJSON要素,调暗其余部分(聚焦+背景):
js
const isTarget = (f) => f.properties.name === "France";
// 目标区域:强调色填充,不透明度1,描边加粗;其他区域:不透明度0.35可通过插值填充不透明度实现高亮动画,或用裁剪蒙版实现擦除效果。保持颜色语义一致,不要在作品中途更改颜色含义。
标签标注——标记点标签或引线框;在标记点落下后淡入/滑入,锚定到以便跟随相机移动。保持标签水平且在安全区域内;不要让标签随地图旋转。
projection([lng,lat])Data-driven sequencing
数据驱动序列
Drive the whole sequence from one ordered array so the map narrates itself and re-renders from data:
jsx
const beats = [
{ lng: 30.52, lat: 50.45, label: "Kyiv", hold: 48 },
{ lng: 24.03, lat: 49.84, label: "Lviv", hold: 36, route: true },
];
// Each beat = a Sequence: camera flies to (lng,lat), pin drops, label pops, route draws if set.
// Total duration = sum(holds); compute it in calculateMetadata, not by hand.One array → camera targets, pin order, route segments, and labels stay in sync. Swap the data, get a new map sequence — no re-keyframing.
通过一个有序数组驱动整个序列,让地图自动叙事并根据数据重新渲染:
jsx
const beats = [
{ lng: 30.52, lat: 50.45, label: "Kyiv", hold: 48 },
{ lng: 24.03, lat: 49.84, label: "Lviv", hold: 36, route: true },
];
// 每个beat对应一个序列:相机飞向(lng,lat),标记点落下,标签弹出,若设置route则绘制路线。
// 总时长=sum(holds);在calculateMetadata中计算,而非手动设置。一个数组即可让相机目标、标记点顺序、路线段和标签保持同步。更换数据即可生成新的地图序列——无需重新设置关键帧。
Output checklist
输出检查清单
- Camera moves are slow and deliberate (establish, don't whip); eases on every keyframe.
- Graphic overlay stutters at 12fps; base footage/camera stays smooth (24fps).
- Pins/routes/labels stay locked to their lat/long as the camera moves (parented to track / shared projection).
- Routes draw on in reading order; highlights use focus + context (target lit, rest dimmed).
- Labels horizontal, legible, inside the safe area; color grammar consistent.
- Data-driven path: one coordinates/beats array is the single source of truth.
- 相机动效缓慢且刻意(重在交代场景,而非快速切换);每个关键帧都应用缓动。
- 图形叠加层以12fps卡顿;底图素材/相机保持流畅(24fps)。
- 标记点/路线/标签在相机移动时固定在对应的经纬度(绑定到跟踪点/共享投影)。
- 路线按阅读顺序绘制;高亮采用聚焦+背景(目标区域明亮,其余区域调暗)。
- 标签水平、清晰、在安全区域内;颜色语义一致。
- 数据驱动路径:单个坐标/beats数组为唯一数据源。
Deliver & verify (rendered stills → MP4)
交付与验证(渲染静帧→MP4)
Packaged helper (): tile your stills withscripts/, then assert the encode withscripts/contact-sheet.sh sheet.png f-hook.png f-mid.png f-end.png. Seescripts/probe-mp4.sh out.mp4 [WxH] [fps].scripts/README.md
The code-renderable path is a Remotion composition — frame-deterministic, so any exact frame renders headlessly with no seek harness. Use this tier when the deliverable is an MP4/GIF (vector map, data-driven, exact labels) or batches per dataset; for an Earth Studio + AE comp, the deliverable is the AE project + rendered footage instead, verified by scrubbing the master comp.
Output contract:
- A Remotion project with the composition registered (+ zod
<Composition>+schema), all motion frame-driven (no timers /defaultProps/Date.now()).Math.random() - Deliverable = the rendered (plus the project + the
out/*.mp4/coordinates array, so the map can be re-rendered from new data).beats - The 12fps stutter is computed from (quantize the frame), not hardcoded — so it survives an fps change.
fps - Duration data-dependent (sum of beat holds)? compute it in , not by hand.
calculateMetadata
Verify loop — render stills → inspect → encode. Render single frames first (cheap, no encode), inspect them, encode only once the frames are right.
bash
undefined打包工具():使用scripts/拼接静帧,然后用scripts/contact-sheet.sh sheet.png f-hook.png f-mid.png f-end.png验证编码。详见scripts/probe-mp4.sh out.mp4 [WxH] [fps]。scripts/README.md
可代码渲染的路径是Remotion合成——帧确定性,因此任意精确帧都可无头渲染,无需搜索工具。当交付物为MP4/GIF(矢量地图、数据驱动、精确标签)或按数据集批量渲染时,使用此方式;对于Earth Studio + AE合成,交付物为AE项目+渲染素材,通过浏览主合成进行验证。
输出规范:
- 已注册合成的Remotion项目(+ zod
<Composition>+schema),所有动效由帧驱动(无计时器/defaultProps/Date.now())。Math.random() - 交付物=渲染后的(加上项目+
out/*.mp4/坐标数组,以便根据新数据重新渲染地图)。beats - 12fps卡顿节奏由计算得出(量化帧),而非硬编码——因此帧率变化时仍能保持效果。
fps - 时长是否依赖数据?在中计算,而非手动设置。
calculateMetadata
验证流程——渲染静帧→检查→编码。 先渲染单帧(成本低,无需编码),检查无误后再编码:
bash
undefinedFrame-exact stills at start / each beat / end — render with the SHIPPED props (the real beats array)
在起始帧/每个beat/结束帧渲染精确静帧——使用交付的props(真实的beats数组)
npx remotion still MapSeq out/f-start.png --frame=0 --props='{...}'
npx remotion still MapSeq out/f-beat2.png --frame=N --props='{...}' # mid a beat's hold
npx remotion still MapSeq out/f-end.png --frame=L --props='{...}' # L = durationInFrames - 1
npx remotion still MapSeq out/f-start.png --frame=0 --props='{...}'
npx remotion still MapSeq out/f-beat2.png --frame=N --props='{...}' # beat持续期间的中间帧
npx remotion still MapSeq out/f-end.png --frame=L --props='{...}' # L = durationInFrames - 1
Inspect each PNG — FIDELITY: every pin sits on the correct lat/long, the route connects the right
检查每个PNG——准确性:每个标记点都在正确的经纬度上,路线连接正确的城市,高亮区域为目标区域,标签拼写正确且清晰可读。同时检查移动过程中的两个相邻帧,确认叠加层保持2帧(12fps步进),而底图相机每帧都在推进。
cities, the highlighted region is the intended one, labels are spelled right and readable. And land
检查瑕疵:标签超出画布/被裁剪,标记点偏离坐标,路线过度延伸,缺失地图瓦片/字体,投影在国际日期变更线处截断。
on TWO adjacent frames inside a move to confirm the overlay holds for 2 frames (the 12fps step),
静帧检查无误后再编码:
while the base camera is still advancing every frame.
—
ARTIFACTS: labels off-canvas/clipped, pins drifting off their coordinate, route overshooting,
—
missing map tiles/fonts, projection cutoff at the antimeridian.
—
Only after the stills check out, encode:
—
npx remotion render MapSeq out/map.mp4 --props='{...}'
- `npx remotion compositions` reads `durationInFrames`/`fps` to pick the end frame and per-beat sample frames.
- **Data-driven / batch:** verify ONE representative `beats` array via stills *before* batch-rendering all datasets — catch a projection/label bug once, not N times.
- **README demo GIF for free:** `npx remotion render MapSeq out/demo.gif --codec=gif`.
**Before you finish:**
1. `npx remotion still` renders cleanly at frame 0, mid each beat, and last — no errors, no missing tiles/fonts.
2. Every pin/route/label lands on the correct projected coordinate at every checked frame (spot-check a known city).
3. Two adjacent overlay frames are identical (12fps step holds) while the base camera advanced — stutter is real and only on the graphic layer.
4. Frame-driven only — no `Date.now()` / `Math.random()` / timers; the **shipped** beats render correctly (not just `defaultProps`).
5. Full MP4 encoded and plays; (optional) GIF rendered for the README.npx remotion render MapSeq out/map.mp4 --props='{...}'
- `npx remotion compositions`读取`durationInFrames`/`fps`以选择结束帧和每个beat的采样帧。
- **数据驱动/批量渲染**:在批量渲染所有数据集前,先通过静帧验证一个代表性的`beats`数组——只需要捕捉一次投影/标签错误,而非N次。
- **免费生成README演示GIF**:`npx remotion render MapSeq out/demo.gif --codec=gif`。
**完成前检查:**
1. `npx remotion still`在起始帧、每个beat中间帧和结束帧都能干净渲染——无错误,无缺失瓦片/字体。
2. 在所有检查帧中,每个标记点/路线/标签都落在正确的投影坐标上(抽查一个已知城市)。
3. 叠加层的两个相邻帧完全相同(12fps步进保持),而底图相机在推进——卡顿效果真实且仅作用于图形层。
4. 仅由帧驱动——无`Date.now()`/`Math.random()`/计时器;交付的beats能正确渲染(而非仅`defaultProps`)。
5. 完整MP4已编码并可播放;(可选)已为README渲染GIF。Reference files
参考文件
- — Google Earth Studio keyframe + export settings, the After Effects
references/earth-studio-to-ae.mdcamera-import workflow, parenting overlays to track points, lat/long ↔ screen-space mapping, and applying the 12fps stutter to the overlay precomp..jsx - — GeoJSON projection setup, vector camera zoom/pan to a lat/long, pin-drop / route draw-on / region-highlight / label-callout recipes, the data-driven
references/vector-maps.mdpattern, and the full 12fps stutter quantization recipe for code and AE.beats
- ——Google Earth Studio关键帧+导出设置、After Effects
references/earth-studio-to-ae.md相机导入流程、叠加层绑定到跟踪点、经纬度↔屏幕空间映射,以及如何对叠加预合成应用12fps卡顿效果。.jsx - ——GeoJSON投影设置、矢量相机缩放/平移至经纬度、标记点落下/路线绘制/区域高亮/标签标注的实现方案、数据驱动
references/vector-maps.md模式,以及代码和AE中完整的12fps卡顿量化方案。beats