'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' });
});
});