# AnimationSpec v2 规范文档 — 自定义"文字动作"

> 本文档译自日语原版。**日语版为正式版本**，如有出入以日语版为准。

本文档是 movlyric 文字动作（歌词动画）定义“AnimationSpec v2”的完整规范。
在应用的“样式编辑器 →“动作”标签 → 文字动作 →“自定义”→ 用 AI 生成…”中使用（在背景绘制图形的“背景特效”是另一套体系，
其规范请见 `GRAPHIC_SPEC.md`）。
以 v1（仅含动效）编写的 JSON 在 v2 中依然有效。v2 新增了颜色、描边、发光以及分轴缩放。
本文档的目标是：只读过此文档的 AI（或人类）也能写出可以直接粘贴到应用中并被接受的 JSON。
文档末尾附有可交给 AI 使用的提示词模板。将本文档整体作为提示词的一部分贴入同样可以正常使用。

## 1. 概述

movlyric 是一个按时间在画面上显示歌词（乐句）的视频引擎。
一句歌词按以下三个阶段运动。

1. **入场（in）**：从乐句显示窗口的开头（乐句开始发声前约 1 秒）起，文字/单词出现的动作
2. **显示中（稳定态）**：入场结束后的静止状态。可选叠加 **activePulse**（正弦波脉动）
3. **退场（out）**：向显示窗口末尾（乐句发声结束后约 1 秒）消失的动作

AnimationSpec 用**基于关键帧（keyframe）的声明式 JSON** 描述 in / out / activePulse 这三部分。
不能写代码或表达式，只能使用以下规范中列出的字段。

## 2. JSON 格式完整规范

### 2.1 根节点：AnimationSpec

```json
{ "in": <Phase>, "out": <Phase>, "activePulse": <Pulse> }
```

| 字段 | 类型 | 必需 | 说明 |
|---|---|---|---|
| `in` | Phase | **必需** | 入场阶段 |
| `out` | Phase | **必需** | 退场阶段（即使不需要退场演出也不能省略。想要无变化，请参见 §2.7） |
| `activePulse` | Pulse | 可选 | 显示中的脉动。不需要时**整个键省略**（不能写 null） |

根节点下能写的键只有这三个。

### 2.2 Phase（in / out 通用）

| 字段 | 类型 | 必需 | 取值范围 | 说明 |
|---|---|---|---|---|
| `durationMs` | 整数 | **必需** | 50～4000 | 单个单元（文字/单词）该阶段所需时间（毫秒）。**不可为小数** |
| `easing` | 字符串 | **必需** | §3 中 31 种之一 | 对整个阶段应用一次的缓动（不能按 keyframe 区间分别指定） |
| `stagger` | Stagger | **必需** | — | 单元之间的时间差。即使不需要时间差，也要像 `{"unit":"none","delayMs":0,"from":"start"}` 这样**三个字段全部必填** |
| `keyframes` | Keyframe[] | **必需** | 2～8 个 | 动作序列。**t 须在 0..1 范围内严格递增（不允许相同值），首个必须 t=0，末个必须 t=1** |

### 2.3 Keyframe

| 字段 | 类型 | 必需 | 取值范围 | 含义、单位 |
|---|---|---|---|---|
| `t` | 数值 | **必需** | 0～1 | 阶段内的归一化时间（0=开始，1=结束） |
| `alpha` | 数值 | 可选 | 0～1 | 不透明度（0=透明，1=不透明） |
| `scale` | 数值 | 可选 | 0.1～3 | 缩放倍率（1=原大小） |
| `dx` | 数值 | 可选 | -2～2 | 横向偏移。**单位是相对字号（fontSize）的比例**（1.0=一个字符的宽度）。正值=向右 |
| `dy` | 数值 | 可选 | -2～2 | 纵向偏移。**单位是相对字号的比例**。正值=向下（例如：从下方入场是正值→0） |
| `rotate` | 数值 | 可选 | -360～360 | 旋转。**单位是度**。正值=顺时针 |
| `scaleX` | 数值 | 可选 | 0.1～3 | 仅横向的缩放倍率，与 `scale` 相乘 |
| `scaleY` | 数值 | 可选 | 0.1～3 | 仅纵向的缩放倍率，与 `scale` 相乘 |
| `tint` | 整数 | 可选 | 0～16777215 | 文字颜色（`0xRRGGBB` 的十进制或十六进制表示） |
| `strokeWidth` | 数值 | 可选 | 0～20 | 描边粗细（px）。0 表示无描边 |
| `strokeColor` | 整数 | 可选 | 0～16777215 | 描边颜色 |
| `glow` | 数值 | 可选 | 0～40 | 发光强度（模糊半径 px）。0 表示无发光 |
| `glowColor` | 整数 | 可选 | 0～16777215 | 发光颜色。省略时以文字颜色发光（自发光） |

