# LayoutSpec 仕様書 — 自作の「歌詞の置き方」

この文書は movlyric の歌詞の置き方（レイアウト）の定義「LayoutSpec」の完全仕様です。
アプリの「スタイルエディタ →「配置」タブ → 配置プリセット →「カスタム」→ AIで作る…」から使います。

**似た名前の別物が2つあります。取り違えないでください。**

| 何を決めるか | 仕様書 |
| --- | --- |
| 歌詞を画面の**どこに置くか**（この文書） | `LAYOUT_SPEC.md` |
| 歌詞の文字が**どう動くか**（登場・退場・明滅） | `ANIMATION_SPEC.md` |
| 歌詞の**背後に描く図形・線** | `GRAPHIC_SPEC.md` |

この文書だけを読んだ AI（または人間）が、アプリにそのまま貼って通る JSON を書けることを目的とします。
文書末尾に AI へ渡すプロンプトテンプレートがあります。この文書全体をプロンプトの一部として貼り付けても機能します。

---

## 1. 概要

LayoutSpec は「1フレーズぶんの歌詞を画面のどこに、どう並べるか」だけを決めます。
時間で変化する要素は一切ありません（動きは AnimationSpec の担当）。

決まるのは次の3つです。

1. **場所** — 画面のどの点を基準に置くか（`anchors` / `placement`）
2. **組み方** — 横書きか縦書きか、何行まで折り返すか（`writing` / `maxLines` / `lineGap` / `scaleMode`）
3. **くずし** — まっすぐ並べずに崩すか（`scatter` / `baselineRotateDeg` / `path` / `charScaleRange` / `focusWord`）

**乱数を使う項目はすべて曲ごとに固定です。** 同じ曲・同じスタイルなら毎回同じ結果になり、
プレビューと書き出しも一致します（`placement: "random"` / `scatter` / `charScaleRange` /
`focusWord.pick: "random"` はすべてスタイルの `seed` から決まります）。

---

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

### 2.1 ルート: LayoutSpec

```json
{
  "writing": "horizontal",
  "anchors": [{ "x": 0.5, "y": 0.5, "align": "center" }],
  "placement": "fixed"
}
```

| キー | 必須 | 型 | 意味 |
| --- | --- | --- | --- |
| `writing` | ✅ | `"horizontal"` \| `"vertical"` | 横書き / 縦書き（上→下・列は右→左） |
| `anchors` | ✅ | Anchor の配列（1個以上） | 配置候補 |
| `placement` | ✅ | `"fixed"` \| `"cycle"` \| `"random"` | フレーズを候補へどう割り当てるか |
| `maxLines` | | 整数 1以上 | 折返し行数（縦書きは列数）。既定 1 = 折り返さない |
| `lineGap` | | 数値 0以上 | 行間（列間）。文字サイズに対する比。既定 0.15 |
| `scaleMode` | | `"fixed"` \| `"fill"` | `fill` = 画面幅（縦書きは高さ）いっぱいの大組み。既定 `fixed` |
| `scatter` | | Scatter | 文字ごとの散らし |
| `baselineRotateDeg` | | 数値 | フレーズ全体の傾き（度）。既定 0 |
| `path` | | Path | 直線でない並べ方（**横書きのみ**） |
| `charScaleRange` | | `[最小, 最大]` | 文字ごとの大きさの揺らぎ（**横書きのみ**） |
| `focusWord` | | FocusWord | 1単語だけ拡大（**横書きのみ**） |

**既定値と同じ値のキーは書かないでください。** 例えば `maxLines: 1` や `scaleMode: "fixed"` は
書かずに省略します（省略と同じ意味で、書くと保存内容が無駄に増えます）。

### 2.2 Anchor（配置候補）

```json
{ "x": 0.5, "y": 0.88, "align": "center" }
```

| キー | 必須 | 型 | 意味 |
| --- | --- | --- | --- |
| `x` | ✅ | 0..1 | 画面の左端 0 ～ 右端 1 |
| `y` | ✅ | 0..1 | 画面の上端 0 ～ 下端 1 |
| `align` | ✅ | `"start"` \| `"center"` \| `"end"` | アンカーに対して歌詞をどちら側へ伸ばすか |

