From a85a507cdcce4deb2a55472208176753abec152b Mon Sep 17 00:00:00 2001 From: ukimsanov Date: Sat, 20 Jun 2026 21:19:46 -0700 Subject: [PATCH] fix(shader-transitions): page-side render dropped shader transitions and final-scene content Page-side compositing (default on) silently dropped HyperShader.init shader transitions in the engine render. The compositor clones the from/to scenes to feed drawElementImage, but cloneNode copies the GSAP opacity-fade, and Chrome won't paint hidden elements, so drawElementImage throws "No cached paint record" and the shader degrades to a hard cut. Force the clones visible before capture, as the html2canvas path already does via forceSceneVisibleInClone. Also fixes the final scene's content dropping in the last beat: the core clip runtime hides it shortly before the composition ends, and page-side screenshots the live page (the layered path survives via forceVisible per-scene capture). Un-hide the settled scene on non-transition frames. Page-side only; retains the ~6.5x page-side speedup. --- .../src/engineModePageComposite.ts | 38 ++++++++++++++++++- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/packages/shader-transitions/src/engineModePageComposite.ts b/packages/shader-transitions/src/engineModePageComposite.ts index beaa4a2f86..ab9445a896 100644 --- a/packages/shader-transitions/src/engineModePageComposite.ts +++ b/packages/shader-transitions/src/engineModePageComposite.ts @@ -196,6 +196,16 @@ export function installPageSideCompositor(options: PageCompositorInstallOptions) return null; } + // Scene on screen at a non-transition time: after the last transition whose + // window has passed. Full transitions list so the index matches scene order. + function settledSceneIdAt(time: number): string | undefined { + let idx = 0; + for (const t of transitions) { + if (time >= t.time + (t.duration ?? defaultDuration)) idx += 1; + } + return scenes[Math.min(idx, scenes.length - 1)]; + } + let currentActive: ResolvedTransition | null = null; let currentProgress = 0; @@ -224,8 +234,24 @@ export function installPageSideCompositor(options: PageCompositorInstallOptions) } while (fromStaging.firstChild) fromStaging.removeChild(fromStaging.firstChild); while (toStaging.firstChild) toStaging.removeChild(toStaging.firstChild); - fromStaging.appendChild(fromEl.cloneNode(true)); - toStaging.appendChild(toEl.cloneNode(true)); + const fromClone = fromEl.cloneNode(true) as HTMLElement; + const toClone = toEl.cloneNode(true) as HTMLElement; + fromStaging.appendChild(fromClone); + toStaging.appendChild(toClone); + + // cloneNode copies the GSAP opacity-fade (opacity:0 / hidden data-start), and + // Chrome won't paint hidden elements — drawElementImage then throws "No cached + // paint record" and the shader degrades to a hard cut. The shader blends from + // full-opacity textures via u_progress, so force the clones visible. Cf. + // forceSceneVisibleInClone (html2canvas path). + for (const clone of [fromClone, toClone]) { + clone.style.opacity = "1"; + clone.style.visibility = "visible"; + clone.querySelectorAll("[data-start]").forEach((el) => { + el.style.opacity = "1"; + el.style.visibility = "visible"; + }); + } // Decode any data-URI images in clones so the browser has current // bitmaps before the micro-screenshot forces a paint pass. @@ -326,6 +352,14 @@ export function installPageSideCompositor(options: PageCompositorInstallOptions) pWin.__hf_page_composite_pending = false; while (fromStaging.firstChild) fromStaging.removeChild(fromStaging.firstChild); while (toStaging.firstChild) toStaging.removeChild(toStaging.firstChild); + // Live-page screenshot parity with the layered path's forceVisible: the + // core clip runtime hides the final scene a beat before the comp ends, so + // un-hide the settled scene (others stay at opacity 0). + const settledId = settledSceneIdAt(time); + const settled = settledId ? document.getElementById(settledId) : null; + if (settled instanceof HTMLElement && settled.style.visibility === "hidden") { + settled.style.visibility = "visible"; + } return result; } currentActive = active;