initial build
This commit is contained in:
Executable
+100
@@ -0,0 +1,100 @@
|
||||
(() => {
|
||||
'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();
|
||||
})();
|
||||
Reference in New Issue
Block a user