Files
site-portfolio/public/_lab/shader-demo.js
T
root a0dda98b83 i18n: build-time ru/en localization + EN homepage & cookie-policy
- 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)
2026-09-15 22:27:51 +02:00

84 lines
3.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Живой прототип 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;