Files
site/apps/web/public/wesp/js/wesp-nav-logo-shimmer.js

153 lines
4.2 KiB
JavaScript

/**
* Блик по буквам логотипа в navbar (тёмная тема) — тот же drawShimmer, что в wesp-logo-loader.
*/
(function () {
const LOGO_SRC = "/static/logo2.png";
const CYCLE_MS = 3000;
const SHIMMER_MS = 1000;
const START_DELAY_MS = 350;
const mounts = new WeakMap();
function isDarkTheme() {
return document.documentElement.getAttribute("data-theme") === "dark";
}
function prefersReducedMotion() {
return globalThis.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches ?? false;
}
function loadImage(src) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = () => reject(new Error(`Failed to load: ${src}`));
img.src = src;
});
}
function disposeMount(link) {
const state = mounts.get(link);
if (!state) return;
state.disposed = true;
if (state.rafId) cancelAnimationFrame(state.rafId);
state.canvas?.remove();
link.classList.remove("nav-logo-link--canvas-shimmer");
mounts.delete(link);
}
async function mountCanvasShimmer(link) {
if (!isDarkTheme() || prefersReducedMotion()) return;
disposeMount(link);
let logoModule;
try {
logoModule = await import("/static/js/wesp-logo-loader.js?v=2");
} catch {
return;
}
const { createNavLogoShimmerBuffer, drawShimmer } = logoModule;
let img;
try {
img = await loadImage(LOGO_SRC);
} catch {
return;
}
const canvas = document.createElement("canvas");
canvas.className = "nav-logo__shimmer-canvas";
canvas.setAttribute("aria-hidden", "true");
link.classList.add("nav-logo-link--canvas-shimmer");
link.appendChild(canvas);
const ctx = canvas.getContext("2d");
const state = {
disposed: false,
rafId: 0,
canvas,
logoCanvas: null,
layout: null,
startAt: performance.now() + START_DELAY_MS,
lastW: 0,
lastH: 0,
};
mounts.set(link, state);
function rebuildBuffer() {
const w = Math.max(1, Math.round(link.clientWidth));
const h = Math.max(1, Math.round(link.clientHeight));
if (w === state.lastW && h === state.lastH && state.logoCanvas) return;
state.lastW = w;
state.lastH = h;
const dpr = Math.min(globalThis.devicePixelRatio || 1, 2);
canvas.width = Math.round(w * dpr);
canvas.height = Math.round(h * dpr);
canvas.style.width = `${w}px`;
canvas.style.height = `${h}px`;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
const built = createNavLogoShimmerBuffer(img, w, h, [255, 255, 255]);
state.logoCanvas = built.logoCanvas;
state.layout = built.layout;
}
function tick(now) {
if (state.disposed) return;
if (!isDarkTheme()) {
disposeMount(link);
return;
}
rebuildBuffer();
const w = state.lastW;
const h = state.lastH;
ctx.clearRect(0, 0, w, h);
const elapsed = now - state.startAt;
if (elapsed >= 0 && state.logoCanvas && state.layout) {
const cycle = elapsed % CYCLE_MS;
if (cycle < SHIMMER_MS) {
const shimmerT = cycle / SHIMMER_MS;
ctx.drawImage(state.logoCanvas, 0, 0, w, h);
drawShimmer(ctx, state.layout, state.logoCanvas, shimmerT, false);
}
}
state.rafId = requestAnimationFrame(tick);
}
rebuildBuffer();
state.rafId = requestAnimationFrame(tick);
}
function init() {
if (prefersReducedMotion()) return;
document
.querySelectorAll(".mobile-navbar .nav-logo-link, .mobile-brand .nav-logo-link")
.forEach((link) => {
if (isDarkTheme()) {
mountCanvasShimmer(link);
} else {
disposeMount(link);
}
});
}
function onThemeChange() {
init();
}
window.WespNavLogoShimmer = { init, disposeMount };
const themeObserver = new MutationObserver(onThemeChange);
themeObserver.observe(document.documentElement, {
attributes: true,
attributeFilter: ["data-theme"],
});
/* init вызывается из wesp-zootech-nav.js после вставки navbar и из wesp-page-enter после enter-done */
})();