AnimationSpec v2 Specification — Building Your Own "Character Motion"
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 AnimationSpec v2, movlyric's definition of character motion (lyric animation). You use it from the app's Style Editor → "Motion" tab → Character motion → "Custom" → "Create with AI…" (background effects — the shapes drawn behind the lyrics — are a separate thing; see GRAPHIC_SPEC (Background Effects) for that spec). JSON written for v1 (motion only) is still valid as-is under v2. v2 adds color, stroke, glow, and per-axis scaling. 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
movlyric is a video engine that displays lyrics (phrases) on screen in sync with time. Each phrase moves through three stages:
- In (in): from the start of the phrase's display window (roughly 1 second before the phrase begins being sung) — the motion by which characters/words appear
- On-screen (steady state): the phrase holds still once the in-motion finishes. An optional activePulse (a sine-wave pulsation) can be layered on top
- Out (out): toward the end of the display window (roughly 1 second after the phrase finishes being sung) — the motion by which it disappears
AnimationSpec describes this in / out / activePulse as declarative, keyframe-based JSON. You cannot write code or expressions — only the fields listed in the spec below.
2. Complete JSON Format Specification
2.1 Root: AnimationSpec
{ "in": <Phase>, "out": <Phase>, "activePulse": <Pulse> }
| Field | Type | Required | Description |
|---|---|---|---|
in |
Phase | Required | The in phase |
out |
Phase | Required | The out phase (still required even if you don't want an out effect — see §2.7 for how to make it a no-op) |
activePulse |
Pulse | Optional | Pulsation while on-screen. Omit the key entirely if you don't need it (null is not allowed) |
Only these three keys may appear directly under the root.
2.2 Phase (shared by in / out)
| Field | Type | Required | Range | Description |
|---|---|---|---|---|
durationMs |
Integer | Required | 50–4000 | The duration of the phase for one unit (character/word), in milliseconds. No decimals |
easing |
String | Required | One of the 31 names in §3 | The easing applied once to the entire phase (cannot be set per keyframe segment) |
stagger |
Stagger | Required | — | The time offset between units. Even with no offset, write all three fields, e.g. {"unit":"none","delayMs":0,"from":"start"} |
keyframes |
Keyframe[] | Required | 2–8 items | The sequence of motion. t must be in strictly ascending order within 0..1 (no equal values); the first must be t=0 and the last must be t=1 |
2.3 Keyframe
| Field | Type | Required | Range | Meaning / unit |
|---|---|---|---|---|
t |
Number | Required | 0–1 | Normalized time within the phase (0 = start, 1 = end) |
alpha |
Number | Optional | 0–1 | Opacity (0 = transparent, 1 = opaque) |
scale |
Number | Optional | 0.1–3 | Scale factor (1 = original size) |
dx |
Number | Optional | -2–2 | Horizontal offset. Unit is a ratio of font size (1.0 = one character's width). Positive = right |
dy |
Number | Optional | -2–2 | Vertical offset. Unit is a ratio of font size. Positive = down (e.g., entering from below goes positive→0) |
rotate |
Number | Optional | -360–360 | Rotation. Unit is degrees. Positive = clockwise |
scaleX |
Number | Optional | 0.1–3 | Horizontal-only scale factor. Multiplied together with scale |
scaleY |
Number | Optional | 0.1–3 | Vertical-only scale factor. Multiplied together with scale |
tint |
Integer | Optional | 0–16777215 | Character color (0xRRGGBB, written as decimal or hex) |
strokeWidth |
Number | Optional | 0–20 | Stroke thickness (px). 0 = no stroke |
strokeColor |
Integer | Optional | 0–16777215 | Stroke color |
glow |
Number | Optional | 0–40 | Glow strength (blur radius in px). 0 = no glow |
glowColor |
Integer | Optional | 0–16777215 | Glow color. If omitted, the glow uses the character's own color (self-illumination) |
Those 13 properties are the only ones you can write. No other names (x, y, opacity, color, blur, shadow, etc.) exist.
The units (dx/dy = ratio of font size, rotate = degrees) match the display units used in the app's animation editor (horizontal/vertical = ratio of font size, rotation = degrees).
"Unspecified" means something different for each property. Getting this wrong produces unintended visuals.
| Property | When unspecified |
|---|---|
alpha / scale / scaleX / scaleY |
1 (no change) |
dx / dy / rotate |
0 (no change) |
tint |
The character color is left unchanged (the style's karaoke sung-color coloring applies as normal) |
strokeWidth / strokeColor |
The style's stroke is used as-is (it does not become 0) |
glow / glowColor |
No glow |
In other words, if you don't want to change the color or stroke, don't write that property at all. Writing tint stops that character from doing its sung/unsung color change and locks it to the color you specified.
Animating scaleX and scaleY separately lets you create a squash-and-stretch effect. This softens bouncing or landing motions.
2.4 Stagger (time offset)
| Field | Type | Required | Range | Description |
|---|---|---|---|---|
unit |
String | Required | "none" / "char" / "word" |
The unit for the time offset. none = the whole phrase moves at once, char = per character, word = per word |
delayMs |
Integer | Required | 0–500 | The delay per unit, in milliseconds. No decimals. Write 0 explicitly even when unit is none |
from |
String | Required | "start" / "end" / "center" |
The starting point. start = from the beginning, end = from the end, center = outward from the center (with an even count, the two center units move first, simultaneously) |
The delay of the nth unit to move is order × delayMs. Ruby (furigana) always moves in the same phase as its parent word.
You can specify different staggers for in and out (e.g., in starts from the beginning while out starts from the end).
2.5 Pulse (activePulse, optional)
| Field | Type | Required | Range | Description |
|---|---|---|---|---|
property |
String | Required | "scale" / "alpha" |
The property to pulsate |
amplitude |
Number | Required | 0–0.5 | The amount of pulsation |
periodMs |
Integer | Required | 200–4000 | The period, in milliseconds. No decimals |
Behavior: only between when a unit finishes entering and before it starts exiting, 1 + amplitude × sin(2π × nowMs / periodMs) is multiplied into the property.
nowMs is the video's absolute time, so the pulse phase stays continuous across phrases (deterministic — preview and export match exactly).
Note: with property:"alpha" and a steady-state alpha of 1, the half-cycle that would exceed 1 gets clamped, so only the "dimming" half is visible (which reads naturally as a flicker effect).
2.6 Interpolation semantics (sparse keyframes)
- Each property is interpolated independently. A given property is linearly interpolated only across the keyframes that include it.
- Example: you can write
alphasparsely at only t=0 and t=1, while writingscaleat t=0 / 0.5 / 1.
- Example: you can write
- Before the first keyframe that includes a given property, the value holds at that keyframe's value; after the last one, it holds at that keyframe's value (nearest-neighbor — no extrapolation).
- For any property that appears in none of the keyframes, follow the "when unspecified" table in §2.3. Motion-related properties default to their identity values (alpha=1, scale=1, scaleX=1, scaleY=1, dx=0, dy=0, rotate=0), but note the difference: color and stroke are "not overwritten" (the style's own values remain in effect).
- Easing is applied exactly once, to the phase's overall progress 0→1, and that eased value is used to interpolate all properties.
2.7 Compositing in and out, and the steady state
in and out are always evaluated simultaneously, and composited as: alpha and scale multiply together, while dx / dy / rotate add together. As a result:
- The
t=1state of in equals the steady on-screen state. Normally this should be the identity value (alpha 1, scale 1, dx 0, dy 0, rotate 0). - The
t=0state of out must always be the identity value. Otherwise the on-screen appearance keeps drifting by the out values. - To remove the out effect entirely, make out a "no-op": write
keyframeswith the same values throughout, e.g.[{"t":0,"alpha":1},{"t":1,"alpha":1}](still respecting ascendingt), and nothing will animate.
The composited alpha is ultimately clamped to 0..1.
2.8 Timeline and compression for short phrases
- The display window has roughly 1000ms of padding before and after the phrase's sung interval.
instarts at the beginning of the window, andoutis placed by working backward so that the last unit'st=1lands exactly at the end of the window. - The total duration of a phase is
durationMs + the largest stagger delay (max order × delayMs). If this exceeds the display window, the entire timeline is compressed proportionally (so characters don't fail to appear just because the window is short). Lyrics with many characters using a largedelayMswithcharstagger are especially prone to compression, so it's recommended to keepdurationMs + character count × delayMsaround 2000ms or less.
3. Easing List (31 types)
The only names you can write for easing are the following 31 names (case-sensitive exact match; alternative spellings like ease-in or easeOutBack are not allowed).
| Name | Description |
|---|---|
linear |
Constant speed |
inQuad / outQuad / inOutQuad |
Quadratic. Gentle acceleration/deceleration/both |
inCubic / outCubic / inOutCubic |
Cubic. Standard acceleration/deceleration/both |
inQuart / outQuart / inOutQuart |
Quartic. Stronger acceleration/deceleration/both |
inQuint / outQuint / inOutQuint |
Quintic. Quite strong acceleration/deceleration/both |
inSine / outSine / inOutSine |
Sine wave. The softest acceleration/deceleration/both |
inExpo / outExpo / inOutExpo |
Exponential. Sharp acceleration/deceleration/both |
inCirc / outCirc / inOutCirc |
Circular arc. Sharp acceleration/deceleration/both at the endpoint |
inBack / outBack / inOutBack |
Overshoots and settles back (recoil) |
inElastic / outElastic / inOutElastic |
Spring-like oscillation |
inBounce / outBounce / inOutBounce |
Bounces like a ball landing |
Important (actual engine behavior): back/elastic-style easings move outside the 0..1 range partway through, but because interpolation clamps to the nearest keyframe, the output stays within the endpoint keyframes' values (it never overshoots past the t=1 value).
If you want a guaranteed "overshoot then settle back" look, write it explicitly with an intermediate keyframe (e.g., scale going 0 → 1.15 → 1).
4. Common Mistakes (Prohibited)
The following will produce a validation error or an unintended appearance. Be sure to avoid them.
- Out-of-range values:
scale: 0or3.5,alpha: 1.2,dx: 2.5,durationMs: 30or5000,delayMs: 600,amplitude: 0.8,periodMs: 100, etc. are all invalid. Follow the range tables in §2 exactly - Nonexistent properties: only the 13 properties in §2.3 may appear in a keyframe (
t, alpha, scale, scaleX, scaleY, dx, dy, rotate, tint, strokeWidth, strokeColor, glow, glowColor).x,y,opacity,color,blur,skew,shadow, etc. are not allowed. Per-keyframe easing is also not allowed - Writing colors as anything other than integers:
"#ff0066"or"red"are not allowed. Write0xRRGGBBas an integer (hex literal0xff0066or decimal16711782) - Writing 0 for a property you don't want to change:
strokeWidth: 0means "remove the stroke." If you want to keep the style's stroke, don't write it at all - Violating
torder: descending or duplicatetvalues are not allowed (strictly ascending). Don't forgett=0at the start andt=1at the end - Wrong number of keyframes: 1 or fewer, or 9 or more, is not allowed (must be 2–8)
- Fields that must not be decimals:
durationMs/delayMs/periodMsare integers only - Inconsistent easing names: must exactly match one of the 31 names in §3
- Omitting in / out: both are required. All three stagger fields (unit / delayMs / from) are also always required
- Setting
t=0of out to anything other than the identity value: the on-screen appearance will drift (§2.7) - Anything that isn't JSON: comments, trailing commas, expressions (e.g.,
"dy": "0.5 * 2") are not allowed. All values must be numeric literals or one of the allowed strings
5. Examples
All three are complete JSON that will pass as-is if pasted into the app.
5.1 Softly fading in
Characters fade in gently from slightly below, and exit by fading out while drifting upward.
{
"in": {
"durationMs": 700,
"easing": "outSine",
"stagger": { "unit": "char", "delayMs": 40, "from": "start" },
"keyframes": [
{ "t": 0, "alpha": 0, "dy": 0.4 },
{ "t": 1, "alpha": 1, "dy": 0 }
]
},
"out": {
"durationMs": 600,
"easing": "inSine",
"stagger": { "unit": "char", "delayMs": 30, "from": "start" },
"keyframes": [
{ "t": 0, "alpha": 1, "dy": 0 },
{ "t": 1, "alpha": 0, "dy": -0.3 }
]
}
}
5.2 Characters rotate in with a bounce
Starting small and rotated, an intermediate keyframe explicitly shows the overshoot (scale 1.15) for a bouncy entrance. On exit, characters shrink with reverse rotation starting from the last character.
This also shows sparse alpha: on in it reaches 1 by t=0.6, and holds at that final value of 1 afterward.
{
"in": {
"durationMs": 800,
"easing": "outBack",
"stagger": { "unit": "char", "delayMs": 60, "from": "start" },
"keyframes": [
{ "t": 0, "alpha": 0, "scale": 0.3, "rotate": -180 },
{ "t": 0.6, "alpha": 1, "scale": 1.15, "rotate": 20 },
{ "t": 1, "scale": 1, "rotate": 0 }
]
},
"out": {
"durationMs": 500,
"easing": "inBack",
"stagger": { "unit": "char", "delayMs": 40, "from": "end" },
"keyframes": [
{ "t": 0, "alpha": 1, "scale": 1, "rotate": 0 },
{ "t": 1, "alpha": 0, "scale": 0.3, "rotate": 180 }
]
}
}
5.3 Slide in word by word from below + slow pulsing while on-screen
Word-level stagger makes words appear in order from below, and while on-screen an activePulse produces a slow flicker. On exit, the entire phrase fades out at once.
{
"in": {
"durationMs": 600,
"easing": "outCubic",
"stagger": { "unit": "word", "delayMs": 120, "from": "start" },
"keyframes": [
{ "t": 0, "alpha": 0, "dy": 0.8 },
{ "t": 1, "alpha": 1, "dy": 0 }
]
},
"out": {
"durationMs": 400,
"easing": "inQuad",
"stagger": { "unit": "none", "delayMs": 0, "from": "start" },
"keyframes": [
{ "t": 0, "alpha": 1 },
{ "t": 1, "alpha": 0 }
]
},
"activePulse": { "property": "alpha", "amplitude": 0.25, "periodMs": 1600 }
}
5.4 Glowing entrance like a neon sign
glow makes the text glow, with glowColor setting the glow's color. On exit, the glow converges to 0. Since tint is not written, the character color continues to follow the sung-color change as normal.
{
"in": {
"durationMs": 500,
"easing": "outCubic",
"stagger": { "unit": "char", "delayMs": 40, "from": "start" },
"keyframes": [
{ "t": 0, "alpha": 0, "glow": 0, "glowColor": 65535 },
{ "t": 0.6, "alpha": 1, "glow": 34, "glowColor": 65535 },
{ "t": 1, "alpha": 1, "glow": 20, "glowColor": 65535 }
]
},
"out": {
"durationMs": 400,
"easing": "inQuad",
"stagger": { "unit": "none", "delayMs": 0, "from": "start" },
"keyframes": [
{ "t": 0, "alpha": 1, "glow": 20, "glowColor": 65535 },
{ "t": 1, "alpha": 0, "glow": 0, "glowColor": 65535 }
]
}
}
5.5 Color shifts as characters appear
Animating tint interpolates the character color. A phrase where tint is written will stop doing the sung-color change, so this spec should decide the color entirely on its own.
{
"in": {
"durationMs": 700,
"easing": "outQuad",
"stagger": { "unit": "char", "delayMs": 30, "from": "center" },
"keyframes": [
{ "t": 0, "alpha": 0, "tint": 16711808 },
{ "t": 1, "alpha": 1, "tint": 16777215 }
]
},
"out": {
"durationMs": 400,
"easing": "inQuad",
"stagger": { "unit": "none", "delayMs": 0, "from": "start" },
"keyframes": [
{ "t": 0, "alpha": 1 },
{ "t": 1, "alpha": 0 }
]
}
}
5.6 Squash and stretch while landing
scaleX and scaleY are animated in opposite directions. The character is stretched tall while falling, squashes flat horizontally on impact, then springs back to its original scale.
{
"in": {
"durationMs": 550,
"easing": "outBack",
"stagger": { "unit": "char", "delayMs": 45, "from": "start" },
"keyframes": [
{ "t": 0, "alpha": 0, "dy": -1.2, "scaleX": 0.75, "scaleY": 1.35 },
{ "t": 0.7, "alpha": 1, "dy": 0, "scaleX": 1.3, "scaleY": 0.7 },
{ "t": 1, "alpha": 1, "dy": 0, "scaleX": 1, "scaleY": 1 }
]
},
"out": {
"durationMs": 350,
"easing": "inQuad",
"stagger": { "unit": "none", "delayMs": 0, "from": "start" },
"keyframes": [
{ "t": 0, "alpha": 1 },
{ "t": 1, "alpha": 0, "scaleY": 0.6 }
]
}
}
6. 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 easing names).
Copy from here
You are an animation designer for movlyric's lyrics. Strictly following the "AnimationSpec v2 Specification" pasted above, create a single AnimationSpec JSON.
Output rules:
- Output JSON only (no explanation, preamble, or closing remarks; wrapping the JSON in a code fence is fine)
- The root is {"in": ..., "out": ...}, adding "activePulse" only if needed
- keyframes must contain 2–8 items, with t in strictly ascending order within 0..1; the first must be t=0 and the last must be t=1
- The only properties allowed in a keyframe are t, alpha(0..1), scale(0.1..3), scaleX(0.1..3), scaleY(0.1..3), dx(-2..2), dy(-2..2), rotate(-360..360), tint(0..16777215), strokeWidth(0..20), strokeColor(0..16777215), glow(0..40), glowColor(0..16777215). Units: dx/dy are a ratio of font size (positive = right/down), rotate is in degrees, strokeWidth/glow are in px, colors are integers in 0xRRGGBB form
- **Don't write a property you don't want to change.** Writing tint stops the sung-color change and locks the color to the value given. If strokeWidth/strokeColor are not written, the style's stroke remains as-is (not writing it ≠ 0)
- To make something glow, use glow (a halo with no offset). Omitting glowColor makes it glow in the character's own color
- Create a squash-and-stretch motion by animating scaleX and scaleY in opposite directions
- Respect durationMs(integer 50..4000) / stagger.delayMs(integer 0..500) / activePulse.periodMs(integer 200..4000). stagger requires all three of unit("none"/"char"/"word"), delayMs, and from("start"/"end"/"center")
- easing must be exactly one of the 31 names in the specification's easing list (exact match)
- The out phase's t=0 keyframe must be the identity value (equivalent to alpha 1, scale 1, dx 0, dy 0, rotate 0)
- Do not write nonexistent properties, comments, trailing commas, or expressions
Motion I want to create:
{describe the motion you want here}
(Only if you're modifying an existing animation, paste the current JSON below. For a new creation, delete this entire section.)
Current JSON:
{current JSON}
Copy up to here