可写的属性只有上面这 13 个。不存在其他名称（如 `x`、`y`、`opacity`、`color`、`blur`、`shadow` 等）。
单位（dx/dy=相对字号的比例，rotate=度）与应用内动画编辑器显示单位（横/纵=相对字号的比例，旋转=度）一致。

**"未指定"的含义因属性而异。** 弄错这一点会导致外观不符合预期。

| 属性 | 未指定时 |
|---|---|
| `alpha` / `scale` / `scaleX` / `scaleY` | 1（无变化） |
| `dx` / `dy` / `rotate` | 0（无变化） |
| `tint` | **不改变文字颜色**（样式的卡拉OK发声变色效果照常生效） |
| `strokeWidth` / `strokeColor` | **沿用样式本身的描边**（不会变成 0） |
| `glow` / `glowColor` | 无发光 |

也就是说，**如果不想改变颜色或描边，请不要写对应的属性**。一旦写了 `tint`，该文字就不再随演唱进行变色（未发声→已发声），而是固定为指定颜色。

分别改变 `scaleX` 和 `scaleY`，可以做出挤压拉伸（squash & stretch）的效果。在弹跳、落地等表现中加入这种效果会让动作更柔和。

### 2.4 Stagger（时间差）

| 字段 | 类型 | 必需 | 取值范围 | 说明 |
|---|---|---|---|---|
| `unit` | 字符串 | **必需** | `"none"` / `"char"` / `"word"` | 时间差的单位。none=整句同时，char=按字符逐个，word=按单词逐个 |
| `delayMs` | 整数 | **必需** | 0～500 | 每个单元的延迟（毫秒）。**不可为小数**。即使 unit 为 none 也要明确写 0 |
| `from` | 字符串 | **必需** | `"start"` / `"end"` / `"center"` | 起点。start=从开头，end=从末尾，center=从中央向两侧（个数为偶数时，中间两个同时最先动） |

第 n 个动作的单元，其延迟为 `顺序 × delayMs`。注音（振假名）始终与**所属单词保持相同相位**运动。
in 和 out 可以分别指定不同的 stagger（例如：入场从开头开始，退场从末尾开始）。

### 2.5 Pulse（activePulse，可选）

| 字段 | 类型 | 必需 | 取值范围 | 说明 |
|---|---|---|---|---|
| `property` | 字符串 | **必需** | `"scale"` / `"alpha"` | 脉动作用的对象 |
| `amplitude` | 数值 | **必需** | 0～0.5 | 摆动幅度 |
| `periodMs` | 整数 | **必需** | 200～4000 | 周期（毫秒）。**不可为小数** |

行为：**仅在每个单元完成入场之后、开始退场之前**，会将 `1 + amplitude × sin(2π × nowMs / periodMs)` 乘到该 property 上。
nowMs 是视频的绝对时间，因此脉动的相位会跨乐句连续（具有确定性 = 预览与导出结果一致）。
注意：当 `property:"alpha"` 且稳定态 alpha 为 1 时，超过 1 的半个周期会被截断，因此只会看到"变暗"的一侧（作为明灭效果这是自然的）。

### 2.6 插值语义（稀疏 keyframe）

- **每个属性各自独立插值**。某个属性只会把“拥有该属性的 keyframe”连起来做线性插值。
  - 例如：`alpha` 只写在 t=0 和 t=1，`scale` 写在 t=0 / 0.5 / 1，像这样稀疏地书写也是可以的。
- 在拥有该属性的**第一个 keyframe 之前**，保持第一个值；在**最后一个 keyframe 之后**，保持最后一个值（最近邻，不做外插）。
- 任何 keyframe 都未写的属性，按 §2.3“未指定时”的表处理。动效类属性的默认值是恒等值（**alpha=1, scale=1, scaleX=1, scaleY=1, dx=0, dy=0, rotate=0**），但**颜色、描边则是"不覆盖"**（保留样式原本的值），这一点不同。
- easing 只对"阶段进度 0→1"应用一次，所有属性都用该缓动后的值来插值。

### 2.7 in 与 out 的合成、稳定态

