# GraphicSpec Specification — Building Your Own "Background Effect"

> This document is a translation of the Japanese original. **The Japanese version is authoritative** — if the two differ, the Japanese version takes precedence.

This document is the complete specification for **GraphicSpec**, movlyric's definition of background effects (shapes and lines drawn behind the lyrics).
You use it from the app's Style Editor → "Background" tab → Background effect → "Custom" → "Create with AI…" (character motion — which animates the lyric text itself — is a separate thing; see `ANIMATION_SPEC.md` for that spec).
The goal is that an AI (or a human) who has read only this document can write JSON that the app accepts as-is.
A prompt template for handing to an AI appears at the end of this document. You can also paste this entire document in as part of the prompt, and it will still work.

## 1. Overview

A background effect is motion graphics that move **behind** the lyrics (the motion of the lyrics themselves is handled by the separate AnimationSpec).

GraphicSpec is written as "arrange N copies of the same shape and animate them." There are four shape types: **circle, rectangle, line, and polygon**.

Each shape's position, size, color, and opacity can be defined in one of two ways:

1. **A standard composite** (`base value + random + sine wave + drift + audio`) — concise to write
2. **A formula** — free-form calculation, e.g. `0.5 + sin(t * 2 + i) * 0.2`. This also lets you write conditional branches and **chained placement that references the previous shape's position**

Formulas are not JavaScript. **You cannot write assignments, loops, or function definitions — a single expression just returns a single number** (the same idea as a spreadsheet formula). Only the names listed in §2.6 can be used.

## 2. Complete JSON Format Specification

### 2.1 Root: GraphicSpec

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

| Field | Type | Required | Description |
|---|---|---|---|
| `layers` | Layer[] | **Required** | 1–4 items. Layers written earlier are drawn **further back**; later ones are drawn in front |

This is the only key that may appear directly under the root.

### 2.2 Layer

| Field | Type | Required | Range | Description |
|---|---|---|---|---|
| `shape` | String | **Required** | `"circle"` / `"rect"` / `"line"` / `"polygon"` | The shape type |
| `count` | Integer | **Required** | 1–200 | Number of shapes |
| `sides` | Integer | Optional | 3–24 | Number of vertices for a polygon (default 5). Ignored for other shapes |
| `star` | Number | Optional | 0–1 | How star-shaped the polygon is. 0 = regular polygon, 0.5 = a typical star. Ignored for other shapes |
| `when` | String | Optional | Formula | Draw condition. Any instance evaluating to 0 is not drawn (§2.6) |
| `spread` | String | Optional | §2.4 | Initial placement across the count of shapes. If omitted, all shapes overlap at the same position |
| `x` | Value | **Required** | — | Horizontal position, **as a ratio of screen width** (0 = left edge, 0.5 = center, 1 = right edge) |
| `y` | Value | **Required** | — | Vertical position, **as a ratio of screen height** (0 = top edge, 1 = bottom edge) |
| `size` | Value | **Required** | — | Size, **as a ratio of screen height**. circle = radius / rect = height / line = length |
| `aspect` | Number | Optional | 0.01–100 | rect's width = size × aspect. For line, this affects thickness. Ignored for circle |
| `rotation` | Value | Optional | — | Rotation, **in degrees**. Ignored for circle |
| `color` | String or Integer | **Required** | §2.5 | Color |
| `alpha` | Value | **Required** | — | Opacity. Clamped to 0–1 |
| `wrap` | Boolean | Optional | — | If true, shapes that go off-screen reappear from the opposite side (a continuous-flow effect) |

### 2.3 Value (how a value is determined)

There are three ways to write it.

| Form | Example | Use |
|---|---|---|
| Number | `"x": 0.5` | A constant |
| **Formula (string)** | `"x": "0.5 + sin(t) * 0.2"` | Free-form calculation, conditional branches, dependence on the previous shape (§2.6) |
| Object | see below | A standard composite of "base value + random + wave + drift + audio" |

**When in doubt, a formula is easier to write.** The object form exists to concisely express common combinations.

**Write a plain number for a constant** (`"x": 0.5`). The object form looks like this:

```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 }
}
```

| Field | Type | Required | Range | Description |
|---|---|---|---|---|
| `base` | Number | **Required** | — | The base value |
| `random` | [Number, Number] | Optional | — | A random value within this range is added to base. **Each shape gets a different value** (deterministic randomness — the same picture every time) |
| `wave` | Object | Optional | — | Oscillates with a sine wave (see below) |
| `drift` | Number | Optional | — | A linear rate of change over time (**per second**). Negative for the opposite direction |
| `audio` | Object | Optional | — | Reacts to audio (see below) |

