Skip to content

fix(core): a canvas in a blurred composition stays where it was laid out - #6975

Merged
vanceingalls merged 2 commits into
mainfrom
fix/motion-blur-nested-canvas-place
Nov 2, 2026
Merged

vanceingalls merged 2 commits into
mainfrom
fix/motion-blur-nested-canvas-place

Conversation

@vanceingalls

@vanceingalls vanceingalls commented Nov 2, 2026 •

Copy link
Copy Markdown
Collaborator

What was wrong

With capture-tier blur on, a <canvas> inside the composition paints without its layout offset. In k3-promo, #hcanvas has top: 200px, and its "K3. Now Open" text and glow landed exactly 200 px too high for frames 411-449. It happens on whole and chunked renders, and on frames where no mover is drawn, so it comes from the capture itself, not the blur trail.

Layout is correct: offsetTop still reads 200 during capture. Only the paint is wrong.

What Chrome does

Measured by editing the film, frames 411-449 against blur off (Chrome 156):

  • Broken (mean 17.2 dB): top: 200px, a top: 200px wrapper around the canvas, overflow: visible on the clip, explicit size, will-change: transform.
  • Correct (mean 47.8 dB): transform: translateY(200px), transform: translateZ(0), transform: translateY(0.001px), translate: 0 0.001px.

So the offset needs a real transform on the canvas, and a zero one is enough.

The fix

While captured, nested canvases get translate: 0 0 0 under :where(), so the rule has no specificity. It is a separate property from transform, so a composition's own transforms still apply, and any composition rule that sets translate wins.

Evidence

Unedited k3-promo, Subtle, compared with blur off:

  • Frames 411-449: mean 17.2 dB before, 47.8 dB after, and the frames match by eye.

  • Frames 450-470: 51.9 dB before, 57.0 dB after.

  • Other frames: unchanged against blur off.

  • With .nudge { translate: 30px 0 } on the canvas, it stays nudged while captured, and the unedited film is pixel-identical to the rule without :where().

The unit test only pins the rule. A jsdom test cannot see the paint.

Limits

  • translate makes the canvas a stacking context, so a non-positioned canvas could now paint over a later sibling. Only while captured.
  • <video> was not checked for the same loss.

🤖 Generated with Claude Code

drawElementImage drops a nested canvas's layout offset unless the canvas
has a transform, so with capture blur on, `top: 200px` painted at 0. A zero
`translate` while captured is enough, and leaves `top`, the composition's
own `transform` and its `translate` alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@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: REQUEST CHANGES · 1 blocker · 0 important · 0 nits · b693e0c

The fix works, but the rule's specificity overrides a composition's own stylesheet translate on a canvas, which the PR says it won't do. ↓ inline

Scope

Read captureMotionBlur/index.ts end-to-end around wrap/unwrap, discover, findCaptureDefect and the backface check. Chrome serializes translate: 0 0 0 as 0px (probed on 152), so the 3D checks (individualIs3d, individualTurnsIn3d) do not misread nested canvases as 3D. CI was still running when this was posted; no job had failed yet.

— Genesis

@github-actions

github-actions Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Edit accuracy: accurate 271 (base branch 271), 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 (0)

Unstable (1)

  • seqplainkeys-none-center-r0-nested-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

A class on a composition's canvas lost to the rule, so its own `translate`
was dropped while captured. Under `:where()` any composition rule wins.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@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 · 1 resolved, 0 open, 0 new · b693e0c → c0468ee

@vanceingalls
vanceingalls merged commit 2bdc866 into main Nov 2, 2026
144 of 145 checks passed
@vanceingalls
vanceingalls deleted the fix/motion-blur-nested-canvas-place branch November 2, 2026 19:41
vanceingalls added a commit that referenced this pull request Nov 3, 2026
…ures have no moving times

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
vanceingalls added a commit that referenced this pull request Nov 4, 2026
…ures have no moving times

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
ameyamg pushed a commit to ameyamg/hyperframes that referenced this pull request Nov 4, 2026
…ture blur (heygen-com#7108)

* test(engine): pixel invariants for capture blur against blur off

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

* test(engine): fixtures for every capture blur failure class seen so far

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

* test(engine): nested canvas fixture reproduces heygen-com#6975, sharp-tile fixtures have no moving times

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

* test(engine): capture blur must match blur off outside the trail and the shutter average inside it

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

* test(engine): differential tests pin known bugs by symptom, history test checks the reference frame

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

* test(engine): a playing preview keeps changing with blur on

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

* test(producer): a multi-worker blur render must match blur off outside the trail

Renders five blur fixtures through the real producer as a PNG sequence with 4
workers (contiguous blocks, three of them starting mid-film, the heygen-com#6967
condition): blur off twice, blur on once. Outside the trail of everything that
moved, blur on must equal blur off in every frame with two neighbours. The
callbacks and scroll-list known differences are pinned by symptom.

Import route: a relative import of blurDifferential.ts, fixtures.ts and
thresholds.ts from packages/engine/src/services/__fixtures__. The producer
tsconfig excludes test files, so I type-checked the file with a temporary
tsconfig (rootDir widened); no error. No package subpath was needed.

RENDER_OUTSIDE_MAX_MAE: render noise is 0 (blur off, twice, bit-identical), the
rewind mutation measured 111.085 when caught, so the 1.0 floor applies.

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

* test(producer): render callbacks with workers that start between its calls

With 4 workers the blocks started at 0, 15, 30, 45, each on a call, so the
rewind mutation showed in a render only 3 times in 5. Seven workers over 60
frames start at 0, 9, 18, 27, 36, 45 and 54, two of them strictly between the
calls at frames 16 and 30; the mutation then fails callbacks in 8 of 8 runs
(worst MAE 97.02), the clean build in none.

A new case checks the partition with distributeFrames. The #list and #label
rects now come from the fixture's markup, and MEASURED_RENDER_REWIND_MUTATION
is 97.02.

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

* test(engine): blur pins bound the symptom, liveness detector reports blindness, fixtures time out

Pinned known bugs now require every bad tile inside the pinned region and a worst
MAE within half to double of the measured one. tileTouches, Rect and the band check
move into blurDifferential.ts with a dpr 2 unit test. The blank-frame census counts
canvases it cannot read and asserts none, and names both readings when no blank frame
is seen. No console.info on passing runs. Fixtures cases get a 30 s timeout.

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

* test(producer): scroll-list render case bounds the race, pins share the engine helpers

The scroll-list loss depends on timing per worker block, so the case now requires
frames 2..27 clean and any later bad tile inside #list, without requiring the loss.
The callbacks pin keeps requiring its symptom, bounds it to the label and to half to
double of its measured size, and its message names the entry to delete. Wording says
multi-worker, not 4-worker.

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

* build(engine): keep the blur test helpers out of the published tarball

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

* test(engine): print the worst outside tile of each arm to explain the CI text diff

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

* test: both arms draw grayscale text so host subpixel settings are not read as blur

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

* test(producer): run the multi-worker blur render alone so it cannot skew other Chrome tests

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

* test: re-pin the producer lane runner after splitting out the heavy blur render

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

* test(engine): read the blank-frame pin from a wrap observer, not an animation frame poll

The pin sampled in requestAnimationFrame, so it could miss the gap between the
wrap and the first draw. A MutationObserver now reads the output canvas the
moment the root is wrapped, which is deterministic, and every fixture must
report a wrapped root.

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

* test(engine): drop the blurs field no fixture could set to false

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

* ci: give the flake gate 60 minutes, it budgets runs by file count not time

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

---------

Co-authored-by: Claude Sonnet 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