in 与 out 始终同时求值，合成方式为：**alpha 与 scale 相乘，dx / dy / rotate 相加**。因此：

- **in 的 t=1 = 显示中的稳定态**。通常应设为恒等值（alpha 1, scale 1, dx 0, dy 0, rotate 0）。
- **out 的 t=0 必须是恒等值**。否则显示中的外观会持续被 out 的值带偏。
- 如果不需要退场演出，可以把 out 设为"无变化"：把 `keyframes` 写成 `[{"t":0,"alpha":1},{"t":1,"alpha":1}]` 这样相同的值即可保持静止（注意 t 仍需保持升序）。

合成后的 alpha 最终会被截断到 0..1 范围内。

### 2.8 时间轴与短乐句的压缩

- 显示窗口在乐句发声区间的前后各留有约 1000ms 的余量。in 从窗口开头开始；out 则反向推算，使"最后一个单元的 t=1 恰好落在窗口末尾"。
- 整个阶段所需的总时长是 `durationMs + 最大 stagger 延迟（最大顺序 × delayMs）`。当它比显示窗口更长时，**整个时间轴会被等比压缩**（这样即使窗口较短，文字也不会一直不出现）。歌词字数多、`char` stagger 使用较大 delayMs 时容易被压缩，建议让 `durationMs + 字数 × delayMs` 控制在 2000ms 左右。

## 3. 缓动函数一览（31 种）

`easing` 可以填写的名称仅限以下**这 31 种**（大小写也须严格匹配。`ease-in`、`easeOutBack` 等其他写法均不可用）。

| 名称 | 说明 |
|---|---|
| `linear` | 匀速 |
| `inQuad` / `outQuad` / `inOutQuad` | 二次方。平缓的加速/减速/两者兼有 |
| `inCubic` / `outCubic` / `inOutCubic` | 三次方。标准的加速/减速/两者兼有 |
| `inQuart` / `outQuart` / `inOutQuart` | 四次方。较强的加速/减速/两者兼有 |
| `inQuint` / `outQuint` / `inOutQuint` | 五次方。相当强的加速/减速/两者兼有 |
| `inSine` / `outSine` / `inOutSine` | 正弦波。最柔和的加速/减速/两者兼有 |
| `inExpo` / `outExpo` / `inOutExpo` | 指数。剧烈的加速/减速/两者兼有 |
| `inCirc` / `outCirc` / `inOutCirc` | 圆弧。末端急促的加速/减速/两者兼有 |
| `inBack` / `outBack` / `inOutBack` | 冲过头再弹回（反冲） |
| `inElastic` / `outElastic` / `inOutElastic` | 类似弹簧的振动 |
| `inBounce` / `outBounce` / `inOutBounce` | 类似球体弹跳的反弹 |

**重要（引擎的实际行为）**：back / elastic 系列在过程中进度会超出 0..1 的范围，但由于插值采用最近邻截断，**输出会停留在端点 keyframe 的值**（不会超出 t=1 处的值）。
如果想确保出现"冲过头再弹回"的效果，请在中间 keyframe 中明确写出（例如把 `scale` 写成 0 → 1.15 → 1）。

## 4. 常见错误（禁止事项）

以下写法会导致校验错误或产生意料之外的外观。**请务必避免**。

- **超出取值范围**：`scale: 0`、`3.5`，`alpha: 1.2`，`dx: 2.5`，`durationMs: 30` 或 `5000`，`delayMs: 600`，`amplitude: 0.8`，`periodMs: 100` 等均不允许。请严格遵循 §2 的取值范围表
- **不存在的属性**：keyframe 中只能写 §2.3 的这 13 个（`t, alpha, scale, scaleX, scaleY, dx, dy, rotate, tint, strokeWidth, strokeColor, glow, glowColor`）。`x`、`y`、`opacity`、`color`、`blur`、`skew`、`shadow` 等均不可。也不能按 keyframe 单独指定 easing
- **用非整数方式写颜色**：`"#ff0066"`、`"red"` 均不可。请以整数写 `0xRRGGBB`（十六进制字面量 `0xff0066` 或十进制 `16711782`）
- **把不想改变的属性写成 0**：`strokeWidth: 0` 表示"取消描边"。想保留样式的描边，就**不要写**这个属性
- **t 的顺序错误**：不允许 t 降序或重复相同值（须严格升序）。**别忘了首个 t=0、末个 t=1**
- **keyframes 的数量**：1 个以下或 9 个以上均不可（须为 2～8 个）
- **禁止使用小数的位置**：`durationMs` / `delayMs` / `periodMs` 只能是整数
- **easing 名称写法不一致**：须与 §3 中的 31 种严格一致
- **省略 in / out**：两者均为必需项。stagger 的三个字段（unit / delayMs / from）也始终必需
- **out 的 t=0 不是恒等值**：会导致显示中的外观发生偏移（§2.7）
- **混入非 JSON 内容**：注释、末尾逗号、表达式（如 `"dy": "0.5 * 2"`）均不可。所有值必须是数值字面量或允许的字符串

