# 运动语法：从一幅画走到下一幅画的六种办法

一部影片先给出一样好东西，再给出另一样毫不相干的好东西，那么不管每一样的动画做得多好，看起来都只是幻灯片。下面这六种机制，都是让下一幅画从当前这幅画**里面出来**的办法。它们是用来组合的语法，绝不是预设：挑故事本来就在要求的那一种，再针对这部影片的题材和媒介把它做出来。

**默认做法不变。** 温和的过渡动作（晕开、翻转、推近）仍然是标准接缝（[workflows/launch-video.md](workflows/launch-video.md) 的「运动（Motion）」一节）。下面每一种机制都是一项带条件的判断。如果它的「何时用」并不明确成立，就用温和的过渡动作。

六种机制都遵守三件事：

- **动作开始之前，目的地已经就位。** 下一个场景已经画好，垫在动作的下面或后面，所以两个场景之间永远不会出现空帧。
- **有东西被带过去**：一个形状、一个位置、一个运动方向。设计出来的接缝和盖在拼接处上的转场，区别就在这里。
- **一部影片用一到两种机制，每种只用在要紧的接缝上。** 30 秒里六种全上，那是一卷动作演示片，不是故事。

## 1. 前景即转场

场景里本来就有的某样东西（一个词、一个记号、一个形状）不断变大或扫过，直到盖满画面；等它退开，下一个场景已经在那里了。

- **何时用**：盖住画面的那样东西是故事的一部分（品牌的墨、产品的标志、主张本身），而两个场景之间没有别的东西可以带过去。
- **何时不用**：遮盖物会是一个随手拿来、只为藏住拼接处的形状；或者它会出现在每一个接缝上——到第三次，它看起来就是一个划变预设。
- **我们的实例**：发布片里的文字帧（`launch3.ts`，`launchCut.ts` 里的 `typeFrame`）。墨从一个点在画面上晕开，词语在墨晕里一笔一笔把自己写到纸面上，然后墨收拢成一个点，落在**下一幅**画上。墨晕把它展开时覆盖的那一帧内容、和它收拢时落到的那一帧内容，都当作数据接收（`type(lines, len, before, after)`），所以下一个场景总是等在它底下。

## 2. 常驻角色

一个物体在整部影片里保持自己的身份，场景在它周围变换。

- **何时用**：故事本来就有这样一个物体（一个光标、产品的圆点、一个角色、一条正在画的线），而且它在它走过的每一个场景里都有出现的理由。
- **何时不用**：它得为了这个目的专门发明出来，或者被拖着穿过一些它什么也不做的场景：一个只在旁边看着的吉祥物就是装饰。
- **我们的实例**：发布模板里的墨滴（`launchTemplate.ts`）。每按一次 Generate，就有一滴墨落进对话串，在那里晕开成回答卡片；到结尾，它最后一次从 Generate 上升起，落下成为片尾卡标志上的那个点，片尾卡就从它展开。`motif.ts` 用规则管住它：接缝两侧的屏幕位置和大小一致，误差在 2 px 以内，否则 `checkRelay` 抛错。

- **通用做法**：`engine/src/canvas-core/morph.ts`。`handoff({ from, to, t0, t1 })` 把一条轮廓从源形状带到目标形状：它一边移动，一边在运动中略微拉长，并在行程的后半段变成目标形状，精确地落在目标上。`coverShape` 把一个形状放大到整页那么大。物体的颜色和边缘在接缝两侧保持一致，并用 `checkShapes`（2 px）守住接缝。`morphDemo.ts`：一个句号变成一张卡片，卡片又变成整页。

## 3. 从选中到展开

一组东西里的某一项被挑出来，不断变大，直到它**就是**下一个场景。

- **何时用**：故事从多走向一：一整面选项墙，然后是正在用的那一个；一个列表，然后是被打开的那一项。观众看得出下一个场景是从哪里来的。
- **何时不用**：那一组东西摆出来只是为了被离开（搭这一组，是因为故事需要展示广度，而不是把它当跳板）；或者那一项的小版本和全画面版本对不上：要把完整场景的第一帧构图得和它所长出来的那张卡片一模一样。
- **我们的实例**：画风墙（`launchGallery.ts`，用在 `launch2.ts` 里）。摄像机猛推进卡片又退出来；最后一次推进不再退出，卡片填满画面，变成场景。锦鲤从一张静帧出发也是这么做：`koiWorld.ts` 把它的第一帧构图得和锦鲤样张一模一样，于是卡片无缝地交接给那个游动的世界。

## 4. 对位拆解

一个整体保持可辨认，同时它的各个部件移动、归成若干组，每个部件从原来的位置到落点都能追踪。

