Skip to content

fix(core): keep overflowing content and SVG clip paths in motion-blurred frames - #7080

Merged
jrusso1020 merged 5 commits into
mainfrom
fix/motion-blur-overflow-and-clip-paths
Nov 3, 2026
Merged

jrusso1020 merged 5 commits into
mainfrom
fix/motion-blur-overflow-and-clip-paths

Conversation

@jrusso1020

Copy link
Copy Markdown
Collaborator

What

Two fixes to page motion blur (captureMotionBlur), both of which made elements flicker during playback and in full renders:

  1. A blurred element is now copied with everything it paints, not just its layout box.
  2. SVG resources (defs, clipPath, mask, pattern, marker, symbol) stay visible while the moving elements are captured alone.

Why

With data-motion-blur="on", an endcard alternated between the full "HyperFrames Studio" lockup and a cut "HyperFra" with no logo. The composition's state was correct at every frame; only the blurred frames were drawn wrong.

  • Cut text. readMoverFrames sized each copy as offsetWidth/offsetHeight plus the glow pad. #brand is 540px wide but its one-line wordmark is 1528px, so blurred frames kept only "HyperFra". Sharp frames were fine, hence the alternation. The filter fallback crops to the same box, so it is affected too.
  • Vanishing logo. To capture the moving elements alone, the isolation rule hides every other element, including the <clipPath> a logo uses through clip-path: url(#...). A hidden clip path clips everything, so the logo captured as nothing.

On v0.9.7, a full render of that endcard had the wordmark cut in 43 of 91 frames and the logo missing in 31 of 44. With --page-motion-blur=off both were 0.

Related work

Refs #7060 (a different motion-blur fix, for WebGL canvases).

How

  • frames.ts: contentBox() grows the mover's box to every descendant's client rect, mapped back into the mover's own px through the inverse of its pose. Elements that clip on both axes are left alone, and at most 256 descendants are read per mover. The frame now carries an origin, so the box can start before the element's own origin.
  • filterPlan.ts: the filter region is offset by that origin.
  • index.ts: the hide rule excludes SVG resources and their contents, and a second rule sets them visible, so they do not inherit hidden from their <svg>.

Test plan

  • Unit tests added/updated
    • frames.test.ts: content wider than its box is included (fails on v0.9.7: expected 540 ≥ 1528); content that stays inside keeps the box.
    • index.test.ts: the isolation rules leave clip paths and their shapes unhidden (fails on v0.9.7).
    • All 192 motion-blur tests pass; typecheck, oxlint and fallow are clean.
  • Manual testing performed on the affected project in headless Chrome with html-in-canvas:
    • Playback, capture tier: 0 cut frames, and the logo is present in blurred frames.
    • Filter tier forced: 0 of 86 bad frames (65 of 107 before).
    • Full render with blur on: 0 bad frames for both the wordmark and the logo.
  • Documentation updated (if applicable)
  • Comments follow CONTRIBUTING.md "Comments"

🤖 Generated with Claude Code

jrusso1020 and others added 5 commits November 3, 2026 09:25
…red frames

A blurred mover was copied only within its own layout box, so text wider
than the box (a one-line wordmark in a fixed-width container) was cut in
blurred frames. The movers-alone capture also hid every non-mover, including
the SVG clipPath a mover references, which empties the clip and makes the
mover vanish. Both showed as flicker during playback and in full renders.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… the mover

A block child is as wide as its mover however long its no-wrap text, so
reading child boxes missed the common overflow layout. Read the text's own
rects through a Range. On a rotated mover each child's upright client rect
covers the mover's corners, which doubled the box at 45 degrees and could
pull a neighbouring mover into the trail; only rects that leave the
mover's own client rect now grow it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The upright client rect of a rotated mover has empty corners, so overflow
that landed there passed the "stays inside" check and was still cropped.
Measure a rotated mover with its own transform off and map the rects back
through its layout pose, then restore its style.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
With only transform: none, a mover with no position stops being the
containing block of its absolute and fixed children, so they jump to an
outer ancestor while measured and their real overflow is cropped. Add
will-change: transform during the swap, and skip the swap when the mover
has no transform of its own.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

Edit accuracy: accurate 267 (base branch 267), smooth 119 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 (8)

Unstable (1)

  • seqplainkeys-none-px-r0-root-z100: tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo true, teleport true / tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo true, teleport true / tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo true, teleport true

@heygengenesis heygengenesis Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Verdict: APPROVE · 0 blocker · 0 important · 0 nits · f755566

I reproduced the containing-block case. The swap now adds will-change: transform, so an absolute child still resolves against the mover while it is measured. A mover that is turned only by an ancestor skips the swap and is measured with the upright box mapped back, which covers more than it needs to but never cuts anything off. Both new tests fail on 88b2de1 and pass here. All 197 motion-blur tests pass locally, and every workflow is green at this head.

Scope

I read all of frames.ts, filterPlan.ts and index.ts (the SVG-resource isolation rules), and the filterTier.ts/composite.ts wiring. rectsAtStyle has the same pre-existing exposure; the author has explicitly left it out of scope, and it is fine as a follow-up.

— Genesis

@jrusso1020
jrusso1020 added this pull request to the merge queue Nov 3, 2026
Merged via the queue into main with commit d89bf35 Nov 3, 2026
97 checks passed
@jrusso1020
jrusso1020 deleted the fix/motion-blur-overflow-and-clip-paths branch November 3, 2026 10:17
@WaterrrForever WaterrrForever mentioned this pull request Nov 3, 2026
1 task done
SanHsien added a commit to SanHsien/hyperframes that referenced this pull request Nov 6, 2026
…view

Adopts heygen-com#7294 (line_unread findings carry the measured frames), heygen-com#7271
(blur mutes discrete callbacks instead of undoing them), heygen-com#7293 (the
filter tier's svg keeps the last child), heygen-com#7292 (a mover holding a fixed
child stays sharp), heygen-com#7288 (auto motion blur draws with the filter tier)
and heygen-com#7298 (Shift no longer re-renders the timeline). The fork's seek
recovery in captureMotionBlur stays. Defers heygen-com#7134 (no packages/share)
and heygen-com#7295 (needs the heygen-com#7080 crop pipeline). Watermark advances to
66cbabc.

Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
Co-authored-by: Vance Ingalls <vance@heygen.com>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

1 participant