# GraphicSpec 仕様書 — 自作の「背景エフェクト」

この文書は movlyric の背景エフェクト（歌詞の背後に図形・線を描く）の定義「GraphicSpec」の完全仕様です。
アプリの「スタイルエディタ →「背景」タブ → 背景エフェクト →「カスタム」→ AIで作る…」から使います（歌詞の文字そのものを動かす「文字の動き」は別物で、
そちらは `ANIMATION_SPEC.md` が仕様です）。
この文書だけを読んだ AI（または人間）が、アプリにそのまま貼って通る JSON を書けることを目的とします。
文書末尾に AI へ渡すプロンプトテンプレートがあります。この文書全体をプロンプトの一部として貼り付けても機能します。

## 1. 概要

背景エフェクトは、歌詞の**背後**で動くモーショングラフィックです（歌詞そのものの動きは別仕様の AnimationSpec が扱います）。

GraphicSpec は「同じ形の図形を N 個並べて動かす」という形で書きます。図形は**円・長方形・線・多角形**の4種類です。

各図形の位置・大きさ・色・不透明度は、2通りの書き方で決められます。

1. **定型の合成**（`基準値 + 乱数 + 正弦波 + 流れ + 音`）— 短く書ける
2. **数式** — `0.5 + sin(t * 2 + i) * 0.2` のように自由に計算できる。条件分岐や、**直前の図形の位置を参照した連鎖配置**も書ける

数式は JavaScript ではありません。**代入・ループ・関数定義は書けず、1つの式が1つの数値を返す**だけです（表計算の数式と同じ考え方）。使える名前は §2.6 の一覧に限られます。

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

### 2.1 ルート: GraphicSpec

```json
{ "layers": [ <Layer>, ... ] }
```

| フィールド | 型 | 必須 | 説明 |
|---|---|---|---|
| `layers` | Layer[] | **必須** | 1〜4個。先に書いたレイヤが**奥**、後が手前に描かれる |

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

### 2.2 Layer

| フィールド | 型 | 必須 | 値域 | 説明 |
|---|---|---|---|---|
| `shape` | 文字列 | **必須** | `"circle"` / `"rect"` / `"line"` / `"polygon"` | 図形の種類 |
| `count` | 整数 | **必須** | 1〜200 | 図形の個数 |
| `sides` | 整数 | 任意 | 3〜24 | polygon の頂点数（省略時 5）。他の shape では無視 |
| `star` | 数値 | 任意 | 0〜1 | polygon の星形度。0=正多角形、0.5 で標準的な星。他の shape では無視 |
| `when` | 文字列 | 任意 | 数式 | 描画条件。0 と評価されたインスタンスは描かない（§2.6） |
| `spread` | 文字列 | 任意 | §2.4 | 個数ぶんの初期配置。省略すると全部同じ位置に重なる |
| `x` | Value | **必須** | — | 横位置。**画面幅に対する比**（0=左端、0.5=中央、1=右端） |
| `y` | Value | **必須** | — | 縦位置。**画面高さに対する比**（0=上端、1=下端） |
| `size` | Value | **必須** | — | 大きさ。**画面高さに対する比**。circle=半径 / rect=高さ / line=長さ |
| `aspect` | 数値 | 任意 | 0.01〜100 | rect の横幅 = size × aspect。line では太さに効く。circle では無視 |
| `rotation` | Value | 任意 | — | 回転（**度**）。circle では無視 |
| `color` | 文字列 or 整数 | **必須** | §2.5 | 色 |
| `alpha` | Value | **必須** | — | 不透明度。0〜1 にクランプされる |
| `wrap` | 真偽 | 任意 | — | true で画面外へ出た図形が反対側から現れる（流れ続ける表現） |

### 2.3 Value（値の決め方）

書き方は3通りあります。

| 書き方 | 例 | 用途 |
|---|---|---|
| 数値 | `"x": 0.5` | 定数 |
| **数式（文字列）** | `"x": "0.5 + sin(t) * 0.2"` | 自由な計算・条件分岐・前の図形への依存（§2.6） |
| オブジェクト | 下記 | 「基準値＋乱数＋波＋流れ＋音」の定型合成 |

**迷ったら数式が書きやすいです。** オブジェクト形式は定型の組み合わせを短く書くための形です。

**数値をそのまま書けば定数**です（`"x": 0.5`）。オブジェクト形式は次のとおりです。

```json
{
  "base": 0.5,
  "random": [-0.2, 0.2],
  "wave": { "amplitude": 0.05, "periodMs": 3000, "phaseByIndex": 0.7 },
  "drift": -0.1,
  "audio": { "band": 2, "scale": 0.3 }
}
```

