Files

13 KiB
Raw Permalink Blame History

Easing, Stagger, and Function-Based Values

Easing

Built-in eases: power1, power2, power3, power4, back, bounce, circ, elastic, expo, sine, none.

Each has .in, .out, .inOut variants.

Ease Use for
power1.out, power2.out Gentle motion for secondary elements (a caption fade, a small shift). NOT the entrance default.
power3.out (house default), power4.out The standard long-tail settle. Entrances, title cards, hero reveals.
sine.inOut Long, slow, calm motion. Crossfades, ambient drift.
back.out(1.7) Overshoot then settle. RARE — explicitly-playful register only, never a default.
elastic.out(1, 0.3) Springy bounce. Same playful-only rule; prefer a baked spring (see Spring Eases below).
expo.inOut Snappy, dramatic. Quick transitions between hero scenes.
none (linear) Camera moves with timed counterpoint, mechanical motion.

Pick .out for entrances, .in for exits, .inOut for symmetric moves and continuous motion.

Smooth beats bouncy — the motion doctrine (rules/spring-pop-entrance.md, the workflows' motion-language.md): entrances default to power3.out or the baked critically-damped spring (see Spring Eases below); overshoot eases (back / elastic / bounce) are a rare, explicitly-playful register, never the house style.

Easing Vocabulary (character & mood)

Easings are tone of voice: a video that only whispers is boring; one that varies between whisper, normal, and punch is engaging. A composition should draw on ~3 easing characters across its beats — but vary within the smooth families by energy (sine / power1 calm → power3 standard → power4 / expo punch); don't reach for overshoot to add variety. Overshoot is a register (explicitly playful), not a spice. One ease everywhere reads flat; bounce everywhere reads cheap — the second failure is worse.

The full palette by character (each family has .in, .out, .inOut variants):

Family Character Typical use
power1–power4 Gentle (1) to aggressive (4) acceleration curves General purpose. power3 is the house workhorse; power2 for gentle secondary motion, power4 for dramatic snaps
back(N) Overshoot then settle. N controls how far past the target (1=subtle, 4=wild) RARE — explicitly-playful register only, never a default. Keep N ≤ 2; prefer a baked spring at ζ 0.6–0.7 (physical settle, see Spring Eases)
elastic(amp, freq) Spring bounce. amp=magnitude, freq=oscillation speed RARE — same playful-only rule; the baked spring (below) is the physical version
bounce Ball-drop bouncing RARE — physical-comedy register only (something literally dropping)
expo Extreme acceleration curve (much steeper than power4) Premium/luxury reveals, dramatic entrances
sine Smooth, organic, no hard edges Ambient float, breathing, Ken Burns, anything that loops. .inOut for yoyo motion
circ Circular acceleration (starts very fast, ends very gentle or vice versa) Camera moves, scene transitions, orbital motion
steps(N) Discrete N-step jumps, no interpolation Typing effects, cursor blink, counter ticks, retro/digital aesthetics

Mood mapping: Match easing character to the beat's emotional content. Smooth/organic easings (sine, power1) feel contemplative and drifting. Aggressive deceleration (power4.out, expo.out) feels snappy and confident. Spring overshoot (back.out) feels bouncy and physical — but bouncy is a register, not an emphasis tool; reach for it only on explicitly-playful beats. The storyboard's mood description should guide which character fits — not a formula.

Defaults

const tl = gsap.timeline({
  paused: true,
  defaults: { duration: 0.6, ease: "power3.out" }, // the house settle — smooth beats bouncy
});

Or globally:

gsap.defaults({ duration: 0.6, ease: "power3.out" });

Setting defaults at timeline scope is preferred — it documents the motion language of that composition in one place.

Spring Eases (baked physics, seek-safe)

The "iOS feel" is a damped spring's velocity curve, not a bounce: a fast launch into a long asymptotic settle. Well-made system animations are critically damped or close to it — they barely overshoot, or don't at all. power3.out / expo.out approximate that curve; when you want the exact one — or a physical overshoot for the rare playful register — bake the spring's closed-form solution into a function ease.

Why not a real-time spring library: an interactive spring is a stateful integrator (velocity accumulates frame to frame), which cannot be seeked deterministically — you'd have to simulate frames 0…N−1 to render frame N. The closed form below is a pure function of progress — no state, nothing to desync, seek-safe by construction. This is also why interaction-lib spring solvers are banned in compositions.

// springEase — a damped spring's exact position curve as a GSAP ease.
// response         ≈ seconds one oscillation would take (0.3–0.6 for entrances)
// dampingFraction  1.0       = critically damped — smooth settle, NO overshoot (house default)
//                  0.80–0.85 ≈ the iOS system register — ~1–1.5% overshoot, felt not seen
//                  0.60–0.70 = explicitly playful — ~5–10% overshoot (rare; replaces back.out)
function springEase({ response = 0.5, dampingFraction = 1 } = {}) {
  const w = (2 * Math.PI) / response; // undamped natural frequency
  const z = dampingFraction;
  let pos; // x(t): 0 → 1, starting at rest (v0 = 0)
  if (z < 1) {
    const wd = w * Math.sqrt(1 - z * z);
    pos = (t) => 1 - Math.exp(-z * w * t) * (Math.cos(wd * t) + ((z * w) / wd) * Math.sin(wd * t));
  } else if (z > 1) {
    const wo = w * Math.sqrt(z * z - 1);
    pos = (t) =>
      1 - Math.exp(-z * w * t) * (Math.cosh(wo * t) + ((z * w) / wo) * Math.sinh(wo * t));
  } else {
    pos = (t) => 1 - Math.exp(-w * t) * (1 + w * t);
  }
  // Settle time: last moment the curve sits outside ±0.1% of target.
  // Fixed-step scan, runs once at setup — deterministic (no Math.random / Date.now).
  const EPS = 0.001;
  const rate = z <= 1 ? z * w : (z - Math.sqrt(z * z - 1)) * w; // slowest decay mode
  const SCAN = 12 / rate;
  const N = 4800;
  let T = SCAN;
  for (let i = N; i >= 0; i--) {
    const t = (i / N) * SCAN;
    if (Math.abs(1 - pos(t)) > EPS) {
      T = ((i + 1) / N) * SCAN;
      break;
    }
  }
  const xT = pos(T);
  return {
    duration: T, // use as the tween's duration — the settle time IS the physics
    ease: (p) => pos(p * T) + p * (1 - xT), // normalized so ease(1) === 1 exactly
  };
}

Usage — take both the ease and the duration from the helper (the settle time is part of the physics; overriding the duration just re-times the same curve, so tune speed via response instead):

const settle = springEase({ response: 0.4 }); // critically damped → duration ≈ 0.59s
tl.fromTo(
  "#hero",
  { scale: 0, opacity: 0 },
  { scale: 1, opacity: 1, duration: settle.duration, ease: settle.ease },
  0.2,
);
dampingFraction overshoot register
1.0 (default) none (monotone) The house settle — the exact curve power3.out approximates. Product / enterprise / serious tone.
0.80–0.85 ~1–1.5% "Alive, not bouncy" — the iOS system default register. The overshoot is felt, not seen.
0.60–0.70 ~5–10% Explicitly-playful ONLY (same rule as back.out, which this replaces — a spring's second-order settle reads physical where back reads cartoon).
< 0.55 > 12% Don't. Cartoon-wobble territory.
response duration (ζ=1) feel
0.25–0.35 0.37–0.51s tight snap — chips, small UI
0.35–0.50 0.51–0.74s standard entrance
0.50–0.70 0.74–1.03s weighted hero landing — check the t ≤ 0.5s visibility rule

Craft notes:

  • ζ=1 vs power3.out: the true spring front-loads harder (~67% vs ~58% travelled at quarter-time) and settles on a longer asymptotic tail; max shape difference ~11%. That long tail is the "premium" read — use it when the settle IS the shot (a wordmark landing, a final lockup).
  • At ζ<1, overshooting curves go on transforms only — never on opacity (it would push past 1) or color. Split opacity onto its own power2.out tween at the same timeline position.
  • Doctrine unchanged: ζ below ~0.8 is still the rare, explicitly-playful exception (rules/spring-pop-entrance.md). The default of this section is ζ=1 — real spring physics is not a license for bounce.

Stagger

gsap.fromTo(".item", { y: 24, opacity: 0 }, { y: 0, opacity: 1, duration: 0.5, stagger: 0.08 });

Object form:

gsap.fromTo(
  ".item",
  { y: 24, opacity: 0 },
  {
    y: 0,
    opacity: 1,
    stagger: {
      each: 0.08, // delay between each
      from: "center", // "start" | "end" | "center" | "edges" | "random" | index
      amount: 0.6, // total stagger time (overrides each if both set)
      grid: "auto", // for 2D stagger
      axis: "x" | "y",
    },
  },
);

Prefer stagger over N separate tweens with manual delays — it stays correct when the target count or order changes. Use fromTo() rather than from() so the start state is explicit (see gsap-timeline-and-labels.md → sub-composition entrances).

Function-Based Values

Any var can be a function (index, target, targets) => value:

gsap.to(".item", {
  x: (i, target, targets) => i * 50,
  rotation: (i) => (i % 2 === 0 ? 5 : -5),
  stagger: 0.1,
});

Use this for per-element values that depend on index, attributes, or measured size. Cheaper and more idiomatic than building tweens in a loop.

gsap.matchMedia (preview only)

matchMedia runs setup only when a media query matches and auto-reverts when it stops matching. It is useful for preview in the browser at different viewport sizes, and for prefers-reduced-motion. It is not a substitute for rendering at the composition's actual data-width/data-height — HyperFrames renders at a fixed viewport.

let mm = gsap.matchMedia();
mm.add(
  {
    isDesktop: "(min-width: 800px)",
    reduceMotion: "(prefers-reduced-motion: reduce)",
  },
  (context) => {
    const { isDesktop, reduceMotion } = context.conditions;
    gsap.to(".box", {
      rotation: isDesktop ? 360 : 180,
      duration: reduceMotion ? 0 : 2,
    });
  },
);