+ Стенды для настройки видa элементов без сборки сайта. Правишь
+ public/css/main.css или файл стенда — страница перезагружается сама (live-reload
+ сервера npm run serve). Когда вид готов, переносишь компонент в
+ src/pages/** и собираешь npm run build:pages.
+
+ Как добавить стенд: скопируй /_lab/template.html в
+ /_lab/<имя>.html, поправь заголовок и секции. Стили компонента — в
+ public/css/main.css, обвязка стенда — в /_lab/lab.css.
+ Переключатель темы подключается автоматически через lab.js.
+
+ Стенд для настройки вида. Сборка не нужна: правим public/css/main.css →
+ страница перезагружается сама (live-reload). Переключатель темы в правом нижнем углу.
+
+
+
+
+
+
+
Финальный вид
+
+ Принятые значения: сила объёма 0.5, блик 0.2, без текстуры.
+ Обычная неуморфная карточка на нейтральном фоне.
+
+
+ Только мета
+
+
+
+ Только заголовок
+
+
+
+ Eyebrow
+ Полный набор
+ Подпись под заголовком
+
+
+
+ Длинный заголовок в несколько строк
+
+ И подпись тоже длинная, чтобы проверить, как карточка растёт по высоте
+
+
+
+
+
+ С иконкой
+ Иконка сверху
+
+
+
+
+ С обложкой
+ Картинка на всю ширину
+
+
+
+ Акцентная
+ --accent
+
+
+
+
+
+
+
Размеры
+
+ Базовый размер задаётся --neu-min-w / --neu-min-h, дальше
+ карточка растёт от содержимого. В сетке — .neu-card--fluid.
+
+ Рядом друг с другом тени соседних карточек накладываются. Здесь видно, не слипаются ли.
+
+
+
Три в ряд, gap 1rem
+
+
+ Проект один
+ Продуктовый дизайн
+
+
+ Проект два
+ Исследование
+
+
+ Проект три
+ Дизайн-система
+
+
+
+
Смешанный рост
+
Разная высота содержимого — проверка на растяжение по сетке.
+
+
+ Короткая
+
+
+ Средняя по объёму
+
+ Здесь подпись занимает две строки, и карточка должна потянуться выше соседней
+
+
+
+ С обложкой
+ Ещё выше
+
+
+
+
+
+
+
Карточка в карточке
+
+ Нижняя карточка больше и включает в себя верхние. Оценка того, как объём читается, когда
+ карточка лежит поверх той же самой поверхности, а не на фоне страницы. Это худший
+ случай для неуморфизма: обе поверхности одинаковые по цвету.
+
+
+
+
+ Внешняя
+ Карточка-контейнер
+ Поверхность второго уровня, вложенные карточки внутри
+
+
+
+ Вложенная
+ Тот же --neu-bg
+
+
+ Вложенная
+ Тот же --neu-bg
+
+
+ Вложенная
+ Тот же --neu-bg
+
+
+
+
+
+
+
+
+
+
+
То же в светлой теме
+
+ Внутренние блики настроены раздельно для тёмной и светлой темы — здесь видна разница.
+
+
+
+
+
+
Светлая тема · тень
+
+ На светлом фоне чистый чёрный 0.25 выглядит грязно. Варианты: цвет тени смещён в
+ холодный тёмно-серый, альфа внешних теней снижена через --neu-drop-mult.
+ Эта секция принудительно светлая.
+
+
+
+
+ Текущий
+ 47,53,75 · drop 0.5
+
+
как в main.css
+
+
+
+ Мягче
+ 70,75,95 · drop 0.4
+
+
светлее тень
+
+
+
+ Глубже
+ 30,35,55 · drop 0.35
+
+
темнее, но меньше альфа
+
+
+
+ Чёрный
+ 0,0,0 · drop 0.5
+
+
было грязно
+
+
+
+
+
+
+
Контактные карточки
+
+ Оригинальная .contact-card с сайта, теперь с неуморфной основой
+ (объём как у .neu-card). Бренд-цвета через модификаторы
+ --brand-tg / --brand-vk / --brand-ya / --brand-email.
+ Цветное наложение — радиальный градиент ::before из --contact-brand.
+
+ Обычная неуморфная карточка (тени снаружи, блик из угла) с живым шейдером
+ внутри, под бликом. Слева — чистая neu-card для сравнения,
+ справа — та же карточка с анимированным mesh-слоем. Объём и блик сохранены,
+ шейдер только подсвечивает поверхность.
+
+
+
Сетка 3 × 2
+
+
+
+ Проект
+ Обычная
+ только тени и блик
+
+
+
+ Проект
+ Шейдер · demo
+ mesh под бликом
+
+
+
+ Проект
+ Шейдер · email
+ mesh под бликом
+
+
+
+
+
+ Проект
+ Шейдер · tg
+ mesh под бликом
+
+
+
+ Проект
+ Шейдер · vk
+ mesh под бликом
+
+
+
+ Проект
+ Шейдер · ya
+ mesh под бликом
+
+
+
+
Одиночная крупная
+
+
+
+ Кейс
+ Neu + shader
+ объём снаружи, анимация внутри
+
+
+
+ Кейс
+ Static + shader
+ блик и анимация, без hover-отклика
+
+
+
+
+
+
+
Mesh gradient
+
+ Единственный используемый шейдер — анимированный mesh gradient (цветные пятна
+ плавно перетекают). Ниже — карточка-пример только с ним. Контактные и матричные
+ карточки выше используют тот же шейдер с брендовыми цветами.
+
+
+
Пример · mesh gradient
+
+
+
+ mesh gradient
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/_lab/noise-test.html b/public/_lab/noise-test.html
new file mode 100644
index 0000000..d44965c
--- /dev/null
+++ b/public/_lab/noise-test.html
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/public/_lab/paper-bundle.js b/public/_lab/paper-bundle.js
new file mode 100644
index 0000000..6d8c788
--- /dev/null
+++ b/public/_lab/paper-bundle.js
@@ -0,0 +1,961 @@
+// node_modules/@paper-design/shaders/dist/vertex-shader.js
+var vertexShaderSource = `#version 300 es
+precision mediump float;
+
+layout(location = 0) in vec4 a_position;
+
+uniform vec2 u_resolution;
+uniform float u_pixelRatio;
+uniform float u_imageAspectRatio;
+uniform float u_originX;
+uniform float u_originY;
+uniform float u_worldWidth;
+uniform float u_worldHeight;
+uniform float u_fit;
+uniform float u_scale;
+uniform float u_rotation;
+uniform float u_offsetX;
+uniform float u_offsetY;
+
+out vec2 v_objectUV;
+out vec2 v_objectBoxSize;
+out vec2 v_responsiveUV;
+out vec2 v_responsiveBoxGivenSize;
+out vec2 v_patternUV;
+out vec2 v_patternBoxSize;
+out vec2 v_imageUV;
+
+vec3 getBoxSize(float boxRatio, vec2 givenBoxSize) {
+ vec2 box = vec2(0.);
+ // fit = none
+ box.x = boxRatio * min(givenBoxSize.x / boxRatio, givenBoxSize.y);
+ float noFitBoxWidth = box.x;
+ if (u_fit == 1.) { // fit = contain
+ box.x = boxRatio * min(u_resolution.x / boxRatio, u_resolution.y);
+ } else if (u_fit == 2.) { // fit = cover
+ box.x = boxRatio * max(u_resolution.x / boxRatio, u_resolution.y);
+ }
+ box.y = box.x / boxRatio;
+ return vec3(box, noFitBoxWidth);
+}
+
+void main() {
+ gl_Position = a_position;
+
+ vec2 uv = gl_Position.xy * .5;
+ vec2 boxOrigin = vec2(.5 - u_originX, u_originY - .5);
+ vec2 givenBoxSize = vec2(u_worldWidth, u_worldHeight);
+ givenBoxSize = max(givenBoxSize, vec2(1.)) * u_pixelRatio;
+ float r = u_rotation * 3.14159265358979323846 / 180.;
+ mat2 graphicRotation = mat2(cos(r), sin(r), -sin(r), cos(r));
+ vec2 graphicOffset = vec2(-u_offsetX, u_offsetY);
+
+
+ // ===================================================
+
+ float fixedRatio = 1.;
+ vec2 fixedRatioBoxGivenSize = vec2(
+ (u_worldWidth == 0.) ? u_resolution.x : givenBoxSize.x,
+ (u_worldHeight == 0.) ? u_resolution.y : givenBoxSize.y
+ );
+
+ v_objectBoxSize = getBoxSize(fixedRatio, fixedRatioBoxGivenSize).xy;
+ vec2 objectWorldScale = u_resolution.xy / v_objectBoxSize;
+
+ v_objectUV = uv;
+ v_objectUV *= objectWorldScale;
+ v_objectUV += boxOrigin * (objectWorldScale - 1.);
+ v_objectUV += graphicOffset;
+ v_objectUV /= u_scale;
+ v_objectUV = graphicRotation * v_objectUV;
+
+ // ===================================================
+
+ v_responsiveBoxGivenSize = vec2(
+ (u_worldWidth == 0.) ? u_resolution.x : givenBoxSize.x,
+ (u_worldHeight == 0.) ? u_resolution.y : givenBoxSize.y
+ );
+ float responsiveRatio = v_responsiveBoxGivenSize.x / v_responsiveBoxGivenSize.y;
+ vec2 responsiveBoxSize = getBoxSize(responsiveRatio, v_responsiveBoxGivenSize).xy;
+ vec2 responsiveBoxScale = u_resolution.xy / responsiveBoxSize;
+
+ #ifdef ADD_HELPERS
+ v_responsiveHelperBox = uv;
+ v_responsiveHelperBox *= responsiveBoxScale;
+ v_responsiveHelperBox += boxOrigin * (responsiveBoxScale - 1.);
+ #endif
+
+ v_responsiveUV = uv;
+ v_responsiveUV *= responsiveBoxScale;
+ v_responsiveUV += boxOrigin * (responsiveBoxScale - 1.);
+ v_responsiveUV += graphicOffset;
+ v_responsiveUV /= u_scale;
+ v_responsiveUV.x *= responsiveRatio;
+ v_responsiveUV = graphicRotation * v_responsiveUV;
+ v_responsiveUV.x /= responsiveRatio;
+
+ // ===================================================
+
+ float patternBoxRatio = givenBoxSize.x / givenBoxSize.y;
+ vec2 patternBoxGivenSize = vec2(
+ (u_worldWidth == 0.) ? u_resolution.x : givenBoxSize.x,
+ (u_worldHeight == 0.) ? u_resolution.y : givenBoxSize.y
+ );
+ patternBoxRatio = patternBoxGivenSize.x / patternBoxGivenSize.y;
+
+ vec3 boxSizeData = getBoxSize(patternBoxRatio, patternBoxGivenSize);
+ v_patternBoxSize = boxSizeData.xy;
+ float patternBoxNoFitBoxWidth = boxSizeData.z;
+ vec2 patternBoxScale = u_resolution.xy / v_patternBoxSize;
+
+ v_patternUV = uv;
+ v_patternUV += graphicOffset / patternBoxScale;
+ v_patternUV += boxOrigin;
+ v_patternUV -= boxOrigin / patternBoxScale;
+ v_patternUV *= u_resolution.xy;
+ v_patternUV /= u_pixelRatio;
+ if (u_fit > 0.) {
+ v_patternUV *= (patternBoxNoFitBoxWidth / v_patternBoxSize.x);
+ }
+ v_patternUV /= u_scale;
+ v_patternUV = graphicRotation * v_patternUV;
+ v_patternUV += boxOrigin / patternBoxScale;
+ v_patternUV -= boxOrigin;
+ // x100 is a default multiplier between vertex and fragmant shaders
+ // we use it to avoid UV presision issues
+ v_patternUV *= .01;
+
+ // ===================================================
+
+ vec2 imageBoxSize;
+ if (u_fit == 1.) { // contain
+ imageBoxSize.x = min(u_resolution.x / u_imageAspectRatio, u_resolution.y) * u_imageAspectRatio;
+ } else if (u_fit == 2.) { // cover
+ imageBoxSize.x = max(u_resolution.x / u_imageAspectRatio, u_resolution.y) * u_imageAspectRatio;
+ } else {
+ imageBoxSize.x = min(10.0, 10.0 / u_imageAspectRatio * u_imageAspectRatio);
+ }
+ imageBoxSize.y = imageBoxSize.x / u_imageAspectRatio;
+ vec2 imageBoxScale = u_resolution.xy / imageBoxSize;
+
+ v_imageUV = uv;
+ v_imageUV *= imageBoxScale;
+ v_imageUV += boxOrigin * (imageBoxScale - 1.);
+ v_imageUV += graphicOffset;
+ v_imageUV /= u_scale;
+ v_imageUV.x *= u_imageAspectRatio;
+ v_imageUV = graphicRotation * v_imageUV;
+ v_imageUV.x /= u_imageAspectRatio;
+
+ v_imageUV += .5;
+ v_imageUV.y = 1. - v_imageUV.y;
+}`;
+
+// node_modules/@paper-design/shaders/dist/shader-mount.js
+var DEFAULT_MAX_PIXEL_COUNT = 1920 * 1080 * 4;
+var ShaderMount = class {
+ parentElement;
+ canvasElement;
+ gl;
+ program = null;
+ uniformLocations = {};
+ /** The fragment shader that we are using */
+ fragmentShader;
+ /** Stores the RAF for the render loop */
+ rafId = null;
+ /** Time of the last rendered frame */
+ lastRenderTime = 0;
+ /** Total time that we have played any animation, passed as a uniform to the shader for time-based VFX */
+ currentFrame = 0;
+ /** The speed that we progress through animation time (multiplies by delta time every update). Allows negatives to play in reverse. If set to 0, rAF will stop entirely so static shaders have no recurring performance costs */
+ speed = 0;
+ /** Actual speed used that accounts for visibility: we pause the shader if the tab is hidden or the element out of the viewport */
+ currentSpeed = 0;
+ /** Uniforms that are provided by the user for the specific shader being mounted (not including uniforms that this Mount adds, like time and resolution) */
+ providedUniforms;
+ /** Names of the uniforms that should have mipmaps generated for them */
+ mipmaps = [];
+ /** Just a sanity check to make sure frames don't run after we're disposed */
+ hasBeenDisposed = false;
+ /** If the resolution of the canvas has changed since the last render */
+ resolutionChanged = true;
+ /** Store textures that are provided by the user */
+ textures = /* @__PURE__ */ new Map();
+ minPixelRatio;
+ maxPixelCount;
+ isSafari = isSafari();
+ uniformCache = {};
+ textureUnitMap = /* @__PURE__ */ new Map();
+ ownerDocument;
+ constructor(parentElement, fragmentShader, uniforms, webGlContextAttributes, speed = 0, frame = 0, minPixelRatio = 2, maxPixelCount = DEFAULT_MAX_PIXEL_COUNT, mipmaps = []) {
+ if (parentElement?.nodeType === 1) {
+ this.parentElement = parentElement;
+ } else {
+ throw new Error("Paper Shaders: parent element must be an HTMLElement");
+ }
+ this.ownerDocument = parentElement.ownerDocument;
+ if (!this.ownerDocument.querySelector("style[data-paper-shader]")) {
+ const styleElement = this.ownerDocument.createElement("style");
+ styleElement.innerHTML = defaultStyle;
+ styleElement.setAttribute("data-paper-shader", "");
+ this.ownerDocument.head.prepend(styleElement);
+ }
+ const canvasElement = this.ownerDocument.createElement("canvas");
+ this.canvasElement = canvasElement;
+ this.parentElement.prepend(canvasElement);
+ this.fragmentShader = fragmentShader;
+ this.providedUniforms = uniforms;
+ this.mipmaps = mipmaps;
+ this.currentFrame = frame;
+ this.minPixelRatio = minPixelRatio;
+ this.maxPixelCount = maxPixelCount;
+ const gl = canvasElement.getContext("webgl2", webGlContextAttributes);
+ if (!gl) {
+ throw new Error("Paper Shaders: WebGL is not supported in this browser");
+ }
+ this.gl = gl;
+ this.initProgram();
+ this.setupPositionAttribute();
+ this.setupUniforms();
+ this.setUniformValues(this.providedUniforms);
+ this.setupResizeObserver();
+ visualViewport?.addEventListener("resize", this.handleVisualViewportChange);
+ this.setupIntersectionObserver();
+ this.setSpeed(speed);
+ this.parentElement.setAttribute("data-paper-shader", "");
+ this.parentElement.paperShaderMount = this;
+ this.ownerDocument.addEventListener("visibilitychange", this.handleDocumentVisibilityChange);
+ }
+ initProgram = () => {
+ const program = createProgram(this.gl, vertexShaderSource, this.fragmentShader);
+ if (!program) return;
+ this.program = program;
+ };
+ setupPositionAttribute = () => {
+ const positionAttributeLocation = this.gl.getAttribLocation(this.program, "a_position");
+ const positionBuffer = this.gl.createBuffer();
+ this.gl.bindBuffer(this.gl.ARRAY_BUFFER, positionBuffer);
+ const positions = [-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1];
+ this.gl.bufferData(this.gl.ARRAY_BUFFER, new Float32Array(positions), this.gl.STATIC_DRAW);
+ this.gl.enableVertexAttribArray(positionAttributeLocation);
+ this.gl.vertexAttribPointer(positionAttributeLocation, 2, this.gl.FLOAT, false, 0, 0);
+ };
+ setupUniforms = () => {
+ const uniformLocations = {
+ u_time: this.gl.getUniformLocation(this.program, "u_time"),
+ u_pixelRatio: this.gl.getUniformLocation(this.program, "u_pixelRatio"),
+ u_resolution: this.gl.getUniformLocation(this.program, "u_resolution")
+ };
+ Object.entries(this.providedUniforms).forEach(([key, value]) => {
+ uniformLocations[key] = this.gl.getUniformLocation(this.program, key);
+ if (value instanceof HTMLImageElement) {
+ const aspectRatioUniformName = `${key}AspectRatio`;
+ uniformLocations[aspectRatioUniformName] = this.gl.getUniformLocation(this.program, aspectRatioUniformName);
+ }
+ });
+ this.uniformLocations = uniformLocations;
+ };
+ /**
+ * The scale that we should render at.
+ * - Used to target 2x rendering even on 1x screens for better antialiasing
+ * - Prevents the virtual resolution from going beyond the maximum resolution
+ * - Accounts for the page zoom level so we render in physical device pixels rather than CSS pixels
+ */
+ renderScale = 1;
+ parentWidth = 0;
+ parentHeight = 0;
+ parentDevicePixelWidth = 0;
+ parentDevicePixelHeight = 0;
+ devicePixelsSupported = false;
+ intersectionObserver = null;
+ isInViewport = true;
+ resizeObserver = null;
+ setupResizeObserver = () => {
+ this.resizeObserver = new ResizeObserver(([entry]) => {
+ if (entry?.borderBoxSize[0]) {
+ const physicalPixelSize = entry.devicePixelContentBoxSize?.[0];
+ if (physicalPixelSize !== void 0) {
+ this.devicePixelsSupported = true;
+ this.parentDevicePixelWidth = physicalPixelSize.inlineSize;
+ this.parentDevicePixelHeight = physicalPixelSize.blockSize;
+ }
+ this.parentWidth = entry.borderBoxSize[0].inlineSize;
+ this.parentHeight = entry.borderBoxSize[0].blockSize;
+ }
+ this.handleResize();
+ });
+ this.resizeObserver.observe(this.parentElement);
+ };
+ setupIntersectionObserver = () => {
+ const view = this.ownerDocument.defaultView;
+ if (!view?.IntersectionObserver) return;
+ this.intersectionObserver = new view.IntersectionObserver(([entry]) => {
+ this.isInViewport = entry?.isIntersecting ?? true;
+ this.updateCurrentSpeed();
+ });
+ this.intersectionObserver.observe(this.parentElement);
+ };
+ // Visual viewport resize handler, mainly used to react to browser zoom changes.
+ // Resize observer by itself does not react to pinch zoom, and although it usually
+ // reacts to classic browser zoom, it's not guaranteed in edge cases.
+ // Since timing between visual viewport changes and resize observer is complex
+ // and because we'd like to know the device pixel sizes of elements, we just restart
+ // the observer to get a guaranteed fresh callback regardless if it would have triggered or not.
+ handleVisualViewportChange = () => {
+ this.resizeObserver?.disconnect();
+ this.setupResizeObserver();
+ };
+ /** Resize handler for when the container div changes size or the max pixel count changes and we want to resize our canvas to match */
+ handleResize = () => {
+ let targetPixelWidth = 0;
+ let targetPixelHeight = 0;
+ const dpr = Math.max(1, window.devicePixelRatio);
+ const pinchZoom = visualViewport?.scale ?? 1;
+ if (this.devicePixelsSupported) {
+ const scaleToMeetMinPixelRatio = Math.max(1, this.minPixelRatio / dpr);
+ targetPixelWidth = this.parentDevicePixelWidth * scaleToMeetMinPixelRatio * pinchZoom;
+ targetPixelHeight = this.parentDevicePixelHeight * scaleToMeetMinPixelRatio * pinchZoom;
+ } else {
+ let targetRenderScale = Math.max(dpr, this.minPixelRatio) * pinchZoom;
+ if (this.isSafari) {
+ const zoomLevel = bestGuessBrowserZoom(this.ownerDocument);
+ targetRenderScale *= Math.max(1, zoomLevel);
+ }
+ targetPixelWidth = Math.round(this.parentWidth) * targetRenderScale;
+ targetPixelHeight = Math.round(this.parentHeight) * targetRenderScale;
+ }
+ const maxPixelCountHeadroom = Math.sqrt(this.maxPixelCount) / Math.sqrt(targetPixelWidth * targetPixelHeight);
+ const scaleToMeetMaxPixelCount = Math.min(1, maxPixelCountHeadroom);
+ const newWidth = Math.round(targetPixelWidth * scaleToMeetMaxPixelCount);
+ const newHeight = Math.round(targetPixelHeight * scaleToMeetMaxPixelCount);
+ const newRenderScale = newWidth / Math.round(this.parentWidth);
+ if (this.canvasElement.width !== newWidth || this.canvasElement.height !== newHeight || this.renderScale !== newRenderScale) {
+ this.renderScale = newRenderScale;
+ this.canvasElement.width = newWidth;
+ this.canvasElement.height = newHeight;
+ this.resolutionChanged = true;
+ this.gl.viewport(0, 0, this.gl.canvas.width, this.gl.canvas.height);
+ this.render(performance.now());
+ }
+ };
+ render = (currentTime) => {
+ if (this.hasBeenDisposed) return;
+ if (this.program === null) {
+ console.warn("Tried to render before program or gl was initialized");
+ return;
+ }
+ const dt = currentTime - this.lastRenderTime;
+ this.lastRenderTime = currentTime;
+ if (this.currentSpeed !== 0) {
+ this.currentFrame += dt * this.currentSpeed;
+ }
+ this.gl.clear(this.gl.COLOR_BUFFER_BIT);
+ this.gl.useProgram(this.program);
+ this.gl.uniform1f(this.uniformLocations.u_time, this.currentFrame * 1e-3);
+ if (this.resolutionChanged) {
+ this.gl.uniform2f(this.uniformLocations.u_resolution, this.gl.canvas.width, this.gl.canvas.height);
+ this.gl.uniform1f(this.uniformLocations.u_pixelRatio, this.renderScale);
+ this.resolutionChanged = false;
+ }
+ this.gl.drawArrays(this.gl.TRIANGLES, 0, 6);
+ if (this.currentSpeed !== 0) {
+ this.requestRender();
+ } else {
+ this.rafId = null;
+ }
+ };
+ requestRender = () => {
+ if (this.rafId !== null) {
+ cancelAnimationFrame(this.rafId);
+ }
+ this.rafId = requestAnimationFrame(this.render);
+ };
+ /** Creates a texture from an image and sets it into a uniform value */
+ setTextureUniform = (uniformName, image) => {
+ if (!image.complete || image.naturalWidth === 0) {
+ throw new Error(`Paper Shaders: image for uniform ${uniformName} must be fully loaded`);
+ }
+ const existingTexture = this.textures.get(uniformName);
+ if (existingTexture) {
+ this.gl.deleteTexture(existingTexture);
+ }
+ if (!this.textureUnitMap.has(uniformName)) {
+ this.textureUnitMap.set(uniformName, this.textureUnitMap.size);
+ }
+ const textureUnit = this.textureUnitMap.get(uniformName);
+ this.gl.activeTexture(this.gl.TEXTURE0 + textureUnit);
+ const texture = this.gl.createTexture();
+ this.gl.bindTexture(this.gl.TEXTURE_2D, texture);
+ this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE);
+ this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE);
+ this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.LINEAR);
+ this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.LINEAR);
+ this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGBA, this.gl.RGBA, this.gl.UNSIGNED_BYTE, image);
+ if (this.mipmaps.includes(uniformName)) {
+ this.gl.generateMipmap(this.gl.TEXTURE_2D);
+ this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.LINEAR_MIPMAP_LINEAR);
+ }
+ const error = this.gl.getError();
+ if (error !== this.gl.NO_ERROR || texture === null) {
+ console.error("Paper Shaders: WebGL error when uploading texture:", error);
+ return;
+ }
+ this.textures.set(uniformName, texture);
+ const location = this.uniformLocations[uniformName];
+ if (location) {
+ this.gl.uniform1i(location, textureUnit);
+ const aspectRatioUniformName = `${uniformName}AspectRatio`;
+ const aspectRatioLocation = this.uniformLocations[aspectRatioUniformName];
+ if (aspectRatioLocation) {
+ const aspectRatio = image.naturalWidth / image.naturalHeight;
+ this.gl.uniform1f(aspectRatioLocation, aspectRatio);
+ }
+ }
+ };
+ /** Utility: recursive equality test for all the uniforms */
+ areUniformValuesEqual = (a, b) => {
+ if (a === b) return true;
+ if (Array.isArray(a) && Array.isArray(b) && a.length === b.length) {
+ return a.every((val, i) => this.areUniformValuesEqual(val, b[i]));
+ }
+ return false;
+ };
+ /** Sets the provided uniform values into the WebGL program, can be a partial list of uniforms that have changed */
+ setUniformValues = (updatedUniforms) => {
+ this.gl.useProgram(this.program);
+ Object.entries(updatedUniforms).forEach(([key, value]) => {
+ let cacheValue = value;
+ if (value instanceof HTMLImageElement) {
+ cacheValue = `${value.src.slice(0, 200)}|${value.naturalWidth}x${value.naturalHeight}`;
+ }
+ if (this.areUniformValuesEqual(this.uniformCache[key], cacheValue)) return;
+ this.uniformCache[key] = cacheValue;
+ const location = this.uniformLocations[key];
+ if (!location) {
+ console.warn(`Uniform location for ${key} not found`);
+ return;
+ }
+ if (value instanceof HTMLImageElement) {
+ this.setTextureUniform(key, value);
+ } else if (Array.isArray(value)) {
+ let flatArray = null;
+ let valueLength = null;
+ if (value[0] !== void 0 && Array.isArray(value[0])) {
+ const firstChildLength = value[0].length;
+ if (value.every((arr) => arr.length === firstChildLength)) {
+ flatArray = value.flat();
+ valueLength = firstChildLength;
+ } else {
+ console.warn(`All child arrays must be the same length for ${key}`);
+ return;
+ }
+ } else {
+ flatArray = value;
+ valueLength = flatArray.length;
+ }
+ switch (valueLength) {
+ case 2:
+ this.gl.uniform2fv(location, flatArray);
+ break;
+ case 3:
+ this.gl.uniform3fv(location, flatArray);
+ break;
+ case 4:
+ this.gl.uniform4fv(location, flatArray);
+ break;
+ case 9:
+ this.gl.uniformMatrix3fv(location, false, flatArray);
+ break;
+ case 16:
+ this.gl.uniformMatrix4fv(location, false, flatArray);
+ break;
+ default:
+ console.warn(`Unsupported uniform array length: ${valueLength}`);
+ }
+ } else if (typeof value === "number") {
+ this.gl.uniform1f(location, value);
+ } else if (typeof value === "boolean") {
+ this.gl.uniform1i(location, value ? 1 : 0);
+ } else {
+ console.warn(`Unsupported uniform type for ${key}: ${typeof value}`);
+ }
+ });
+ };
+ /** Gets the current total animation time from 0ms */
+ getCurrentFrame = () => {
+ return this.currentFrame;
+ };
+ /** Set a frame to get a deterministic result, frames are literally just milliseconds from zero since the animation started */
+ setFrame = (newFrame) => {
+ this.currentFrame = newFrame;
+ this.lastRenderTime = performance.now();
+ this.render(performance.now());
+ };
+ /** Set an animation speed (or 0 to stop animation) */
+ setSpeed = (newSpeed = 1) => {
+ this.speed = newSpeed;
+ this.updateCurrentSpeed();
+ };
+ /** Apply the target speed, pausing (0) while the tab is hidden or the element is out of the viewport */
+ updateCurrentSpeed = () => {
+ this.setCurrentSpeed(this.ownerDocument.hidden || !this.isInViewport ? 0 : this.speed);
+ };
+ setCurrentSpeed = (newSpeed) => {
+ this.currentSpeed = newSpeed;
+ if (this.rafId === null && newSpeed !== 0) {
+ this.lastRenderTime = performance.now();
+ this.rafId = requestAnimationFrame(this.render);
+ }
+ if (this.rafId !== null && newSpeed === 0) {
+ cancelAnimationFrame(this.rafId);
+ this.rafId = null;
+ }
+ };
+ /** Set the maximum pixel count for the shader, this will limit the number of pixels that will be rendered */
+ setMaxPixelCount = (newMaxPixelCount = DEFAULT_MAX_PIXEL_COUNT) => {
+ this.maxPixelCount = newMaxPixelCount;
+ this.handleResize();
+ };
+ /** Set the minimum pixel ratio for the shader */
+ setMinPixelRatio = (newMinPixelRatio = 2) => {
+ this.minPixelRatio = newMinPixelRatio;
+ this.handleResize();
+ };
+ /** Update the uniforms that are provided by the outside shader, can be a partial set with only the uniforms that have changed */
+ setUniforms = (newUniforms) => {
+ this.setUniformValues(newUniforms);
+ this.providedUniforms = { ...this.providedUniforms, ...newUniforms };
+ this.render(performance.now());
+ };
+ handleDocumentVisibilityChange = () => {
+ this.updateCurrentSpeed();
+ };
+ /** Dispose of the shader mount, cleaning up all of the WebGL resources */
+ dispose = () => {
+ this.hasBeenDisposed = true;
+ if (this.rafId !== null) {
+ cancelAnimationFrame(this.rafId);
+ this.rafId = null;
+ }
+ if (this.gl && this.program) {
+ this.textures.forEach((texture) => {
+ this.gl.deleteTexture(texture);
+ });
+ this.textures.clear();
+ this.gl.deleteProgram(this.program);
+ this.program = null;
+ this.gl.bindBuffer(this.gl.ARRAY_BUFFER, null);
+ this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, null);
+ this.gl.bindRenderbuffer(this.gl.RENDERBUFFER, null);
+ this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, null);
+ this.gl.getError();
+ }
+ if (this.resizeObserver) {
+ this.resizeObserver.disconnect();
+ this.resizeObserver = null;
+ }
+ if (this.intersectionObserver) {
+ this.intersectionObserver.disconnect();
+ this.intersectionObserver = null;
+ }
+ visualViewport?.removeEventListener("resize", this.handleVisualViewportChange);
+ this.ownerDocument.removeEventListener("visibilitychange", this.handleDocumentVisibilityChange);
+ this.uniformLocations = {};
+ this.canvasElement.remove();
+ delete this.parentElement.paperShaderMount;
+ };
+};
+function createShader(gl, type, source) {
+ const shader = gl.createShader(type);
+ if (!shader) return null;
+ gl.shaderSource(shader, source);
+ gl.compileShader(shader);
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
+ console.error("An error occurred compiling the shaders: " + gl.getShaderInfoLog(shader));
+ gl.deleteShader(shader);
+ return null;
+ }
+ return shader;
+}
+function createProgram(gl, vertexShaderSource2, fragmentShaderSource) {
+ const format = gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.MEDIUM_FLOAT);
+ const precision = format ? format.precision : null;
+ if (precision && precision < 23) {
+ vertexShaderSource2 = vertexShaderSource2.replace(/precision\s+(lowp|mediump)\s+float;/g, "precision highp float;");
+ fragmentShaderSource = fragmentShaderSource.replace(/precision\s+(lowp|mediump)\s+float/g, "precision highp float").replace(/\b(uniform|varying|attribute)\s+(lowp|mediump)\s+(\w+)/g, "$1 highp $3");
+ }
+ const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource2);
+ const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);
+ if (!vertexShader || !fragmentShader) return null;
+ const program = gl.createProgram();
+ if (!program) return null;
+ gl.attachShader(program, vertexShader);
+ gl.attachShader(program, fragmentShader);
+ gl.linkProgram(program);
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
+ console.error("Unable to initialize the shader program: " + gl.getProgramInfoLog(program));
+ gl.deleteProgram(program);
+ gl.deleteShader(vertexShader);
+ gl.deleteShader(fragmentShader);
+ return null;
+ }
+ gl.detachShader(program, vertexShader);
+ gl.detachShader(program, fragmentShader);
+ gl.deleteShader(vertexShader);
+ gl.deleteShader(fragmentShader);
+ return program;
+}
+var defaultStyle = `@layer paper-shaders {
+ :where([data-paper-shader]) {
+ isolation: isolate;
+ position: relative;
+
+ & canvas {
+ contain: strict;
+ display: block;
+ position: absolute;
+ inset: 0;
+ z-index: -1;
+ width: 100%;
+ height: 100%;
+ border-radius: inherit;
+ corner-shape: inherit;
+ }
+ }
+}`;
+function isSafari() {
+ const ua = navigator.userAgent.toLowerCase();
+ return ua.includes("safari") && !ua.includes("chrome") && !ua.includes("android");
+}
+function bestGuessBrowserZoom(doc) {
+ const viewportScale = visualViewport?.scale ?? 1;
+ const viewportWidth = visualViewport?.width ?? window.innerWidth;
+ const scrollbarWidth = window.innerWidth - doc.documentElement.clientWidth;
+ const innerWidth = viewportScale * viewportWidth + scrollbarWidth;
+ const ratio = outerWidth / innerWidth;
+ const zoomPercentageRounded = Math.round(100 * ratio);
+ if (zoomPercentageRounded % 5 === 0) {
+ return zoomPercentageRounded / 100;
+ }
+ if (zoomPercentageRounded === 33) {
+ return 1 / 3;
+ }
+ if (zoomPercentageRounded === 67) {
+ return 2 / 3;
+ }
+ if (zoomPercentageRounded === 133) {
+ return 4 / 3;
+ }
+ return ratio;
+}
+
+// node_modules/@paper-design/shaders/dist/shader-utils.js
+var declarePI = `
+#define TWO_PI 6.28318530718
+#define PI 3.14159265358979323846
+`;
+var rotation2 = `
+vec2 rotate(vec2 uv, float th) {
+ return mat2(cos(th), sin(th), -sin(th), cos(th)) * uv;
+}
+`;
+var textureRandomizerR = `
+ float randomR(vec2 p) {
+ vec2 uv = floor(p) / 100. + .5;
+ return texture(u_noiseTexture, fract(uv)).r;
+ }
+`;
+var fiberNoise = `
+float fiberRandom(vec2 p) {
+ vec2 uv = floor(p) / 100.;
+ return texture(u_noiseTexture, fract(uv)).b;
+}
+
+float fiberValueNoise(vec2 st) {
+ vec2 i = floor(st);
+ vec2 f = fract(st);
+ float a = fiberRandom(i);
+ float b = fiberRandom(i + vec2(1.0, 0.0));
+ float c = fiberRandom(i + vec2(0.0, 1.0));
+ float d = fiberRandom(i + vec2(1.0, 1.0));
+ vec2 u = f * f * (3.0 - 2.0 * f);
+ float x1 = mix(a, b, u.x);
+ float x2 = mix(c, d, u.x);
+ return mix(x1, x2, u.y);
+}
+
+float fiberNoiseFbm(in vec2 n, vec2 seedOffset) {
+ float total = 0.0, amplitude = 1.;
+ for (int i = 0; i < 4; i++) {
+ n = rotate(n, .7);
+ total += fiberValueNoise(n + seedOffset) * amplitude;
+ n *= 2.;
+ amplitude *= 0.6;
+ }
+ return total;
+}
+
+float fiberNoise(vec2 uv, vec2 seedOffset) {
+ float epsilon = 0.001;
+ float n1 = fiberNoiseFbm(uv + vec2(epsilon, 0.0), seedOffset);
+ float n2 = fiberNoiseFbm(uv - vec2(epsilon, 0.0), seedOffset);
+ float n3 = fiberNoiseFbm(uv + vec2(0.0, epsilon), seedOffset);
+ float n4 = fiberNoiseFbm(uv - vec2(0.0, epsilon), seedOffset);
+ return length(vec2(n1 - n2, n3 - n4)) / (2.0 * epsilon);
+}
+`;
+
+// node_modules/@paper-design/shaders/dist/shaders/paper-texture.js
+var paperTextureFragmentShader = `#version 300 es
+precision mediump float;
+
+uniform vec2 u_resolution;
+uniform float u_pixelRatio;
+
+uniform vec4 u_colorFront;
+uniform vec4 u_colorBack;
+
+uniform sampler2D u_image;
+uniform float u_imageAspectRatio;
+
+uniform float u_contrast;
+uniform float u_roughness;
+uniform float u_fiber;
+uniform float u_fiberSize;
+uniform float u_crumples;
+uniform float u_crumpleSize;
+uniform float u_folds;
+uniform float u_foldCount;
+uniform float u_drops;
+uniform float u_seed;
+uniform float u_fade;
+
+uniform sampler2D u_noiseTexture;
+
+in vec2 v_imageUV;
+
+out vec4 fragColor;
+
+float getUvFrame(vec2 uv) {
+ float aax = 2. * fwidth(uv.x);
+ float aay = 2. * fwidth(uv.y);
+
+ float left = smoothstep(0., aax, uv.x);
+ float right = 1. - smoothstep(1. - aax, 1., uv.x);
+ float bottom = smoothstep(0., aay, uv.y);
+ float top = 1. - smoothstep(1. - aay, 1., uv.y);
+
+ return left * right * bottom * top;
+}
+
+${declarePI}
+${rotation2}
+${textureRandomizerR}
+float valueNoise(vec2 st) {
+ vec2 i = floor(st);
+ vec2 f = fract(st);
+ float a = randomR(i);
+ float b = randomR(i + vec2(1.0, 0.0));
+ float c = randomR(i + vec2(0.0, 1.0));
+ float d = randomR(i + vec2(1.0, 1.0));
+ vec2 u = f * f * (3.0 - 2.0 * f);
+ float x1 = mix(a, b, u.x);
+ float x2 = mix(c, d, u.x);
+ return mix(x1, x2, u.y);
+}
+float fbm(vec2 n) {
+ float total = 0.0, amplitude = .4;
+ for (int i = 0; i < 3; i++) {
+ total += valueNoise(n) * amplitude;
+ n *= 1.99;
+ amplitude *= 0.65;
+ }
+ return total;
+}
+
+
+float randomG(vec2 p) {
+ vec2 uv = floor(p) / 50. + .5;
+ return texture(u_noiseTexture, fract(uv)).g;
+}
+float roughness(vec2 p) {
+ p *= .1;
+ float o = 0.;
+ for (float i = 0.; ++i < 4.; p *= 2.1) {
+ vec4 w = vec4(floor(p), ceil(p));
+ vec2 f = fract(p);
+ o += mix(
+ mix(randomG(w.xy), randomG(w.xw), f.y),
+ mix(randomG(w.zy), randomG(w.zw), f.y),
+ f.x);
+ o += .2 / exp(2. * abs(sin(.2 * p.x + .5 * p.y)));
+ }
+ return o / 3.;
+}
+
+${fiberNoise}
+
+vec2 randomGB(vec2 p) {
+ vec2 uv = floor(p) / 50. + .5;
+ return texture(u_noiseTexture, fract(uv)).gb;
+}
+float crumpledNoise(vec2 t, float pw) {
+ vec2 p = floor(t);
+ float wsum = 0.;
+ float cl = 0.;
+ for (int y = -1; y < 2; y += 1) {
+ for (int x = -1; x < 2; x += 1) {
+ vec2 b = vec2(float(x), float(y));
+ vec2 q = b + p;
+ vec2 q2 = q - floor(q / 8.) * 8.;
+ vec2 c = q + randomGB(q2);
+ vec2 r = c - t;
+ float w = pow(smoothstep(0., 1., 1. - abs(r.x)), pw) * pow(smoothstep(0., 1., 1. - abs(r.y)), pw);
+ cl += (.5 + .5 * sin((q2.x + q2.y * 5.) * 8.)) * w;
+ wsum += w;
+ }
+ }
+ return pow(wsum != 0.0 ? cl / wsum : 0.0, .5) * 2.;
+}
+float crumplesShape(vec2 uv) {
+ return crumpledNoise(uv * .25, 16.) * crumpledNoise(uv * .5, 2.);
+}
+
+
+vec2 folds(vec2 uv) {
+ vec3 pp = vec3(0.);
+ float l = 9.;
+ for (float i = 0.; i < 15.; i++) {
+ if (i >= u_foldCount) break;
+ vec2 rand = randomGB(vec2(i, i * u_seed));
+ float an = rand.x * TWO_PI;
+ vec2 p = vec2(cos(an), sin(an)) * rand.y;
+ float dist = distance(uv, p);
+ l = min(l, dist);
+
+ if (l == dist) {
+ pp.xy = (uv - p.xy);
+ pp.z = dist;
+ }
+ }
+ return mix(pp.xy, vec2(0.), pow(pp.z, .25));
+}
+
+float drops(vec2 uv) {
+ vec2 iDropsUV = floor(uv);
+ vec2 fDropsUV = fract(uv);
+ float dropsMinDist = 1.;
+ for (int j = -1; j <= 1; j++) {
+ for (int i = -1; i <= 1; i++) {
+ vec2 neighbor = vec2(float(i), float(j));
+ vec2 offset = randomGB(iDropsUV + neighbor);
+ offset = .5 + .5 * sin(10. * u_seed + TWO_PI * offset);
+ vec2 pos = neighbor + offset - fDropsUV;
+ float dist = length(pos);
+ dropsMinDist = min(dropsMinDist, dropsMinDist*dist);
+ }
+ }
+ return 1. - smoothstep(.05, .09, pow(dropsMinDist, .5));
+}
+
+float lst(float edge0, float edge1, float x) {
+ return clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0);
+}
+
+void main() {
+
+ vec2 imageUV = v_imageUV;
+ vec2 patternUV = v_imageUV - .5;
+ patternUV = 5. * (patternUV * vec2(u_imageAspectRatio, 1.));
+
+ vec2 roughnessUv = 1.5 * (gl_FragCoord.xy - .5 * u_resolution) / u_pixelRatio;
+ float roughness = roughness(roughnessUv + vec2(1., 0.)) - roughness(roughnessUv - vec2(1., 0.));
+
+ vec2 crumplesUV = fract(patternUV * .02 / u_crumpleSize - u_seed) * 32.;
+ float crumples = u_crumples * (crumplesShape(crumplesUV + vec2(.05, 0.)) - crumplesShape(crumplesUV));
+
+ vec2 fiberUV = 2. / u_fiberSize * patternUV;
+ float fiber = fiberNoise(fiberUV, vec2(0.));
+ fiber = .5 * u_fiber * (fiber - 1.);
+
+ vec2 normal = vec2(0.);
+ vec2 normalImage = vec2(0.);
+
+ vec2 foldsUV = patternUV * .12;
+ foldsUV = rotate(foldsUV, 4. * u_seed);
+ vec2 w = folds(foldsUV);
+ foldsUV = rotate(foldsUV + .007 * cos(u_seed), .01 * sin(u_seed));
+ vec2 w2 = folds(foldsUV);
+
+ float drops = u_drops * drops(patternUV * 2.);
+
+ float fade = u_fade * fbm(.17 * patternUV + 10. * u_seed);
+ fade = clamp(8. * fade * fade * fade, 0., 1.);
+
+ w = mix(w, vec2(0.), fade);
+ w2 = mix(w2, vec2(0.), fade);
+ crumples = mix(crumples, 0., fade);
+ drops = mix(drops, 0., fade);
+ fiber *= mix(1., .5, fade);
+ roughness *= mix(1., .5, fade);
+
+ normal.xy += u_folds * min(5. * u_contrast, 1.) * 4. * max(vec2(0.), w + w2);
+ normalImage.xy += u_folds * 2. * w;
+
+ normal.xy += crumples;
+ normalImage.xy += 1.5 * crumples;
+
+ normal.xy += 3. * drops;
+ normalImage.xy += .2 * drops;
+
+ normal.xy += u_roughness * 1.5 * roughness;
+ normal.xy += fiber;
+
+ normalImage += u_roughness * .75 * roughness;
+ normalImage += .2 * fiber;
+
+ vec3 lightPos = vec3(1., 2., 1.);
+ float res = dot(normalize(vec3(normal, 9.5 - 9. * pow(u_contrast, .1))), normalize(lightPos));
+
+ vec3 fgColor = u_colorFront.rgb * u_colorFront.a;
+ float fgOpacity = u_colorFront.a;
+ vec3 bgColor = u_colorBack.rgb * u_colorBack.a;
+ float bgOpacity = u_colorBack.a;
+
+ imageUV += .02 * normalImage;
+ float frame = getUvFrame(imageUV);
+ vec4 image = texture(u_image, imageUV);
+ image.rgb += .6 * pow(u_contrast, .4) * (res - .7);
+
+ frame *= image.a;
+
+ vec3 color = fgColor * res;
+ float opacity = fgOpacity * res;
+
+ color += bgColor * (1. - opacity);
+ opacity += bgOpacity * (1. - opacity);
+ opacity = mix(opacity, 1., frame);
+
+ color -= .007 * drops;
+
+ color.rgb = mix(color, image.rgb, frame);
+
+ fragColor = vec4(color, opacity);
+}
+`;
+
+// node_modules/@paper-design/shaders/dist/get-shader-noise-texture.js
+function getShaderNoiseTexture() {
+ if (typeof window === "undefined") {
+ return void 0;
+ }
+ const img = new Image();
+ img.src = noiseSrc;
+ return img;
+}
+var noiseSrc = "data:image/png;base64,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";
+export {
+ ShaderMount,
+ getShaderNoiseTexture,
+ paperTextureFragmentShader
+};
diff --git a/public/_lab/render.html b/public/_lab/render.html
new file mode 100644
index 0000000..43e6607
--- /dev/null
+++ b/public/_lab/render.html
@@ -0,0 +1,56 @@
+
+
+ bake
+
+
+
+
+
diff --git a/public/_lab/shader-demo.js b/public/_lab/shader-demo.js
new file mode 100644
index 0000000..46bcb50
--- /dev/null
+++ b/public/_lab/shader-demo.js
@@ -0,0 +1,83 @@
+// Живой прототип 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;
diff --git a/public/_lab/template.html b/public/_lab/template.html
new file mode 100644
index 0000000..313ac00
--- /dev/null
+++ b/public/_lab/template.html
@@ -0,0 +1,74 @@
+
+
+
+
+
+
+ Стенд: <название элемента>
+
+
+
+
+
+
+
+
+
+
<название элемента>
+
+ Стенд для настройки вида. Сборка не нужна: правим public/css/main.css →
+ страница перезагрузится сама. Переключатель темы в правом нижнем углу.
+
+
+
+
+
+
+
Демо
+
Опиши, что смотрим. Здесь — сам элемент во всех состояниях.
+
+
+
+ Состояние
+ Подпись
+
+
+
+
+
+
Токены
+
Числа, которыми управляется вид. Значение по умолчанию — в скобках.
+
+
+
--neu-bg--bg-elevated
+
Нейтральная заливка карточки.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/assets/lab/grain-l.webp b/public/assets/lab/grain-l.webp
new file mode 100644
index 0000000..01d36c9
Binary files /dev/null and b/public/assets/lab/grain-l.webp differ
diff --git a/public/assets/lab/grain-m.webp b/public/assets/lab/grain-m.webp
new file mode 100644
index 0000000..30ef71e
Binary files /dev/null and b/public/assets/lab/grain-m.webp differ
diff --git a/public/assets/lab/grain-s.webp b/public/assets/lab/grain-s.webp
new file mode 100644
index 0000000..7bdad6b
Binary files /dev/null and b/public/assets/lab/grain-s.webp differ
diff --git a/public/assets/lab/grain-xl.webp b/public/assets/lab/grain-xl.webp
new file mode 100644
index 0000000..167c6d2
Binary files /dev/null and b/public/assets/lab/grain-xl.webp differ
diff --git a/public/assets/lab/grain-xs.webp b/public/assets/lab/grain-xs.webp
new file mode 100644
index 0000000..7036bb7
Binary files /dev/null and b/public/assets/lab/grain-xs.webp differ
diff --git a/public/cases/ai-assistant-figma/index.html b/public/cases/ai-assistant-figma/index.html
index 0f48471..0b7baad 100644
--- a/public/cases/ai-assistant-figma/index.html
+++ b/public/cases/ai-assistant-figma/index.html
@@ -11,9 +11,9 @@
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+ Skip to case content
+
+