---
name: map-motion
description: "地图动效视频：用高德地图（真实路线、行政区边界、标准/卫星瓦片底图）做旅行与解说视频里的地图动画，出竖屏/横屏 MP4 和 GIF。十三种镜头：地球俯冲定位（从地球放大到某地）、Vlog「我在这里」瞄准锁定＋坐标卡、落点标注、公园/景区/校园地块轮廓描边与绕行、路线描线（驾车/步行/骑行/GPX，车随线走、里程表滚动、镜头跟车、海拔剖面）、飞线航线、行政区描边高亮、一对多辐射飞线、多站旅程（路线+邮戳+照片+返程）、全景收束、标题卡、机位揭秘（咔嚓翻出照片，可做 Live Photo）、猜地点；另有 3D 地形模式（真实山体立体、卫星贴图、地球俯冲到鸟瞰、跟着人沿轨迹走、里程/海拔/爬升/小地图/海拔剖面、沿途弹照片）；十三种样式（高德标准、高德暗色、高德灰、高德复古、卫星、境外卫星、暗色科技、极简白、水彩手账、复古纸、赛博霓虹、国风水墨、工程蓝图）。当用户说「地图动画」「地图动效」「轨迹动画」「出行轨迹视频」「旅行路线动图」「从地球放大到」「定位到某地」「飞线」「航线动画」「行政区高亮」「门店分布动画」「Vlog 地图片头」「标出某个公园/景点」「徒步/登山/骑行轨迹」「海拔剖面」「我在这里坐标卡」「3D 地形」「立体地图」「像 Google Earth 那样的轨迹」「两步路/六只脚那种 3D 轨迹回放」，或要把行程、GPX、照片位置做成地图视频时使用。"
---

# 地图动效 map-motion

> 「找机位」（照片 → 机位揭秘视频）有单独的子 skill：`photo-spot/`，流程和坑都在那里；本文件只讲引擎。

一份「镜头表」JSON → 编译（高德解析地名、取路线和边界、算镜头）→ 逐帧渲染（Chromium 画布 + ffmpeg）。

```sh
D=~/.claude/skills/map-motion/scripts
$D/make.sh 我的片子.json 成片.mp4 --gif 动图.gif           # 一键：编译 + 渲染
$D/make.sh 我的片子.json 静帧/ --stills 1,4.5,9            # 先抽几帧看构图（快）
```
首次使用：`uv run --with playwright playwright install chromium`；高德 key 见下。

## 工作流

1. **定内容**：问清地点/路线/日期/照片、发哪个平台（定 `size`）、要什么感觉（定 `style`）。不确定样式时，同一份 spec 换 `style` 各渲一帧给用户挑（至少三种）。
2. **写 spec**：从 `assets/examples/` 挑最近的一份复制改。镜头类型与全部参数见 [references/spec.md](references/spec.md)。
3. **编译并核对地点**：编译会逐个打印「地点 X → 高德解析结果（级别）[坐标]」。**每一行都核对**：同名地点会解析错（实测「香格里拉」曾被解析成某家酒店）。不对就换更具体的名字，或直接写坐标。
4. **抽静帧**：`--stills` 取每个镜头中段和结尾各一帧，用 Read 看图：标签有没有挤、路线有没有出画、照片有没有压住关键地点。
5. **出片**：渲染整片（30fps 约每秒 2.5 帧画面；40 秒片子约 1.5–2 分钟），需要时加 `--gif`、`--audio`。
6. **交付**：说清时长、文件位置、用了哪些真实数据（高德路线公里数等），以及哪些是示意。

## 镜头一览

