# Animation

> Keyframes with LayerAnimation, easing, composing animations, procedural drivers, kinetic typography and 3D transforms on 2D layers.

Animations are data. Build one with `LayerAnimation`, attach it with `.animate()`. All frames are **local to the layer** and **integers**.

## Two keyframes: `fromTo`

```ts
import { LayerAnimation } from "framefields";

const entrance = LayerAnimation.create()
  .fadeIn(0, 20, "power2.out")                                    // opacity 0 → 1, frames 0–20
  .fromTo("y", 460, 400, { start: 0, end: 24, ease: "expo.out" })  // to the layout y, not 0
  .fromTo("scale", 0.95, 1, { start: 0, end: 30, ease: "back.out(1.4)" });

card.animate(entrance);
```

Shortcuts: `fadeIn(start, end, ease)`, `fadeOut(start, end, ease)`, `slideInX/slideInY(from, to, start, end, ease)`, `rotateX/rotateY/rotateZ`, `translateZ`, `perspective`, `letterSpacing`, `typewriter(start, end)`, `kineticSweep(fromOffset, toOffset, start, end, ease)`.

## Many keyframes: `keys`

Each tuple is `[frame, value, ease?]`; the ease shapes motion **into** that key. Frames are rounded, so beat math can go straight in.

```ts
const pop = LayerAnimation.create()
  .keys("scale", [[0, 0], [10, 1.12, "back.out(1.6)"], [16, 1, "power2.out"]])
  .keys("opacity", [[0, 0], [6, 1]]);

LayerAnimation.keys("rotation", [[0, -8], [20, 0, "expo.out"]]); // static form
```

Color props (`fill`, `color`, `borderColor`, …) keyframe too, blended in OKLab.

## Easing

`<name>.<dir>` with optional parameters: `"expo.out"`, `"back.out(1.7)"`, `"power2.inOut"`.

| Names | `none` `power1` `power2` `power3` `sine` `circ` `expo` `back` `elastic` `bounce` `spring` `cubic` `hold` |
| --- | --- |
| Directions | `in` `out` `inOut` (`"linear"` is an alias for `none`) |

| Use | Ease |
| --- | --- |
| Snap-overshoot entrances | `back.out(1.4–1.7)` |
| Decelerating moves | `expo.out`, `power3.out`, `spring` |
| Exits | `power2.in` |
| Loops, counters, spinners | `linear` (and nothing else) |

## Composing animations

`.animate()` **adds** to what a node has. Tracks on the same prop merge into one sorted track; a later key wins on a shared frame.

```ts
const rainIn = LayerAnimation.keys("y", [[0, -300], [d, -300], [d + 12, 0, "back.out(1.5)"]]);
const press  = LayerAnimation.keys("y", [[40, 0], [42, 6, "power2.in"], [47, 0, "power2.out"]]);
const glow   = LayerAnimation.keys("color", [[42, "#ffd400"], [60, "#2a2b2d", "sine.inOut"]]);

key.animate(rainIn).animate(press, glow);      // or LayerAnimation.merge(rainIn, press, glow)
```

## Procedural drivers

```ts
LayerAnimation.create()
  .wiggle("x", { frequency: 1.5, amplitude: 25, seed: 3 })  // seeded noise, deterministic
  .spring("scale", { damping: 14, stiffness: 190, mass: 1 });
```

Bind any prop to a signal with `.signal(prop, signal, { multiplier, offset, smoothing })` — see [Audio & signals](/docs/guides/audio-and-signals/).

## Kinetic typography

Text animators work per glyph, word or line on the GPU. Add one to the text, then sweep its selection with `kineticSweep`.

```ts
import { Layer, LayerAnimation, TextAnimator } from "framefields";

Layer.text("TypeScript in.", {
  fontFamily: "Archivo", fontWeight: 800, fontSize: 200, fill: "#e6e8e6",
  animators: [TextAnimator.waveRise({ y: 90, rotationX: 70 })],
}).animate(LayerAnimation.create().kineticSweep(-1, 1, 0, 40));
```

| Preset | Effect |
| --- | --- |
| `TextAnimator.waveRise({ y, rotationX, opacity, blur, unit })` | glyphs rise and untilt in a wave |
| `TextAnimator.wordPop({ scale, y })` | words pop in |
| `TextAnimator.blurIn(...)` | glyphs resolve from blur |
| `TextAnimator.flip3D(...)` | glyphs flip in around X |
| `TextAnimator.colorSweep(...)` | color passes through the text |

`unit` is `"character"` (default), `"word"` or `"line"`. For terminal-style typing use `.typewriter(start, end)`.

## 3D transforms on 2D layers

Any layer can tilt in perspective.

```ts
Layer.box({
  width: 600, height: 400, background: "#181715", borderRadius: 24,
  transformStyle: "preserve-3d", perspective: 1200,
}).animate(
  LayerAnimation.create()
    .fromTo("rotateX", 25, 0, { start: 0, end: 30, ease: "expo.out" })
    .fromTo("rotateY", -35, 0, { start: 0, end: 30, ease: "expo.out" })
    .fromTo("translateZ", -200, 0, { start: 0, end: 30, ease: "expo.out" }),
);
```

For cameras, lights and meshes, see [3D](/docs/guides/3d/).

## Gotchas

- Keyframes past `durationFrames` never play (`animation_truncated`).
- Animating `x`/`y` overrides the layout position.
- A track whose keys all hold one value does nothing (`static_track`).
