feat(core,producer): decide blur defects once per timeline, off on routes that cannot host it by vanceingalls · Pull Request #5990 · heygen-com/hyperframes · GitHub
Skip to content

feat(core,producer): decide blur defects once per timeline, off on routes that cannot host it - #5990

Open
vanceingalls wants to merge 1 commit into
feat/capture-blur-checkfrom
feat/capture-blur-scan-hdr
Open

vanceingalls wants to merge 1 commit into
feat/capture-blur-checkfrom
feat/capture-blur-scan-hdr

Conversation

@vanceingalls

@vanceingalls vanceingalls commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator

What

  • Defects are decided once per timeline. The mover scan already seeks the whole timeline, so the defect scan samples along it: a defect that only appears mid-timeline (a root that rotates, a card that flips) turns blur off from frame 0, not partway through a render. Defects now apply on every Chrome version, because the preview, the pinned render browser and Docker's browser can differ and a render must match what the preview showed. GSAP's identity matrix on a rewound root transform no longer counts as a transform.
  • The per-frame check is a backstop that costs one frame. For what the scan missed (a root shake shorter than the scan step, a backface flip between samples, a canvas a script adds later) the frame renders sharp from the plain DOM and the runtime logs once; blur resumes when the defect is gone, instead of ending for the rest of the render. Measured with the review fix on the CLI render path (Chrome 152, quality draft, frames compared with blur off), before the change: a 1-frame shake sharpened 179 frames of a 6 s 60 fps clip, a 60 s clip with a 10.05–10.2 s shake sharpened 598 frames; after, 1 and 4 frames.
  • Four detector gaps closed. A root moved by the individual scale, rotate or translate properties (identity values ignored) is a root transform; a root mix-blend-mode other than normal is a defect (root-blend-mode); the backface check treats an individual rotate with an axis or 3D angle (rotate: y 180deg, rotate: 0 1 0 180deg) and a translate with z as 3D (shared individualIs3d); and the iframe-in-root entry covers everything that is torn down and re-initialised when the root moves: <iframe>, <object>, <embed> and defined custom elements (hyphenated name with customElements.get). Measured on Chrome 152 against the previous build: a scale: .5 root, a rotate, a translate, a multiply root, a rotate: y 180deg face, <object>, <embed> and a defined custom element all reported on and now report off with their code; plain compositions and flat uses (scale: 1, rotate: 90deg on a face outside 3D) still report on.
  • A block raised by the scan is lifted when its cause is removed or the timeline changes. Capture-failure blocks stay for the life of the instance.
  • The WebGL / WebGPU check covers the whole document: a render's shader-transition compositor appends its canvas to <body>, while preview puts it inside the root.
  • Blur is off on routes that cannot host it. HDR renders and the layered compositor screenshot each DOM layer after hiding the others, which does not reach blur's canvases; a distributed (Lambda / cloud) chunk render has no Studio preview to match and no guarantee of html-in-canvas in the worker's Chrome. The producer sets pageMotionBlur to hdr-render, layered-composite or distributed-render there, and the runtime reports the renderer turned it off (…). The distributed plan's compile-time hint no longer forces a data-motion-blur composition onto screenshot capture: it freezes the same capture mode as the same composition without the attribute.
  • The mover scan reads its baseline at t=0, so every worker of a parallel render finds the same movers whatever time its scan starts. An ancestor's pan is not counted as its child's move (a first change is judged against the element's own baseline style at that time).

Why

Part 7 of 9 in the capture motion blur stack: #5875 → #5967 → #5968 → #5969 → #5970 → #5971 → #5990 → #5991 → #5992. It builds on the defect list in #5971.

Stack goal: opt-in capture motion blur for compositions, in Studio preview and in renders, drawn with Chrome's html-in-canvas. A composition must blur in every frame the scan trusts, and be sharp in the rest.

Notes

  • Defects apply on every Chrome version, so a composition that Chrome 157 would capture correctly still has blur off if an older preview browser would not. Revisit when the render pin and the support floor both pass the last affected major.
  • Enabling blur runs a timeline scan that can leave about one level of noise inside blend-mode / filter effects even when blur ends up off.
  • Documented, not fixed: an ancestor's pan can still be read as a child's move in some edge cases; the per-frame backstop re-reads backface on its cached candidates, which costs more when a stylesheet resets *{backface-visibility:hidden}. The candidates include the animation-derived faces (feat(core): block capture motion blur on html-in-canvas defects #5971); each rescan adds to them and they are kept for the rest of the scan, because the last sample can fall after an animation has ended (the per-frame backstop then still reads them).
  • Output is not bit-identical across worker counts, at about the size of the chunk-split noise a render without blur already has. Motion shorter than one scan step (SCAN_STEPS) is under-measured.
  • The HDR / layered / distributed routes render without blur, as before this stack existed.

Testing

  • htmlInCanvasDefects.test.ts: the scan finds a defect that appears at any sampled time and says where it first appeared; identity transforms are ignored; a root scale / rotate / translate (identity values ignored), a root mix-blend-mode, <object> / <embed> / defined custom elements (an undefined hyphenated tag is ignored), and a backface-hidden face turned by rotate: y 180deg, rotate: 0 1 0 180deg or a z translate (a flat rotate: 90deg is not); the document-wide WebGL check; facesNow reads the scan's backface candidates at the current time (a hidden face that flips into 3D after the scan is seen without rescanning; a face an animation keys to hidden is a candidate, stays one after the animation is gone, and is found by the scan and by facesNow).
  • Core captureMotionBlur/index.test.ts: blur off from frame 0 for a mid-timeline defect; a defect the scan missed renders only that frame sharp and blur resumes; an engine frame that is sharp resolves at once and leaves the composite step idle; a block lifts when its cause is gone or the timeline changes, and a capture-failure block does not; the distributed route is named as the reason.
  • discover.test.ts: the same movers whatever time a scan starts, an ancestor's pan is no child's move when the child's style first changes, and the scan reports each step after its seek.
  • Engine pageMotionBlur.test.ts and producer motionBlurRoute.test.ts: the off reason for the HDR, layered and distributed routes; producer distributed plan.test.ts (the chunkFrames.test.ts for every chunk session is in feat(core,engine): report why blur is off, and what each render session did #5991): the plan probe session and every chunk session stand blur down, and the frozen capture mode matches the plain composition's; orchestrator request validation.
  • bun run build, tsc --noEmit in core, engine and producer pass; comment ratchet against feat(core): block capture motion blur on html-in-canvas defects #5971's branch passes; every package is within its size budget.

🤖 Generated with Claude Code

@miguel-heygen miguel-heygen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed exact head 4f40902c27d2c38ee4ee35f73e80983af29dd480 relative to a03da4f7d8af648e4dd020030ea1f844333ab876.

Audited t=0 mover baseline, sampled defects/cached candidates/per-frame fallback, root individual transforms/blend, global GPU, and HDR/layered/distributed off reasons through capture consumers. Own delta has no additional demonstrated blocker. It does preserve #5971's animated hidden-face miss: its cached scan and facesNow never inspect that omitted face. This is a delta approval only; the composition remains held on #5971.

Independent revision-bound review of the nine-PR composition. Earlier approvals/requests covered older heads. Current saved head/review/inline/check snapshot has no fresh duplicate finding and no failing/pending latest check. Stacked broad-suite skips are not executed tests.

Audited: Timeline-baseline discovery, cached backface scan and per-frame backstop; root individual transforms/blend and document-wide GPU; HDR/layered/distributed off decisions and capture-option consumers.
Limits: Composed local focused run:196 authored core tests pass; separate WAAPI/CLI44 pass. Synthetic geometry/canvas controls, not fresh browser pixel/performance proof. No app/model/provider call, source fix, merge or release.

— Magi / desktop-review

Verdict: APPROVE
Reasoning: No additional blocker in this own delta. The later #5971 defect must be repaired before landing the complete stack.

…utes that cannot host it

Decide the html-in-canvas defects once for the whole timeline instead of at
the frame being drawn. The mover scan already seeks the timeline, so the
defect scan samples along it and a defect that only appears mid-timeline (a
rotated root, a flipped card) turns blur off from frame 0. Defects now block
on every Chrome version, so a render matches the preview it came from, and
GSAP's identity transform on the root no longer counts as one.

A per-frame check stays as a backstop for what scripts add later, and it
costs only that frame: a defect the scan missed (a short root shake, a
backface flip between samples) renders that frame sharp and logs once, and
blur resumes when the defect is gone. A block raised by the scan is lifted
when its cause is removed or the timeline changes; a capture failure stays
for the life of the instance.

The candidates a scan checks include faces whose backface-visibility: hidden
arrives through an animation. Each rescan adds to them and the per-frame
backstop reads the same set, so an animated-only flip card is caught by both.

Look for WebGL canvases in the whole document: a render's shader-transition
compositor appends its canvas to body, while preview puts it inside the root.

Turn page blur off on routes that cannot host it. The HDR and layered
compositors screenshot each DOM layer after hiding the others, which does
not reach blur's canvases, and a distributed (cloud or Lambda) chunk render
has no Studio preview to match and no guarantee of html-in-canvas in the
worker's Chrome. The producer sets pageMotionBlur to hdr-render,
layered-composite or distributed-render there, the distributed plan's
compile-time hint no longer forces screenshot capture, and the runtime says
which route turned it off.

Find the same movers whatever time a scan starts: the baseline is read at
t=0, so each worker of a parallel render picks the same set. An ancestor's
pan is not counted as its child's move.

Folded review fixes: R5, R10, R15, R19.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@vanceingalls
vanceingalls force-pushed the feat/capture-blur-check branch from a03da4f to 3d6c41b Compare October 21, 2026 07:39
@vanceingalls
vanceingalls force-pushed the feat/capture-blur-scan-hdr branch from 4f40902 to 83f05ec Compare October 21, 2026 07:39

@miguel-heygen miguel-heygen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed exact head 83f05ec5fd2d0bcb3f8188e7bc1f9cbc5f63ece9 against its actual parent 3d6c41b95fe38297b778db01f73eed91a2bac20b and the previously reviewed tree.

The same animation-derived candidates reach the timeline scan and frame-time backstop. The candidate set now grows across rescans, so an animation disappearing after discovery cannot erase its face. The moved chunkFrames test is restored byte-identically in #5991.

Independent verification: own-head and composed-tip synthetic computed-style controls 4/4 pass; predecessor witness remains red (inline positive passes). Exact-source authored detector suites run locally:31 own-head +47 tip tests pass, including WAAPI containment, named/multiple animations and cached-candidate retention. The computed animation-name seam is explicit because jsdom does not expand animation shorthand. These are deterministic detector controls, not fresh Chrome pixel/performance proof.

Current exact-head check snapshot has no failed or pending check. Read completed regression gate logs and executed #5991 preflight lint/format output; stacked broad-suite skips and aggregate gate success do not prove execution of the new pixel/unit suites. Author Chrome152 experiments, 26ms scan cost and package-budget margin remain attributed.

Known sampling limit remains: a short WAAPI-only face absent at every scan cannot enter the cached set. This bounded sampling residual differs from the repaired omission of ordinary animation-derived candidates; no universal detection guarantee is claimed. No new source blocker found in this successor composition. No browser/app/model call, merge or release.

— Magi / desktop-review

Verdict: APPROVE
Reasoning: The earlier animated-face candidate blocker is repaired and retained through the restacked composition; no additional blocking change was found.

This branch has not been deployed

No deployments
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.

2 participants