`align` は横書きなら左揃え / 中央揃え / 右揃え、縦書きなら上揃え / 中央揃え / 下揃えです。

**画面端に置くときは `align` を合わせてください。** 例えば `x: 0.08` に `align: "center"` を
指定すると、長いフレーズが画面の左へはみ出します。左端に寄せたいなら `align: "start"` です。

### 2.3 placement（フレーズごとの割り当て）

| 値 | 意味 |
| --- | --- |
| `"fixed"` | 常に1個目の候補を使う（候補が1個ならこれ） |
| `"cycle"` | フレーズ順に候補を巡回する |
| `"random"` | 候補からランダムに選ぶ（曲ごとに固定） |

**候補が1個のときは3つとも同じ結果になります。** 候補を2個以上書いたときだけ意味を持ちます。

### 2.4 Scatter（文字ごとの散らし）

```json
{ "offsetRatio": 0.25, "rotateDeg": 8 }
```

| キー | 必須 | 型 | 意味 |
| --- | --- | --- | --- |
| `offsetRatio` | ✅ | 数値 0以上 | 位置のずれ幅。文字サイズに対する比 |
| `rotateDeg` | ✅ | 数値 0以上 | 回転の振れ幅（度） |

どちらも曲ごとに固定です。0.3 / 12 くらいでかなり崩れた印象になります。

### 2.5 Path（直線でない並べ方・横書きのみ）

```json
{ "kind": "wave", "amplitude": 0.3, "period": 8 }
```

| キー | 必須 | 型 | 意味 |
| --- | --- | --- | --- |
| `kind` | ✅ | `"wave"` \| `"arc"` \| `"steps"` | 波 / 弧 / 階段 |
| `amplitude` | ✅ | 数値 | 振れ幅。文字サイズに対する比 |
| `period` | | 数値 0より大 | 何文字で1周するか。省略時は `wave` 8 / `steps` 3 |

`wave` と `arc` は**文字の回転が曲線の接線に追随します**（並びに沿って傾く）。
`arc` は `period` を使いません（フレーズ全体で1つの弧を描く）。
複数行のときは各行で独立に適用されます。

### 2.6 charScaleRange（文字の大きさの揺らぎ・横書きのみ）

```json
[0.85, 1.3]
```

`[最小, 最大]` の2要素。文字ごとに曲ごと固定の倍率が決まります。
文字サイズと送り幅の両方に効くので、行幅の計算・折返し・中央揃えも揺らぎ込みで一貫します。

### 2.7 FocusWord（1単語だけ拡大・横書きのみ）

```json
{ "scale": 1.4, "pick": "longest" }
```

| キー | 必須 | 型 | 意味 |
| --- | --- | --- | --- |
| `scale` | ✅ | 数値 0より大 | 対象単語の倍率 |
| `pick` | ✅ | `"longest"` \| `"random"` | 文字数最長（同数なら先頭）/ ランダム（曲ごとに固定） |

`charScaleRange` と併用すると倍率は掛け算になります。

---

## 3. よくある間違い（禁止事項）

- **縦書きに `path` / `charScaleRange` / `focusWord` を書かない。** engine が横書きでのみ解釈するため、
  書いても何も起きません（エラーにもならないので気づけません）。
- **`anchors` を空配列にしない。** 配置先が無くなり歌詞が表示されません。
- **`x` / `y` にピクセル値を書かない。** 0〜1 の比です（`960` ではなく `0.5`）。
- **既定値と同じキーを書かない**（`maxLines: 1` / `lineGap: 0.15` / `scaleMode: "fixed"` / `placement` が
  実質無意味な候補1個での `"cycle"` など）。
- **時間や音に反応する値を書かない。** LayoutSpec に数式・アニメーションはありません。
  動かしたいなら AnimationSpec（`ANIMATION_SPEC.md`）の担当です。
- 存在しないプロパティ・コメント・末尾カンマを書かない。

---

## 4. 作例

### 4.1 画面下部に1行（カラオケの定番）

```json
{
  "writing": "horizontal",
  "anchors": [{ "x": 0.5, "y": 0.88, "align": "center" }],
  "placement": "fixed"
}
```