- **何时用**：要点在于产品对观众刚刚看到的东西做了整理、提取或重组：拥挤的一周变成一份计划，一份文档变成一个个字段。
- **何时不用**：部件跟不住的时候。同时移动的超过大约七个，或者部件的路径彼此交叉，观众就丢了地图；把它们错开，或者成组移动而不是逐项移动。
- **我们的实例**：`productUI.ts`（发布模板里的一条 `kind: "ui"` 请求）。界面由数据画出两个状态。按一下产品的操作，每一项都按 id 匹配，从它在前一状态里的位置弹到后一状态里的位置：行移到新的槽位，数值滚动到新的数字，新项进场，旧项离场。

## 5. 连续画布

所有场景都放在一张比画面大的布局里，摄像机在它们之间移动，而不是切换。

- **何时用**：这些地方在空间上真的有关系（一页里的几个区块、一栋楼里的几个房间、一条路上的几个步骤），而它们之间的路线正好告诉观众是什么关系。
- **何时不用**：这些地方互不相关（这时移动会凭空编出一种并不存在的关系）；或者两站之间的路程又长又空：每一秒都必须有变化（[craft-bar.md](craft-bar.md)，不要冷场）。所有平面共用一台摄像机（[camera.md](camera.md)）。
- **我们的实例**：`webTour.ts`。一张长页面上的三个区块，一个看得见的光标，以及一台先展示整页、再在每一次点击和每一个字段上推近的摄像机。`koiWorld.ts` 是手绘版：一个比画面大得多的池塘，景物放在世界里，所以随着鱼游动，景物也在变化。

## 6. 爆炸分层

一个物体沿着纵深分开成它的各个层，停住让每一层都能被看清，然后再合拢回这个物体。

- **何时用**：里面是什么正是要点：一样东西是怎么造出来的，一个堆栈由什么组成。这些层必须是题材里真实的部件，按真实的顺序排列。
- **何时不用**：题材本身没有真正的层，层是为了效果编出来的；或者各部分分开的时间不够长，来不及看清。如果它再也不合拢，那它就是一张图解，而这也许正是这部影片想要的；要决定是哪一种。
- **我们的实例，只有一个方向**：`paperCraft.ts`，玩具剧场。场景从后往前、一片布景接一片布景地搭起来，布景片之间留着真实的间隙：每一片在移动时都被托在这一摞的上方，影子又宽又远，落下时影子收拢。这是合拢的那一半。我们还没有哪部影片是从完整状态开始再分开的；在有一部做到之前，把分开的那一半当作未经验证。

## 三个现成的接缝

对于一次普通的换场、但仍想带一点手作笔触的情况，`engine/src/canvas-core/transitions.ts` 提供三个调用。每个都接收两个场景的绘制函数和一个进度，适用任意画面尺寸，进来的场景已经在底下画完整。`transitionsDemo.ts` 把每一种各演示一次。

- **`brushWipe`**：一支蘸满颜料的笔刷横拖过去；带 `ink` 时，新画面前方有一条湿墨带（不带时是一道干净的刷毛边缘）。用于手绘调子（drawn register），方向顺着故事本来就在走的方向。不用于简洁调子（clean register），一部影片里也不超过一两次。
- **`iris`**：从一个点圆形张开，或者收拢到一个点。从下一个场景讲的那样东西上张开，在这一个场景讲的那样东西上收拢；不以任何东西为中心，它就只是一个形状。墨用 `shape: "blot"`，简洁调子用普通圆形。
- **`flashCut`**：在一阵短促的闪光下硬切，所以硬切的条件同样适用（一个强拍，出现在能量要求它的作品里）。绝不只有一帧，默认绝不全白，绝不快速重复：把一部影片的闪光声明给 `checkFlashes`，任意两次相隔不到半秒它就抛错。

## 带着运动的切

两个都在运动的镜头之间的硬切，可以是最顺滑的接缝，也可以是最粗糙的。区别在于算术，`engine/src/canvas-core/seams.ts` 负责算。