| type | 效果 | 典型用途 |
|---|---|---|
| `globe` | 地球自转 → 俯冲到地点，落点弹标签 | 片头、地点引入 |
| `locate` | 地球俯冲 → 瞄准镜锁定 → 坐标卡（经纬度滚动、海拔、日期） | Vlog 片头「我在这里」 |
| `pins` | 多个地点依次落针＋涟漪 | 打卡点、门店 |
| `route` | 高德真实路线或 GPX 描线，车/人沿线走，可镜头跟车、里程表、海拔剖面、终点插旗 | 自驾、徒步登山、骑行、跑步 |
| `flight` | 大圆航线向上拱起，飞机沿线飞 | 出差、旅行、跨城 |
| `region` | 行政区描边 → 填色 → 写名 | 城市群、区域介绍 |
| `area` | 公园/景区/校园轮廓（OSM 边界）描边填色，可让小人绕一圈 | 介绍一个地点、游园路线 |
| `radiate` | 一个中心向多点放射飞线 | 总部/仓库辐射、业务覆盖 |
| `trip` | 多站旅程：逐段路线＋累计里程＋到站邮戳＋照片＋原路返程 | 假期出行复盘 |
| `overview` | 镜头框住至今所有地点 | 片尾收束 |
| `title` | 画面中央标题卡 | 开场、章节 |
| `snap` | 地球俯冲 → 机位（相机图标）＋被摄地 → 取景扇形 → 取景框对焦 → 咔嚓（自动配快门声）→ 照片从机位翻折立起，背景是这张照片的虚化；`live` 可做 iPhone Live Photo 播放效果 | 「这张照片在哪拍的」、机位揭秘、摄影 Vlog |
| `guess` | 整屏照片＋「这张照片在哪拍的？」＋提示＋倒数，闪白露出地球，接 `locate` 揭晓 | 猜地点系列、悬念片头 |

### 3D 地形模式（`"mode": "3d"`）