| フィールド | 型 | 必須 | 値域 | 説明 |
|---|---|---|---|---|
| `base` | 数値 | **必須** | — | 基準値 |
| `random` | [数値, 数値] | 任意 | — | この範囲の乱数を base に足す。**図形ごとに違う値**になる（毎回同じ絵になる決定論的な乱数） |
| `wave` | オブジェクト | 任意 | — | 正弦波で揺らす（下記） |
| `drift` | 数値 | 任意 | — | 時間で線形に動く量（**1秒あたり**）。負で逆方向 |
| `audio` | オブジェクト | 任意 | — | 音に反応させる（下記） |

**wave**

| フィールド | 型 | 必須 | 値域 | 説明 |
|---|---|---|---|---|
| `amplitude` | 数値 | **必須** | — | 揺れ幅 |
| `periodMs` | 整数 | **必須** | 50〜60000 | 1往復にかかる時間（ミリ秒）。**小数不可** |
| `phaseByIndex` | 数値 | 任意 | — | 図形ごとに位相をずらす量。**省略すると全部が一斉に同じ動きをする**ので、ばらけさせたいときは 0.3〜1.0 程度を入れる |

**audio**

| フィールド | 型 | 必須 | 値域 | 説明 |
|---|---|---|---|---|
| `band` | 整数 | **必須** | 0〜63 | 周波数バンド。**0 が最低音（キック）**、数字が大きいほど高音 |
| `scale` | 数値 | **必須** | — | バンドのレベル（0〜1）に掛けて base に足す量 |

低音に反応させるなら `band` は 0〜3、中音は 8〜16、高音は 24 以上が目安です。

### 2.4 spread（初期配置）

x/y の値に**加算**されます。

| 値 | 配置 |
|---|---|
| `"none"`（省略時） | 全部同じ位置。`random` で散らす前提のときに使う |
| `"random"` | 画面全体にランダムに散らす（毎回同じ配置になる決定論的な乱数） |
| `"row"` | 横一列に等間隔 |
| `"column"` | 縦一列に等間隔 |
| `"ring"` | 円周上に等間隔 |
| `"spiral"` | 中心から外へ渦を描く（黄金角刻みで重なりにくい） |
| `"grid"` | 正方に近い格子 |

### 2.5 color（色）

**パレット参照を推奨します。** ユーザーが配色を変えたときエフェクトも一緒に変わります。

| 値 | 意味 |
|---|---|
| `"text"` | 文字色 |
| `"accent"` | 強調色（サビの色） |
| `"stroke"` | 縁取り色 |
| 整数 | `0xRRGGBB` の直値（例: 赤 = 16711680）。**文字列の `"#ff0000"` は不可** |

### 2.6 数式

値のところに文字列を書くと数式になります。**200文字まで**です。

```json
{ "x": "0.5 + sin(t * 2 + i * 0.5) * 0.2" }
```

#### 使える変数

| 名前 | 意味 |
|---|---|
| `t` | 経過秒 |
| `at` | 音の経過秒（音に反応させるときはこちら） |
| `i` | この図形の番号（0 から） |
| `n` | 図形の総数 |
| `p` | `i/(n-1)`（0〜1 に正規化した位置。n=1 では 0） |
| `beat` | 拍の進行 0〜1（0 が拍の頭） |
| `energy` | セクションのエネルギー 0〜1 |
| `chorus` | サビ中なら 1、そうでなければ 0 |
| `aspect` | 画面の縦横比（幅/高さ） |
| `px` `py` `psize` `prot` | **直前の図形**の位置・大きさ・回転（§2.7） |
| `PI` `TAU` | 円周率、その2倍 |

#### 使える関数

`sin` `cos` `tan` `asin` `acos` `atan` `atan2` `abs` `sqrt` `exp` `log` `sign` `floor` `ceil` `round` `min` `max` `pow` `hypot` `mod` `clamp` `lerp` `step` `smoothstep` `rand` `band`

- `mod(a, b)` — 常に正の剰余（`mod(-1, 3)` = 2）
- `clamp(v, min, max)` / `lerp(a, b, t)` / `step(edge, v)` / `smoothstep(e0, e1, v)`
- `rand(k)` — **この図形固有**の乱数 0〜1（`k` を変えると別の系列。毎回同じ値になる決定論的な乱数）
- `band(k)` — 周波数バンド `k` のレベル 0〜1（0 が最低音）

#### 演算子

`+` `-` `*` `/` `%` `^`（べき乗）、比較 `<` `<=` `>` `>=` `==` `!=`、論理 `&&` `||` `!`、そして**三項演算子 `条件 ? A : B`**。

