Skip to content

fix(core): capture blur strobing, backdrop box, glow drop-out and callback motion - #6106

Merged
vanceingalls merged 3 commits into
mainfrom
feat/capture-blur-quality
Oct 23, 2026
Merged

vanceingalls merged 3 commits into
mainfrom
feat/capture-blur-quality

Conversation

@vanceingalls

Copy link
Copy Markdown
Collaborator

What

Four fixes to capture motion blur, found by rendering Jake's eight comparison shots ("Motion blur, two ways") on the merged code and fixing each place it lost.

  1. Strobing at default settings. A fast move used a fixed 16 taps, so a 120 px smear had 8 px steps. With no data-motion-blur-samples, taps are now sized to the smear (about one per 2 px, 2 to 128); an explicit count still wins.
  2. Lighter card body. Canvas quantizes globalAlpha to 1/255 when drawing an image, so 1/n tap weights left coverage short (n = 104 summed to 0.81). Weights are now whole 1/255 units that sum to exactly 255. This also affected the old thinned and explicit counts.
  3. A lighter box behind text and a hard-edged box around a glow; glow and shadow dropped out. The crop came from a capture of the whole scene, so the backdrop under a see-through mover smeared with it, and ink outside the mover's box was cut off. Each frame now captures the movers alone on a transparent layer (everything else visibility:hidden), and the crop grows by the mover's ink reach (box-shadow, text-shadow, filter, outline, measured during the scan).
  4. Callback-driven motion was never found. The scan always seeked with callbacks suppressed, so a render(t) driven from onUpdate reported 0 movers even with data-motion-blur-events="on". With events on, the scan now seeks the way frame reads do. Without it nothing changes.

Notes for review

  • Chrome 152 draws a mover with a rotate or scale transform at the origin, untransformed, when its root is visibility:hidden. Translation is fine. The isolation therefore keeps the root visible (background, border, shadow and outline cleared) and hides everything beneath it except the movers. There is a real-browser test for rotate and scale that fails if the root is hidden again.
  • A frame is still two paints. The output is cleared on wrap instead of showing the previous wrap's last frame, since the first paint is no longer a drawable sharp frame (a blank for about two paints at play start in preview).
  • The engine's forced finish before the background capture now falls back to a plain-DOM frame (counted in failedFrames) rather than a half frame.
  • The scan contract changed on purpose: with -events="on" it fires timeline callbacks. Authors who opt into events already get callbacks at arbitrary sub-frame times.
  • Core's unpacked size budget goes to 3,950,000 for the new ink.ts module (a later PR in this stack raises it again for the filter tier).

Evidence

Rendered Jake's eight shots before and after (frames named in his notes):

  • whip frame 34: the stair-stepped edge becomes a smooth ramp; the card body no longer lightens.
  • waterfall frame 24: the lighter box and erased background dots are gone.
  • glow frame 10 and 20: the glow smears with the button and the hard-edged box is gone.
  • render(t) frame 13: 1 mover found (was 0) with events on.

Tests

  • Core runtime 163 tests; engine real-Chrome pixel tests (new: backdrop untouched under a see-through mover, glow carried along, rotate and scale kept in place). Each new guard was broken on purpose and a test failed: tap spacing, tap weights, the isolation CSS, the ink pad, the visible-root rule, the callback scan.
  • Not run: the 400-composition corpus.

Stack: this is the bottom of three (quality fixes, then the filter fallback, then the automatic tier and defaults). Review bottom-up.

🤖 Generated with Claude Code

vanceingalls and others added 3 commits October 22, 2026 20:10
…exactly

A fast move strobed at the default 16 taps (8 px steps on a 1560 px whip). With no
data-motion-blur-samples set, taps are now about one per 2 px of travel (2 to 128); an
explicit samples count still wins.

Canvas quantizes image globalAlpha to 1/255, so 1/n weights left coverage short (n = 104
summed to 0.81, a lighter card body). Each tap now carries a whole number of 1/255 units
that sum to exactly 255.

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

The crop came from a capture of the whole scene, so the backdrop under a see-through mover
smeared with it (a lighter box with the dotted background erased) and a glow or shadow
outside the mover's box was cut off, leaving a hard-edged box.

