From 8a63190f5a800139afbfa8c2e1601f9f4b1726d0 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 28 Sep 2026 15:47:21 +0200 Subject: [PATCH] fix(mesh-shader): re-init shader after bfcache restore (back/forward nav) --- public/js/modules/ui/page-mesh-shader.js | 76 ++++++++++++++++++------ 1 file changed, 57 insertions(+), 19 deletions(-) diff --git a/public/js/modules/ui/page-mesh-shader.js b/public/js/modules/ui/page-mesh-shader.js index 1f1b88a..bfbdea8 100644 --- a/public/js/modules/ui/page-mesh-shader.js +++ b/public/js/modules/ui/page-mesh-shader.js @@ -40,11 +40,29 @@ function colorVec4Array(colors) { return out; } -export function initPageMeshShader() { - const host = document.querySelector(".page-mesh-bg"); - if (!host) return markMeshReady(); // нет хоста — hero всё равно проявляем - if (prefersReducedMotion()) return markMeshReady(); // статичный CSS-fallback, hero сразу виден +// Сигнал готовности фона: проявляем hero (см. .hero в main.css). +// Ставится во всех ветках инициализации, чтобы hero никогда не висел скрытым. +function markMeshReady() { + document.documentElement.classList.add("mesh-bg-ready"); +} +let mount = null; +let onTheme = null; +let pagehideBound = false; +let pageshowBound = false; + +function disposeMount() { + if (onTheme) { + window.removeEventListener("site-theme-applied", onTheme); + onTheme = null; + } + if (mount) { + try { mount.dispose(); } catch { /* noop */ } + mount = null; + } +} + +function buildMount(host, root) { const sizing = () => ({ u_fit: ShaderFitOptions[defaultObjectSizing.fit], u_scale: defaultObjectSizing.scale, @@ -57,7 +75,6 @@ export function initPageMeshShader() { u_worldHeight: defaultObjectSizing.worldHeight, }); - const root = document.documentElement; const base = paletteFromTheme(root) || ["#241d9a", "#f75092", "#9f50d3", "#0e6b8f"]; const uniforms = { @@ -70,7 +87,6 @@ export function initPageMeshShader() { u_grainOverlay: 0.04, }; - let mount; try { mount = new ShaderMount( host, @@ -83,7 +99,7 @@ export function initPageMeshShader() { 2_500_000 // maxPixelCount: потолок ~1600x1000, фон размытый — качество не страдает ); // не даём IntersectionObserver ставить паузу вне вьюпорта — фон на весь экран - try { mount.intersectionObserver?.disconnect(); } catch {} + try { mount.intersectionObserver?.disconnect(); } catch { /* noop */ } // Первый кадр уже отрисован в конструкторе — отмечаем готовность, // чтобы CSS плавно проявил canvas (fade-in) и hero. @@ -92,29 +108,51 @@ export function initPageMeshShader() { } catch (e) { console.warn("Mesh shader mount failed, fallback to static bg", e); markMeshReady(); + mount = null; return; } // Обновляем палитру при смене акцента/темы. - const onTheme = () => { + onTheme = () => { const next = paletteFromTheme(root); - if (!next) return; + if (!next || !mount) return; mount.setUniforms({ u_colors: colorVec4Array(next), u_colorsCount: next.length, }); }; window.addEventListener("site-theme-applied", onTheme, { passive: true }); - - // при уходе со страницы — чистим - window.addEventListener("pagehide", () => { - window.removeEventListener("site-theme-applied", onTheme); - try { mount.dispose(); } catch {} - }, { once: true }); } -// Сигнал готовности фона: проявляем hero (см. .hero в main.css). -// Ставится во всех ветках инициализации, чтобы hero никогда не висел скрытым. -function markMeshReady() { - document.documentElement.classList.add("mesh-bg-ready"); +export function initPageMeshShader() { + const host = document.querySelector(".page-mesh-bg"); + if (!host) return markMeshReady(); // нет хоста — hero всё равно проявляем + if (prefersReducedMotion()) return markMeshReady(); // статичный CSS-fallback, hero сразу виден + + // Повторный вызов (или восстановление из bfcache): убираем старый canvas/ mount, + // чтобы не накапливать мёртвые WebGL-контексты. + disposeMount(); + host.classList.remove("is-ready"); + const stale = host.querySelector("canvas"); + if (stale) stale.remove(); + + buildMount(host, document.documentElement); + + // Чистим ресурсы при нормальном уходе со страницы. + if (!pagehideBound) { + pagehideBound = true; + window.addEventListener("pagehide", () => { + disposeMount(); + }); + } + + // Браузерная Back/Forward Cache (bfcache): при возврате на страницу модуль + // main.js НЕ перезапускается, а canvas был удалён в pagehide → фон пустой. + // pageshow с persisted=true = восстановление из bfcache → пересобираем шэйдер. + if (!pageshowBound) { + pageshowBound = true; + window.addEventListener("pageshow", (e) => { + if (e.persisted) initPageMeshShader(); + }); + } }