## 5. 示例

以下三个都是可以直接粘贴到应用中并通过校验的完整 JSON。

### 5.1 轻柔浮现

文字从稍下方柔和地淡入，退场时向上飘出并淡出。

```json
{
  "in": {
    "durationMs": 700,
    "easing": "outSine",
    "stagger": { "unit": "char", "delayMs": 40, "from": "start" },
    "keyframes": [
      { "t": 0, "alpha": 0, "dy": 0.4 },
      { "t": 1, "alpha": 1, "dy": 0 }
    ]
  },
  "out": {
    "durationMs": 600,
    "easing": "inSine",
    "stagger": { "unit": "char", "delayMs": 30, "from": "start" },
    "keyframes": [
      { "t": 0, "alpha": 1, "dy": 0 },
      { "t": 1, "alpha": 0, "dy": -0.3 }
    ]
  }
}
```

### 5.2 文字旋转弹跳登场

从略微旋转的状态开始，通过中间 keyframe 明确写出冲过头（scale 1.15）的效果，做出弹跳入场。退场时从末尾文字开始反向旋转并缩小。
`alpha` 是稀疏指定的示例：in 中在 t=0.6 前就达到 1，之后保持末值 1。

```json
{
  "in": {
    "durationMs": 800,
    "easing": "outBack",
    "stagger": { "unit": "char", "delayMs": 60, "from": "start" },
    "keyframes": [
      { "t": 0, "alpha": 0, "scale": 0.3, "rotate": -180 },
      { "t": 0.6, "alpha": 1, "scale": 1.15, "rotate": 20 },
      { "t": 1, "scale": 1, "rotate": 0 }
    ]
  },
  "out": {
    "durationMs": 500,
    "easing": "inBack",
    "stagger": { "unit": "char", "delayMs": 40, "from": "end" },
    "keyframes": [
      { "t": 0, "alpha": 1, "scale": 1, "rotate": 0 },
      { "t": 1, "alpha": 0, "scale": 0.3, "rotate": 180 }
    ]
  }
}
```

### 5.3 逐词从下方滑入 + 显示中缓慢明灭

以单词为单位的 stagger 从下方依次出现，显示中通过 activePulse 缓慢明灭。退场时整句同时淡出。

```json
{
  "in": {
    "durationMs": 600,
    "easing": "outCubic",
    "stagger": { "unit": "word", "delayMs": 120, "from": "start" },
    "keyframes": [
      { "t": 0, "alpha": 0, "dy": 0.8 },
      { "t": 1, "alpha": 1, "dy": 0 }
    ]
  },
  "out": {
    "durationMs": 400,
    "easing": "inQuad",
    "stagger": { "unit": "none", "delayMs": 0, "from": "start" },
    "keyframes": [
      { "t": 0, "alpha": 1 },
      { "t": 1, "alpha": 0 }
    ]
  },
  "activePulse": { "property": "alpha", "amplitude": 0.25, "periodMs": 1600 }
}
```

### 5.4 如霓虹灯般发光登场

用 `glow` 制造发光，用 `glowColor` 指定光的颜色。退场时让发光收敛到 0。由于没有写 `tint`，文字颜色仍会随演唱正常变色。

```json
{
  "in": {
    "durationMs": 500,
    "easing": "outCubic",
    "stagger": { "unit": "char", "delayMs": 40, "from": "start" },
    "keyframes": [
      { "t": 0, "alpha": 0, "glow": 0, "glowColor": 65535 },
      { "t": 0.6, "alpha": 1, "glow": 34, "glowColor": 65535 },
      { "t": 1, "alpha": 1, "glow": 20, "glowColor": 65535 }
    ]
  },
  "out": {
    "durationMs": 400,
    "easing": "inQuad",
    "stagger": { "unit": "none", "delayMs": 0, "from": "start" },
    "keyframes": [
      { "t": 0, "alpha": 1, "glow": 20, "glowColor": 65535 },
      { "t": 1, "alpha": 0, "glow": 0, "glowColor": 65535 }
    ]
  }
}
```

