Skip to content

fix(core): a mover's ::before and ::after outside its box stay in the blur - #7295

Merged
vanceingalls merged 1 commit into
mainfrom
fix/blur-pseudo-outside-box
Nov 6, 2026
Merged

vanceingalls merged 1 commit into
mainfrom
fix/blur-pseudo-outside-box

Conversation

@vanceingalls

Copy link
Copy Markdown
Collaborator

What

A mover's ::before or ::after drawn outside the mover's own box was cut off in both blur tiers (sweep defect D: 3480 and 3346 px differ from the 32-sample oracle for capture, 1248 for the filter tier). grownBox grew the blur box over the mover's text and its child elements but not over generated content, and both the capture crop and the filter region take their size from that box.

pseudoBox(el, which) now returns the pseudo-element's box in the mover's own local px: null unless computed content is not none/normal and position is absolute or fixed; otherwise [left, top, left + width, top + height] from the resolved px, offset by the mover's clientLeft/clientTop, ignoring the pseudo's own transform and margins. auto or non-finite values skip the pseudo rather than guessing. grownBox grows with both pseudo-elements of the mover itself, not its descendants. One change at the single input to both tiers.

Tests

  • The two pseudo-after sweep cases (capture and filter) lose their bug fields and S.D_CAPTURE / S.D_FILTER and pass as ordinary inside checks. A new text-free pseudo-before fixture adds the mirrored case (a ::before to the left of the box) for both tiers.
  • All four failed before the change and pass after; making pseudoBox return null fails exactly those four. Engine sweep 19/19, src/services/captureMotionBlur 154/154, core captureMotionBlur 212/212 on Chrome 153. The sweep file is 19.3 s idle locally on main and 23.5 to 26 s with the two new cases, against the Flake gate's 120 s per run.
  • No unit test for pseudoBox: jsdom ignores the pseudo argument and returns content: normal, position: static, so the browser cases are the coverage.

Limits

  • Cost: two getComputedStyle(el, pseudo) reads per mover per frame (212 for a 106-mover film), on a style that is already clean at that point.
  • A position: fixed pseudo of a mover that is not a containing block for fixed descendants resolves against the viewport, so its box is in the wrong frame: same family as defect F. The worst case is an over-grown crop (capture turns blur off for that mover through maxCropArea; the filter tier has no area guard).
  • A display: none pseudo with authored px geometry still grows the box (costs a larger crop, no wrong pixels).
  • Not covered by a fixture: right/bottom-anchored pseudos and width: auto pseudos (Chrome returns used px for them).

🤖 Generated with Claude Code

… blur

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

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

@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 · 2 nits · head a02c5ffe

Single input to both tiers, grow-only, so a wrong pseudo box can cost crop area but not cut pixels the old box kept. CI, regression and Windows render verification are green on this head (Flake gate included).

Scope

Read the full diff plus grownBox/contentBox callers and mover discovery. Did not re-run the "four cases fail without the change" claim locally (needs Chrome); CI runs the un-pinned cases green. No prior reviews.

— Claude

function pseudoBox(el: HTMLElement, which: "::before" | "::after"): Box | null {
const cs = getComputedStyle(el, which);
if (NO_CONTENT.has(cs.content) || !POSITIONED.has(cs.position)) return null;
const left = parseFloat(cs.left) + el.clientLeft;

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.

nit: the most common oversized pseudo is probably a centred halo — left: 50%; top: 50%; width: 160%; transform: translate(-50%, -50%). Ignoring the pseudo's transform puts this box w/2, h/2 down-right of the painted one, so the top/left of the halo is still cut. Mapping the four corners through cs.transform about cs.transformOrigin (or at least worth a Limits bullet naming this idiom) would cover it.


function pseudoBox(el: HTMLElement, which: "::before" | "::after"): Box | null {
const cs = getComputedStyle(el, which);
if (NO_CONTENT.has(cs.content) || !POSITIONED.has(cs.position)) return null;

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.

nit: the wrong-frame limit isn't just fixed. An absolute pseudo resolves against the mover only when the mover is a containing block. On the !ownTransform path of contentBox a mover can be position: static with no transform (moved by layout, e.g. animated margin), and left/top then belong to an ancestor. A single check that el is positioned or has a transform/will-change/filter would cover both this and the fixed case in the Limits list.

@vanceingalls
vanceingalls added this pull request to the merge queue Nov 6, 2026
Merged via the queue into main with commit 4bee04e Nov 6, 2026
98 checks passed
@vanceingalls
vanceingalls deleted the fix/blur-pseudo-outside-box branch November 6, 2026 05:26
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