- **两边都静止，或两边同速。** 从运动的镜头切到静止的镜头（或者反过来）看起来是一下顿挫。运动方向转弯、速度减半或加倍的切也一样。
- **加速出去，在最快的那一帧之后切，到达并停稳。** 出去的镜头在大约 0.2 到 0.4 秒内加速；进来的镜头以同样的速度、朝同样的屏幕方向开场，在 0.8 到 1.4 秒内停稳。出场是短的那一边。`matchedMove({ dir, exitDist, exitF, entryF })` 给出两半，分别加到各个场景的摄像机取景上；进场距离由出场速度算出，所以两边总是一致。切点那一帧归进来的镜头：出去的镜头留在屏幕上的最后一帧是切点的前一帧，而那一帧就是它最快的一帧。
- **穿过切点，在纵深方向上。** `matchedZoom({ push, exitF, entryF })` 对一台飞进一个镜头、又在下一个镜头里飞出来的摄像机做同样的事。缩放是一个比率，所以两边都在对数空间里运行，跨过切点时每帧以相同的速率增长。用默认的推进量，进来的镜头开场时大约是它尺寸的三分之二，所以它的场景必须画得比画面宽出那么多；会让它在 0.4 以下开场的缩放会被拒绝。
- **速度有上限。** 超过大约每秒两个半画面宽（`speedCeiling(W, fps)`），模糊就不再读作运动，而是显出一道带阶梯状重影的拖痕。需要比这更快的移动，就该缩短行程、把其余部分藏到切点后面，或者在拍点上直接硬切。快速接缝用 `--blur auto` 渲染。
- **所有东西都对上，并且只出现一次。** 跨过切点的任何东西都保持它的位置、大小、颜色和边缘；在切点那一帧突变的属性会跳一下。静止姿势只展示一次：如果一个镜头停稳在某个姿势上、下一个镜头又以这个姿势开场，那么第一个镜头的最后一帧是这个动作的最后一张中间帧，绝不是姿势本身，否则影片会停住、定格、再走。
- **绝不要让同一样东西的两个副本互相叠化。** 到中点时两者都只出现一半，眼睛看到的是重影。移动一个物体；在它行程的中途改变它是什么。

`checkSeam(velocity before, velocity after, { W, fps })` 返回一次切哪里不对，或者什么都不返回：一边静止、转向超过 30 度、速度骤降或骤升。`checkSeamPlan` 读取影片的接缝清单，标出被第三次使用的设计接缝；普通切和匹配切不受此限，影片唯一的招牌动作可以用三次（开头、中段、结尾）。两个场景都有空间朝同一方向移动时用这一套。没有空间时，让两边都停稳，在拍点上切。

## 发一手牌

任由自己挑，每部影片都会伸手去拿同样那三个动作。`node tools/deck.mjs deal --kind story --energy high --seconds 27` 从 `references/motion-deck.json` 发出一手牌：一个开场、一个收尾，以及一组接缝、运镜、揭示和表演提示，每张都带着它的数值、它的引擎调用，以及何时不该用它。发牌会向适合这部影片的牌加权，并且可以用打印出来的种子复现；`--avoid` 让下一部影片避开上一部用过的牌。

一张牌是一个提示，绝不是命令。留下它，针对这部影片的题材和媒介改编它，或者丢掉它，并在 brief 里写明是哪一种。本页开头的默认做法依然成立：「何时不用」成立的牌就丢掉，由温和的过渡动作顶上。

## 标注与标签交接

`engine/src/canvas-core/callout.ts` 给画面里的一样东西命名：一个点落在它上面，一条引线被画出来，末端写上一个标签。摄像机移动时，点跟着那样东西走（用 `cam.toScreen` 钉住，见 [camera.md](camera.md)）；标签保持不动，好让人读。它是叠在画面上的文字，所以那一套判断完全适用（[workflows/launch-video.md](workflows/launch-video.md) 的「文字（Type）」一节）：一块安静的区域，一个说出所指之物的标签，同一时间屏幕上只有一两个。画面繁杂的地方，给标签加一个 `backing`，或者干脆不用标注。

标签也可以充当跨过接缝的常驻角色。用 `handoff` 时，点和线收回，标签移动到下一个场景开始的位置，在那个场景的第一帧上落成它的标题；下一个场景从那里用 `drawLabel` 以同样的姿态把它画出来。`checkHandoff` 把两边约束在 2 px 以内，接缝一跳就抛错。`calloutDemo.ts` 把两种用法都演示了。

## 怎么选

先问这个接缝要告诉观众什么，再拿能说出这句话的那种机制：

| 接缝在说 | 机制 |
|---|---|
| 「现在是这个」，除了品牌之外没有共享任何东西 | 前景即转场 |
| 「还是那样东西，往后走了一步」 | 常驻角色 |
| 「那么多里面，就是这一个」 | 从选中到展开 |
| 「还是那些东西，排好了顺序」 | 对位拆解 |
| 「在那个旁边，就在这儿」 | 连续画布 |
| 「这就是里面的东西」 | 爆炸分层 |

如果这些都不是这个接缝要说的，那它就是一次普通的换场：用温和的过渡动作，或者在条件成立时用一次匹配的硬切。在整部影片完整渲染之前，把每一个设计接缝都当作一对前后静帧、再当作它们之间的一条帧序列来审一遍。
