<!-- kernel -->
> Creatomate AI reference — topic page "Animations"; the index of all pages is https://creatomate.com/llms.txt
<!-- /kernel -->

### Animations

Attach presets via the `animations` array:

```json
{ "animations": [ { "type": "fade", "time": 0, "duration": 1 } ] }
```

`time` is seconds from the element's own start (not from the video's: an element with `"time": 0.5` and an animation with `"time": 0.5` arrives at 1.0 s, so a delayed element's entrance has no `time` of its own), or `"start"`/`"end"` to stick to an edge; omitted = the animation spans the element. `easing` accepts the catalog below.

**Presets and their options.** Options are extra keys on the animation object; each preset lists its roles, then its options with type, range and default. `fade` blends opacity during the effect and is on by default except for film-roll, stripe and the oscillators. Invalid option values and misspelled option names do not error — they silently fall back to the defaults shown, and only the rendered frame tells.

- **`fade`** (entrance/exit, transition): as a transition, `enable` picks which side fades
  - `enable`: `both` \| `first-only` \| `second-only`; default `"both"` — transitions only: `first-only` fades the outgoing element out while the incoming one is already there, `second-only` fades the incoming element in over the still-visible outgoing one
- **`scale`** (entrance/exit, transition): `scope: "element"` scales the element's own drawing inside its box, after its clip; the preset's own anchors set the scale origin. Start from `"1%"` rather than `"0%"` when `fade` is off
  - `scope`: `composition` \| `element`; default `"composition"`
  - `axis`: `x` \| `y` \| `both`; default `"both"`
  - `start_scale`: number; ≥ 0; default `"0%"`
  - `end_scale`: number; ≥ 0; default `"100%"`
  - `x_anchor`: number; default `"50%"`
  - `y_anchor`: number; default `"50%"`
  - `fade`: `true` \| `false`; default `true`