**条件分岐は三項演算子で書きます。**

```json
{ "size": "chorus ? 0.15 : 0.05" }
```

#### 書けないもの

変数への代入、ループ、関数定義、文字列は書けません。**1つの式で1つの数値を出す**だけです。上の表に無い名前（`Math`、`Date`、`random` など）を書くとエラーになります。

### 2.7 前の図形に依存する計算

`px` / `py` / `psize` / `prot` は、**直前の図形（`i-1`）の確定値**です。これで連鎖・積み上げが書けます。値は**画面比のまま**渡ります。

```json
{ "x": "px + 0.15", "y": "py + sin(i) * 0.05" }
```

先頭（`i = 0`）では 0 から始まります。「1個目だけ別扱い」にしたいときは `i` で分岐してください。

```json
{ "size": "i == 0 ? 0.1 : psize * 0.8" }
```

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

- **値域違反**: `count: 0` や `300`、`periodMs: 10` や `100000`、`band: 64`、`layers` が 5個以上はすべて不可
- **存在しないプロパティ**: Layer に書けるのは §2.2 のもの、Value のオブジェクト形式に書けるのは §2.3 の5個だけ。`speed`, `opacity`, `radius`, `points` などは存在しません
- **数式に無い名前を書く**: `Math.sin(t)` は不可（`sin(t)` と書く）。`Date`, `random`, `window` などは使えません
- **数式にループや代入を書く**: `for`, `=`, `let` は書けません。1つの式で1つの数値です
- **色を文字列で書く**: `"#ff0066"` や `"red"` は不可。パレット名か整数
- **小数の禁止箇所**: `count` / `periodMs` / `band` は整数のみ
- **単位の取り違え**: x/y/size は**ピクセルではなく画面比**。`"size": 40` は「画面高さの40倍」になり画面を埋め尽くします。**大きさは 0.01〜0.2 程度が普通です**
- **`phaseByIndex` の付け忘れ**: 付けないと全図形が一斉に同じ動きをして、機械的で不自然に見えます
- **JSON 以外の混入**: コメント、末尾カンマは不可（**数式は文字列として書けます** — §2.6）

## 4. 作例

いずれもこのままアプリに貼って通る完全な JSON です。

### 4.1 ふわふわ漂う光の粒

ランダムに散らばった円がゆっくり上へ流れ、横に揺れながら明滅します。画面上端を抜けたら下から再登場します。

```json
{
  "layers": [
    {
      "shape": "circle",
      "count": 24,
      "spread": "random",
      "x": { "base": 0, "wave": { "amplitude": 0.02, "periodMs": 7000, "phaseByIndex": 0.9 } },
      "y": { "base": 0, "drift": -0.05 },
      "size": { "base": 0.03, "random": [0, 0.03] },
      "color": "accent",
      "alpha": { "base": 0.18, "wave": { "amplitude": 0.1, "periodMs": 4000, "phaseByIndex": 0.6 } },
      "wrap": true
    }
  ]
}
```

### 4.2 音に反応するイコライザー

画面下部に並んだ縦棒が、低音から高音まで各バンドのレベルで伸び縮みします。

```json
{
  "layers": [
    {
      "shape": "rect",
      "count": 32,
      "spread": "row",
      "x": 0.5,
      "y": 0.95,
      "size": { "base": 0.02, "audio": { "band": 4, "scale": 0.5 } },
      "aspect": 0.4,
      "color": "accent",
      "alpha": 0.75
    }
  ]
}
```

### 4.3 斜めに流れる線

背景を斜めの線がゆっくり横切り続けます。奥に薄い層、手前に濃い層を重ねて奥行きを出しています。

```json
{
  "layers": [
    {
      "shape": "line",
      "count": 14,
      "spread": "random",
      "x": { "base": 0, "drift": 0.06 },
      "y": 0,
      "size": { "base": 0.5, "random": [0, 0.4] },
      "aspect": 0.03,
      "rotation": -24,
      "color": "text",
      "alpha": 0.06,
      "wrap": true
    },
    {
      "shape": "line",
      "count": 5,
      "spread": "random",
      "x": { "base": 0, "drift": 0.11 },
      "y": 0,
      "size": { "base": 0.7, "random": [0, 0.3] },
      "aspect": 0.05,
      "rotation": -24,
      "color": "accent",
      "alpha": 0.14,
      "wrap": true
    }
  ]
}
```

### 4.4 キックで脈打つ輪

円周上に並んだ点が、低音（キック）に合わせて外側へ広がります。

