Skip to content

fix(check): stop false sweep_static and text-overlap alarms on compositions that are fine - #5147

Merged
miguel-heygen merged 5 commits into
mainfrom
fix/check-sweep-static-sees-paint-and-media
Oct 7, 2026
Merged

miguel-heygen merged 5 commits into
mainfrom
fix/check-sweep-static-sees-paint-and-media

Conversation

@miguel-heygen

@miguel-heygen miguel-heygen commented Oct 7, 2026 •

Copy link
Copy Markdown
Collaborator

What

hyperframes check stops raising two false alarms on compositions that are fine:

  1. "Timeline did not advance under seek" (sweep_static) on compositions that do animate, and it tells authors of real stills how to say so.
  2. "Two text blocks overlap" (content_overlap) on words that wrap at a tight line-height, where no glyphs touch.

sweep_static

The guard compares a fingerprint of everything visible at each seek sample. That fingerprint saw only geometry, opacity, font axes, clip-path, text, controls, generated content and media pixels. So a composition whose only motion was one of these failed check:

  • a filter (blur) tween
  • a text color tween
  • a color-graded video (the runtime draws it into a WebGL canvas that reads back blank, so the pixel probe sees a constant)
  • an audio-only timeline (<audio> has no box)

The shared motion classifier (motion-signature.browser.js) now also signs:

  • Box paint: filter, backdrop-filter, background-color, background-image, background-position, box-shadow, plus each border side's and the outline's color where that side or outline is actually drawn. Blink computes those colors as currentColor even when nothing is drawn, so signing them unconditionally would count every text color change twice, including on content the browser skips.
  • Content paint: color, text-shadow, and SVG fill and stroke color.
  • Video time: the currentTime of every <video> under the composition root, bucketed to 0.1 s for liveness. A playing video is a moving picture even when its pixels cannot be read. Media on a data-layout-ignore layer is skipped, like every other channel.

Both sweep_static and keepsMoving read this classifier, so they keep counting the same things as motion.

Audio is handled apart, and this is a deliberate trade. Audio time shows the timeline ran, but it is not a picture. Counting it as motion would let a broken animation (tweens on a timeline the runtime never registered) pass whenever a soundtrack plays, and would hide a frozen picture from keepsMoving. So:

  • keepsMoving never counts audio time.
  • When audio time is the only thing that advanced, sweep_static is a warning, not a failure: "Only the audio advanced under seek; nothing on screen moved." An audio-only composition no longer fails check. A broken animation with music is still called out, as a warning instead of an error, and --strict still fails it.

A composition that really is still (a title with an empty timeline, or the blank template before anything is animated) still fails, because nothing changes. The fix hint for both tiers now says what to do: "If the composition is meant to be still, add data-no-timeline to the element with data-composition-id."

SVG stroke drawing (stroke-dasharray / stroke-dashoffset) is left to #3914, which covers it in the same classifier.

content_overlap

The check measured overlap on each line's text rect, which is the font's whole content area (ascent plus descent). At line-height below about 0.95, the content areas of stacked lines overlap while the letters do not. Inline-block words wrapping in a headline at line-height: 0.8 therefore failed. The flex/grid exemption did not cover them, since this is normal inline flow.