- **`slide`** (entrance/exit, transition): `distance` is a share of the scope's extent along `direction` (`"100%"` = the full width or height in that direction, from off-box to place)
  - `scope`: `composition` \| `element`; default `"composition"`
  - `direction`: number; 0–360; default `"0°"`
  - `distance`: number; %; ≥ 0; default `"100%"` (a percentage of the scope's extent; px is not accepted)
  - `fixed`: `none` \| `first-only` \| `second-only`; default `"none"` — in a transition, `second-only` keeps the outgoing element still while the new one slides in
  - `fade`: `true` \| `false`; default `true`
- **`rotate-slide`** (entrance/exit, transition)
  - `direction`: `left` \| `right` \| `up` \| `down`; default `"right"`
  - `rotation`: number; default `"90°"`
  - `clockwise`: `true` \| `false`; default `true`
  - `fade`: `true` \| `false`; default `true`
- **`pan`** (scene)
  - `scope`: `composition` \| `element`; default `"composition"`
  - `start_scale`: number; default `"150%"`
  - `start_x`: number; default `"0%"`
  - `start_y`: number; default `"50%"`
  - `end_scale`: number; default `"150%"`
  - `end_x`: number; default `"100%"`
  - `end_y`: number; default `"50%"`
- **`wipe`** (entrance/exit, transition): also accepts `direction` (a bearing) as a shorthand that sets both angles and starts the line at the near end
  - `x_anchor`: number; 0–100; default `"50%"`
  - `start_angle`: number; default `"0°"`
  - `end_angle`: number; default `"0°"`
  - `clip`: `both` \| `first-only` \| `second-only`; default `"both"`
  - `fade`: `true` \| `false`; default `true`
- **`color-wipe`** (entrance/exit, transition)
  - `color`: color; default `"#e67e22"`
  - `direction`: number; 0–360; default `"0°"`
  - `hold`: number; 0–100; default `"0%"` — how long the colour covers everything between the sweep in and the sweep out
- **`circular-wipe`** (entrance/exit, transition)
  - `ring_color`: color; default `"None"`
  - `ring_width`: number; ≥ 0; default `"50%"`
  - `scope`: `composition` \| `elements`; default `"composition"`
  - `x_anchor`: number; default `"50%"`
  - `y_anchor`: number; default `"50%"`
  - `clip`: `both` \| `first-only` \| `second-only`; default `"both"`
  - `fade`: `true` \| `false`; default `true`
- **`film-roll`** (entrance/exit, transition)
  - `direction`: `left` \| `right` \| `up` \| `down`; default `"right"`
  - `repetitions`: number; ≥ 2; default `2`
  - `fade`: `true` \| `false`; default `false`
- **`squash`** (entrance/exit, transition)
  - `direction`: `left` \| `right` \| `up` \| `down`; default `"right"`
  - `amount`: number; 0–100; default `"100%"`
  - `fade`: `true` \| `false`; default `true`
- **`spin`** (entrance/exit, transition)
  - `scope`: `composition` \| `element`; default `"element"`
  - `rotation`: number; default `"180°"`
  - `x_anchor`: number; default `"50%"`
  - `y_anchor`: number; default `"50%"`
  - `fade`: `true` \| `false`; default `true`
- **`stripe`** (entrance/exit, transition)
  - `slices`: number; 1–100; default `10`
  - `overlap`: number; 0–100; default `"25%"` — the stagger: `100%` starts all bands together, `0%` runs them strictly one after another
  - `direction`: number; 0–360; default `"0°"` — the reveal crossing to the right at `0°`; the bands stand across it and fill a quarter turn clockwise from it
  - `fade`: `true` \| `false`; default `false`
- **`flip`** (entrance/exit, transition)
  - `x_rotation`: number; default `"0°"`
  - `y_rotation`: number; default `"180°"`
  - `x_anchor`: number; default `"50%"`
  - `y_anchor`: number; default `"50%"`
  - `backface_visible`: `true` \| `false`; default `false`
  - `fade`: `true` \| `false`; default `true`
- **`shake`** (scene): an oscillator: prefer `count` over `frequency`, which is not a true clock across different element lengths
  - `count`: number; ≥ 0.01; default `3`
  - `frequency`: number; Hz; ≥ 0.01; no fixed default — the rate is `count ÷ duration` unless `frequency` is given alone; when both are given, `count` wins
  - `phase`: number; default `"0°"`
  - `ramp`: `both` \| `decay`; default `"both"` — `decay` starts at full swing and settles, which makes the oscillator an arrival
  - `ramp_duration`: number; %; 0 to `count` × 50 (150 at the default count); default `"100%"`
  - `randomness`: number; 0–100; default `"0%"` — deterministic per element
  - `fade`: `true` \| `false`; default `false`
  - `direction`: number; 0–360; default `"0°"`
  - `distance`: number; ≥ 0; default `"5%"`
- **`bounce`** (scene): an oscillator: prefer `count` over `frequency`, which is not a true clock across different element lengths
  - `count`: number; ≥ 0.01; default `3`
  - `frequency`: number; Hz; ≥ 0.01; no fixed default — the rate is `count ÷ duration` unless `frequency` is given alone; when both are given, `count` wins
  - `phase`: number; default `"0°"`
  - `ramp`: `both` \| `decay`; default `"both"` — `decay` starts at full swing and settles, which makes the oscillator an arrival
  - `ramp_duration`: number; %; 0 to `count` × 50 (150 at the default count); default `"100%"`
  - `randomness`: number; 0–100; default `"0%"`
  - `fade`: `true` \| `false`; default `false`
  - `axis`: `x` \| `y` \| `both`; default `"both"`
  - `scale`: number; ≥ 0; default `"20%"`
  - `x_anchor`: number; default `"50%"`
  - `y_anchor`: number; default `"50%"`
- **`wiggle`** (scene): an oscillator: prefer `count` over `frequency`, which is not a true clock across different element lengths
  - `count`: number; ≥ 0.01; default `3`
  - `frequency`: number; Hz; ≥ 0.01; no fixed default — the rate is `count ÷ duration` unless `frequency` is given alone; when both are given, `count` wins
  - `phase`: number; default `"0°"`
  - `ramp`: `both` \| `decay`; default `"both"` — `decay` starts at full swing and settles, which makes the oscillator an arrival
  - `ramp_duration`: number; %; 0 to `count` × 50 (150 at the default count); default `"100%"`
  - `randomness`: number; 0–100; default `"0%"`
  - `fade`: `true` \| `false`; default `false`
  - `x_rotation`: number; default `"0°"`
  - `y_rotation`: number; default `"0°"`
  - `z_rotation`: number; default `"5°"`
  - `x_anchor`: number; default `"50%"`
  - `y_anchor`: number; default `"50%"`
- **`shift`** (scene): content scrolls through the element, clipped, and wraps around: a dynamic value repeats in time, not side by side
  - `direction`: `left` \| `right` \| `up` \| `down`; default `"right"`
  - `repetitions`: number; ≥ 1; default `"1"`
- **`text-appear`** (entrance/exit, text): its easing shapes the stagger, not each letter's motion; use `linear` for a plain fade
  - `split`: `line` \| `word` \| `letter`; default `"word"`
  - `overlap`: number; 0–100; default `"25%"`
  - `order`: `default` \| `reversed` \| `random`; default `"default"`
  - `fade`: `true` \| `false`; default `true`
  - `start_opacity`: number; 0–100; default `"0%"`
  - `background_effect`: `disabled` \| `animated`; default `"disabled"`
- **`text-scale`** (entrance/exit, text): still staggers per split with `scope: "element"`; a whole-block pop is `x_scale`/`y_scale` keyframes
  - `split`: `line` \| `word` \| `letter`; default `"word"`
  - `overlap`: number; 0–100; default `"25%"`
  - `order`: `default` \| `reversed` \| `random`; default `"default"`
  - `fade`: `true` \| `false`; default `true`
  - `axis`: `x` \| `y` \| `both`; default `"both"`
  - `start_scale`: number; ≥ 0; default `"0%"`
  - `scope`: `element` \| `text` \| `split`; default `"split"`
  - `x_anchor`: number; default `"50%"`
  - `y_anchor`: number; default `"50%"`
- **`text-slide`** (entrance/exit, text): moves each split by a share of its own width; `scope: "split-clip"` slides each unit inside its own clip (a rolling number)
  - `split`: `line` \| `word` \| `letter`; default `"word"`
  - `overlap`: number; 0–100; default `"25%"`
  - `order`: `default` \| `reversed` \| `random`; default `"default"`
  - `fade`: `true` \| `false`; default `true`
  - `scope`: `composition` \| `element` \| `split` \| `split-clip`; default `"composition"`
  - `direction`: number; 0–360; default `"0°"`
  - `distance`: number; default `"100%"`
  - `background_effect`: `disabled` \| `sliding` \| `scaling` \| `scaling-clip`; default `"disabled"`
- **`text-reveal`** (entrance/exit, text): a clip-box reveal along `axis`
  - `split`: `line` \| `word` \| `letter`; default `"word"`
  - `overlap`: number; 0–100; default `"25%"`
  - `order`: `default` \| `reversed` \| `random`; default `"default"`
  - `axis`: `x` \| `y` \| `both`; default `"both"`
  - `x_anchor`: number; 0–100; default `"50%"`
  - `y_anchor`: number; 0–100; default `"50%"`
- **`text-fly`** (entrance/exit, text): flies the splits in with perspective
  - `split`: `line` \| `word` \| `letter`; default `"word"`
  - `overlap`: number; 0–100; default `"25%"`
  - `order`: `default` \| `reversed` \| `random`; default `"default"`
  - `fade`: `true` \| `false`; default `true`
  - `perspective`: number; 0–100; default `"100%"`
  - `x_anchor`: number; default `"50%"`
  - `y_anchor`: number; default `"50%"`
- **`text-spin`** (entrance/exit, text)
  - `split`: `line` \| `word` \| `letter`; default `"word"`
  - `overlap`: number; 0–100; default `"25%"`
  - `order`: `default` \| `reversed` \| `random`; default `"default"`
  - `fade`: `true` \| `false`; default `true`
  - `direction`: number; 0–360; default `"0°"`
  - `distance`: number; default `"100%"`
  - `rotation`: number; -1080–1080; default `"360°"`
  - `x_anchor`: number; default `"50%"`
  - `y_anchor`: number; default `"50%"`
- **`text-wave`** (entrance/exit, text): `speed` in Hz (`frequency` still reads as the released alias)
  - `split`: `line` \| `word` \| `letter`; default `"word"`
  - `overlap`: number; 0–100; default `"25%"`
  - `order`: `default` \| `reversed` \| `random`; default `"default"`
  - `fade`: `true` \| `false`; default `true`
  - `distance`: number; default `"25%"`
  - `speed`: number; ≥ 0; default `"0.5 Hz"`
  - `wavelength`: number; ≥ 1; default `"100%"`
- **`text-counter`** (entrance/exit, text): animates the digit run in the text and rewrites every number in the value while it runs, so keep it off dynamic values whose digits must not change
  - `direction`: `up` \| `down`; default `"up"`
  - `counting_start`: number; seconds inside the animation's own window, 0 to its duration; default `0`
  - `counting_duration`: number; seconds, 0 to the animation's duration; default `0` — `0` = the whole span
- **`text-typewriter`** (entrance/exit, text): the cursor is still blinking at `snapshot_time` unless `cursor_frequency: 0`
  - `typing_start`: number; seconds inside the animation's own window, 0 to its duration; default `0.5`
  - `typing_duration`: number; seconds, 0 to the animation's duration; default `0` — `0` = the whole span
  - `cursor_frequency`: number; ≥ 0; default `"2 Hz"`

**Exits — the classic trap:** presets animate from effect to normal, i.e. they are **entrances** by default. For an exit, set `"reversed": true` (typically with `"time": "end"`): a fade-out is `{ "type": "fade", "time": "end", "duration": 1, "reversed": true }`. Without `reversed` the element sits in its effect state the whole time and animates IN at the very end. The `text-*` presets are entrances only: pair them with a generic reversed preset (e.g. reversed `fade`) for the exit.

**Transitions:** `"transition": true` on an animation of the SECOND of two consecutive same-track elements turns it into a transition between them. The overlap **subtracts** from the timeline — the video gets shorter. Presets flagged for transitions: fade, scale, slide, rotate-slide, wipe, color-wipe, circular-wipe, film-roll, squash, spin, stripe, flip.

**Angles**: `0°` points right and degrees run counterclockwise, so up is 90, left is 180, down is 270. `direction` is a bearing (0–360), `axis` the same angle unsigned (0–180), `angle` where a line sits, `rotation` how far something turns. Degrees and `left`/`right`/`up`/`down` are interchangeable.

**Easings:** `linear`; `elastic-`, `bounce-`, `sinusoid-`, `quadratic-`, `cubic-`, `quartic-`, `quintic-`, `exponential-`, `circular-`, `back-` each with `-in`, `-out`, `-in-out`; `steps(n)`; `cubic-bezier(x1,y1,x2,y2)`.

The default is `quadratic-in-out`. A `cubic-bezier` whose y values leave 0–1 is honoured (overshoot and anticipation). Caution: a misspelled easing name does NOT error — it silently falls back to `linear`. Animation type names, by contrast, fail loudly with the full valid list.

**Direct keyframes** on a property:

```json
{ "x_rotation": [ { "time": 0, "value": "0°" }, { "time": 2, "value": "360°", "easing": "cubic-in-out" } ] }
```

Easing is per segment and belongs to the keyframe being **approached** — `easing` on a keyframe shapes the movement TO it (as in the example above); an easing on the first keyframe of a pair has no effect.

See the structural exceptions in "Properties on all elements" (https://creatomate.com/llms/elements.md). A keyframe given a `name` and `dynamic: true` becomes modifiable per render — see "Modifications" (https://creatomate.com/llms/modifications.md).

### Preset behaviour worth knowing

- `scale` with `scope: "element"` scales the element's own drawing inside its box, after the element's clip (an image with `"clip": true` grows in place: the Ken Burns move); `scope: "composition"` scales the whole composition. The preset's own `x_anchor`/`y_anchor` set the scale origin independently of the element's anchors. `x_scale`/`y_scale` keyframes scale the element's box itself. Two scale animations on one element multiply. Start from `"1%"` rather than `"0%"` when `fade` is off; a zero scale blanked frames on older engines.
- `slide`: `distance` is a share of the scope's extent along `direction` (`"100%"` travels the element's full width or height in that direction, from off-box to place); in a transition, `fixed: "second-only"` keeps the outgoing element still while the new one slides in.
- **Entrance and exit presets fade by default** (`fade: true`): a `scale` or `slide` needs no `fade` beside it, and a preset that spans an element's whole life, such as the slow zoom on a photo, needs `"fade": false` or the element fades in over the entire video.
- `bounce-out` touches down at 36% of its segment and rebounds at 73% and 91%: time a reaction (a shadow, a floor kick) to those moments, not to the end.
- The oscillators' `frequency` is not a true clock across different element lengths; prefer `count`. A constant rate at any length is a linear keyframe pair far past the element's end ("Units & properties on all elements" (https://creatomate.com/llms/elements.md), keyframe rules).
- **Text presets always split the value first** (`split`, default word). `text-scale` with `scope: "element"` still staggers per split, word by word at the default; a whole-block pop is `x_scale`/`y_scale` keyframes or the plain `scale` preset. `overlap: "100%"` starts every split together. `text-appear`'s easing shapes the stagger, not each letter's motion (use `linear` for a plain fade). `text-slide` moves each split by a share of its own width. `text-counter` rewrites every digit run in the value while it runs, so keep it off dynamic values whose digits must not change. `text-typewriter`'s cursor is still blinking at `snapshot_time` unless `cursor_frequency: 0`.

### How it is used

**Motion**

- **Rise and fade in** (the entrance most designs use): keyframes on `y` and `opacity` to the settled position, `quartic-out` for a crisp arrival, `cubic-out` for a soft one.
  ```json
  { "y": [ { "time": 0, "value": "58%" }, { "time": 0.5, "value": "54%", "easing": "quartic-out" } ],
    "opacity": [ { "time": 0, "value": "0%" }, { "time": 0.5, "value": "100%", "easing": "quartic-out" } ] }
  ```
  Keyframe values are absolute positions, never offsets. Inside a flow use `{ "type": "slide", "direction": 90, "distance": "30%", "duration": 0.5, "easing": "quartic-out" }` instead.
- **Pop, stamp, settle**: `x_scale`/`y_scale` keyframes landing at `"100%"`. Pop: from 60–80 % with `back-out`. Stamp: from 130 % with `back-out` and a fade. Settle: 106 % → 100 % with `quartic-out`. The anchors are the pivot.
- **A hold before a move**: repeat the value at the moment the move starts. A value interpolates over the whole gap between two keyframes, so without a hold a one-second move spreads over the element's whole life.
- **A rule, bar or progress line drawing itself**: `x_scale` from `"0%"` to `"100%"` with `"x_anchor": "0%"` at the edge it grows from; for a ring or a scribble, `stroke_end` keyframes from `"0%"` on a shape path.
- **Constant rotation or scroll at any length**: one linear pair far past the element's end, e.g. `"z_rotation": [ { "time": 0, "value": "0°" }, { "time": 1000, "value": "360000°", "easing": "linear" } ]`. Keyframes past the end keep interpolating, so the rate holds however long the element runs.
- **A stagger**: the same keyframes on each element, shifted by 0.1–0.2 s per element. For letters and words use the split text presets instead.
- **A drop with a bounce**: `y` keyframes with `bounce-out`. It touches down at 36 % of the segment and rebounds at 73 % and 91 %, so time a shadow or a floor reaction to those moments.
- **Flip, fold, flap**: real 3D is `y_rotation` or `x_rotation` with `perspective` and `backface_visible`; the flat version is `y_scale` keyframes with `y_anchor` at the hinge. Two-faced cards are two compositions, one per face.
- **An exit**: a reversed preset at the end, `{ "type": "fade", "time": "end", "duration": 0.5, "reversed": true }`.

**Text effects**

- **Words landing one by one**: `{ "type": "text-slide", "split": "word", "direction": "up", "duration": 1 }`. Letters typing: `text-typewriter`. A phrase wiping on: `text-reveal` along the x axis. Each text preset splits the value first, so a whole-block pop is `x_scale`/`y_scale` keyframes, not `text-scale`.
- **A number counting up** (static numbers only): `text-counter`. It rewrites every digit run in the value while it runs, so keep it off dynamic values whose digits must not change.
- **Captions that follow a voice**: `transcript_source` naming the audio element plus `transcript_effect` (`highlight` draws a chip behind the spoken word, `karaoke` fills progressively, `color` tints the current word); `transcript_maximum_length` sets how much text one page shows (`1` = one word at a time). No keyframes and no second animation on top: "Subtitles" (https://creatomate.com/llms/subtitles.md).
