115 lines
3.9 KiB
JavaScript
115 lines
3.9 KiB
JavaScript
|
|
/* ============================================================
|
|||
|
|
ALWISP – main.js
|
|||
|
|
============================================================ */
|
|||
|
|
|
|||
|
|
'use strict';
|
|||
|
|
|
|||
|
|
// ── NAV: scroll shadow + mobile toggle ──────────────────────
|
|||
|
|
(function () {
|
|||
|
|
const header = document.getElementById('site-header');
|
|||
|
|
const toggle = document.getElementById('nav-toggle');
|
|||
|
|
const menu = document.getElementById('nav-menu');
|
|||
|
|
const navLinks = menu ? menu.querySelectorAll('.nav__link') : [];
|
|||
|
|
|
|||
|
|
// Scroll shadow
|
|||
|
|
if (header) {
|
|||
|
|
const onScroll = () => header.classList.toggle('scrolled', window.scrollY > 10);
|
|||
|
|
window.addEventListener('scroll', onScroll, { passive: true });
|
|||
|
|
onScroll();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Mobile toggle
|
|||
|
|
if (toggle && menu) {
|
|||
|
|
toggle.addEventListener('click', () => {
|
|||
|
|
const isOpen = menu.classList.toggle('is-open');
|
|||
|
|
toggle.setAttribute('aria-expanded', isOpen);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Close on nav link click
|
|||
|
|
navLinks.forEach(link => {
|
|||
|
|
link.addEventListener('click', () => {
|
|||
|
|
menu.classList.remove('is-open');
|
|||
|
|
toggle.setAttribute('aria-expanded', 'false');
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Close on outside click
|
|||
|
|
document.addEventListener('click', e => {
|
|||
|
|
if (!header.contains(e.target)) {
|
|||
|
|
menu.classList.remove('is-open');
|
|||
|
|
toggle.setAttribute('aria-expanded', 'false');
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Active nav link
|
|||
|
|
const currentPath = window.location.pathname.replace(/\/$/, '') || '/';
|
|||
|
|
navLinks.forEach(link => {
|
|||
|
|
const href = link.getAttribute('href').replace(/\/$/, '') || '/';
|
|||
|
|
if (href === currentPath) link.classList.add('nav__link--active');
|
|||
|
|
});
|
|||
|
|
}());
|
|||
|
|
|
|||
|
|
|
|||
|
|
// ── COUNTER ANIMATION (stats bar) ───────────────────────────
|
|||
|
|
(function () {
|
|||
|
|
const counters = document.querySelectorAll('.stat__value[data-count]');
|
|||
|
|
if (!counters.length) return;
|
|||
|
|
|
|||
|
|
const easeOut = t => 1 - Math.pow(1 - t, 3);
|
|||
|
|
|
|||
|
|
function animateCounter(el) {
|
|||
|
|
const target = parseInt(el.dataset.count, 10);
|
|||
|
|
const duration = 1400;
|
|||
|
|
const start = performance.now();
|
|||
|
|
|
|||
|
|
function step(now) {
|
|||
|
|
const elapsed = now - start;
|
|||
|
|
const progress = Math.min(elapsed / duration, 1);
|
|||
|
|
el.textContent = Math.floor(easeOut(progress) * target);
|
|||
|
|
if (progress < 1) requestAnimationFrame(step);
|
|||
|
|
else el.textContent = target;
|
|||
|
|
}
|
|||
|
|
requestAnimationFrame(step);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const observer = new IntersectionObserver(entries => {
|
|||
|
|
entries.forEach(entry => {
|
|||
|
|
if (entry.isIntersecting) {
|
|||
|
|
animateCounter(entry.target);
|
|||
|
|
observer.unobserve(entry.target);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}, { threshold: 0.4 });
|
|||
|
|
|
|||
|
|
counters.forEach(c => observer.observe(c));
|
|||
|
|
}());
|
|||
|
|
|
|||
|
|
|
|||
|
|
// ── SCROLL REVEAL (cards, sections) ─────────────────────────
|
|||
|
|
(function () {
|
|||
|
|
const revealEls = document.querySelectorAll(
|
|||
|
|
'.card, .stat, .why__text, .section__header, .cta-band__content'
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
if (!revealEls.length || !('IntersectionObserver' in window)) return;
|
|||
|
|
|
|||
|
|
revealEls.forEach((el, i) => {
|
|||
|
|
el.style.opacity = '0';
|
|||
|
|
el.style.transform = 'translateY(22px)';
|
|||
|
|
el.style.transition = `opacity 0.55s ease ${i * 0.07}s, transform 0.55s ease ${i * 0.07}s`;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const observer = new IntersectionObserver(entries => {
|
|||
|
|
entries.forEach(entry => {
|
|||
|
|
if (entry.isIntersecting) {
|
|||
|
|
entry.target.style.opacity = '1';
|
|||
|
|
entry.target.style.transform = 'translateY(0)';
|
|||
|
|
observer.unobserve(entry.target);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}, { threshold: 0.12 });
|
|||
|
|
|
|||
|
|
revealEls.forEach(el => observer.observe(el));
|
|||
|
|
}());
|