Verification
The fixtures
Each fixture is there for one specific behavior, and says which in a sentence. That sentence is what a failure prints.
| Fixture | What it pins |
|---|---|
| spring-settled | 2-kf spring that reaches rest in its span → Motion exports a native spring |
| spring-cut | AUDIT M2 — a spring the timeline cuts mid-overshoot must not run on to rest |
| merged-union-stops | AUDIT M1 — tracks with different keyframe times share one CSS @keyframes |
| constant-transform | AUDIT H1 — a pinned transform value must survive the animation's `both` fill |
| two-constant-transforms | AUDIT H2 — two pinned transform values must not overwrite each other |
| delayed-opacity | AUDIT H3 — a delayed track must hold kf0 from t=0, not its default value |
| multi-kf-eases | per-segment easing across a 3-keyframe track (preset then bezier) |
| spring-inside-multi | a spring segment inside a keyframe array — the old bezier stand-in was wrong here |
| independent-delays | transform and opacity animate on separate clocks with separate delays |
| rotate-and-scale | transform composition order must match the engine |
| noncommuting-rotate-scalex | THE order fixture. rotate∘scaleX ≠ scaleX∘rotate, so this fails outright if any dialect composes transforms in a different order from the engine. Uniform scale commutes with rotation, which is why the original five properties could not catch an ordering mistake.gsap exempt — GSAP composes rotation BEFORE scale; the engine and Motion compose scale before rotation. Measured here: b = 0.707 vs 1.414, exactly the two orderings. The order lives inside GSAP's CSSPlugin and is not configurable, so this is a property of the target library rather than of our output — the only way around it would be to stop using GSAP's transform handling at all. |
| skew-pair | skewX/skewY reach the matrix through b and c, which nothing else touchesgsap exempt — GSAP's fixed transform order again — it places skew after rotation and before scale, the engine and Motion place both skews last. Same cause as noncommuting-rotate-scalex. |
| scale-axes-independent | scaleX and scaleY on separate timings — a merged transform must keep both |
| rotate-3d | rotateX/rotateY flatten into the 2D matrix without perspective; the gate compares what the browser resolved, so this pins the flattening toogsap exempt — GSAP applies rotationY before rotationX; the engine and Motion apply rotateX first. Measured: GSAP leaves b = 0 throughout (Ry·Rx has no vertical shear) where the engine reaches -0.612 (Rx·Ry does). Not a tolerance question — a different composition, fixed inside CSSPlugin. |
| filter-family | the whole filter group in one string. Motion cannot give its members independent timing (it interpolates `filter` as one value), so this is the fixture that proves the merged stops still land on the engine. |
| filter-spring | a spring on a filter member — sampled into the merged stops, not native |
| transform-and-filter | both groups animating at once must not interfere — they are separate CSS properties and separate @keyframes, and a bug in the group loop would show up as one of them silently overwriting the other |
| drop-shadow-strength | dropShadow is one number driving two lengths; parsing it back must agree |
| color-hue-cross | OKLCH interpolation crossing hues — the case that proves the dialects are not silently interpolating in sRGB, where red→blue detours through grey |
| color-from-grey | An achromatic endpoint has no hue, so a fade from grey must travel out along the TARGET's hue rather than swinging onto it through a tint |
| color-spring-and-transform | A color on a spring alongside a transform — color must carry the same curve vocabulary as everything else, and the two must stay in step |
| path-arc-with-scale-and-fade | An arc into place while it scales up and fades in — the path composes BEFORE the transform, the distance is a percentage, and the element must land exactly home |
| path-loop-follow-delayed | A loop that turns to face its direction of travel, starting late — `offset-rotate: auto` has to survive every dialect, and a delayed distance must hold the path's start. Paced at 2s: Motion is sampled live, and a faster pass through the loop turns a few ms of sampling jitter into more error than the export has (1.3s still missed 1 run in 2) |
| path-orbit-spring | A spring round an orbit — Motion's native spring on a percentage (rest thresholds pinned), CSS's sampled linear() and GSAP's sampled stops, all on a distance that the path folds into a smaller displacement. A soft spring, for the live-sampling reason the loop gives: a launch fast enough to be a typical spring measured the sampler, not the export (stiffness 50 still missed 1 run in 4) |
| path-wave-keyframes | Three distances on a wave with different curves — keyframe arrays and merged stops carrying `%` values, where a bare number would be pixels along the path |
25 fixtures, each checked in all three dialects unless its row says a dialect is exempt. The list is read from the fixtures themselves, so it’s always current.