**wave**

| Field | Type | Required | Range | Description |
|---|---|---|---|---|
| `amplitude` | Number | **Required** | — | The oscillation amount |
| `periodMs` | Integer | **Required** | 50–60000 | Time for one full cycle, in milliseconds. **No decimals** |
| `phaseByIndex` | Number | Optional | — | How much to offset the phase per shape. **If omitted, every shape moves in lockstep**, so use roughly 0.3–1.0 when you want them spread out |

**audio**

| Field | Type | Required | Range | Description |
|---|---|---|---|---|
| `band` | Integer | **Required** | 0–63 | The frequency band. **0 is the lowest pitch (kick)**, and higher numbers mean higher pitch |
| `scale` | Number | **Required** | — | The amount multiplied by the band's level (0–1) and added to base |

As a guideline, use `band` 0–3 for bass, 8–16 for mids, and 24 or higher for treble.

### 2.4 spread (initial placement)

This is **added** to the x/y value.

| Value | Placement |
|---|---|
| `"none"` (default) | All shapes at the same position. Used when you plan to scatter them with `random` |
| `"random"` | Scattered randomly across the whole screen (deterministic randomness — the same layout every time) |
| `"row"` | Evenly spaced in a horizontal row |
| `"column"` | Evenly spaced in a vertical column |
| `"ring"` | Evenly spaced around a circle |
| `"spiral"` | Spirals outward from the center (spaced by the golden angle to avoid overlap) |
| `"grid"` | A roughly square grid |

### 2.5 color

**Referencing the palette is recommended.** This way the effect updates along with the effect whenever the user changes the color scheme.

| Value | Meaning |
|---|---|
| `"text"` | The text color |
| `"accent"` | The accent color (the chorus color) |
| `"stroke"` | The stroke color |
| Integer | A direct `0xRRGGBB` value (e.g., red = 16711680). **The string form `"#ff0000"` is not allowed** |

### 2.6 Formulas

Writing a string as a value makes it a formula. **Up to 200 characters.**

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

#### Available variables

| Name | Meaning |
|---|---|
| `t` | Elapsed seconds |
| `at` | Elapsed seconds of audio (use this when reacting to audio) |
| `i` | This shape's index (starting from 0) |
| `n` | The total number of shapes |
| `p` | `i/(n-1)` (position normalized to 0–1; 0 when n=1) |
| `beat` | Beat progress 0–1 (0 at the start of a beat) |
| `energy` | The section's energy, 0–1 |
| `chorus` | 1 while in the chorus, otherwise 0 |
| `aspect` | The screen's aspect ratio (width/height) |
| `px` `py` `psize` `prot` | The **previous shape's** position, size, and rotation (§2.7) |
| `PI` `TAU` | π and 2π |

#### Available functions

`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)` — always returns a positive remainder (`mod(-1, 3)` = 2)
- `clamp(v, min, max)` / `lerp(a, b, t)` / `step(edge, v)` / `smoothstep(e0, e1, v)`
- `rand(k)` — a random value 0–1 **unique to this shape** (changing `k` gives a different series; deterministic — the same value every time)
- `band(k)` — the level of frequency band `k`, 0–1 (0 is the lowest pitch)

#### Operators

`+` `-` `*` `/` `%` `^` (exponentiation), comparisons `<` `<=` `>` `>=` `==` `!=`, logical `&&` `||` `!`, and the **ternary operator `condition ? A : B`**.

**Write conditional branches with the ternary operator.**

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

#### What you cannot write

Assignment to variables, loops, function definitions, and strings are all disallowed. **A single expression must produce a single number.** Any name not in the tables above (`Math`, `Date`, `random`, etc.) will cause an error.

### 2.7 Calculations that depend on the previous shape

`px` / `py` / `psize` / `prot` are **the resolved values of the previous shape (`i-1`)**. This lets you write chains and stacking effects. Values are passed through **still as screen ratios**.

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

For the first shape (`i = 0`), these start at 0. If you want to treat "just the first one" differently, branch on `i`.

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

## 3. Common Mistakes (Prohibited)

