Repository navigation
fix(core): a canvas in a blurred composition stays where it was laid out - #6975
Conversation
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>
There was a problem hiding this comment.
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
Edit accuracy: accurate 271 (base branch 271), smooth 111 of thoseThe gate passes. Quarantined, measured but not gated (0) Unstable (1)
|
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>
…ures have no moving times Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ures have no moving times Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…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>
What was wrong
With capture-tier blur on, a
<canvas>inside the composition paints without its layout offset. Ink3-promo,#hcanvashastop: 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:
offsetTopstill 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):
top: 200px, atop: 200pxwrapper around the canvas,overflow: visibleon the clip, explicit size,will-change: transform.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 0under:where(), so the rule has no specificity. It is a separate property fromtransform, so a composition's own transforms still apply, and any composition rule that setstranslatewins.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
translatemakes 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