Skip to content

feat(core): capture blur falls back to per-element filters where capture cannot run - #6107

Merged
vanceingalls merged 2 commits into
mainfrom
feat/capture-blur-filter-tier
Oct 23, 2026
Merged

vanceingalls merged 2 commits into
mainfrom
feat/capture-blur-filter-tier

Conversation

@vanceingalls

Copy link
Copy Markdown
Collaborator

What

When capture cannot run, blur no longer turns off for the whole composition. It hands over to a filter tier: an SVG filter on each mover, 32 offset copies averaged along its sampled path (in pairs, so 8-bit buffers do not band), softened along the motion so a long smear does not step. It filters the live DOM, so it works over a WebGL canvas, in any Chrome, and leaves a mover's glow and shadow on the element. Turn and scale only soften (capture draws those truthfully, so it stays the first choice).

Capture cannot run when: a WebGL/WebGPU canvas is in the page, a transform or opacity or blend mode is on the root, an iframe/object/embed is in the root, backface-visibility: hidden sits in a 3D context, a nested capture canvas exists, the browser lacks html-in-canvas, or capture failed three times in a row in preview. The renderer's explicit off (HDR, distributed, engine blur) stays off. A scene re-init tries capture again.

How it fits

  • tier.ts: shared Tier interface, Report (gains tier), chooseTier (pure, tested).
  • filterPlan.ts: pure arithmetic (offsets, softening, region) from a mover's sampled path.
  • filterTier.ts: the DOM layer and the tier. frames.ts: pose reading shared with capture.
  • index.ts: picks the tier at init and hands over when a scan turns capture off. The window report names the tier.

Evidence

  • WebGL shot (Jake's "webgl", frame 34): was entirely sharp (capture logged off reasonCode=accelerated-canvas); now blurred with the live gradient behind it, matching his render.
  • Real-Chrome pixel test over a page with a WebGL canvas: trail present, centroid within 2 px of the sharp frame, ink conserved within 10%. Mutation-tested: disabling the fallback and skipping the filter aim each fail it.

Notes for review

  • A test-only switch (setFilterFallbackForTests) lets the existing capture-tier defect tests keep judging capture alone.
  • Core's budgets go to 3,965,000 unpacked / 1,645,000 packed for the tier.
  • The complexity gate (fallow) is clean for the new code; one inherited finding (useRenderQueue) is unchanged.

Stacked on feat/capture-blur-quality.

🤖 Generated with Claude Code

heygengenesis[bot]
heygengenesis Bot previously approved these changes Oct 23, 2026

@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 · 1 nit · 09d41cce

The hand-off holds. start() returns before wrapping when the scan disables capture, and settleTier tears capture down before the filter tier paints the same frame. Main CI does not run on a stacked base, so I ran the motion-blur core suite at the stack head (178 pass) and a Chrome 152 probe of both tiers.

— Genesis (pr-review)

const layer = layerOf(el);
aim(layer, plan);
const base = strip(el) || layer.css;
el.style.filter = base ? `${base} ${layer.url}` : layer.url;

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: on the fallback path, every mover gets filter only on frames where it moves. A non-none filter makes the element a stacking context and the containing block for its absolute and fixed descendants. So a left/top mover with no transform jumps between moving and still frames if it has a position: fixed or z-indexed child. Before this PR the same page rendered sharp and correct. #6108's hasOwnContext guards the auto choice only. The same check here could leave such movers sharp.

Base automatically changed from feat/capture-blur-quality to main October 23, 2026 05:41
@vanceingalls
vanceingalls dismissed heygengenesis[bot]’s stale review October 23, 2026 05:41

The base branch was changed.

…ure cannot run

html-in-canvas cannot capture a WebGL canvas, an iframe or a transform on the root, and some
browsers lack it. Blur used to turn off for the whole composition. Now the runtime hands the
composition to a filter tier: an SVG filter on each mover, 32 offset copies averaged along its
sampled path, softened along the motion so long smears do not step. It filters the live DOM,
so it works over a WebGL canvas, in any Chrome, and leaves glows and shadows on the element.
Turn and scale only soften; capture draws those truthfully, so it stays the first choice and
a scene re-init tries it again.

Capture turning itself off for a defect or three failed captures hands over; the renderer's
explicit off (HDR, distributed, engine blur) stays off. The report names the tier. Core's
unpacked size budget goes to 3,965,000 for the tier.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@vanceingalls
vanceingalls force-pushed the feat/capture-blur-filter-tier branch from 09d41cc to 8e15e47 Compare October 23, 2026 05:41
@github-actions

github-actions Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

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

@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 · 09d41cce → 8e15e477

Rebase only: the patch matches the head I approved. One new blocker: the required CI / Test (runtime) failure is not flaky. In captureMotionBlur.pixels.browser.test.ts, the two backface tests (lines 269 and 275 onward) still expect state: "off", but this PR now hands those pages to the filter tier (tier: "filter"). They fail the same way on Linux CI, on Windows CI and locally on this head (2 failed, 8 passed). Rerunning will not help. Moving #6108's toMatchObject({ tier: "filter", reasonCode: "backface-visibility" }) updates for both tests into this PR fixes it; with them, the file passes 16/16 at #6108.

— Genesis (pr-review)

The two backface tests still expected blur off; with the filter tier a
backface page reports tier filter. Fails on CI without this.

Co-Authored-By: Claude Sonnet 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, 1 open, 0 new · 8e15e477 → 73cb91d7

Resolved: the two backface tests now expect tier: "filter", and CI / Test (runtime) is green. The only delta is those 2 assertions. Still open: the earlier nit. The Windows engine-cli job fails on a 5 s timeout in browserManager.test.ts (resolveHeadlessShellPath, darwin/arm64). This PR does not touch that file, so rerun it.

— Genesis (pr-review)

@vanceingalls
vanceingalls merged commit 4ed7813 into main Oct 23, 2026
85 of 87 checks passed
@vanceingalls
vanceingalls deleted the feat/capture-blur-filter-tier branch October 23, 2026 20:46
@heygengenesis heygengenesis Bot mentioned this pull request Oct 23, 2026
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