const DEFAULT_BLUE_SRC = "/static/logo2.png"; const LOGO_SRC_WIDTH = 2222; const LOGO_SRC_HEIGHT = 1024; /** ~28 строк сетки, как у старого logo2 (305px @ cell 11). */ const INITIAL_CELL = 37; const DURATION_MS = 4750; const WAVE2_START_MS = 0.36 * 4500; const WAVE2_SPAN_MS = 0.34 * 6800; const WAVE2_START = WAVE2_START_MS / DURATION_MS; const WAVE2_SPAN = WAVE2_SPAN_MS / DURATION_MS; const WAVE2_END = (WAVE2_START_MS + WAVE2_SPAN_MS) / DURATION_MS; const HOLD_END = 0.94; const PIXEL_SCALE = WAVE2_START / 0.36; const WAVE0_START = 0.04 * PIXEL_SCALE; const WAVE0_SPAN = 0.20 * PIXEL_SCALE; const WAVE1_START = WAVE0_START + WAVE0_SPAN - 0.04 * PIXEL_SCALE; const WAVE1_SPAN = 0.20 * PIXEL_SCALE; const PIXEL_FADE = 0.012; const PIXEL_GAP = 1; const ASSEMBLY_FEATHER = 10; const COLOR_CROSSFADE = 0.024; const SHIMMER_MS = 1400; const SHIMMER_PROGRESS_START = 88; const BRAND_BLUE = [44, 123, 229]; const BRAND_ORGANIC = [72, 129, 109]; const BRAND_ORGANIC_HC = [47, 107, 87]; const BRAND_WHITE = [238, 240, 243]; const PIXEL_GRAY = [148, 152, 158]; const BG = [246, 246, 244]; const GRID_STEP = 10; function shimmerDurationMs() { return SHIMMER_MS; } function wave2EndMs(durationMs) { return WAVE2_END * durationMs; } function animationEndMs(durationMs) { return Math.max(durationMs, wave2EndMs(durationMs) + SHIMMER_MS + 180); } /** Фактическая длительность проигрывания (с учётом totalDurationMs). */ function resolvePlaybackEndMs(durationMs, totalDurationMs) { const natural = animationEndMs(durationMs); if (totalDurationMs == null || !Number.isFinite(totalDurationMs)) { return natural; } return Math.min(natural, Math.max(400, totalDurationMs)); } export const LOGO_LOADER_DURATION_MS = DURATION_MS; export { animationEndMs, resolvePlaybackEndMs, drawShimmer, LOGO_SRC_WIDTH, LOGO_SRC_HEIGHT }; /** Буфер логотипа для navbar-shimmer (выравнивание слева, как object-position: left). */ export function createNavLogoShimmerBuffer(img, slotW, slotH, inkRgb = [255, 255, 255]) { const srcW = img.naturalWidth || LOGO_SRC_WIDTH; const srcH = img.naturalHeight || LOGO_SRC_HEIGHT; const scale = Math.min(slotW / srcW, slotH / srcH); const drawWidth = srcW * scale; const drawHeight = srcH * scale; const layout = { offsetX: 0, offsetY: (slotH - drawHeight) / 2, drawWidth, drawHeight, }; const canvas = document.createElement("canvas"); canvas.width = slotW; canvas.height = slotH; const ctx = canvas.getContext("2d"); ctx.clearRect(0, 0, slotW, slotH); ctx.drawImage(img, layout.offsetX, layout.offsetY, drawWidth, drawHeight); knockoutBackgroundFromLogo(canvas, slotW, slotH); if (inkRgb) recolorVisiblePixels(canvas, slotW, slotH, inkRgb); return { logoCanvas: canvas, layout }; } function prefersReducedMotion() { return globalThis.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches ?? false; } /** light | organic | dark — для цвета логотипа в анимации загрузки */ function resolveLoaderTheme(options = {}) { const explicit = options.theme; if (explicit === "light" || explicit === "organic" || explicit === "dark") { return explicit; } const docTheme = document.documentElement.getAttribute("data-theme"); if (docTheme === "light" || docTheme === "organic" || docTheme === "dark") { return docTheme; } try { const saved = localStorage.getItem("theme"); if (saved === "light" || saved === "organic" || saved === "dark") return saved; } catch { /* ignore */ } if (options.darkTheme) return "dark"; return "light"; } function prefersHighContrast() { const attr = document.documentElement.getAttribute("data-high-contrast"); if (attr === "true") return true; if (attr === "false") return false; return globalThis.matchMedia?.("(prefers-contrast: more)")?.matches ?? false; } function loaderAccentRgb(theme) { if (theme === "dark") return BRAND_WHITE; if (theme === "organic") return prefersHighContrast() ? BRAND_ORGANIC_HC : BRAND_ORGANIC; return BRAND_BLUE; } function loaderUsesDarkShimmer(theme) { return theme === "dark"; } function loadImage(src) { return new Promise((resolve, reject) => { const img = new Image(); img.onload = () => resolve(img); img.onerror = () => reject(new Error(`Failed to load image: ${src}`)); img.src = src; }); } function isLogoInk(data, index) { const r = data[index]; const g = data[index + 1]; const b = data[index + 2]; const a = data[index + 3]; if (a < 20) return false; if (r + g + b < 80) return false; return b > 70 && b >= r && b >= g * 0.85; } function isLogoBlueFringe(data, index) { const r = data[index]; const g = data[index + 1]; const b = data[index + 2]; const a = data[index + 3]; if (a < 1) return false; if (isLogoInk(data, index)) return true; return b > r + 5 && b > g - 2 && b > 68 && r + g + b < 700; } function knockoutBackgroundFromLogo(canvas, width, height) { const ctx = canvas.getContext("2d"); const imageData = ctx.getImageData(0, 0, width, height); const data = imageData.data; for (let i = 0; i < data.length; i += 4) { if (data[i + 3] < 1) continue; if (isLogoBlueFringe(data, i)) continue; data[i + 3] = 0; } ctx.putImageData(imageData, 0, 0); } function recolorVisiblePixels(canvas, width, height, rgb) { const ctx = canvas.getContext("2d"); const imageData = ctx.getImageData(0, 0, width, height); const data = imageData.data; for (let i = 0; i < data.length; i += 4) { if (data[i + 3] < 1) continue; data[i] = rgb[0]; data[i + 1] = rgb[1]; data[i + 2] = rgb[2]; } ctx.putImageData(imageData, 0, 0); } function cloneLogoBuffer(sourceCanvas, width, height, rgb = null) { const canvas = document.createElement("canvas"); canvas.width = width; canvas.height = height; const ctx = canvas.getContext("2d"); ctx.drawImage(sourceCanvas, 0, 0); if (rgb) recolorVisiblePixels(canvas, width, height, rgb); return canvas; } function drawLogoToBuffer(img, width, height, knockoutBackground = false) { const canvas = document.createElement("canvas"); canvas.width = width; canvas.height = height; const ctx = canvas.getContext("2d"); ctx.clearRect(0, 0, width, height); const scale = Math.min(width / img.naturalWidth, height / img.naturalHeight); const drawWidth = img.naturalWidth * scale; const drawHeight = img.naturalHeight * scale; const offsetX = (width - drawWidth) / 2; const offsetY = (height - drawHeight) / 2; ctx.drawImage(img, offsetX, offsetY, drawWidth, drawHeight); if (knockoutBackground) knockoutBackgroundFromLogo(canvas, width, height); return canvas; } function hashCell(col, row) { return ((col * 92837111) ^ (row * 689287499)) >>> 0; } function easeOutQuad(t) { return 1 - (1 - t) * (1 - t); } function easeInOutQuad(t) { return t < 0.5 ? 2 * t * t : 1 - (-2 * t + 2) ** 2 / 2; } function rgbaString(r, g, b, a) { return `rgba(${r | 0}, ${g | 0}, ${b | 0}, ${a})`; } function desaturateColor(r, g, b, amount = 0.72) { const gray = (r + g + b) / 3; return [ r + (gray - r) * amount, g + (gray - g) * amount, b + (gray - b) * amount, ]; } function lerpColor(c1, c2, t) { return [ c1[0] + (c2[0] - c1[0]) * t, c1[1] + (c2[1] - c1[1]) * t, c1[2] + (c2[2] - c1[2]) * t, ]; } function colorCss(color) { return `rgb(${color[0] | 0}, ${color[1] | 0}, ${color[2] | 0})`; } function displayProgress(t, elapsedMs = null, durationMs = DURATION_MS) { const shimmerStartMs = wave2EndMs(durationMs); const shimmerEndMs = shimmerStartMs + shimmerDurationMs(); if (elapsedMs != null && elapsedMs >= shimmerStartMs) { if (elapsedMs >= shimmerEndMs) return 100; const shimmerP = (elapsedMs - shimmerStartMs) / shimmerDurationMs(); return Math.round( SHIMMER_PROGRESS_START + shimmerP * (100 - SHIMMER_PROGRESS_START), ); } if (t >= WAVE2_END) return SHIMMER_PROGRESS_START; if (t >= WAVE2_START) { const p = (t - WAVE2_START) / (WAVE2_END - WAVE2_START); return Math.round( 65 + Math.min(1, p) * (SHIMMER_PROGRESS_START - 65), ); } if (t >= WAVE1_START) { const wave1End = WAVE1_START + WAVE1_SPAN; const p = Math.min(1, (t - WAVE1_START) / Math.max(0.001, wave1End - WAVE1_START)); return Math.round(35 + p * 30); } if (t >= WAVE0_START) { const wave0End = WAVE0_START + WAVE0_SPAN; const p = Math.min(1, (t - WAVE0_START) / Math.max(0.001, wave0End - WAVE0_START)); return Math.round(p * 35); } return 0; } function progressBarColor(progress, loaderTheme = "light") { const accent = loaderAccentRgb(loaderTheme); if (progress >= 65) return colorCss(accent); if (progress >= 35) { const p = (progress - 35) / 30; return colorCss(lerpColor(PIXEL_GRAY, accent, p)); } return colorCss(PIXEL_GRAY); } function withLogoTransform(ctx, layout, t, drawFn) { drawFn(); } /** Фаза 0 — серые пиксели; фаза 1 — синие/белые; обе слева направо. */ function buildLogoCells(logoData, logoWidth, logoHeight, cell, accentColor = null) { const cols = Math.ceil(logoWidth / cell); const rows = Math.ceil(logoHeight / cell); const colDenom = Math.max(1, cols - 1); const cells = []; for (let row = 0; row < rows; row += 1) { for (let col = 0; col < cols; col += 1) { const cx = Math.min(logoWidth - 1, col * cell + Math.floor(cell / 2)); const cy = Math.min(logoHeight - 1, row * cell + Math.floor(cell / 2)); const index = (cy * logoWidth + cx) * 4; if (!isLogoInk(logoData, index)) continue; const r = logoData[index]; const g = logoData[index + 1]; const b = logoData[index + 2]; const noise = (hashCell(col, row) % 1000) / 1000; const sweep = Math.max(0, Math.min(1, col / colDenom + (noise - 0.5) * 0.028)); const color = accentColor ?? [r, g, b]; cells.push({ col, row, wave0At: WAVE0_START + sweep * WAVE0_SPAN, wave1At: WAVE1_START + sweep * WAVE1_SPAN, color, gray: desaturateColor(r, g, b), }); } } return cells; } function wave2FrontX(t, layout) { if (t < WAVE2_START) { return layout.offsetX; } const progress = easeInOutQuad(Math.min(1, (t - WAVE2_START) / WAVE2_SPAN)); return layout.offsetX + layout.drawWidth * progress; } function drawBackground(ctx, width, height, layout, t, plain = false, transparent = false) { if (transparent) { ctx.clearRect(0, 0, width, height); return; } if (plain) { ctx.fillStyle = "#ffffff"; ctx.fillRect(0, 0, width, height); return; } const cx = width / 2; const cy = layout ? layout.offsetY + layout.drawHeight / 2 : height * 0.44; const bgGrad = ctx.createRadialGradient(cx, cy, 0, cx, cy, Math.max(width, height) * 0.72); bgGrad.addColorStop(0, "#FAFAF8"); bgGrad.addColorStop(1, "#F0F0EE"); ctx.fillStyle = bgGrad; ctx.fillRect(0, 0, width, height); const animating = t > 0 && t < HOLD_END; const logoCx = layout ? layout.offsetX + layout.drawWidth / 2 : cx; const logoCy = layout ? layout.offsetY + layout.drawHeight / 2 : cy; const focusRadius = layout ? Math.max(layout.drawWidth, layout.drawHeight) * 0.72 : Math.min(width, height) * 0.35; for (let x = GRID_STEP / 2; x < width; x += GRID_STEP) { for (let y = GRID_STEP / 2; y < height; y += GRID_STEP) { let alpha = 0.055; if (layout && animating) { const dist = Math.hypot(x - logoCx, y - logoCy); if (dist < focusRadius) { alpha += 0.045 * (1 - dist / focusRadius); } } ctx.fillStyle = `rgba(26, 30, 28, ${alpha})`; ctx.fillRect(x, y, 1, 1); } } const vig = ctx.createRadialGradient(cx, cy, Math.min(width, height) * 0.28, cx, cy, Math.max(width, height) * 0.82); vig.addColorStop(0, "rgba(26, 30, 28, 0)"); vig.addColorStop(1, "rgba(26, 30, 28, 0.07)"); ctx.fillStyle = vig; ctx.fillRect(0, 0, width, height); } function ensureLoaderChrome(root, hideChrome = false) { let canvas = root.querySelector(".wesp-logo-loader__canvas"); let footer = root.querySelector(".wesp-logo-loader__footer"); if (!canvas) { canvas = document.createElement("canvas"); canvas.className = "wesp-logo-loader__canvas"; root.appendChild(canvas); } if (!footer && !hideChrome) { footer = document.createElement("div"); footer.className = "wesp-logo-loader__footer"; footer.innerHTML = `
`; root.appendChild(footer); } return { canvas, percentEl: footer?.querySelector(".wesp-logo-loader__percent") ?? null, barFill: footer?.querySelector(".wesp-logo-loader__bar-fill") ?? null, taglineEl: footer?.querySelector(".wesp-logo-loader__tagline") ?? null, }; } function computeLogoLayout(viewport, logoWidth, logoHeight, embedded = false) { const maxWidth = viewport.w * (embedded ? 0.94 : 0.88); const maxHeight = viewport.h * (embedded ? 0.88 : 0.34); const scale = Math.min(maxWidth / logoWidth, maxHeight / logoHeight); const drawWidth = logoWidth * scale; const drawHeight = logoHeight * scale; const offsetX = (viewport.w - drawWidth) / 2; const offsetY = embedded ? (viewport.h - drawHeight) / 2 : viewport.h * 0.44 - drawHeight / 2; return { scale, drawWidth, drawHeight, offsetX, offsetY }; } function drawSharpLogo(ctx, offBlue, layout, alpha = 1) { if (alpha <= 0) return; ctx.save(); ctx.imageSmoothingEnabled = true; ctx.globalAlpha = alpha; ctx.drawImage(offBlue, layout.offsetX, layout.offsetY, layout.drawWidth, layout.drawHeight); ctx.restore(); } function pixelGrid(layout, cell, logoWidth, logoHeight) { const cols = Math.ceil(logoWidth / cell); const rows = Math.ceil(logoHeight / cell); return { originX: layout.offsetX, originY: layout.offsetY, cellW: layout.drawWidth / cols, cellH: layout.drawHeight / rows, }; } function cellRect(grid, col, row) { const x0 = grid.originX + col * grid.cellW; const y0 = grid.originY + row * grid.cellH; const x1 = grid.originX + (col + 1) * grid.cellW; const y1 = grid.originY + (row + 1) * grid.cellH; return { x: Math.round(x0), y: Math.round(y0), w: Math.max(1, Math.round(x1) - Math.round(x0)), h: Math.max(1, Math.round(y1) - Math.round(y0)), }; } function drawPixelBlock(ctx, rect, color, alpha) { if (alpha <= 0) return; const gap = Math.min(PIXEL_GAP, Math.max(0, rect.w - 1), Math.max(0, rect.h - 1)); const inset = gap * 0.5; const x = rect.x + inset; const y = rect.y + inset; const w = Math.max(1, rect.w - gap); const h = Math.max(1, rect.h - gap); ctx.fillStyle = rgbaString(color[0], color[1], color[2], alpha); ctx.fillRect(x, y, w, h); } function pixelAppear(t, startAt) { const appear = Math.min(1, (t - startAt) / PIXEL_FADE); return appear >= 0.7 ? 1 : appear; } function resolvePixelColor(item, t) { if (t < item.wave1At) return item.gray; const blend = Math.min(1, (t - item.wave1At) / COLOR_CROSSFADE); if (blend >= 1) return item.color; return lerpColor(item.gray, item.color, blend); } function resolvePixelAlpha(item, t) { if (t < item.wave0At) return 0; let alpha = pixelAppear(t, item.wave0At); if (t >= item.wave1At) { alpha = Math.max(alpha, pixelAppear(t, item.wave1At)); } return alpha; } function featherPixelAlpha(alpha, cellMid, frontX, feather) { if (cellMid <= frontX || cellMid >= frontX + feather) return alpha; return alpha * ((cellMid - frontX) / feather); } function drawSharpLogoFeathered(ctx, logoCanvas, layout, frontX, feather) { const left = layout.offsetX; const top = layout.offsetY; const width = layout.drawWidth; const height = layout.drawHeight; const softStart = Math.max(left, frontX - feather); if (softStart > left + 0.5) { ctx.save(); ctx.beginPath(); ctx.rect(left, top, softStart - left, height); ctx.clip(); drawSharpLogo(ctx, logoCanvas, layout, 1); ctx.restore(); } const bandLeft = softStart; const bandRight = Math.min(left + width, frontX); if (bandRight <= bandLeft + 0.5) return; ctx.save(); ctx.beginPath(); ctx.rect(bandLeft, top, bandRight - bandLeft, height); ctx.clip(); drawSharpLogo(ctx, logoCanvas, layout, 1); ctx.globalCompositeOperation = "destination-in"; const grad = ctx.createLinearGradient(bandLeft, 0, bandRight, 0); grad.addColorStop(0, "rgba(255,255,255,1)"); grad.addColorStop(1, "rgba(255,255,255,0)"); ctx.fillStyle = grad; ctx.fillRect(bandLeft, top, bandRight - bandLeft, height); ctx.restore(); } function drawShimmer(ctx, layout, logoCanvas, shimmerT, loaderTheme = "light") { const darkShimmer = loaderUsesDarkShimmer(loaderTheme); const bandW = layout.drawWidth * (darkShimmer ? 0.36 : 0.14); const x = layout.offsetX + (layout.drawWidth + bandW) * shimmerT - bandW; const left = layout.offsetX; const top = layout.offsetY; const width = layout.drawWidth; const height = layout.drawHeight; ctx.save(); ctx.beginPath(); ctx.rect(left, top, width, height); ctx.clip(); if (darkShimmer) { const [gr, gg, gb] = PIXEL_GRAY; const grayWave = ctx.createLinearGradient(x, 0, x + bandW, 0); grayWave.addColorStop(0, "rgba(255,255,255,0)"); grayWave.addColorStop(0.22, "rgba(255,255,255,0)"); grayWave.addColorStop(0.5, `rgba(${gr},${gg},${gb},0.95)`); grayWave.addColorStop(0.78, "rgba(255,255,255,0)"); grayWave.addColorStop(1, "rgba(255,255,255,0)"); ctx.globalCompositeOperation = "source-atop"; ctx.fillStyle = grayWave; ctx.fillRect(left, top, width, height); ctx.restore(); return; } const peak = 0.72; const highlight = ctx.createLinearGradient(x, 0, x + bandW, 0); highlight.addColorStop(0, "rgba(255,255,255,0)"); highlight.addColorStop(0.3, "rgba(255,255,255,0)"); highlight.addColorStop(0.5, `rgba(255,255,255,${peak})`); highlight.addColorStop(0.7, "rgba(255,255,255,0)"); highlight.addColorStop(1, "rgba(255,255,255,0)"); ctx.globalCompositeOperation = "lighter"; ctx.fillStyle = highlight; ctx.fillRect(left, top, width, height); ctx.globalCompositeOperation = "destination-in"; ctx.drawImage(logoCanvas, left, top, width, height); ctx.restore(); } /** Фаза 0 — серые; фаза 1 — цветные; фаза 2 — плавная сборка по контуру надписи. */ function drawWaves(ctx, layout, cell, cells, t, logoCanvas, logoWidth, logoHeight) { const frontX = wave2FrontX(t, layout); const sharpRight = layout.offsetX + layout.drawWidth; const grid = pixelGrid(layout, cell, logoWidth, logoHeight); const inWave2 = t >= WAVE2_START; if (inWave2 && frontX > layout.offsetX + 1) { drawSharpLogoFeathered(ctx, logoCanvas, layout, frontX, ASSEMBLY_FEATHER); } if (inWave2 && frontX >= sharpRight - 1) { return; } for (const item of cells) { if (t < item.wave0At) continue; const rect = cellRect(grid, item.col, item.row); const cellMid = rect.x + rect.w * 0.5; if (inWave2 && cellMid < frontX - ASSEMBLY_FEATHER) continue; let alpha = resolvePixelAlpha(item, t); if (inWave2) { alpha = featherPixelAlpha(alpha, cellMid, frontX, ASSEMBLY_FEATHER); } if (alpha <= 0) continue; drawPixelBlock(ctx, rect, resolvePixelColor(item, t), alpha); } } function updateProgressChrome( percentEl, barFill, t, getExternalProgress, loaderTheme, elapsedMs = null, durationMs = DURATION_MS, ) { let progress = displayProgress(t, elapsedMs, durationMs); const external = getExternalProgress(); if (external != null) { progress = Math.max(progress, Math.min(100, Math.round(external * 100))); } if (percentEl) percentEl.textContent = `${String(progress).padStart(2, "0")}%`; if (barFill) { barFill.style.width = `${progress}%`; barFill.style.background = progressBarColor(progress, loaderTheme); } return progress; } export async function mountWespLogoLoader(root, options = {}) { if (!root) { throw new Error("mountWespLogoLoader: root element is required"); } const blueSrc = options.blueSrc ?? options.whiteSrc ?? DEFAULT_BLUE_SRC; const initialCell = options.cell ?? options.pixel ?? INITIAL_CELL; const loop = options.loop !== false; const durationMs = options.durationMs ?? DURATION_MS; const totalDurationMs = options.totalDurationMs ?? null; const playbackEndMs = resolvePlaybackEndMs(durationMs, totalDurationMs); const embedded = !!options.embedded; const plainBackground = !!options.plainBackground; const transparentBackground = !!options.transparentBackground; const knockoutBackground = !!options.knockoutBackground; const loaderTheme = resolveLoaderTheme(options); const staticLogo = !!options.staticLogo; const hideChrome = !!options.hideChrome; const onComplete = typeof options.onComplete === "function" ? options.onComplete : null; const getExternalProgress = typeof options.progress === "function" ? options.progress : () => (options.progress ?? null); root.classList.add("wesp-logo-loader"); if (embedded) root.classList.add("wesp-logo-loader--embedded"); if (plainBackground) root.classList.add("wesp-logo-loader--plain"); if (transparentBackground) root.classList.add("wesp-logo-loader--transparent"); if (hideChrome) root.classList.add("wesp-logo-loader--no-chrome"); const { canvas, percentEl, barFill } = ensureLoaderChrome(root, hideChrome); const ctx = canvas.getContext("2d", { willReadFrequently: true }); if (transparentBackground) { canvas.style.background = "transparent"; } const blueImg = await loadImage(blueSrc); const logoWidth = blueImg.naturalWidth || LOGO_SRC_WIDTH; const logoHeight = blueImg.naturalHeight || LOGO_SRC_HEIGHT; const offBlue = drawLogoToBuffer( blueImg, logoWidth, logoHeight, knockoutBackground, ); const accentRgb = loaderAccentRgb(loaderTheme); const displayLogo = loaderTheme === "light" ? offBlue : cloneLogoBuffer(offBlue, logoWidth, logoHeight, accentRgb); const waveAccent = loaderTheme === "light" ? null : accentRgb; const logoData = offBlue.getContext("2d").getImageData(0, 0, logoWidth, logoHeight).data; const logoCells = buildLogoCells( logoData, logoWidth, logoHeight, initialCell, waveAccent, ); let viewport = { w: 0, h: 0, dpr: 1 }; let layout = null; let rafId = 0; let start = performance.now(); let disposed = false; let completedFired = false; function finishIfDone(elapsedMs) { if (loop || elapsedMs < playbackEndMs || completedFired || !onComplete) return; completedFired = true; window.setTimeout(() => onComplete(), 120); } function readViewportSize() { if (embedded) { const rect = root.getBoundingClientRect(); return { w: Math.max(1, rect.width || root.clientWidth || 1), h: Math.max(1, rect.height || root.clientHeight || 1), }; } return { w: globalThis.innerWidth, h: globalThis.innerHeight }; } function resize() { const dpr = Math.min(globalThis.devicePixelRatio || 1, 2); const size = readViewportSize(); viewport = { w: size.w, h: size.h, dpr }; canvas.width = viewport.w * dpr; canvas.height = viewport.h * dpr; canvas.style.width = `${viewport.w}px`; canvas.style.height = `${viewport.h}px`; ctx.setTransform(dpr, 0, 0, dpr, 0, 0); ctx.imageSmoothingEnabled = false; layout = computeLogoLayout(viewport, logoWidth, logoHeight, embedded); } function drawStaticLogo() { resize(); drawBackground(ctx, viewport.w, viewport.h, layout, 1, plainBackground, transparentBackground); if (layout) drawSharpLogo(ctx, displayLogo, layout, 1); updateProgressChrome( percentEl, barFill, 1, getExternalProgress, loaderTheme, animationEndMs(durationMs), durationMs, ); finishIfDone(playbackEndMs); } function drawAnimatedFrame(t, elapsedMs) { const shimmerMs = shimmerDurationMs(); const shimmerStartMs = wave2EndMs(durationMs); withLogoTransform(ctx, layout, t, () => { if (t >= WAVE2_END) { drawSharpLogo(ctx, displayLogo, layout, 1); if ( elapsedMs >= shimmerStartMs && elapsedMs < shimmerStartMs + shimmerMs ) { drawShimmer( ctx, layout, displayLogo, (elapsedMs - shimmerStartMs) / shimmerMs, loaderTheme, ); } return; } drawWaves(ctx, layout, initialCell, logoCells, t, displayLogo, logoWidth, logoHeight); }); } function drawStaticIntroFrame(t) { drawBackground(ctx, viewport.w, viewport.h, layout, t, plainBackground, transparentBackground); if (!layout) return; const alpha = easeOutQuad(Math.min(1, t / 0.82)); drawSharpLogo(ctx, displayLogo, layout, alpha); } function cycleElapsedMs(elapsed) { if (!loop) return elapsed; return elapsed % durationMs; } function drawFrame(now) { if (disposed) return; const elapsed = now - start; const cycleMs = cycleElapsedMs(elapsed); const t = loop ? cycleMs / durationMs : Math.min(1, elapsed / durationMs); const shimmerMs = shimmerDurationMs(); const shimmerStartMs = wave2EndMs(durationMs); const inShimmer = cycleMs >= shimmerStartMs && cycleMs < shimmerStartMs + shimmerMs; updateProgressChrome( percentEl, barFill, t, getExternalProgress, loaderTheme, cycleMs, durationMs, ); if (staticLogo) { drawStaticIntroFrame(t); finishIfDone(elapsed); if (loop || elapsed < playbackEndMs) { rafId = requestAnimationFrame(drawFrame); } return; } drawBackground(ctx, viewport.w, viewport.h, layout, t, plainBackground, transparentBackground); if (!layout) return; if (t < HOLD_END || inShimmer) { drawAnimatedFrame(t, cycleMs); } else if (loop) { withLogoTransform(ctx, layout, t, () => { const fade = 1 - easeOutQuad((t - HOLD_END) / (1 - HOLD_END)); drawSharpLogo(ctx, displayLogo, layout, fade); }); } else { withLogoTransform(ctx, layout, t, () => { drawSharpLogo(ctx, displayLogo, layout, 1); }); } finishIfDone(elapsed); if (loop || elapsed < playbackEndMs) { rafId = requestAnimationFrame(drawFrame); } } resize(); globalThis.addEventListener("resize", resize); if (prefersReducedMotion() || staticLogo) { if (staticLogo) { rafId = requestAnimationFrame(drawFrame); } else { drawStaticLogo(); } } else { rafId = requestAnimationFrame(drawFrame); } return function disposeWespLogoLoader() { if (disposed) return; disposed = true; cancelAnimationFrame(rafId); globalThis.removeEventListener("resize", resize); root.classList.add("wesp-logo-loader--exiting"); window.setTimeout(() => { root.classList.add("wesp-logo-loader--hidden"); window.setTimeout(() => { if (root.isConnected) root.remove(); }, 480); }, 520); }; } export async function showWespLogoLoader(options = {}) { const root = document.createElement("div"); root.id = options.id ?? "wespBootLoader"; root.className = "wesp-logo-loader"; root.setAttribute("aria-busy", "true"); root.setAttribute("aria-label", options.label ?? "Загрузка"); document.body.appendChild(root); return mountWespLogoLoader(root, options); }