'use strict'; /* ═══════════════════════════════════════════════════════════════ ПРЕЛОУДЕР — 0→100 счётчик (makemepulse style) Fade-out exit при 100% ═══════════════════════════════════════════════════════════════ */ (function () { const pl = document.getElementById('pl'); const num = document.getElementById('plNum'); const fill = document.getElementById('plFill'); const T = 1800, t0 = performance.now(); let raf; function tick(now) { const p = Math.min((now - t0) / T, 1); // ease-in-out cubic const e = p < .5 ? 4*p*p*p : 1 - Math.pow(-2*p+2,3)/2; const v = Math.round(e * 100); num.textContent = v; fill.style.width = v + '%'; if (p < 1) { raf = requestAnimationFrame(tick); } else { done(); } } raf = requestAnimationFrame(tick); function done() { cancelAnimationFrame(raf); setTimeout(() => { document.body.classList.remove('loading'); pl.classList.add('exit'); setTimeout(() => pl.remove(), 700); // Hero — анимация слов const delays = [0, 300, 550]; [document.getElementById('hw1'), document.getElementById('hw2'), document.getElementById('hw3')].forEach((el, i) => { if (!el) return; setTimeout(() => el.classList.add('in'), delays[i]); }); }, 200); } })(); /* ═══════════════════════════════════════════════════════════════ КАСТОМНЫЙ КУРСОР — smooth lerp Dot следует сразу, ring — с lerp-задержкой ═══════════════════════════════════════════════════════════════ */ (function () { const dot = document.getElementById('cr-dot'); const ring = document.getElementById('cr-ring'); const label = document.getElementById('cr-label'); if (!dot) return; let mx = -200, my = -200; let rx = -200, ry = -200; const LERP = 0.1; function loop() { rx += (mx - rx) * LERP; ry += (my - ry) * LERP; dot.style.transform = `translate(calc(${mx}px - 50%), calc(${my}px - 50%))`; ring.style.transform = `translate(calc(${rx}px - 50%), calc(${ry}px - 50%))`; label.style.transform= `translate(calc(${rx}px - 50%), calc(${ry}px - 50%))`; requestAnimationFrame(loop); } loop(); window.addEventListener('mousemove', e => { mx = e.clientX; my = e.clientY; dot.classList.add('vis'); ring.classList.add('vis'); }, { passive: true }); // Hover: ссылки и кнопки document.querySelectorAll('a, button').forEach(el => { el.addEventListener('mouseenter', () => ring.classList.add('hov')); el.addEventListener('mouseleave', () => ring.classList.remove('hov')); }); // Hover: кейсы → «Открыть» document.querySelectorAll('.case-card').forEach(el => { el.addEventListener('mouseenter', () => { ring.classList.add('img'); label.textContent = 'Открыть'; label.classList.add('vis'); }); el.addEventListener('mouseleave', () => { ring.classList.remove('img'); label.classList.remove('vis'); }); }); // Hover: фото → «Смотреть» document.querySelectorAll('.about-photo').forEach(el => { el.addEventListener('mouseenter', () => { ring.classList.add('img'); label.textContent = 'Смотреть'; label.classList.add('vis'); }); el.addEventListener('mouseleave', () => { ring.classList.remove('img'); label.classList.remove('vis'); }); }); document.addEventListener('mouseleave', () => { dot.classList.remove('vis'); ring.classList.remove('vis'); }); document.addEventListener('mouseenter', () => { dot.classList.add('vis'); ring.classList.add('vis'); }); })(); /* ═══════════════════════════════════════════════════════════════ НАВИГАЦИЯ — пилюля снизу (otisbay) Левая часть (лого): hover=magnetic, click=scrollTop Правая часть (бургер): click=открыть полноэкранное меню ═══════════════════════════════════════════════════════════════ */ (function () { const navHome = document.getElementById('navHome'); const burger = document.getElementById('burger'); const mobMenu = document.getElementById('mob-menu'); const menuClose= document.getElementById('menuClose'); if (!navHome || !burger) return; // — Магнитный hover на логотипе navHome.addEventListener('mousemove', e => { const r = navHome.getBoundingClientRect(); const dx = (e.clientX - (r.left + r.width / 2)) * 0.22; const dy = (e.clientY - (r.top + r.height / 2)) * 0.22; navHome.style.transform = `translate(${dx}px, ${dy}px)`; }); navHome.addEventListener('mouseleave', () => { navHome.style.transition = 'transform .4s cubic-bezier(.16,1,.3,1)'; navHome.style.transform = 'none'; setTimeout(() => { navHome.style.transition = ''; }, 400); }); // — Клик по логотипу = наверх navHome.addEventListener('click', () => { window.scrollTo({ top: 0, behavior: 'smooth' }); }); // — Открыть / закрыть меню function openMenu() { burger.classList.add('open'); mobMenu.classList.add('open'); document.body.classList.add('no-scroll'); burger.setAttribute('aria-expanded', 'true'); } function closeMenu() { burger.classList.remove('open'); mobMenu.classList.remove('open'); document.body.classList.remove('no-scroll'); burger.setAttribute('aria-expanded', 'false'); } burger.addEventListener('click', () => { burger.classList.contains('open') ? closeMenu() : openMenu(); }); if (menuClose) menuClose.addEventListener('click', closeMenu); // Закрыть при клике на пункт mobMenu.querySelectorAll('.mob-link').forEach(a => { a.addEventListener('click', closeMenu); }); // Закрыть по Escape document.addEventListener('keydown', e => { if (e.key === 'Escape' && mobMenu.classList.contains('open')) closeMenu(); }); })(); /* ═══════════════════════════════════════════════════════════════ ГОРИЗОНТАЛЬНЫЙ СКРОЛЛ — ЭТАПЫ Wrapper = высокий div, sticky = 100vh При скролле через wrapper → track двигается translateX ═══════════════════════════════════════════════════════════════ */ (function () { const wrapper = document.getElementById('process-wrapper'); const track = document.getElementById('processTrack'); if (!wrapper || !track) return; // Размеры пересчитываются при resize function setWrapperHeight() { const scrollDist = track.scrollWidth - window.innerWidth; wrapper.style.height = Math.max(scrollDist + window.innerHeight, window.innerHeight) + 'px'; } setWrapperHeight(); window.addEventListener('resize', setWrapperHeight, { passive: true }); // Двигаем track при скролле let ticking = false; function onScroll() { if (ticking) return; requestAnimationFrame(() => { const rect = wrapper.getBoundingClientRect(); const wrapH = wrapper.offsetHeight; const scrollDist = track.scrollWidth - window.innerWidth; if (rect.top <= 0 && rect.bottom >= window.innerHeight) { const progress = Math.min(-rect.top / (wrapH - window.innerHeight), 1); track.style.transform = `translateX(${-progress * scrollDist}px)`; } else if (rect.top > 0) { track.style.transform = 'translateX(0)'; } else { track.style.transform = `translateX(${-scrollDist}px)`; } ticking = false; }); ticking = true; } window.addEventListener('scroll', onScroll, { passive: true }); onScroll(); // initial })(); /* ═══════════════════════════════════════════════════════════════ SCROLL-АНИМАЦИИ IntersectionObserver, threshold 12%, срабатывает один раз ═══════════════════════════════════════════════════════════════ */ (function () { const els = document.querySelectorAll('.fade-up,.fade-in,.fade-left,.fade-right'); const io = new IntersectionObserver(entries => { entries.forEach(e => { if (e.isIntersecting) { e.target.classList.add('in'); io.unobserve(e.target); } }); }, { threshold: 0.12, rootMargin: '0px 0px -36px 0px' }); els.forEach(el => io.observe(el)); })(); /* ═══════════════════════════════════════════════════════════════ SMOOTH SCROLL для якорей (не работает с process-wrapper) ═══════════════════════════════════════════════════════════════ */ document.querySelectorAll('a[href^="#"]').forEach(a => { a.addEventListener('click', e => { const id = a.getAttribute('href').slice(1); const el = document.getElementById(id); if (!el) return; e.preventDefault(); el.scrollIntoView({ behavior: 'smooth', block: 'start' }); }); });