feat(mesh-bg): fade-in шейдера фона + modulepreload графа

- canvas стартует с opacity:0 и плавно проявляется (0.9s) при .is-ready
- initPageMeshShader ставит is-ready после первого кадра
- modulepreload page-mesh-shader.js для параллельной загрузки с main.js
- пересборка страниц (build:pages)
This commit is contained in:
root committed 2026-09-24 21:37:40 +02:00
1 parent b38f1a5332
commit daa623fe73
170 files changed
+11582 -1496

No files matched your search

@@ -24,6 +24,6 @@ export async function saveEdits(payload) {
export function isContentEditHost() {
const host = window.location.hostname;
const ALLOWED_HOSTS = ["localhost", "127.0.0.1", "dev.mamadaevv.site", "f2ogw6fzi6trnp7cp43x8wsd.31.129.103.242.sslip.io"];
const ALLOWED_HOSTS = ["localhost", "127.0.0.1", "10.200.201.3", "dev.mamadaevv.site", "f2ogw6fzi6trnp7cp43x8wsd.31.129.103.242.sslip.io"];
return ALLOWED_HOSTS.includes(host);
}
+112
View File
@@ -0,0 +1,112 @@
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;
}
export function initPageMeshShader() {
const host = document.querySelector(".page-mesh-bg");
if (!host) return;
if (prefersReducedMotion()) return; // статичный CSS-fallback остаётся виден
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 root = document.documentElement;
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,
};
let mount;
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 {}
// Первый кадр уже отрисован в конструкторе — отмечаем готовность,
// чтобы CSS плавно проявил canvas (fade-in), без рывка.
host.classList.add("is-ready");
} catch (e) {
console.warn("Mesh shader mount failed, fallback to static bg", e);
return;
}
// Обновляем палитру при смене акцента/темы.
const onTheme = () => {
const next = paletteFromTheme(root);
if (!next) 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 });
}