Video Studio
GitHub
Reference motion-studio

Motion

Every animation answers one question: what does it communicate? Hierarchy, story, feedback or a state change. If you cannot say it in a sentence, cut it.

Easing and springs

  • Closed-form spring for entrances: damping ratio ζ 0.8 to 0.9, ω about 9 (0.5 to 0.7 s). At most a tiny overshoot. No bouncy easing.
  • GSAP names that match: expo.out for entrances (0.5 s), power2.in for exits (0.26 s), power3.inOut for rules and wipes.
  • Enter moving, leave accelerating. Exits are faster than entrances.
  • Stagger 40 to 80 ms between items, or put each item on its spoken word.
function spring(z, w) { // normalised so it ends exactly at 1
  const wd = w * Math.sqrt(1 - z * z);
  const raw = (t) => 1 - Math.exp(-z * w * t) * (Math.cos(wd * t) + ((z * w) / wd) * Math.sin(wd * t));
  const end = raw(1);
  return (t) => (t <= 0 ? 0 : t >= 1 ? 1 : raw(t) / end);
}
const tw = (t, a, b, ease) => ease(Math.min(1, Math.max(0, (t - a) / (b - a))));

Kinetic type

  • Word enter: rise 0.42 em out of an 8 px blur, opacity 0 → 1, 0.5 s expo.out, starting 60 ms before the spoken word.
  • Caption exit (content swap): opacity 0, y −14 px, blur 6 px, 0.26 s power2.in; the next caption starts right after.
  • A reference line under a number fades up 0.25 s after the last word.
  • Hook lines stack (each phrase on its own line); the last word of the hook can carry the accent.
  • Never put will-change on text the camera scales (blurry text).

Camera (for screen content)

  • Keys with holds: a list of {t, scale, focusX, focusY, ease}. Between two moves there is always a hold. The camera only moves when a spoken word motivates it; never idle drift.
  • Screen-recorder zoom: zoom the content inside a fixed window frame (frame stays, content scales), focus point in recorded CSS pixels, clamp so no edge shows.
  • Interpolate zoom in log space (exp(lerp(log a, log b, u))) so the speed feels constant.
  • One named move per shot: push in to the composer, pull back to the conversation. Never zoom in and straight back out.
  • Typical scales: 1.0 whole screen, 1.4 to 1.6 a panel or table, 1.8 to 2.1 a button or field.
  • Keys must stay in time order; when hold times derive from word timings, clamp with Math.max(previous).
  • Update the camera at the start of each frame: overlays (spotlight, cursor, labels) are projected from it.

Spotlight

  • Dim everything but the region (box-shadow 0 0 0 3000px rgba(dark, 0.42)), 3 px accent outline, radius 12, padding 8 px.
  • Rects in recorded CSS px, mapped through the camera every frame.
  • Liquid move: when the spotlight jumps from one row to the next, the edge in the direction of travel leads (0.30 s) and the trailing edge follows (0.42 s, starting 0.1 s later), so the highlight stretches and settles.
  • Fade in 0.3 s; when a spot hands over to the next one directly, glide instead of blinking.

Cursor

  • SVG arrow with a soft drop shadow, constant screen size.
  • Appears 1.15 s before the click, glides in 0.9 s with inOut, tilts up to 12° toward its travel direction and straightens on arrival.
  • Press: scale −15 % for about 80 ms at the click; an accent ring expands from 0.3× to 1.3× and fades over 0.5 s.
  • Leaves 0.3 to 0.55 s after the click. Keep it inside the frame at every zoom and off the key text of the final state.
  • Every UI change after a click happens 80 to 150 ms after the press.

Transitions with meaning

TransitionUse it forRecipe
Flood and contractA change of world (problem → solution, product → end card)A circle clip grows from the last element (a full stop, a document) past the corners in 0.35 to 0.5 s; the next scene is underneath
Shared elementContinuity: the thing named becomes the next sceneA file card scales into the full document; a chain lifts away while the window rises in its place
Rise and settleBringing in the main surfacetranslateY 160 px → 0 and scale 0.94 → 1 on a spring
Content swap with blurCaption or label changesexit blur 6 px in 0.26 s, enter from 8 px blur
Hard cut on the beatContrast, energyOnly on a downbeat, hidden in a motion peak; max three per film
Velocity handoffMomentum across a cut (pans, pushes, whips)Exit accelerates to 25 to 40 px/frame at 60 fps, entry starts 30 to 55 and decays about 0.93 per frame (transitions.md)
Hidden cutA cut that should read as one takeOccluder sweeps across and the cut hides inside it; light-leak burn; line carry (transitions.md)

