Files
site/apps/web/public/wesp/js/wesp-logo-loader.js

870 lines
27 KiB
JavaScript

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 = `
<div class="wesp-logo-loader__footer-row">
<span class="wesp-logo-loader__label">Загрузка</span>
<span class="wesp-logo-loader__percent">00%</span>
</div>
<div class="wesp-logo-loader__bar">
<div class="wesp-logo-loader__bar-fill"></div>
</div>
<p class="wesp-logo-loader__tagline" aria-live="polite"></p>`;
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);
}