# GraphicSpec 사양서 — 나만의 "배경 이펙트" 만들기

> 이 문서는 일본어 원문의 번역본입니다. **일본어판이 정본**이며, 내용에 차이가 있을 경우 일본어판이 우선합니다.

이 문서는 movlyric의 배경 이펙트(가사 뒤에 도형·선을 그리는 것)의 정의인 "GraphicSpec"의 완전한 사양입니다.
앱의 "스타일 에디터 → 「배경」 탭 → 배경 이펙트 → 「사용자 지정」 → AI로 만들기…"에서 사용합니다(가사 글자 자체를 움직이는 "문자 움직임"은 별개이며,
그쪽은 `ANIMATION_SPEC.md`이 사양입니다).
이 문서만 읽은 AI(또는 사람)가 앱에 그대로 붙여 넣어 통과하는 JSON을 작성할 수 있게 하는 것이 목적입니다.
문서 끝에 AI에게 전달할 프롬프트 템플릿이 있습니다. 이 문서 전체를 프롬프트의 일부로 붙여 넣어도 작동합니다.

## 1. 개요

배경 이펙트는 가사의 **뒤쪽**에서 움직이는 모션 그래픽입니다(가사 자체의 움직임은 별도 사양인 AnimationSpec이 담당합니다).

GraphicSpec은 "같은 모양의 도형을 N개 나열해 움직인다"는 형태로 작성합니다. 도형은 **원·직사각형·선·다각형**의 4종류입니다.

각 도형의 위치·크기·색상·불투명도는 두 가지 방식으로 정할 수 있습니다.

1. **정형화된 합성**(`기준값 + 난수 + 사인파 + 흐름 + 소리`) — 짧게 쓸 수 있음
2. **수식** — `0.5 + sin(t * 2 + i) * 0.2`처럼 자유롭게 계산할 수 있음. 조건 분기나, **직전 도형의 위치를 참조한 연쇄 배치**도 쓸 수 있음

수식은 JavaScript가 아닙니다. **대입·반복·함수 정의는 쓸 수 없고, 하나의 식이 하나의 숫자를 반환**할 뿐입니다(표 계산 소프트웨어의 수식과 같은 개념). 사용할 수 있는 이름은 §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` | 문자열 또는 정수 | **필수** | §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` | 숫자 | 선택 | — | 시간에 따라 선형으로 움직이는 양(**초당**). 음수면 반대 방향 |
| `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" }
```

#### 쓸 수 없는 것

변수 대입, 반복, 함수 정의, 문자열은 쓸 수 없습니다. **하나의 식으로 하나의 숫자**를 낼 뿐입니다. 위 표에 없는 이름(`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부터 시작합니다. "첫 번째만 별도 취급"하고 싶을 때는 `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`은 쓸 수 없습니다. 하나의 식으로 하나의 숫자입니다
- **색상을 문자열로 쓰기**: `"#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 이어지며 뻗어가는 선(앞 도형에 의존)

첫 번째 선부터 순서대로, 직전 선의 끝점 부근에 다음 선을 두어 연쇄시킵니다. `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을 하나 작성해 주세요.

출력 규칙:
- 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}
```

**여기까지 복사**