Each frame now captures the movers alone, everything else visibility:hidden, so a mover's
pixels carry their true alpha and no backdrop; the background capture is unchanged. The crop
grows by the mover's ink reach (box-shadow, text-shadow, filter, outline, measured during the
scan), so glows travel with it. Two paints per frame, as before.

The output is cleared on wrap instead of showing the previous wrap's last frame, since the
first paint is no longer a drawable sharp frame. Core's unpacked size budget goes to 3,950,000
for the new module.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The mover scan seeked with callbacks suppressed even where the root allows them, so a
composition whose motion lives in a timeline callback (a render(t) driven from onUpdate)
reported 0 movers and rendered sharp. With data-motion-blur-events="on" the scan now seeks
the way the frame reads do. Without it nothing changes: callbacks stay suppressed.

The no-movers finding in hyperframes check now names the attribute.

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

Copy link
Copy Markdown
Contributor

Edit accuracy: accurate 271 (base branch 271), smooth 159 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: APPROVE · 0 blocker · 1 important · 0 nits · aabbb1d5

Tap weights check out (whole 1/255 units summing to 255 for every n ≤ 255; taps cap at 128). The visible-root isolation keeps rotated and scaled movers in place. One gap in the isolation CSS, see the inline comment.

Scope

Read index.ts, composite.ts, ink.ts, discover.ts and pose.ts end to end. Reproduced findings in chrome-headless-shell 152.0.7977.30 with the built runtime, comparing main (f6cbf87) with this head. CI is green.

Not flagged: a static element stacked above a mover is now covered by the trail more than on main (bar pixel 70,0,185 on main, 168,0,87 here). The docs in #6108 list this as a known capture limit.

— Genesis (pr-review)

Comment on lines +210 to +211
`[${ONLY_ATTR}]::before, [${ONLY_ATTR}]::after, [${ONLY_ATTR}] * { visibility: hidden !important; }`,
`[${ONLY_ATTR}] [${MOVER_ATTR}], [${ONLY_ATTR}] [${MOVER_ATTR}] * { visibility: visible !important; }`,

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.

important: [ONLY] [MOVER] * { visibility: visible !important } un-hides anything inside a mover that the author hid with visibility: hidden, so it gets cropped and smeared into the trail. Repro (Chrome 152, built runtime): a red box mover with a visibility:hidden green child gives 0 green px in the sharp frame and 80 green px in the blurred frame (main: 0). This persists through #6108 for anything still on capture.

Keep the hide rule off the mover subtree and let the mover's descendants inherit. With this change the repro gives 0 green px, and the root rule is untouched:

Suggested change
`[${ONLY_ATTR}]::before, [${ONLY_ATTR}]::after, [${ONLY_ATTR}] * { visibility: hidden !important; }`,
`[${ONLY_ATTR}] [${MOVER_ATTR}], [${ONLY_ATTR}] [${MOVER_ATTR}] * { visibility: visible !important; }`,
`[${ONLY_ATTR}]::before, [${ONLY_ATTR}]::after, [${ONLY_ATTR}] *:not([${MOVER_ATTR}], [${MOVER_ATTR}] *) { visibility: hidden !important; }`,
`[${ONLY_ATTR}] [${MOVER_ATTR}] { visibility: visible !important; }`,

@vanceingalls
vanceingalls merged commit e85f56e into main Oct 23, 2026
82 checks passed
@vanceingalls
vanceingalls deleted the feat/capture-blur-quality branch October 23, 2026 05:41
vanceingalls added a commit that referenced this pull request Oct 23, 2026
The isolation CSS re-showed every descendant of a mover, so a child the
author hid with visibility:hidden was cropped and smeared into the trail
(240 green px in the repro; 0 in the sharp frame). The hide rule now skips
the mover subtree and the mover alone is made visible, so its descendants
inherit and keep their own visibility.

Found by review of #6106.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
vanceingalls added a commit that referenced this pull request Oct 24, 2026
The isolation CSS re-showed every descendant of a mover, so a child the
author hid with visibility:hidden was cropped and smeared into the trail
(240 green px in the repro; 0 in the sharp frame). The hide rule now skips
the mover subtree and the mover alone is made visible, so its descendants
inherit and keep their own visibility.

Found by review of #6106.

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