For a pair whose content areas overlap by more than a fifth, the check now measures again on the glyph extent (the font's actual ascent and descent, from the canvas measureText the clip check already uses) and flags only when the glyph boxes still overlap by more than a fifth of the smaller glyph area. text-transform is applied before measuring (uppercase, lowercase, and now capitalize, which uppercases the first character of each word the way Chromium draws it, so -ace is measured as -Ace while 3go and _go stay as written), so title-case words are measured at cap height. The glyph-extent math is shared with the clip check (inkRect), and pairs that do not overlap are never measured.

A trade to know about. An opaque box covering another block's words, with no letters touching, no longer counts toward this finding at that sample. On the vignelli example one caption box sits over the tail of a label for part of its hold. The overlap is still reported, but it is held for less time, so it drops from error to warning. Text covered by a box is what text_occluded reports, not content_overlap.

Proof

hyperframes check from this branch's build, on the reported repro fixtures:

Fixture main this PR
title with a blur tween failed passed
title with a color tween failed passed
color-graded video failed passed
audio-only timeline failed passed, with the sweep_static warning and the data-no-timeline hint
ungraded video passed passed
transform/opacity motion passed passed
still title with data-no-timeline passed passed
still title, no script passed passed
still title with an empty timeline failed failed, hint names data-no-timeline
blank template (0.8.139) failed failed, hint names data-no-timeline
headline of inline-block words at line-height: 0.8 failed (content_overlap x2) passed

Checks

  • New real-Chromium motion cases:
    • blur, text color, background color and box-shadow changes on a title that never moves
    • an SVG fill color change
    • a drawn border and a drawn outline changing color
    • the time of a display:none video, in both samplers
    • audio time counts for the sweep but not for liveness
    • a color change on a content-visibility: hidden host does not count, with no border or with one border side drawn in its own color
    • media time on a data-layout-ignore layer is ignored
  • New real-Chromium overlap cases: words at line-height: 0.8 (no finding); the same words at 0.4, where the glyphs collide; two headings 60 px apart whose letters collide; capitalized words 60 px apart whose capitals collide, including words led by punctuation, and digit-led words that stay lowercase and collide on their descenders; two headings on the same spot (each a finding).
  • check cases: an audio-only advance is one warning with the hint and the report stays ok; any on-screen change gives no finding; the existing sweep_static error names data-no-timeline.
  • Every case that tests a new detection fails against main's sources. The cases that guard against new false alarms or misses (skipped hosts, the colliding headings, which main already catches) pass on main. Each new case is killed by a mutation of the line it covers, including the text-color case, which a border-color signature would otherwise have kept green.
  • The 7 touched or related test files: 343/343, 3 runs in a row. CLI typecheck, lint, comment checks, fallow and the skills manifest check pass.
  • Docs (docs/packages/cli.mdx) and the CLI skill's check reference describe the new motion channels, the audio warning and data-no-timeline.

@github-actions

github-actions Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Edit accuracy: accurate 2059 (base branch 2059), smooth 1569 of those

The gate passes.
Smoothness is reported in the artifact, not gated. A case fails only if it fails 2 of 3 runs.

Quarantined, measured but not gated (0)

Unstable (1)

  • crop-tween-px-r30-nested-z200: tracking 0.02, pressJump 0, drop 40.02, reload 40.02, render 34.78, renderKey -, undo true, teleport true / tracking 0.02, pressJump 0, drop 0.02, reload 0.05, render 0.3, renderKey -, undo true, teleport true / tracking 0.02, pressJump 0, drop 0.02, reload 0.05, render 0.3, renderKey -, undo true, teleport true

@mintlify

mintlify Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated
hyperframes 🟢 Ready View Preview Oct 7, 2026, 4:40 AM

💡 Tip: Enable Automations to automatically generate PRs for you.

@miguel-heygen miguel-heygen changed the title fix(check): see color, filter and media time under seek, and name data-no-timeline for stills fix(check): stop false sweep_static and text-overlap alarms on compositions that are fine Oct 7, 2026
@miguel-heygen
miguel-heygen marked this pull request as ready for review October 7, 2026 04:56

@jrusso1020 jrusso1020 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving at 78ee279a.

Motion signature. The new paint channels sit in the shared classifier, so sweep_static and keepsMoving keep counting the same things, as the header comment requires. Border and outline colors are signed only where a side is actually drawn. That gate matters: if I sign them unconditionally (a mutation), two tests fail, because Blink's currentColor would count every text color change twice. Video time goes through the same isOptedOut skip as every other channel. Audio is kept after \u001f and only reaches detectSweepStatic, through seenPart. keepsMoving never sees it. __hyperframesLayoutGeometry has one consumer (checkBrowser.collectLayoutGeometry → layoutStateSignatures), so the appended audio part can't leak into another comparison.

Glyph overlap. The re-measure runs only for pairs whose content areas already overlap by a fifth, and it shares inkRect with the clip check.

Ran.

  • Real Chromium 153: motion-signature.browser.chromium, layout-audit.chromium and check.test.ts all pass, 131/131.
  • Mutations killed 6 of 7. Each of these fails a test:
    • removing contentPaintChannel
    • dropping the glyph re-check
    • signing border/outline colors unconditionally
    • removing the isOptedOut skip for media
    • dropping the audio-only warning
    • not applying capitalize
  • The one survivor: ignoring the 0.1 s liveness bucket for video time. It's harmless as written.

Should-fix, non-blocking. A line clipped at its top by an overflow:hidden ancestor loses a real collision that main reports. glyphRects runs inkRect on block.rects, but those come from visibleTextClientRects, which has already cut each rect to its overflow-clipping ancestors. inkRect derives its scale from rect.height. A top-clipped rect therefore gets a smaller scale, and the glyph box is placed lower than the letters really are. I ran a probe with HELLO at top 100 and WORLD at top 160, both 120px Arial, where WORLD sits in an overflow:hidden box spanning 180–300. The visible glyphs collide at 180–203 against heights of about 86 and 83, roughly 27%. Main reports content_overlap. This branch reports nothing, because its computed glyph box starts at about 194. Cases clipped at the bottom, or not clipped, agree with main. This shape is common for a line held part-way through a mask reveal. Fix: compute the glyph box from the unclipped Range rect, then intersect it with the clips. visibleTextClientRects could take an optional per-rect map applied before clipping.

CI. The red "Studio: timeline viewport gate" is skipped on main pushes, and its log isn't available until the run ends, so I couldn't confirm the #5109 overscan cause from the log. This PR touches only CLI and docs, so I'm not treating it as this PR's.

— Rames

@miguel-heygen
miguel-heygen added this pull request to the merge queue Oct 7, 2026
Merged via the queue into main with commit 2d98d5c Oct 7, 2026
156 of 158 checks passed
@miguel-heygen
miguel-heygen deleted the fix/check-sweep-static-sees-paint-and-media branch October 7, 2026 05:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants