motiscope

ground truth

We authored them. We recorded them.
motiscope measured them.

Four ordinary UI animations, written with constants we chose. Each was rendered to a video — a screen recording in every sense — and fed back through the pipeline. This page publishes the error.

Nothing is borrowed and nothing is hidden. The demos are our own code, so the authored numbers are not estimates: they are in the source. Every measurement below comes from an unedited report.md, linked per section. Where motiscope is wrong, the number is in the table and the reason is in the prose.

The scoreboard

four animations, one honest column

DemoAuthoredMeasuredVerdict
Hero entrance 700ms, cubic-bezier(.16,1,.3,1) 520ms, ease-out class right, duration 26% short
Staggered grid 120ms between cards 117ms within 2.5%
Loader loop 1200ms period 1200ms, conf 0.83 exact — given enough cycles
Scroll reveal 420px of scroll, no duration 1120ms ease-out the seconds are the recorder’s

A tool that only publishes its wins is a brochure. Two of these four are partly wrong, and both failures are the ones the docs already warned about.

1 · Hero entrance

fade + rise + scale on load · the classic

live · open
A card fades in, rises and scales up.
the recording motiscope was given
AuthoredMeasured
Duration700 ms520 ms
Easing classease-outease-out
Control points(.16, 1, .3, 1)(0, 0, .58, 1)
Structuremove, then holdmove, hold, 40ms move, hold
Why 520ms and not 700ms. A strong ease-out puts nearly all of its displacement in the first half. The tail crawls — and sub-pixel motion is invisible in the 32×32 analysis thumbnails, so the last ~180ms falls under the hold threshold. The docs say exactly this; here is the number. The class is recovered, the control points are the nearest of seven library curves, never the author’s. Also predicted, also true.
Tier 1easing class exactduration under-reads on strong ease-out

raw report.md ↗

2 · Staggered grid

six cards, each 500ms, 120ms apart

live · open
Six cards animate in one after another.
the recording motiscope was given
AuthoredMeasured
Stagger interval120 ms117 ms  (−2.5%)
Directionleft→right, then wraptop-to-bottom (conf 0.73)
Span600 ms of offsets584 ms

117 against an authored 120. The stagger interval is the number people most want and least expect to get, and it comes back within a frame at 25fps.

The direction is reported as top-to-bottom rather than left-to-right — and it isn’t wrong. The grid is 3×2, so the later cards really are both further right and further down. The energy centroid travels diagonally; motiscope names the dominant axis. Read the frames to break the tie — that’s the division of labour the whole tool is built on.

Tier 1stagger within 2.5%direction is the dominant axis

raw report.md ↗

3 · Loader loop

a ring that breathes, 1200ms per cycle

live · open
A ring scales and fades in and out, over and over.
the recording motiscope was given
AuthoredMeasured
Period1200 ms1200 ms
Confidence0.83  (threshold 0.70)
With only 3 cycles1200 msrejected — conf 0.66
Loop detection needs repetitions. On a 3.6s clip (three cycles) the autocorrelation peaks in exactly the right place but only reaches 0.66, under the 0.70 guard — so motiscope says nothing rather than guessing. Give it six cycles and it returns 1200 ms at 0.83. The guard is doing its job in both cases; a rejected loop is not a missing loop, it is an unproven one.

The loader that hides its own loop

Three dots bouncing out of phase.
three dots, phase-staggered — a real loader

Our first loader was three bouncing dots, offset by 140ms each. motiscope reported no loop, correlation 0.18.

It is right, and the reason is lovely: a good loader is designed so that something is always moving. The dots’ aggregate motion is nearly constant, and aggregate motion is precisely the signal loop detection reads. The better the loader looks, the flatter its energy curve.

The docs already say a constant-velocity spinner is undetectable for the same reason. This is the same fact wearing a different hat.

Tier 1period exactneeds ≥ ~4 cyclesflat-energy loaders undetectable

raw report.md ↗ · the dots demo

4 · Scroll reveal

opacity and offset are functions of scroll, not of time

live — scroll inside the frame · open
A card fades and rises as the page is scrolled.
a scroll capture — someone moved the wheel
AuthoredMeasured
Triggerscroll position
Reveal distance420 px of scrollnot a thing motiscope reports
Durationthere isn’t one1120 ms
Easingthere isn’t oneease-out

Both measured numbers are correct descriptions of the recording, and both are useless as descriptions of the design.

There is no duration in the source — the CSS reads opacity: clamp(0, calc(var(--s) / 420), 1). The 1120 ms is how long we took to scroll. The ease-out is an artefact of the reveal finishing at 420px while the scroll kept going. Replay those seconds and you reproduce the recording, not the website.

This is why the spec has a scroll_driven flag, and why recreate binds motion to scroll position instead of a timer. What is intrinsic to a scroll-driven scene is a ratio — see the parallax example, where six layer ratios are recovered and one is honestly reported as unrecoverable.

Tier 2you add the triggerseconds belong to the recorder

raw report.md ↗

A bug this page found

dogfooding earns its keep

The loader demo is dark-themed, like this site. blackdetect fires on any frame that is ≥98% dark pixels, so it flagged the entire clip as black — and motiscope duly reported one long fade-in spanning 0–2360 ms, losing every bit of the real motion.

before:  fade-in  0..2360ms          # the whole clip, one "transition"
after:   move     0..1080ms  hold    # the breathing ring, as authored
         move  1320..2280ms  hold
         move  2520..3560ms

A clip that is dark from beginning to end is not a fade — darkness that never resolves is the design. A black interval covering ≥ DARK_CLIP_FRACTION (0.85) of the clip is now ignored, with regression tests both ways: a real 0.4s fade in a 2.4s clip is still detected, and the same 0.5s of black is a fade in a 5s clip but the whole story in a 0.55s one.

Every dark-mode UI animation was affected. It took authoring an animation we already knew the answer to before anyone noticed.

Reproduce it

the demos are the source of truth — read the constants in the CSS

motiscope analyze hero.mp4 --preset balanced     # -> report.md + curated frames

Each demo freezes on an exact time via #t=<ms> (or #s=<px> for the scroll one), which is how the recordings were rendered frame by frame. The authored constants sit in a comment directly above the keyframes.