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:

  1. 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
  2. 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
  3. 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 alpha sparsely at only t=0 and t=1, while writing scale at t=0 / 0.5 / 1.
  • 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=1 state 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=0 state 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 keyframes with the same values throughout, e.g. [{"t":0,"alpha":1},{"t":1,"alpha":1}] (still respecting ascending t), 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. in starts at the beginning of the window, and out is placed by working backward so that the last unit's t=1 lands 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 large delayMs with char stagger are especially prone to compression, so it's recommended to keep durationMs + character count × delayMs around 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: 0 or 3.5, alpha: 1.2, dx: 2.5, durationMs: 30 or 5000, 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. Write 0xRRGGBB as an integer (hex literal 0xff0066 or decimal 16711782)
  • Writing 0 for a property you don't want to change: strokeWidth: 0 means "remove the stroke." If you want to keep the style's stroke, don't write it at all
  • Violating t order: descending or duplicate t values are not allowed (strictly ascending). Don't forget t=0 at the start and t=1 at 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 / periodMs are 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=0 of 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