# LayoutSpec 사양서 — 나만의 "가사 배치" 만들기

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

이 문서는 movlyric의 가사 배치(레이아웃)의 정의인 "LayoutSpec"의 완전한 사양입니다.
앱의 "스타일 에디터 → 「배치」 탭 → 배치 프리셋 → 「사용자 지정」 → AI로 만들기…"에서 사용합니다.

**이름이 비슷한 별개의 것이 2개 있습니다. 혼동하지 마세요.**

| 무엇을 정하는가 | 사양서 |
| --- | --- |
| 가사를 화면의 **어디에 둘지**(이 문서) | `LAYOUT_SPEC.md` |
| 가사 글자가 **어떻게 움직일지**(등장·퇴장·명멸) | `ANIMATION_SPEC.md` |
| 가사의 **뒤에 그리는 도형·선** | `GRAPHIC_SPEC.md` |

이 문서만 읽은 AI(또는 사람)가 앱에 그대로 붙여 넣어 통과하는 JSON을 작성할 수 있게 하는 것이 목적입니다.
문서 끝에 AI에게 전달할 프롬프트 템플릿이 있습니다. 이 문서 전체를 프롬프트의 일부로 붙여 넣어도 작동합니다.

---

## 1. 개요

LayoutSpec은 "구절 하나 분량의 가사를 화면의 어디에, 어떻게 배열할지"만을 정합니다.
시간에 따라 변화하는 요소는 일절 없습니다(움직임은 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보다 큼 | 몇 글자로 한 바퀴 도는지. 생략 시 `wave` 8 / `steps` 3 |

`wave`와 `arc`는 **글자의 회전이 곡선의 접선을 따라갑니다**(배열을 따라 기울어짐).
`arc`는 `period`를 사용하지 않습니다(구절 전체로 하나의 호를 그림).
여러 줄일 때는 각 줄마다 독립적으로 적용됩니다.

### 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`를 쓰지 않기.** 엔진이 가로쓰기에서만 해석하므로,
  써도 아무 일도 일어나지 않습니다(오류도 나지 않아 알아차릴 수 없습니다).
- **`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 화면 아래쪽에 한 줄(가라오케의 정석)

```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을 하나 작성해 주세요.

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

**여기까지 복사**
