Skip to content

Animation

View .mdllms.txt

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

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).

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

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.

<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)

.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.

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)
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.

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

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).

Any layer can tilt in perspective.

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.

  • 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).