feat(core): capture motion blur runtime lifecycle by vanceingalls · Pull Request #5967 · heygen-com/hyperframes · GitHub
Skip to content

feat(core): capture motion blur runtime lifecycle - #5967

Open
vanceingalls wants to merge 2 commits into
mainfrom
feat/capture-blur-runtime
Open

vanceingalls wants to merge 2 commits into
mainfrom
feat/capture-blur-runtime

Conversation

@vanceingalls

@vanceingalls vanceingalls commented Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator

What

Wires the pure modules from #5875 into the composition runtime.

  • captureMotionBlur/index.ts: reads the root's data-motion-blur attributes (on/off, -shutter 0–720° default 180, -samples 2–64 default 16, -events="on" to run timeline callbacks at sub-frame samples), scans for movers once per timeline, wraps the root in a capture canvas with an output canvas after it, and draws each frame in two phases (capture the root and crop the movers, hide them and capture the background, then redraw each mover along its path). window.__hfMotionBlur.report() says whether blur is on and, when it is off, why.
  • init.ts / entry.ts: the runtime hooks (init, seek, play / pause, and a final-frame release that the engine and CLI capture wait on), and tracking of WebGL canvases.
  • pageMotionBlur.ts and the @hyperframes/core export of PAGE_MOTION_BLUR_OFF_GLOBAL, the switch the engine sets to tell the page to stand down. Nothing sets it until feat(producer,studio): wire capture motion blur into renders and Studio #5969.

A frame whose paint never comes (2000 ms after release) is not left half-wrapped: the root goes back into the document as plain DOM, the movers are revealed, the failure is counted in report().failedFrames and logged once per 25, and the engine's late composite step finds nothing to finish. A paint timeout never counts toward giving up (failures keeps its streak meaning) and the next frame wraps again.

Blur turns itself off, and logs why, when html-in-canvas or drawElementImage is missing, when the engine's own sub-frame blur is on, when the composition already nests a capture canvas, or when a WebGL canvas is present or appears after init.

Why

Part 2 of 9 in the capture motion blur stack: #5875 → #5967 → #5968 → #5969 → #5970 → #5971 → #5990 → #5991 → #5992. It builds on #5875, which has the modules this wires together.

Stack goal: opt-in capture motion blur for compositions, in Studio preview and in renders, drawn with Chrome's html-in-canvas. Nothing blurs without data-motion-blur="on" on the root.

Notes

  • The root is wrapped in the capture canvas only while playing or rendering, and only when the scan found movers. Paused or scrubbing it is plain DOM, so Studio click-to-select works. Compositions with nothing moving are never wrapped.
  • Pose reads are timeline seeks (no paint), interpolated with a decomposed affine.
  • Preview needs html-in-canvas: Chrome 155 and earlier need chrome://flags/#html-in-canvas; 157 and 158 builds expose it without a flag (whether it ships in stable is not confirmed).
  • This PR is the lifecycle skeleton. Device-pixel output, the render fallback for capture exceptions, the lifecycle hardening and the tests for the painting paths are in feat(core): harden the capture motion blur runtime lifecycle #5968; render routing and Studio are feat(producer,studio): wire capture motion blur into renders and Studio #5969; the html-in-canvas defect list is feat(core): block capture motion blur on html-in-canvas defects #5971.
  • A user seek while a blurred frame is in flight is not specially handled: the frame in flight is not abandoned, and the request waits as the newest pending one (documented, not fixed).
  • Limits inherited from feat(core): capture motion blur pose, discovery and compositor #5875 apply (trails over foreground elements, a halo on transparent movers, trails leaking past overflow:hidden, a fast child inside a parent that moved earlier is not blurred, SVG elements are not scanned).

Testing

captureMotionBlur/index.test.ts (opt-in and off switches, each reason blur stays off, wrapping and unwrapping, and paint timeouts: a released frame whose paint never comes ends as plain DOM with the failure counted, for a first frame whose output was never drawn and for a frame after one that painted, in engine mode, plus a preview run; each guard was shown to fail with its mutation applied) and entry.test.ts (WebGL canvas tracking is installed with the runtime). The tests for engine-driven frames, painting, and unwrap-on-pause come with #5968. bun run build, core tsc --noEmit and the core runtime tests pass; comment ratchet against #5875's branch passes; core stays under 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 head69441a0e01070d8bc8d0e90d6c9cdae0a750620d and the engine integration. Pause/teardown restores plain DOM; the deadline path does not.

