diff --git a/packages/cli/src/commands/layout-audit.browser.js b/packages/cli/src/commands/layout-audit.browser.js index 8c86391cce..c49a496044 100644 --- a/packages/cli/src/commands/layout-audit.browser.js +++ b/packages/cli/src/commands/layout-audit.browser.js @@ -1517,7 +1517,18 @@ const parts = elements.map((element) => { const rect = toRect(element.getBoundingClientRect()); const opacity = round(opacityChain(element)); - return `${rect.left},${rect.top},${rect.width},${rect.height},${opacity}`; + // Variable-font axis animation (font-variation-settings) is a real, + // visible motion channel that moves no geometry and no opacity, so a + // box+opacity fingerprint reads it as a frozen timeline. Worse in a + // DUPLEXED face (Recursive holds an identical advance width at every + // weight by design), where not even the line width shifts — the whole + // run then false-positives sweep_static. Fold the computed axis string + // in; it is "normal" for every element that does not use it, so this + // adds nothing to the fingerprint of an ordinary composition. + const axes = getComputedStyle(element).fontVariationSettings; + return `${rect.left},${rect.top},${rect.width},${rect.height},${opacity},${ + axes && axes !== "normal" ? axes : "" + }`; }); for (const media of root.querySelectorAll("canvas, video")) { if (!isVisibleElement(media)) continue; diff --git a/packages/cli/src/commands/layout-audit.browser.test.ts b/packages/cli/src/commands/layout-audit.browser.test.ts index fbc527f87b..0c1e018f40 100644 --- a/packages/cli/src/commands/layout-audit.browser.test.ts +++ b/packages/cli/src/commands/layout-audit.browser.test.ts @@ -106,6 +106,79 @@ describe("layout-audit.browser", () => { expect(revealed).not.toBe(fading); }); + // Variable-font axis animation (registry block `weight-wave`): a crest of + // weight travels along a headline by rewriting each character's + // font-variation-settings, and NOTHING else changes — no geometry, no + // opacity, no canvas. A duplexed face makes it total: Recursive holds one + // advance width at every weight by design, so not even the line width + // shifts and all six sweep samples hashed identically until the axis string + // joined the fingerprint. `check` then failed a working composition with + // sweep_static, and the documented remedies (spread the reveal, keep an + // element animating) cannot help — the motion is real, the fingerprint was + // just blind to it. + it("changes the sweep fingerprint when only font-variation-settings moves", () => { + document.body.innerHTML = ` +