mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 23:03:09 +00:00
fix(shader-transitions): real opacity crossfade for CSS transitions in engine mode
Address Copilot round-3 review: the previous engine-mode timeline used `tl.set(toId, opacity:1, T)` + `tl.set(fromId, opacity:0, T+dur)` for every transition. That keeps BOTH scenes at opacity:1 throughout the transition window. The Node-side layered compositor handles this fine — it captures each scene separately, masks opacity per layer, and runs the blend itself — but the page-side compositing path (one opaque RGB screenshot per frame, opt-in via EngineConfig.enablePageSideCompositing) relies on the page to produce a correct frame. With `shader === undefined` the page-side compositor skips the entry, so the screenshot would show both scenes stacked at 100% opacity (visible ghosting) instead of a blend. Fix: schedule an actual opacity-crossfade tween in `initEngineMode` when `t.shader === undefined`. Shader transitions keep the existing opacity-flip pattern because the Node-side compositor needs both scenes fully visible to capture them. The crossfade is harmless in the layered Node path because `applyDomLayerMask` overrides per-scene opacity during each capture anyway. Also corrects docstrings in engineModePageComposite.ts and at the installPageSideCompositor call site that previously claimed the GSAP timeline "handles the blend" — it now actually does. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -44,10 +44,11 @@ interface PageCompositeTransitionConfig {
|
||||
time: number;
|
||||
/**
|
||||
* Shader id. Undefined entries are CSS crossfades — the page-side
|
||||
* compositor skips them so the GSAP opacity timeline handles the blend,
|
||||
* but the entry stays in the array to preserve `transitions[i]` ↔
|
||||
* `scenes[i]`/`scenes[i+1]` index alignment for the surrounding shader
|
||||
* entries.
|
||||
* compositor skips them, and the GSAP timeline in `initEngineMode`
|
||||
* schedules an actual opacity-crossfade tween for those entries so the
|
||||
* single page screenshot contains a correct blended frame. The entry
|
||||
* stays in the array to preserve `transitions[i]` ↔ `scenes[i]`/
|
||||
* `scenes[i+1]` index alignment for the surrounding shader entries.
|
||||
*/
|
||||
shader?: ShaderName;
|
||||
duration?: number;
|
||||
|
||||
@@ -2253,13 +2253,28 @@ function initEngineMode(
|
||||
if (!fromId || !toId) continue;
|
||||
|
||||
const dur = t.duration ?? DEFAULT_DURATION;
|
||||
const ease = t.ease ?? DEFAULT_EASE;
|
||||
const T = t.time;
|
||||
|
||||
// During the transition both scenes need to be visible so the engine
|
||||
// can composite each side; afterwards the outgoing scene must drop out
|
||||
// so it stops contributing to the normal-frame layer composite.
|
||||
tl.set(`#${toId}`, { opacity: 1 }, T);
|
||||
tl.set(`#${fromId}`, { opacity: 0 }, T + dur);
|
||||
if (t.shader === undefined) {
|
||||
// CSS-crossfade transition: schedule an actual opacity tween so the
|
||||
// page produces a correct blended frame at every seek time. This
|
||||
// matters when the producer captures with page-side compositing
|
||||
// (one opaque screenshot per frame) — there is no Node-side blend
|
||||
// step in that path, so the page must show the correct mix. Even
|
||||
// in the layered Node path the crossfade is harmless (it merely
|
||||
// mirrors what `crossfade()` computes from the per-scene buffers).
|
||||
tl.fromTo(`#${toId}`, { opacity: 0 }, { opacity: 1, duration: dur, ease }, T);
|
||||
tl.fromTo(`#${fromId}`, { opacity: 1 }, { opacity: 0, duration: dur, ease }, T);
|
||||
} else {
|
||||
// Shader transition: both scenes must stay at opacity=1 during the
|
||||
// transition window so the Node-side layered compositor can capture
|
||||
// each scene separately and blend them itself. The from-scene drops
|
||||
// out at T+dur so it stops contributing to the next normal-frame
|
||||
// layer composite.
|
||||
tl.set(`#${toId}`, { opacity: 1 }, T);
|
||||
tl.set(`#${fromId}`, { opacity: 0 }, T + dur);
|
||||
}
|
||||
}
|
||||
|
||||
// Page-side compositing opt-in (default OFF). When the producer launches
|
||||
@@ -2289,7 +2304,9 @@ function initEngineMode(
|
||||
// Pass the full transitions array so transition[i] still pairs with
|
||||
// scenes[i]/scenes[i+1]. The compositor itself skips entries with
|
||||
// `shader === undefined` while preserving the index↔scene mapping.
|
||||
// (CSS crossfades remain driven by the GSAP opacity timeline.)
|
||||
// CSS crossfades produce a correct blended frame via the actual
|
||||
// opacity-crossfade tween scheduled above (search `t.shader === undefined`
|
||||
// in this function).
|
||||
installPageSideCompositor({
|
||||
scenes,
|
||||
transitions,
|
||||
|
||||
Reference in New Issue
Block a user