- build-pages.mjs: per-locale page/partial overrides (*.en.html), i18n token
resolution with ru fallback, <html lang> + assetPrefix per locale
- UI dictionaries src/i18n/{ru,en}.json; lang-switch partial
- headers/footer/partials converted to {{i18n:…}} tokens
- root-absolute static asset refs so /en/ subpath loads css/js/vendor
- EN content: homepage (index.en.html + partials) and cookie-policy.en.html
(cookie-policy body translated via apinex.bond /v1/tools/translator)
84 lines
3.3 KiB
JavaScript
84 lines
3.3 KiB
JavaScript
// Живой прототип Paper Shaders на стенде _lab (без правки прод-кода).
|
||
// Пакет скопирован локально в /vendor/paper-shaders (zero-dep, ESM).
|
||
import {
|
||
ShaderMount,
|
||
meshGradientFragmentShader,
|
||
getShaderColorFromString,
|
||
defaultObjectSizing,
|
||
ShaderFitOptions,
|
||
} from '/vendor/paper-shaders/index.js';
|
||
|
||
// Цвета по бренду (как у .contact-card / .neu-card--shader)
|
||
const BRAND = {
|
||
tg: ['#0088cc', '#00bcd4', '#0a66c2'],
|
||
vk: ['#0077ff', '#4a90ff', '#0033aa'],
|
||
ya: ['#f8604a', '#ffae00', '#ff3d00'],
|
||
email: ['#7c5cff', '#b06cff', '#5a3fd6'],
|
||
demo: ['#7c5cff', '#00c2c7', '#ff5c8a', '#ffb347'],
|
||
};
|
||
|
||
// Официальная конвертация hex/HSL/RGB -> [r,g,b,a] (0..1), как ожидает u_colors (vec4[]).
|
||
const c4 = (arr) => arr.map(getShaderColorFromString);
|
||
|
||
// Sizing uniforms обязательны (vertex-шейдер делит на u_scale — при 0 будет NaN
|
||
// и весь шейдер чёрный). Берём дефолты из пакета, как в React-обёртке.
|
||
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,
|
||
});
|
||
|
||
// Единственный используемый шейдер: mesh gradient, анимированный.
|
||
const MESH = {
|
||
frag: meshGradientFragmentShader,
|
||
speed: 1.0,
|
||
uniforms: (c) => ({
|
||
...sizing(),
|
||
u_colors: c4(c), u_colorsCount: c.length,
|
||
u_distortion: 1.2, u_swirl: 0.8, u_grainMixer: 0.08, u_grainOverlay: 0.04,
|
||
}),
|
||
};
|
||
|
||
const mounts = new WeakMap();
|
||
|
||
function mountShader(host, colors) {
|
||
const wrap = host.querySelector('.card-shader');
|
||
if (!wrap) return;
|
||
if (mounts.has(host)) { try { mounts.get(host).dispose(); } catch {} mounts.delete(host); }
|
||
while (wrap.firstChild) wrap.removeChild(wrap.firstChild);
|
||
try {
|
||
const m = new ShaderMount(wrap, MESH.frag, MESH.uniforms(colors),
|
||
{ preserveDrawingBuffer: true, antialias: true }, MESH.speed);
|
||
// отключаем IntersectionObserver: иначе вне вьюпорта ставит currentSpeed=0
|
||
// и рендер замирает (у пользователя карточка внизу страницы -> пауза).
|
||
try { m.intersectionObserver?.disconnect(); } catch {}
|
||
mounts.set(host, m);
|
||
} catch (e) {
|
||
console.error('ShaderMount failed', e);
|
||
}
|
||
}
|
||
|
||
function mountAll(root = document) {
|
||
// любые карточки с шейдером: у .card-shader есть data-shader (бренд),
|
||
// сама карточка — его родитель (contact-card, neu-card--shader, shader-cell...)
|
||
root.querySelectorAll('.card-shader[data-shader]').forEach((wrap) => {
|
||
const host = wrap.parentElement;
|
||
const brand = wrap.dataset.shader;
|
||
mountShader(host, BRAND[brand] || BRAND.demo);
|
||
});
|
||
}
|
||
|
||
if (document.readyState === 'loading') {
|
||
document.addEventListener('DOMContentLoaded', mountAll);
|
||
} else {
|
||
mountAll();
|
||
}
|
||
|
||
window.__mountLabShaders = mountAll;
|