map-animation

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Map 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 needPathOutput
Photoreal terrain, real satellite/3D buildings, cinematic cameraGoogle Earth Studio → After EffectsImage sequence + camera
.jsx
, composited in AE
Clean editorial vector look, full control, data-drivenVector (GeoJSON/SVG) in Remotion or D3Code-rendered MP4/GIF or web SVG
Pins / routes / highlights / labels on top of eitherOverlay layerAE 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图像序列 + 相机
.jsx
脚本,在AE中合成
简洁的编辑风格矢量效果、完全可控、基于数据驱动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
    posterizeTime(12)
    to the layer/comp, OR nest the graphics in a 12fps precomp placed inside the 24fps master.
  • 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操作:对图层/合成应用
    posterizeTime(12)
    ,或将图形嵌套在12fps的预合成中,再放入24fps的主合成。
  • 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
.jsx
script that rebuilds the exact 3D camera in After Effects.
  1. 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.
  2. 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
    .jsx
    and track-point files alongside the frames.
  3. In After Effects: import the image sequence as footage, then File → Scripts → Run Script File… and run the exported
    .jsx
    . It creates a comp with a 3D camera and null layers whose keyframes match the Earth Studio move frame-for-frame.
  4. 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
    .jsx
    includes track points for any coordinates you marked in Earth Studio).
  5. Apply the stutter to the overlay precomp only (
    posterizeTime(12)
    ), color-grade the base footage, then render.
See
references/earth-studio-to-ae.md
for the exact keyframe/export settings, the
.jsx
import step, and how track points map screen-space to lat/long.
Earth Studio是一款浏览器工具,可在谷歌3D地球上为相机设置关键帧,并导出两种可导入AE的内容:图像序列(渲染后的帧)和相机跟踪
.jsx
脚本
,用于在After Effects中重建完全一致的3D相机。
  1. 在Earth Studio(浏览器)中设置关键帧:为相机位置(纬度/经度/高度)、倾斜角度、朝向和视野设置关键帧。使用自动缓动或为每个关键帧设置自定义缓动。保持动效缓慢且刻意——重在交代场景,而非快速切换。
  2. 导出→渲染:选择图像序列(JPG/PNG),设置分辨率和帧率,并启用“3D相机导出”/After Effects复选框,以便在导出帧的同时附带
    .jsx
    和跟踪点文件。
  3. 在After Effects中操作:将图像序列作为素材导入,然后通过**文件→脚本→运行脚本文件…**运行导出的
    .jsx
    脚本。它会创建一个包含3D相机和空对象图层的合成,其关键帧与Earth Studio中的动效逐帧匹配。
  4. 将叠加层绑定到跟踪点:将标记点/标签空对象图层父级链接到导入的3D相机/跟踪空对象,以便在相机移动时,它们能固定在正确的地图位置(
    .jsx
    包含你在Earth Studio中标记的所有坐标的跟踪点)。
  5. 仅对叠加预合成应用卡顿效果
    posterizeTime(12)
    ),对底图素材进行调色,然后渲染。
详见
references/earth-studio-to-ae.md
,其中包含精确的关键帧/导出设置、
.jsx
导入步骤,以及跟踪点如何将屏幕空间映射到经纬度。

Path 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 pixel
Render
geojson.features
as
<path d={toPath(f)} />
. The same
projection([lng,lat])
converts every pin coordinate to a screen position, so pins, routes, and labels all share one coordinate space as the camera (a
scale
/
translate
transform) moves.
Camera 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])
可将所有标记点坐标转换为屏幕位置,因此当相机(通过
scale
/
translate
变换)移动时,标记点、路线和标签都共享同一坐标空间。
矢量地图中的相机动效是通过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-dashoffset
:
js
// 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"
normalizes any route length so one progress value (0→1) draws it. Add a traveling dot at
offset-distance
to lead the line. For a great-circle/curved flight path, sample intermediate points with
d3.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 .35
Animate 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
projection([lng,lat])
so it tracks the camera. Keep labels horizontal and inside the safe area; never let them rotate with the map.
标记点落下(落下、稳定、可选脉冲效果)——在坐标数组中错开时间:
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-dashoffset
实现描边动画:
js
// 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"
会标准化任意路线长度,因此一个进度值(0→1)即可完成绘制。可在
offset-distance
处添加移动点引导路线。如需大圆弧/弯曲飞行路径,可使用
d3.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 (
scripts/
): tile your stills with
scripts/contact-sheet.sh sheet.png f-hook.png f-mid.png f-end.png
, then assert the encode with
scripts/probe-mp4.sh out.mp4 [WxH] [fps]
. See
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 (
    <Composition>
    + zod
    schema
    +
    defaultProps
    ), all motion frame-driven (no timers /
    Date.now()
    /
    Math.random()
    ).
  • Deliverable = the rendered
    out/*.mp4
    (plus the project + the
    beats
    /coordinates array, so the map can be re-rendered from new data).
  • The 12fps stutter is computed from
    fps
    (quantize the frame), not hardcoded — so it survives an fps change.
  • Duration data-dependent (sum of beat holds)? compute it in
    calculateMetadata
    , not by hand.
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项目(
    <Composition>
    + zod
    schema
    +
    defaultProps
    ),所有动效由帧驱动(无计时器/
    Date.now()
    /
    Math.random()
    )。
  • 交付物=渲染后的
    out/*.mp4
    (加上项目+
    beats
    /坐标数组,以便根据新数据重新渲染地图)。
  • 12fps卡顿节奏由
    fps
    计算得出(量化帧),而非硬编码——因此帧率变化时仍能保持效果。
  • 时长是否依赖数据?在
    calculateMetadata
    中计算,而非手动设置。
验证流程——渲染静帧→检查→编码。 先渲染单帧(成本低,无需编码),检查无误后再编码:
bash
undefined

Frame-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

参考文件

  • references/earth-studio-to-ae.md
    — Google Earth Studio keyframe + export settings, the After Effects
    .jsx
    camera-import workflow, parenting overlays to track points, lat/long ↔ screen-space mapping, and applying the 12fps stutter to the overlay precomp.
  • references/vector-maps.md
    — GeoJSON projection setup, vector camera zoom/pan to a lat/long, pin-drop / route draw-on / region-highlight / label-callout recipes, the data-driven
    beats
    pattern, and the full 12fps stutter quantization recipe for code and AE.
  • references/earth-studio-to-ae.md
    ——Google Earth Studio关键帧+导出设置、After Effects
    .jsx
    相机导入流程、叠加层绑定到跟踪点、经纬度↔屏幕空间映射,以及如何对叠加预合成应用12fps卡顿效果。
  • references/vector-maps.md
    ——GeoJSON投影设置、矢量相机缩放/平移至经纬度、标记点落下/路线绘制/区域高亮/标签标注的实现方案、数据驱动
    beats
    模式,以及代码和AE中完整的12fps卡顿量化方案。