# AnimationSpec v2 仕様書 — 自作の「文字の動き」

この文書は movlyric の文字の動き（歌詞アニメーション）の定義「AnimationSpec v2」の完全仕様です。
アプリの「スタイルエディタ →「動き」タブ → 文字の動き →「カスタム」→ AIで作る…」から使います（背景に図形を描く「背景エフェクト」は別物で、
そちらは `GRAPHIC_SPEC.md` が仕様です）。
v1（モーションのみ）で書かれた JSON はそのまま v2 として有効です。v2 で色・縁取り・発光・軸別の伸縮が追加されました。
この文書だけを読んだ AI（または人間）が、アプリにそのまま貼って通る JSON を書けることを目的とします。
文書末尾に AI へ渡すプロンプトテンプレートがあります。この文書全体をプロンプトの一部として貼り付けても機能します。

## 1. 概要

movlyric は歌詞（フレーズ）を時間に合わせて画面に表示する動画エンジンです。
1つのフレーズは次の3段階で動きます。

1. **入場（in）**: フレーズ表示窓の先頭（フレーズ発声開始の約1秒前）から、文字/単語が現れる動き
2. **表示中（定常）**: 入場が終わった状態で静止。任意で **activePulse**（sin 波の脈動）を掛けられる
3. **退場（out）**: 表示窓の末尾（フレーズ発声終了の約1秒後）に向けて消える動き

AnimationSpec はこの in / out / activePulse を **keyframe ベースの宣言的 JSON** で記述します。
コードや式は書けません。書けるのは以下の仕様にあるフィールドだけです。

## 2. JSON 形式の完全仕様

### 2.1 ルート: AnimationSpec

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

| フィールド | 型 | 必須 | 説明 |
|---|---|---|---|
| `in` | Phase | **必須** | 入場フェーズ |
| `out` | Phase | **必須** | 退場フェーズ（退場演出が不要でも省略不可。無変化にしたい場合は §2.7 参照） |
| `activePulse` | Pulse | 任意 | 表示中の脈動。不要なら**キーごと省略**する（null は不可） |

ルート直下に書けるキーはこの3つだけです。

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

| フィールド | 型 | 必須 | 値域 | 説明 |
|---|---|---|---|---|
| `durationMs` | 整数 | **必須** | 50〜4000 | 1ユニット（文字/単語）のフェーズ所要時間（ミリ秒）。**小数不可** |
| `easing` | 文字列 | **必須** | §3 の31種のいずれか | フェーズ全体に1回適用されるイージング（keyframe 区間ごとには指定できない） |
| `stagger` | Stagger | **必須** | — | ユニット間の時間差。時間差なしでも `{"unit":"none","delayMs":0,"from":"start"}` のように**3フィールドすべて必須** |
| `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=文字1つ分）。正=右 |
| `dy` | 数値 | 任意 | -2〜2 | 縦オフセット。**単位は文字サイズ比**。正=下（例: 下から入場は正→0） |
| `rotate` | 数値 | 任意 | -360〜360 | 回転。**単位は度**。正=時計回り |
| `scaleX` | 数値 | 任意 | 0.1〜3 | 横だけの拡大率。`scale` と掛け合わされる |
| `scaleY` | 数値 | 任意 | 0.1〜3 | 縦だけの拡大率。`scale` と掛け合わされる |
| `tint` | 整数 | 任意 | 0〜16777215 | 文字色（`0xRRGGBB` の10進または16進表記） |
| `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` | **文字色を変えない**（スタイルのカラオケ発声色がそのまま効く） |
| `strokeWidth` / `strokeColor` | **スタイルの縁取りをそのまま使う**（0 にはならない） |
| `glow` / `glowColor` | 発光なし |

つまり**色や縁取りを変えたくないなら、そのプロパティを書かないでください**。`tint` を書くと、その文字は歌唱に合わせた色替え（未発声→発声済み）をしなくなり、指定した色で固定されます。

`scaleX` と `scaleY` を別々に動かすと、潰れて伸びる動き（squash & stretch）が作れます。弾む・着地する表現では、これがあると動きが柔らかくなります。

### 2.4 Stagger（時間差）

| フィールド | 型 | 必須 | 値域 | 説明 |
|---|---|---|---|---|
| `unit` | 文字列 | **必須** | `"none"` / `"char"` / `"word"` | 時間差の単位。none=フレーズ全体が一斉、char=文字ごと、word=単語ごと |
| `delayMs` | 整数 | **必須** | 0〜500 | ユニット1つ分の遅延（ミリ秒）。**小数不可**。unit が none でも 0 を明記する |
| `from` | 文字列 | **必須** | `"start"` / `"end"` / `"center"` | 起点。start=先頭から、end=末尾から、center=中央から外へ（偶数個は中央2つが同時に最初） |

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」に1回だけ適用され、その eased 値で全プロパティを補間します。

### 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` | 2次。ゆるやかな加速/減速/両方 |
| `inCubic` / `outCubic` / `inOutCubic` | 3次。標準的な加速/減速/両方 |
| `inQuart` / `outQuart` / `inOutQuart` | 4次。強めの加速/減速/両方 |
| `inQuint` / `outQuint` / `inOutQuint` | 5次。かなり強い加速/減速/両方 |
| `inSine` / `outSine` / `inOutSine` | sin 波。最も柔らかい加速/減速/両方 |
| `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` を整数で書く（16進リテラル `0xff0066` または10進 `16711782`）
- **変えたくないプロパティを 0 で書く**: `strokeWidth: 0` は「縁取りを消す」指定。スタイルの縁取りを残したいなら**書かない**
- **t の順序違反**: t の降順・同値の重複は不可（狭義昇順）。**先頭 t=0 と末尾 t=1 を忘れない**
- **keyframes の個数**: 1個以下・9個以上は不可（2〜8個）
- **小数の禁止箇所**: `durationMs` / `delayMs` / `periodMs` は整数のみ
- **easing 名の表記揺れ**: §3 の31種と厳密一致で書く
- **in / out の省略**: 両方必須。stagger の3フィールド（unit / delayMs / from）も常に必須
- **out の t=0 を恒等値以外にする**: 表示中の見た目がずれる（§2.7）
- **JSON 以外の混入**: コメント、末尾カンマ、式（`"dy": "0.5 * 2"` など）は不可。値はすべて数値リテラルか許可された文字列

## 5. 作例

3つともこのままアプリに貼って通る完全な 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 文字が回転しながら弾んで登場

小さく回転した状態から、行き過ぎ（scale 1.15）を中間 keyframe で明示して弾む入場。退場は末尾の文字から逆回転で縮む。
`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 が値域や easing 名を誤ります）。

**ここからコピー**

```text
あなたは movlyric の歌詞アニメーション設計者です。上に貼り付けた「AnimationSpec v2 仕様書」に厳密に従い、AnimationSpec の JSON を1つ作成してください。

出力規則:
- 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") の3つすべて必須
- easing は仕様書のイージング一覧にある31種の名前のみ（厳密一致）
- out の t=0 の keyframe は恒等値（alpha 1, scale 1, dx 0, dy 0, rotate 0 に相当）にする
- 存在しないプロパティ・コメント・末尾カンマ・式を書かない

作りたい動き:
{ここに作りたい動きを書く}

（既存のアニメーションを修正したい場合のみ、以下に現在の JSON を貼ってください。新規作成の場合はこの節ごと削除してください）
現在の JSON:
{現在の JSON}
```

**ここまでコピー**
