Animation capture

How MotionPR finds animations in the page, decides when motion has settled, and replays it deterministically.

On this page
  1. Discover
  2. The window
  3. Settle
  4. Replay
  5. Restore
  6. What a capture cannot see

A capture turns what moved in the page into data. It happens in the browser, inside the page under test, in four phases per checkpoint: discover, settle, replay, restore.

Interactive timeline: needs JavaScript.

Discover

Before any of your scripts run, MotionPR injects its agent (window.__MOTIONPR_AGENT__). It finds native animations by polling document.getAnimations() every frame and by wrapping Element.prototype.animate, so short-lived animations are seen even if they finish between two frames.

Found nativelyRecord typeEngine label
CSS transitionscss-transitionNative CSS
CSS animations (@keyframes)css-animationNative CSS
Web Animations (element.animate)web-animationWAAPI
CSS scroll-driven animationscss-animationmetadata only (their progress follows scroll)

JavaScript engines that write inline styles every frame are invisible to the browser's animation list. They report through adapters in your page: GSAP, Anime.js, and Lenis for scrolling.

The window

Each capture step closes a window that opened at the previous checkpoint (or at page load). Its origin, t = 0, is the first motion after the first interaction in it: the earliest animation start, or the first frame of scroll movement. The time from the input event to that first motion is recorded as latency (the tutorial's menu: 11.2 ms).

  • Animations that started before the interaction (a page-load spinner) belong to the previous state. They are excluded, and the checkpoint gets a PRE_TRIGGER_ANIMATIONS note saying how many.
  • A window longer than capture.maxWindow (5000 ms) is cut, with a WINDOW_CAPPED warning.
  • Infinite animations never block settling. One iteration is sampled and an INFINITE_ANIMATIONS note is added.

Settle

A checkpoint waits until no finite animation is running, no smooth scroll is advancing, and nothing new started for capture.settleTime (120 ms), up to capture.waitForAnimationsTimeout (10 s). The quiet period catches animations that begin a frame or two after an interaction. If motion never settles, the capture continues with an ANIMATIONS_NOT_SETTLED warning naming what was still running.

Replay

Then MotionPR renders the whole window again, at times it chooses:

  • Native animations are cloned (target.animate(keyframes, timing)) and the paused clones are seeked. The originals never dispatch transitionend, animationend or finish again.
  • Adapters render their engines at exact times with callbacks muted: GSAP with totalTime(t, true), Anime.js with seek(t, true).
  • Everything else is frozen, so each frame shows only the state at that time.

At capture.samples (24) evenly spaced times per animation, MotionPR reads computed styles, the decomposed transform and the element's box. At capture.frames.fps (30) it renders a filmstrip frame, up to capture.frames.maxFrames (90) per checkpoint. Identical frames are stored once. Because nothing is recorded in real time, the same build produces the same frames on every run.

Restore

Every animation, engine and timeline gets back exactly the state it had, and capture moves on to the next step. Your app keeps working: a menu timeline that was replayed still opens and closes on the next click.

What a capture cannot see

  • Motion drawn by your own requestAnimationFrame loop with inline styles, on <canvas> or in WebGL, unless you write an adapter.
  • Values written by scroll-linked tweens (GSAP ScrollTrigger). Their progress is recorded live and compared against scroll position; the values they write are not sampled.
  • Scroll itself is never replayed; it is recorded live. See Lenis.

The full list is in Limitations. The internals are described in Architecture.