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;