mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-11-04 13:07:11 +00:00
Deliverable one of issue #4010: average K sub-frame captures into each output frame so rotation, scale and opacity blur the way After Effects does, which the translation-only catalog component (#4008) cannot do. Opt in per render with CaptureOptions.motionBlur; absent means the capture path is unchanged. The blocker was quantizeTimeToFrame flooring every seek onto the output frame grid, which collapsed all K samples onto one instant and made the averaged frame byte-identical to the unblurred one. quantizeSeekTime now owns that decision at all three quantize sites (runtime player, its no-timeline fallback, and the init transport) and refines the grid by an integer subdivision count, which can only contain the output grid, never move a frame-time instant off it. The finer grid rounds rather than floors: a sample time round-trips through the page bridge as a float, and past a few million ticks that error exceeds the floor epsilon. Accumulation sits inside captureFrameCore AFTER the static-dedup early return, so a static frame still costs one reused buffer and only moving frames pay K. It is not a before-capture hook, which would have disabled dedup outright. Callback invariant: one eventful seek per output frame at the frame time, then K suppressed sample seeks, then a suppressed restore to the frame time. The restore matters because a suppressed seek still moves the playhead, so without it the next frame's eventful seek would swallow the callbacks inside the shutter window. Scope of this deliverable: screenshot capture mode and PNG output. beginframe, drawelement and JPEG encoding are rejected with a named error rather than silently rendering unblurred. Video content is rejected for the same reason it is a documented limitation: injected frames are extracted per output frame and cannot follow a sub-frame seek. Blend defaults to sRGB, matching AE's non-linearised 8-bpc working space and the catalog component. Linear light is an explicit opt-in. Samples are folded into a running average as they are captured rather than held until the end, so peak memory is one accumulator plus one decoded sample instead of all K: at 1080p that is a constant 41 MB rather than 141 MB at 16 samples and 540 MB at the 64 maximum, on every frame of the render. PNG chunk CRC32 now has one owner in utils/crc32.ts, shared by the new encoder and ffprobe's metadata reader. The encoder previously imported zlib's crc32 by name, which throws at module evaluation on Node 22.0 and 22.1; ffprobe already carried the guarded version and the comment explaining why. The sRGB transfer function is likewise exported once and shared. Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>