### 5.5 变色出现

改变 `tint` 会让文字颜色随之插值。**写了 `tint` 的乐句不会再随演唱变色**，因此颜色需要全部由这份 spec 决定。

```json
{
  "in": {
    "durationMs": 700,
    "easing": "outQuad",
    "stagger": { "unit": "char", "delayMs": 30, "from": "center" },
    "keyframes": [
      { "t": 0, "alpha": 0, "tint": 16711808 },
      { "t": 1, "alpha": 1, "tint": 16777215 }
    ]
  },
  "out": {
    "durationMs": 400,
    "easing": "inQuad",
    "stagger": { "unit": "none", "delayMs": 0, "from": "start" },
    "keyframes": [
      { "t": 0, "alpha": 1 },
      { "t": 1, "alpha": 0 }
    ]
  }
}
```

### 5.6 挤压拉伸落地（squash & stretch）

让 `scaleX` 与 `scaleY` 反方向变化。下落过程中呈纵向拉长，落地瞬间横向挤扁，然后再恢复原始大小。

```json
{
  "in": {
    "durationMs": 550,
    "easing": "outBack",
    "stagger": { "unit": "char", "delayMs": 45, "from": "start" },
    "keyframes": [
      { "t": 0, "alpha": 0, "dy": -1.2, "scaleX": 0.75, "scaleY": 1.35 },
      { "t": 0.7, "alpha": 1, "dy": 0, "scaleX": 1.3, "scaleY": 0.7 },
      { "t": 1, "alpha": 1, "dy": 0, "scaleX": 1, "scaleY": 1 }
    ]
  },
  "out": {
    "durationMs": 350,
    "easing": "inQuad",
    "stagger": { "unit": "none", "delayMs": 0, "from": "start" },
    "keyframes": [
      { "t": 0, "alpha": 1 },
      { "t": 1, "alpha": 0, "scaleY": 0.6 }
    ]
  }
}
```

## 6. 供 AI 使用的提示词模板

请复制以下框内内容交给 AI。推荐的用法是：先贴上本规范文档全文，紧接着再贴这份模板（如果不附带规范文档、只把模板单独交给 AI，AI 容易在取值范围或 easing 名称上出错）。

**以下为复制起点**

```text
你是 movlyric 的歌词动画设计师。请严格遵循上面贴出的“AnimationSpec v2 规范文档”，创建一个 AnimationSpec 的 JSON。

输出规则:
- 只输出 JSON（不要写说明、前言或结语；可以用代码围栏包裹 JSON）
- 根节点为 {"in": ..., "out": ...}，仅在需要时加入 "activePulse"
- keyframes 为 2～8 个，t 须在 0..1 范围内严格递增，首个必须 t=0，末个必须 t=1
- keyframe 中只能写 t, alpha(0..1), scale(0.1..3), scaleX(0.1..3), scaleY(0.1..3), dx(-2..2), dy(-2..2), rotate(-360..360), tint(0..16777215), strokeWidth(0..20), strokeColor(0..16777215), glow(0..40), glowColor(0..16777215)。单位: dx/dy 是相对字号的比例（正值=右/下），rotate 是度，strokeWidth/glow 是 px，颜色为 0xRRGGBB 形式的整数
- **不想改变的属性请不要写。** 写了 tint 会让该文字停止随演唱变色，固定为指定颜色。不写 strokeWidth/strokeColor 的话样式本身的描边会保留（不写 ≠ 0）
- 想要发光效果时使用 glow（无偏移的光晕）。省略 glowColor 会以文字自身颜色发光
- 挤压拉伸（squash & stretch）动作通过让 scaleX 与 scaleY 反方向变化来实现
- 请遵守 durationMs(整数 50..4000) / stagger.delayMs(整数 0..500) / activePulse.periodMs(整数 200..4000)。stagger 的 unit("none"/"char"/"word")、delayMs、from("start"/"end"/"center") 三项全部必填
- easing 只能使用规范文档缓动一览中的 31 种名称之一（须严格一致）
- out 阶段 t=0 的 keyframe 必须是恒等值（相当于 alpha 1, scale 1, dx 0, dy 0, rotate 0）
- 不要写不存在的属性、注释、末尾逗号、表达式

想要的动作:
{在此写下你想要的动作}

（仅在想修改已有动画时，请在下方粘贴当前的 JSON。若是新建，请删除这一整节）
当前 JSON:
{当前 JSON}
```

**复制到此为止**
