diff --git a/public/js/modules/ui/page-mesh-shader.js b/public/js/modules/ui/page-mesh-shader.js index bfbdea8..33f4616 100644 --- a/public/js/modules/ui/page-mesh-shader.js +++ b/public/js/modules/ui/page-mesh-shader.js @@ -29,6 +29,26 @@ function paletteFromTheme(root) { ]; } +// Статичный CSS-fallback фона. Переписывает --page-bg напрямую, чтобы +// обновляться при смене акцента даже если WebGL отключён (мобильные). +function setStaticPageBg(root) { + const h = parseFloat(root.style.getPropertyValue("--theme-h")) || 210; + const s = parseFloat(root.style.getPropertyValue("--theme-s")) || 90; + const l = parseFloat(root.style.getPropertyValue("--theme-l")) || 40; + const isDark = root.dataset.theme === "dark"; + const mixAndStop = isDark ? [[52, 62], [44, 64], [36, 68]] : [[38, 62], [32, 64], [26, 68]]; + const hueShift = isDark ? 180 : 90; // тёмная: accent-square-3/+180, светлая: +90 + const hues = [h + hueShift, h + 90, h + 270].map(x => ((x % 360) + 360) % 360); + const bg = `color-mix(in srgb, var(--bg) 100%, transparent)`; + let bgStr = ""; + for (let i = 0; i < 3; i++) { + const [mix, stop] = mixAndStop[i]; + const color = `hsl(${Math.round(hues[i])} calc(${s}% * 1) calc(${Math.round(l)}% * 1))`; + bgStr += `radial-gradient(calc(48% * 1) calc(56% * 1) at var(--mesh-a-x) var(--mesh-a-y), color-mix(in srgb, ${color}, ${bg}) ${mix}%, transparent ${stop}%)${i < 2 ? "," : ""}`; + } + root.style.setProperty("--page-bg", bgStr); +} + function colorVec4Array(colors) { // Шейдер ждёт u_colors как vec4[10] → массив из 10 под-массивов [r,g,b,a]. // setUniformValues сам flatten'ит массив массивов в uniform4fv. @@ -62,6 +82,25 @@ function disposeMount() { } } +function updateCanvasVisibility(root) { + const host = root.querySelector(".page-mesh-bg canvas"); + if (!host) return; + const shouldHide = root.dataset.meshBg === "off" || document.body.dataset.scrollHidden === "1"; + host.style.visibility = shouldHide ? "hidden" : "visible"; +} + +function handleScroll(root) { + // Шэйдер закрывается, когда прокручено больше 1000px — в дизайне + // mesh-bg перекрывается фоном секций и всё равно не виден. + // Это освобождает GPU на весь остальной скролл. + if (document.body.scrollTop > 1000 || document.documentElement.scrollTop > 1000) { + root.dataset.scrollHidden = "1"; + } else { + delete root.dataset.scrollHidden; + } + updateCanvasVisibility(root); +} + function buildMount(host, root) { const sizing = () => ({ u_fit: ShaderFitOptions[defaultObjectSizing.fit], @@ -115,11 +154,17 @@ function buildMount(host, root) { // Обновляем палитру при смене акцента/темы. onTheme = () => { const next = paletteFromTheme(root); - if (!next || !mount) return; + if (!next || !mount) { + setStaticPageBg(root); + updateCanvasVisibility(root); + return; + } mount.setUniforms({ u_colors: colorVec4Array(next), u_colorsCount: next.length, }); + setStaticPageBg(root); + updateCanvasVisibility(root); }; window.addEventListener("site-theme-applied", onTheme, { passive: true }); } @@ -138,6 +183,11 @@ export function initPageMeshShader() { buildMount(host, document.documentElement); + // Скролл-контроль видимости canvas: при скролле >1000px фон перекрывается + // секциями и не виден — закрываем canvas, чтобы не тратить GPU. + window.addEventListener("scroll", () => handleScroll(document.documentElement), { passive: true }); + handleScroll(document.documentElement); + // Чистим ресурсы при нормальном уходе со страницы. if (!pagehideBound) { pagehideBound = true;