import { ShaderMount, meshGradientFragmentShader, getShaderColorFromString, defaultObjectSizing, ShaderFitOptions } from "/vendor/paper-shaders/index.js"; import { prefersReducedMotion } from "../shared/media.js"; // Живой mesh-gradient фон главной страницы (Paper Shaders, zero-dep WebGL). // Цвета берутся из акцентной темы сайта (--theme-h/s/l), чтобы фон всегда // совпадал с выбранным пользователем акцентом. function hsl(h, s, l, a = 1) { // getShaderColorFromString понимает "hsl(h, s%, l%)" — собираем ровно в этом виде. return `hsl(${Math.round(h)}, ${Math.round(s)}%, ${Math.round(l)}%)`; } function paletteFromTheme(root) { const cs = getComputedStyle(root); const h = parseFloat(cs.getPropertyValue("--theme-h")); const s = parseFloat(cs.getPropertyValue("--theme-s")); const l = parseFloat(cs.getPropertyValue("--theme-l")); if (![h, s, l].every(Number.isFinite)) return null; // Монохромная палитра: все цвета — вариации акцента (±18°/±36° hue, // без крупных сдвигов), чтобы фон оставался однотонным, не разноцветным. const a1 = (h + 36) % 360; const a2 = (h + 18) % 360; const a3 = (h - 18 + 360) % 360; return [ hsl(a1, s, Math.min(l + 2, 54)), hsl(a2, s, Math.min(l, 50)), hsl(a3, s, Math.min(l - 2, 46)), hsl(h, s, Math.min(l + 6, 58)), ]; } function colorVec4Array(colors) { // Шейдер ждёт u_colors как vec4[10] → массив из 10 под-массивов [r,g,b,a]. // setUniformValues сам flatten'ит массив массивов в uniform4fv. const out = []; for (let i = 0; i < 10; i++) { const c = colors[i % colors.length]; out.push(getShaderColorFromString(c)); } return out; } // Сигнал готовности фона: проявляем 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, u_rotation: defaultObjectSizing.rotation, u_offsetX: defaultObjectSizing.offsetX, u_offsetY: defaultObjectSizing.offsetY, u_originX: defaultObjectSizing.originX, u_originY: defaultObjectSizing.originY, u_worldWidth: defaultObjectSizing.worldWidth, u_worldHeight: defaultObjectSizing.worldHeight, }); const base = paletteFromTheme(root) || ["#241d9a", "#f75092", "#9f50d3", "#0e6b8f"]; const uniforms = { ...sizing(), u_colors: colorVec4Array(base), u_colorsCount: base.length, u_distortion: 0.85, u_swirl: 0.35, u_grainMixer: 0.06, u_grainOverlay: 0.04, }; try { mount = new ShaderMount( host, meshGradientFragmentShader, uniforms, { antialias: true, alpha: true, preserveDrawingBuffer: true }, 1.0, // speed 0, // frame 1, // minPixelRatio: ровно CSS-пиксели, без 2x апскейла (главный источник нагрузки) 2_500_000 // maxPixelCount: потолок ~1600x1000, фон размытый — качество не страдает ); // не даём IntersectionObserver ставить паузу вне вьюпорта — фон на весь экран try { mount.intersectionObserver?.disconnect(); } catch { /* noop */ } // Первый кадр уже отрисован в конструкторе — отмечаем готовность, // чтобы CSS плавно проявил canvas (fade-in) и hero. host.classList.add("is-ready"); markMeshReady(); } catch (e) { console.warn("Mesh shader mount failed, fallback to static bg", e); markMeshReady(); mount = null; return; } // Обновляем палитру при смене акцента/темы. onTheme = () => { const next = paletteFromTheme(root); if (!next || !mount) return; mount.setUniforms({ u_colors: colorVec4Array(next), u_colorsCount: next.length, }); }; window.addEventListener("site-theme-applied", onTheme, { passive: true }); } 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(); }); } }