- **Out-of-range values**: `count: 0` or `300`, `periodMs: 10` or `100000`, `band: 64`, `layers` with 5 or more items are all invalid
- **Nonexistent properties**: only the fields in §2.2 may appear in a Layer, and only the 5 fields in §2.3 may appear in a Value object. `speed`, `opacity`, `radius`, `points`, etc. do not exist
- **Writing a name that doesn't exist in a formula**: `Math.sin(t)` is not allowed (write `sin(t)`). `Date`, `random`, `window`, etc. cannot be used
- **Writing loops or assignments in a formula**: `for`, `=`, `let` cannot be written. A single expression produces a single number
- **Writing colors as strings**: `"#ff0066"` or `"red"` are not allowed. Use a palette name or an integer
- **Fields that must not be decimals**: `count` / `periodMs` / `band` are integers only
- **Mixing up units**: x/y/size are **a ratio of the screen, not pixels**. `"size": 40` means "40 times the screen height" and fills the whole screen. **Sizes around 0.01–0.2 are typical**
- **Forgetting `phaseByIndex`**: without it, every shape moves in lockstep, which looks mechanical and unnatural
- **Anything that isn't JSON**: comments and trailing commas are not allowed (**formulas can be written as strings** — see §2.6)

## 4. Examples

All of these are complete JSON that will pass as-is if pasted into the app.

### 4.1 Softly drifting particles of light

Randomly scattered circles slowly drift upward, swaying side to side while flickering. Once they pass the top of the screen, they reappear from the bottom.

```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 An audio-reactive equalizer

Vertical bars lined up at the bottom of the screen stretch and shrink with the level of each frequency band, from bass to treble.

```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 Diagonal streaming lines

Diagonal lines slowly sweep across the background continuously. A thin, faint layer sits behind a bolder, denser layer to create depth.

```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 Rings pulsing on the kick

Points arranged around a ring expand outward in time with the bass (kick).

```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 Stars that only appear during the chorus (conditional branch + polygon)

Star shapes appear and rotate only during the chorus. `when` toggles the whole layer on and off, and its size pulses with the beat.

```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 Chained, extending lines (depends on the previous shape)

Starting from the first line, each subsequent line is chained near the endpoint of the previous one. This example uses `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 Prompt Template

Copy the block below and hand it to an AI. It's recommended to paste this entire specification first, and follow it immediately with the template (handing the template alone to an AI, without the spec, tends to produce mistakes in ranges or units).

**Copy from here**

```text
You are a background effects designer for movlyric. Strictly following the "GraphicSpec Specification" pasted above, create a single GraphicSpec JSON.

Output rules:
- Output JSON only (no explanation, preamble, or closing remarks; wrapping the JSON in a code fence is fine)
- The root is {"layers": [...]}, with 1–4 layers
- The only fields allowed in a Layer are shape, count, sides, star, when, spread, x, y, size, aspect, rotation, color, alpha, wrap
- shape is one of "circle" / "rect" / "line" / "polygon". polygon can have sides(3..24) and star(0..1)
- count is an integer from 1 to 200
- x, y, size, and alpha are required. Each can be written in one of three ways:
  (a) a plain number (constant)
  (b) **a formula string** (up to 200 characters)
  (c) {"base":..., "random":[min,max], "wave":{"amplitude":...,"periodMs":integer 50..60000,"phaseByIndex":...}, "drift":..., "audio":{"band":integer 0..63,"scale":...}}
- The only variables usable in a formula are t, at, i, n, p, beat, energy, chorus, aspect, px, py, psize, prot, PI, TAU
- The only functions usable in a formula are 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)
- Formulas cannot contain assignment, loops, function definitions, or strings. Write conditional branches with the ternary operator `condition ? A : B`
- **Writing Math.sin(t) is not allowed** (write sin(t)). Names like Date / random / window cannot be used
- To depend on the previous shape, use px / py / psize / prot (passed through as screen ratios; 0 when i=0)
- To toggle a whole layer on and off, put a formula in `when` (not drawn if it evaluates to 0)
- **x/y/size are ratios of the screen** (not pixels). size is typically around 0.01–0.2. x/y of 0–1 is on-screen
- color is one of "text" / "accent" / "stroke", or an integer in 0xRRGGBB form. String color codes are not allowed
- When animating multiple shapes, offset their phase with wave's phaseByIndex (without it, everything moves in lockstep and looks unnatural)
- To react to audio, use audio. band 0 is the lowest pitch (kick), and higher numbers mean higher pitch
- To keep shapes flowing off-screen continuously, combine drift with wrap:true
- Do not write nonexistent properties, comments, trailing commas, or expressions

Effect I want to create:
{describe the motion you want here}

(Only if you're modifying an existing effect, paste the current JSON below. For a new creation, delete this entire section.)
Current JSON:
{current JSON}
```

**Copy up to here**