Banned: cross-dissolves between unrelated shots, wipes and star transitions, zooms that go in and straight out. More recipes (velocity-matched cut, occluder, light-leak, line carry, "X becomes Y" morph vocabulary): transitions.md.

Pacing

  • Hold important frames: a number or the CTA needs 1.5 to 2 s of stillness.
  • Alternate long and short beats so it breathes; cut on the beat grid of the music.
  • Reserve showpieces (a big reveal, a flood) for two or three moments.

Motion blur

Render 4 subframes per output frame centred on the frame time and average them (8 for fast moves with a shutter of about 0.7 of a frame, see gotchas.md): ffmpeg ... -vf "tmix=frames=4,select='not(mod(n+1\,4))',setpts=N/(60*TB)" with the page seeked at t + (j − 1.5)/(fps·4).

Gotchas

  • Never set opacity or filter on a preserve-3d element; fade a wrapper instead.
  • Text inside a morphing container needs its own enter and exit timing or old and new text overlap.
  • A seek-based renderer must not depend on state carried between frames. Compute everything from t.

Measure motion, do not guess

When a move "pauses then snaps", stop retrying eased variants. Sample the timing function (a pure function of frame) and plot value and velocity over the move: inspect.mjs plot (see skills/motion-review), or in a scratch script. Look for a zero-velocity run in the middle of a move, kinks, and false overshoot; then fix the cause and plot again. Why: in one production write-up five plausible fixes failed over about an hour; a velocity plot showed zero velocity for 16 percent of the move in 30 seconds. The cause was a bob wave covering only the rise phase, not the element's whole lifetime. Evidence: one detailed first-hand article with numbers (F-A3, F-B5).

Buoyant bob. A rising element keeps drifting through its peak instead of freezing: a cubic ease plus a small sine bob whose span is the whole lifetime of the element, not just the rise. Helper: see studio/src/lib/motion.ts (buoyant()). Use for floating cards and badges, not for UI that should feel mechanical.

Camera speed as a table

For any glide, write the camera language as speed shares of cruise at timestamps and integrate to position, instead of position keys:

t (s)Share of cruise
00
0.60.5
1.61.0
1.6 until the ease-outheld 0.94 to 1.1
8.30.12 (easing out)

(Example from a 3D fly-over prompt; your numbers differ.) Interpolate with smoothstep and integrate analytically (see learnings: speed-curve camera). Zoom runs in log space; rise and dive are separate eased passes. Each scene starts at the zoom the last one ended on. Check: inspect.mjs flow on a 30 fps draft. Speed must never dip and zoom must not turn into zoom-in during a flight; a surge or a stop that stills cannot show shows up as a dip in the flow plot. Why: the table makes the brief's camera language reviewable and flow measurement catches "no surges, no stops". Evidence: precise in the prompt, film posted; flow-test results not shown (F-B4). Integrated speed curves are already in learnings; the new part is the table and the test.

Rhythm and type-sync recipes

Why: cheap, deterministic, look authored rather than templated. Evidence: documented with demos; parameters tuned on placeholder material (F-A16).

  • Karaoke fill (voice-led films, type hooks): each word fills left to right with a soft edge at speaking rate; current word in accent, finished word in ink; a pointer underline on the active word; word times from the voice; 4 to 10 frames of gap for breath. Studio fit: yes.
  • Weight pump (type hooks): a variable-font weight envelope on the beat (about 250 to 900); glyphs set in slots laid out at the heaviest weight so the line never reflows; beats must be spaced wider than the decay window. Studio fit: yes. Remember learnings: set font-weight on the glyphs too, not just the measurement.
  • Frame-rate shift (optional spice): a mover steps every 3 frames (about 69 px per step, a different pose each step), then switches in one frame to every-frame motion with hard overshoot; the change of cadence is the highlight. Under 30 px per step it reads as dropped frames, and any tween on the switch kills it.
  • Trailer bumper (optional spice): three framings of 9 frames each, zero transitions, each pushing in 1 to 4 percent so it is not a slideshow, then 6 frames of black, then the title.

Transition recipes (velocity-matched cut, occluder, impact slam, light-leak, line carry, "X becomes Y") are in transitions.md. Failure-derived checks (8 subframes for fast moves, minimum fade length) are in gotchas.md.