```json
{
  "layers": [
    {
      "shape": "circle",
      "count": 24,
      "spread": "ring",
      "x": { "base": 0.5, "audio": { "band": 0, "scale": 0.06 } },
      "y": { "base": 0.5, "audio": { "band": 0, "scale": 0.06 } },
      "size": { "base": 0.012, "audio": { "band": 0, "scale": 0.02 } },
      "color": "accent",
      "alpha": 0.5
    }
  ]
}
```

### 4.5 サビだけ舞う星（条件分岐 + 多角形）

サビの間だけ星形が現れて回ります。`when` でレイヤごと出し入れし、大きさは拍で脈打ちます。

```json
{
  "layers": [
    {
      "shape": "polygon",
      "sides": 5,
      "star": 0.55,
      "count": 18,
      "spread": "random",
      "when": "chorus",
      "x": 0,
      "y": { "base": 0, "drift": -0.03 },
      "size": "0.02 + (1 - beat) * 0.015 + rand(0) * 0.02",
      "rotation": "t * 40 + i * 20",
      "color": "accent",
      "alpha": 0.5,
      "wrap": true
    }
  ]
}
```

### 4.6 連なって伸びる線（前の図形に依存）

1本目から順に、直前の線の終点あたりへ次の線を置いて連鎖させます。`px` / `py` を使った例です。

```json
{
  "layers": [
    {
      "shape": "line",
      "count": 40,
      "x": "i == 0 ? 0.05 : px + 0.022",
      "y": "i == 0 ? 0.5 : py + sin(t + i * 0.4) * 0.012",
      "size": 0.04,
      "aspect": 0.06,
      "rotation": "sin(t + i * 0.4) * 40",
      "color": "accent",
      "alpha": "0.15 + p * 0.5"
    }
  ]
}
```

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

以下の枠内をコピーして AI に渡してください。この仕様書全体を先に貼り付け、その直後にテンプレートを続ける使い方を推奨します（仕様書なしでテンプレート単体を渡すと AI が値域や単位を誤ります）。

**ここからコピー**

```text
あなたは movlyric の背景エフェクト設計者です。上に貼り付けた「GraphicSpec 仕様書」に厳密に従い、GraphicSpec の JSON を1つ作成してください。

出力規則:
- JSON のみを出力する（説明文・前置き・後書きは書かない。JSON をコードフェンスで囲むのは可）
- ルートは {"layers": [...]} で、layers は 1〜4 個
- Layer に書けるのは shape, count, sides, star, when, spread, x, y, size, aspect, rotation, color, alpha, wrap のみ
- shape は "circle" / "rect" / "line" / "polygon" のいずれか。polygon は sides(3..24) と star(0..1) を持てる
- count は 1〜200 の整数
- x, y, size, alpha は必須。次の3通りで書ける:
  (a) 数値そのまま（定数）
  (b) **数式の文字列**（200文字まで）
  (c) {"base":..., "random":[min,max], "wave":{"amplitude":...,"periodMs":整数50..60000,"phaseByIndex":...}, "drift":..., "audio":{"band":整数0..63,"scale":...}}
- 数式で使える変数は t, at, i, n, p, beat, energy, chorus, aspect, px, py, psize, prot, PI, TAU のみ
- 数式で使える関数は sin cos tan asin acos atan atan2 abs sqrt exp log sign floor ceil round min max pow hypot mod clamp lerp step smoothstep rand(k) band(k) のみ
- 数式では代入・ループ・関数定義・文字列は書けない。条件分岐は三項演算子 `条件 ? A : B` で書く
- **Math.sin(t) のような書き方は不可**（sin(t) と書く）。Date / random / window などの名前は使えない
- 直前の図形に依存させたいときは px / py / psize / prot を使う（画面比のまま渡る。i=0 では 0）
- レイヤごと出し入れしたいときは when に数式を書く（0 と評価されたら描かれない）
- **x/y/size は画面に対する比**（ピクセルではない）。size は 0.01〜0.2 程度が普通。x/y は 0〜1 が画面内
- color は "text" / "accent" / "stroke" のいずれか、または 0xRRGGBB の整数。文字列の色コードは不可
- 複数の図形を動かすときは wave に phaseByIndex を入れて位相をずらす（付けないと全部が一斉に動いて不自然になる）
- 音に反応させるときは audio を使う。band 0 が最低音（キック）、数字が大きいほど高音
- 画面外へ流し続けるときは drift と wrap:true を組み合わせる
- 存在しないプロパティ・コメント・末尾カンマ・式を書かない

作りたいエフェクト:
{ここに作りたい動きを書く}

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

**ここまでコピー**