P2 blocker at captureMotionBlur/index.ts:417–421: a released frame receiving no paint event times out into finishJob, which settles/drops pending without capture or unwrap. The root remains inside the capture canvas, output has never drawn (or contains an earlier frame), and failures remains0. The later forced engine composite cannot recover because pending is already null. The engine waits for seek completion before forcing its screenshot, so that fallback can run after this timer. Unwrap/reveal to a sharp frame or complete a verified capture on timeout, account for the failure, and cover both first-frame and previously-painted-frame timeouts. #5968's thrown-capture fallback does not repair this branch.

The independent exact-module control reproduces zero captures, root parent CANVAS, state on/failures0 after2001ms and after the forced resolver. This is a recording Canvas boundary, not a claim of a newly measured Chrome hang. Authored focused suites passed124 tests; hosted checks currently all pass/skip. No prior human finding duplicates this. The separate #5875 pose issue remains inherited. No live render, paid call, merge or release.

— Magi (Home)
Verdict: REQUEST CHANGES
Reasoning: The deadline resolves a render frame while leaving undrawn/stale output active, bypassing the advertised sharp-frame fallback.

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.

P2: This deadline needs the plain-frame fallback before settling. finishJob reveals movers and clears pending, but never captures or unwraps. Released engine frame + no paint event +2001ms leaves root inside CANVAS, drawElementImage never called, report on/failures0. The engine then forces a paint after waiting for seek completion, but its resolver cannot finish a now-null pending job. A first frame can have undrawn output and a later one stale output. Unwrap to plain DOM (or finish a verified sharp capture), account for the timeout, and test first/later-frame recovery. The thrown-capture repair in #5968 does not cover this branch.

vanceingalls and others added 2 commits October 20, 2026 21:23
Add the three pure modules capture motion blur is built from: pose (an
element's box as an affine in composition space, and the poses between two
times), discover (find the elements that move fast enough for a smear to
show) and composite (redraw each mover along its path into one frame).
Nothing calls them yet; the runtime that wires them up lands in the next
change.

The pose reads the individual translate, rotate and scale properties in CSS
order, adds the offsetParent border back and subtracts ancestor scroll, so it
matches getBoundingClientRect for a box centered with translate: -50% -50%.

Folded review fixes: R3 (pose half; the scan's 3D skip for individual
transforms lands with the mover scan).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Wire the pure modules into the composition runtime. A root that opts in with
data-motion-blur wraps itself in a capture canvas and, per frame, redraws
its fast movers along their paths into an output canvas. The runtime turns
itself off, and says why, when the browser lacks html-in-canvas, when a
WebGL or nested capture canvas is present, or when the engine's own
sub-frame blur is on. It unwraps when paused so Studio can select inside.
A frame whose paint never comes within 2000 ms is rendered as plain DOM
(root back in the page, movers revealed), counted in report().failedFrames
and never counted toward giving up.
The page-side off switch, PAGE_MOTION_BLUR_OFF_GLOBAL, is exported from
@hyperframes/core for the engine to set.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@vanceingalls
vanceingalls force-pushed the feat/capture-motion-blur branch from e809a44 to af29b40 Compare October 21, 2026 05:17
@vanceingalls
vanceingalls force-pushed the feat/capture-blur-runtime branch from 16b4b2e to 254e20a Compare October 21, 2026 05:18
miguel-heygen
miguel-heygen previously approved these changes Oct 21, 2026

@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 254e20a5d84aafbb29dea1e9744694aa1b258326 relative to af29b40ef15eb11e1c347b1feed2a62e6b398ae0.

The no-paint deadline blocker is repaired: captureMotionBlur/index.ts:onPaintTimeout counts the failed frame, unwraps and settles; a late forced resolver cannot leave stale output active. Independent final-runtime released-engine-frame+2001ms control returns plain DOM with zero captures and failedFrames1/failures0. Own deadline branch and authored first/later-frame tests inspected. Lifecycle/pause/teardown and engine seek/composite consumer checked.

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: Runtime acquisition/release: wrap/pause/end/teardown, pending paint/queue, timeout and first/later frame recovery; engine seek/composite ordering.
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.

Base automatically changed from feat/capture-motion-blur to main October 21, 2026 12:45
@vanceingalls
vanceingalls dismissed miguel-heygen’s stale review October 21, 2026 12:45

The base branch was changed.

@github-actions

Copy link
Copy Markdown
Contributor

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