真立体山体（AWS Terrain Tiles 高程 + Esri 卫星，MapLibre GL 渲染）。整片是固定一套：卫星地球俯冲 → 整条轨迹 3D 鸟瞰（白线 + 标题）→ 压低贴到起点 → 跟着人走（黄线、里程、海拔、累计爬升、小地图、底部海拔剖面；到照片点停下弹照片）→ 拉回全景、出总数据。写法见 [references/spec.md](references/spec.md#3d-地形模式)；流程、例子和检查命令在子 skill `map-3d/`。
- 坐标一律 **WGS-84**（GPX、OSM、卫星、高程都是），所以国内也用 Esri 卫星；高德坐标写 `"gcj": true`。
- 没有 GPX：`"track": {"via": [途经点…]}` 沿 OSM 小路取最短路连起来（山里 OSM 常断头，不连通就加中间点）。
- 软件 WebGL（SwiftShader）渲染，约 1–1.5 秒一帧，30 秒片子 15–25 分钟，放后台跑；先 `--stills` 抽帧看。

镜头之间自动「飞过去」（van Wijk 平滑缩放：远距离先拉远再推近，和 Mapbox flyTo 同一公式），不需要手写转场。

## 样式

| style | 底图 | 适合 | 最大推近 |
|---|---|---|---|
| `amap` | 高德标准路网瓦片 | 通用、街道级路线 | 17.5 |
| `amap-dark` | 高德瓦片反色 | 夜景感、科技向 | 17.5 |
| `amap-gray` | 高德瓦片去色，红色路线 | 数据可视化、新闻图解 | 17.5 |
| `amap-sepia` | 高德瓦片棕褐调，手写字 | 怀旧、城市故事 | 17.5 |
| `satellite` | 高德卫星＋路名，地球也是真实卫星图 | 片头俯冲、户外、航线 | 17.5 |
| `satellite-world` | Esri 全球卫星（无路名），署名 Esri | **境外地点**（高德卫星境外只有城市级，再推近是灰底） | 17.5 |
| `dark` | 矢量：深底发光边界与飞线 | 数据/业务分布 | 8 |
| `light` | 矢量：白底极简 | 区域介绍、信息图 | 8 |
| `journal` | 矢量：纸纹水彩省份、红虚线、邮戳、手写字 | 旅行手账 | 8 |
| `vintage` | 矢量：复古牛皮纸、棕墨线 | 历史、怀旧 | 8 |
| `neon` | 矢量：深紫底、品红海岸线、青色发光飞线 | 潮流、夜生活、音乐 | 8 |
| `ink` | 矢量：宣纸底、水墨陆地、朱砂路线与印章 | 国风、文旅、历史路线 | 8 |
| `blueprint` | 矢量：蓝底白线网格、虚线路线 | 工程、规划、科普 | 8 |

矢量样式的边界是抽稀过的（约 2 km 精度），编译时自动把镜头封顶在 8 级（约一个地级市范围）并提示；要街道细节就用前三种。

## 高德

- key：`export AMAP_KEY=…` 或写入 `~/.config/map-motion/amap_key`（Web服务类型）。没 key 时编译直接报错并说明怎么申请。不要把 key 写进 spec 或项目目录。
- API 响应缓存在 `~/.cache/map-motion/api/`，瓦片缓存在 `~/.cache/map-motion/tiles/`；改 spec 重编译不重复消耗配额。
- 用户的 GPS/照片 EXIF/GPX 坐标是 WGS-84，spec 里写 `"wgs84": true` 转成高德坐标。
- 海拔：`elevation: true` 用 OpenTopoData SRTM 30m（免费，每天约 1000 次请求，已缓存）；查之前把高德坐标换回 WGS-84，否则山区高度会错。
- 高德瓦片属非官方取用：个人/学习视频可用并保留右下角署名；商业投放需高德授权或改矢量样式。国界、南海九段线照内置数据画，不要删改。详见 [references/amap.md](references/amap.md)。

## 要改画法时

- 画面全在 `scripts/runtime/mm.js`：样式表 `ST`（颜色、字体、是否发光/纸纹/虚线）、各图层绘制函数（`route` `pin` `region` `hudTitle` `odometer` `caption` `photo`）、地球（`globeTexture`/`globeFill` 逐像素反投影）。加样式＝往 `ST` 加一项（地球颜色用 `globeOcean`/`globeLand` 单独给，暗色样式要拉开对比），用瓦片的再加进 `compile.py` 的 `TILED`。
- 镜头逻辑在 `scripts/compile.py` 的 `shot_*` 函数；加镜头类型＝写一个 `shot_xxx` 并登记到 `SHOTS`。
- 改完用 `assets/examples/showcase.json`（覆盖全部镜头）在两三种样式下抽帧回归。

## 踩过的坑（改代码前先看）

- 3D：MapLibre 5.6 在地球↔地形切换时抛 `clearFadeHold` 错 → 用 5.24（`render.py` 的 `VENDOR`）。`setData` 在 worker 里异步，`idle` 可能先到，线会慢一帧 → `t3d.js` 先等 `sourcedata(isSourceLoaded)` 再等 `idle`。
- 3D：单帧抽图看着没问题、连续出片却在远处露黑块（天空/远处地形没画）——**抽帧检查要用 `--from/--to` 渲一小段**，别只看 `--stills`。现在的处理：推近到 12.2 级后从地球投影换成平面投影（地球投影下雾和天空不生效；12 级前换会跳一下），底色透明、天空由 2D 画布自己画渐变，全景/片尾俯角压到 40–44° 不露地平线。
- 3D：雪山上白字看不清 → HUD 上下压暗渐变。俯视全景按透视近大远小会出画 → 自动挑让轨迹顺着视线方向的朝向，再退 0.35 级。

- 正射地球上用多边形填陆地会破（背面点贴地平圈后围出错误区域，整球变白）→ 现在逐像素反投影取纹理；海岸线、省界只描线。
- 墨卡托下逐点做 ±180° 归位，环会在屏幕两侧来回跳、画出横贯全屏的线 → `Pline` 只对首点归位，之后按经度差连续累加。
- 推得很近时顶点投影到几十万像素外，画布填充破成楔形 → `clipRing` 先按屏幕外扩框裁剪。
- 地名先做 POI 搜索会把行政区名搜成同名商户；只信地理编码又会把「青海湖」编成乌鲁木齐同名小区 → `geocode` 先编码，落到兴趣点/道路/公交站/住宅区级才退 POI。编译打印的地点行仍要逐行核对。
- 过渡阶段的透明度要从外层一路传进 `drawGlobe`，内部重设 `globalAlpha=1` 会让交叉淡化失效。
- 从行政区推到街道级，区域填色会染满整屏 → 推近前那一镜写 `clear`（编译主循环统一处理）。
- OSM 主站常 504、单个镜像会超时 → `osm.py` 轮流试三个镜像并缓存；按名字查会查到同名地铁站，已按 railway/public_transport 标签排除。
