1
0
forked from jason/scj
Files
scj-impec/SCJ.src/script.js
T

101 lines
3.3 KiB
JavaScript
Raw Normal View History

2026-06-30 22:23:36 -05:00
(() => {
'use strict';
const nav = document.getElementById('nav');
const burger = document.getElementById('burger');
const navLinks = document.querySelector('.nav__links');
const yearEl = document.getElementById('year');
const parallaxEls = Array.from(document.querySelectorAll('[data-parallax]'));
const revealEls = Array.from(document.querySelectorAll('.reveal'));
if (yearEl) yearEl.textContent = new Date().getFullYear();
/* ---- Sticky / scrolled nav ---- */
const onScroll = () => {
if (window.scrollY > 30) nav.classList.add('is-scrolled');
else nav.classList.remove('is-scrolled');
updateParallax();
};
/* ---- Mobile menu toggle ---- */
if (burger && navLinks) {
burger.addEventListener('click', () => {
const open = navLinks.classList.toggle('is-open');
burger.classList.toggle('is-open', open);
burger.setAttribute('aria-expanded', String(open));
});
navLinks.querySelectorAll('a').forEach((a) =>
a.addEventListener('click', () => {
navLinks.classList.remove('is-open');
burger.classList.remove('is-open');
burger.setAttribute('aria-expanded', 'false');
})
);
}
/* ---- Parallax (rAF-throttled) ---- */
let ticking = false;
const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
function updateParallax() {
if (reduced) return;
if (ticking) return;
ticking = true;
requestAnimationFrame(() => {
const vh = window.innerHeight;
const vw = window.innerWidth;
parallaxEls.forEach((el) => {
const rect = el.getBoundingClientRect();
// Only process elements roughly in / near the viewport
if (rect.bottom < -vh || rect.top > vh * 2) return;
const speed = parseFloat(el.dataset.parallax) || 0.2;
// Distance of element center from viewport center
const center = rect.top + rect.height / 2 - vh / 2;
const offset = -center * speed;
// Use translate3d for GPU acceleration
el.style.transform = `translate3d(0, ${offset.toFixed(1)}px, 0)`;
});
ticking = false;
});
}
/* ---- Scroll reveal via IntersectionObserver ---- */
if ('IntersectionObserver' in window) {
const io = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add('is-visible');
io.unobserve(entry.target);
}
});
},
{ threshold: 0.15, rootMargin: '0px 0px -60px 0px' }
);
revealEls.forEach((el) => io.observe(el));
} else {
revealEls.forEach((el) => el.classList.add('is-visible'));
}
/* ---- Smooth-scroll offset for fixed nav ---- */
document.querySelectorAll('a[href^="#"]').forEach((a) => {
a.addEventListener('click', (e) => {
const id = a.getAttribute('href');
if (id.length < 2) return;
const target = document.querySelector(id);
if (!target) return;
e.preventDefault();
const navH = nav.offsetHeight;
const top = target.getBoundingClientRect().top + window.scrollY - navH + 4;
window.scrollTo({ top, behavior: 'smooth' });
});
});
/* ---- Listeners ---- */
window.addEventListener('scroll', onScroll, { passive: true });
window.addEventListener('resize', updateParallax);
// initial paint
onScroll();
updateParallax();
})();