### 4.2 上下に振り分けて2行まで折り返す

```json
{
  "writing": "horizontal",
  "anchors": [
    { "x": 0.5, "y": 0.3, "align": "center" },
    { "x": 0.5, "y": 0.7, "align": "center" }
  ],
  "placement": "cycle",
  "maxLines": 2,
  "lineGap": 0.4
}
```

フレーズごとに上→下→上…と入れ替わります。

### 4.3 縦書きで右から3列

```json
{
  "writing": "vertical",
  "anchors": [{ "x": 0.82, "y": 0.15, "align": "start" }],
  "placement": "fixed",
  "maxLines": 3,
  "lineGap": 0.5
}
```

縦書きは上から下へ流れ、列は右から左へ増えます。`align: "start"` で上端を揃えています。

### 4.4 画面いっぱいの大組みを斜めに

```json
{
  "writing": "horizontal",
  "anchors": [{ "x": 0.5, "y": 0.5, "align": "center" }],
  "placement": "fixed",
  "scaleMode": "fill",
  "baselineRotateDeg": -6
}
```

`scaleMode: "fill"` はフレーズを画面幅いっぱいまで拡大するので、文字数によって大きさが変わります。

### 4.5 四隅に散らして崩す

```json
{
  "writing": "horizontal",
  "anchors": [
    { "x": 0.2, "y": 0.25, "align": "start" },
    { "x": 0.8, "y": 0.4, "align": "end" },
    { "x": 0.25, "y": 0.62, "align": "start" },
    { "x": 0.75, "y": 0.8, "align": "end" }
  ],
  "placement": "random",
  "scatter": { "offsetRatio": 0.3, "rotateDeg": 12 },
  "charScaleRange": [0.85, 1.3]
}
```

端のアンカーには `align` を `start` / `end` で合わせ、はみ出さないようにしています。

### 4.6 波打たせて1単語を強調

```json
{
  "writing": "horizontal",
  "anchors": [{ "x": 0.5, "y": 0.5, "align": "center" }],
  "placement": "fixed",
  "path": { "kind": "wave", "amplitude": 0.35, "period": 6 },
  "focusWord": { "scale": 1.5, "pick": "longest" }
}
```

---

## 5. AI 用プロンプトテンプレート

この文書全体を貼り付けたうえで、続けて以下を貼ってください。

**ここからコピー**

```text
あなたは movlyric の歌詞レイアウト設計者です。上に貼り付けた「LayoutSpec 仕様書」に厳密に従い、LayoutSpec の JSON を1つ作成してください。

出力規則:
- JSON のみを出力する（説明文・前置き・後書きは書かない。JSON をコードフェンスで囲むのは可）
- ルートに書けるのは writing, anchors, placement, maxLines, lineGap, scaleMode, scatter, baselineRotateDeg, path, charScaleRange, focusWord のみ
- writing, anchors, placement は必須。それ以外は必要なときだけ書く
- **既定値と同じキーは書かない**（maxLines:1 / lineGap:0.15 / scaleMode:"fixed"）
- writing は "horizontal" / "vertical" のいずれか
- anchors は 1〜8 個。各要素は {"x":0..1, "y":0..1, "align":"start"|"center"|"end"}
- x, y は**画面に対する比**（ピクセルではない）。0.5 が中央
- 画面端に寄せるときは align を start / end に合わせる（center のままだとはみ出す）
- placement は "fixed" / "cycle" / "random"。anchors が1個なら "fixed" にする
- scatter は {"offsetRatio":0以上, "rotateDeg":0以上}
- path は {"kind":"wave"|"arc"|"steps", "amplitude":数値, "period":0より大（任意）}
- charScaleRange は [最小, 最大] の2要素
- focusWord は {"scale":0より大, "pick":"longest"|"random"}
- **writing が "vertical" のときは path / charScaleRange / focusWord を書かない**（無視されるため）
- 時間・音・乱数に反応する値や数式は書けない（動きは別仕様の AnimationSpec が担当）
- 存在しないプロパティ・コメント・末尾カンマを書かない

作りたい歌詞の置き方:
{ここに作りたい置き方を書く}

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

**ここまでコピー**
