You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

157 lines
6.5 KiB
JavaScript

/* Art.n Architects — shared site JS (quiet edition).
GSAP + ScrollTrigger + Lenis, all vendored locally. Every feature is
guarded by element existence so one file serves every page. */
(function () {
'use strict';
document.documentElement.classList.add('js');
var reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
var mobile = matchMedia('(max-width: 860px)').matches;
/* ---------- header ---------- */
var hdr = document.querySelector('header.site');
if (hdr) {
var onScroll = function () { hdr.classList.toggle('scrolled', window.scrollY > 40); };
addEventListener('scroll', onScroll, { passive: true }); onScroll();
}
/* ---------- mobile menu ---------- */
var menu = document.getElementById('site-menu');
var menuBtn = document.querySelector('.menu-label');
var closeBtn = document.querySelector('.menu-close');
if (menu && menuBtn && closeBtn) {
var setMenu = function (open) {
menu.hidden = !open; menu.classList.toggle('open', open);
menuBtn.setAttribute('aria-expanded', String(open));
if (open) closeBtn.focus(); else menuBtn.focus();
};
menuBtn.addEventListener('click', function () { setMenu(true); });
closeBtn.addEventListener('click', function () { setMenu(false); });
menu.addEventListener('click', function (e) { if (e.target.tagName === 'A') setMenu(false); });
addEventListener('keydown', function (e) { if (e.key === 'Escape' && !menu.hidden) setMenu(false); });
}
/* ---------- reveals (CSS does the easing; IO adds the class) ---------- */
var revealEls = document.querySelectorAll('.reveal');
if (revealEls.length) {
if (reduce || !('IntersectionObserver' in window)) {
revealEls.forEach(function (el) { el.classList.add('is-in'); });
} else {
var ro = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
if (e.isIntersecting) { e.target.classList.add('is-in'); ro.unobserve(e.target); }
});
}, { rootMargin: '0px 0px -8% 0px' });
revealEls.forEach(function (el) { ro.observe(el); });
}
}
/* ---------- page-transition veil ---------- */
var veil = document.querySelector('.veil-page');
if (veil && !reduce && window.gsap) {
if (sessionStorage.getItem('an-veil') === '1') {
sessionStorage.removeItem('an-veil');
gsap.fromTo(veil, { scaleY: 1, transformOrigin: 'bottom' },
{ scaleY: 0, duration: .6, ease: 'power3.inOut', delay: .05 });
}
document.addEventListener('click', function (e) {
if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
var a = e.target.closest('a');
if (!a || a.target === '_blank' || a.hasAttribute('download')) return;
var href = a.getAttribute('href') || '';
if (!href || href[0] === '#' || /^(https?:)?\/\//.test(href) && a.host !== location.host) return;
if (/^(mailto|tel|javascript):/.test(href)) return;
if (a.pathname === location.pathname && a.hash) return;
e.preventDefault();
sessionStorage.setItem('an-veil', '1');
gsap.fromTo(veil, { scaleY: 0, transformOrigin: 'top' }, {
scaleY: 1, duration: .5, ease: 'power3.in',
onComplete: function () { location.href = a.href; }
});
});
addEventListener('pageshow', function (e) { if (e.persisted) gsap.set(veil, { scaleY: 0 }); });
}
if (!window.gsap || reduce) return; // fully usable without motion below this line
gsap.registerPlugin(ScrollTrigger);
/* ---------- Lenis inertial scroll ---------- */
if (window.Lenis && !mobile) {
var lenis = new Lenis({ lerp: 0.085 });
lenis.on('scroll', ScrollTrigger.update);
gsap.ticker.add(function (t) { lenis.raf(t * 1000); });
gsap.ticker.lagSmoothing(0);
}
/* ---------- hero statement: lines rise once ---------- */
var heroLines = document.querySelectorAll('.hero-quiet h1 .line > span');
if (heroLines.length) {
gsap.from(heroLines, {
yPercent: 112, duration: 1.2, ease: 'power4.out',
stagger: .12, delay: .2
});
gsap.from('.hero-quiet .meta', { opacity: 0, y: 14, duration: 1, delay: 1, ease: 'power2.out' });
}
/* ---------- slow parallax on full-bleed panels & tagged imagery ---------- */
document.querySelectorAll('.panel-full .photo').forEach(function (ph) {
gsap.fromTo(ph, { yPercent: -7 }, {
yPercent: 7, ease: 'none',
scrollTrigger: { trigger: ph.parentNode, start: 'top bottom', end: 'bottom top', scrub: true }
});
});
document.querySelectorAll('[data-inner]').forEach(function (img) {
gsap.fromTo(img, { yPercent: -7 }, {
yPercent: 0, ease: 'none',
scrollTrigger: { trigger: img, start: 'top bottom', end: 'bottom top', scrub: true }
});
});
/* ---------- gallery items drift in ---------- */
var gItems = document.querySelectorAll('.gallery a');
if (gItems.length) {
gsap.set(gItems, { opacity: 0, y: 30 });
ScrollTrigger.batch(gItems, {
start: 'top 92%',
onEnter: function (batch) {
gsap.to(batch, { opacity: 1, y: 0, duration: 1, ease: 'power2.out', stagger: .09 });
}
});
}
/* ---------- scroll-scrubbed model orbit (loads model-viewer on approach) ---------- */
var mv = document.querySelector('model-viewer[data-orbit-scrub]');
if (mv) {
var mvLoaded = false;
var loadMV = function () {
if (mvLoaded) return; mvLoaded = true;
var s = document.createElement('script');
s.type = 'module'; s.src = '/assets/vendor/model-viewer.min.js';
document.head.appendChild(s);
};
if ('IntersectionObserver' in window) {
var io = new IntersectionObserver(function (entries) {
if (entries.some(function (e) { return e.isIntersecting; })) { loadMV(); io.disconnect(); }
}, { rootMargin: '800px 0px' });
io.observe(mv);
} else loadMV();
var section = mv.closest('section') || mv;
var state = { theta: -30 };
gsap.to(state, {
theta: 330, ease: 'none',
scrollTrigger: { trigger: section, start: 'top 85%', end: 'bottom 15%', scrub: 1 },
onUpdate: function () {
mv.setAttribute('camera-orbit', state.theta.toFixed(1) + 'deg 76deg auto');
}
});
ScrollTrigger.create({
trigger: section, start: 'top 85%', end: 'bottom 15%',
onToggle: function (self) { if (self.isActive) mv.removeAttribute('auto-rotate'); else mv.setAttribute('auto-rotate', ''); }
});
}
/* ---------- GLightbox (project galleries) ---------- */
if (window.GLightbox && document.querySelector('.glightbox')) {
GLightbox({ selector: '.glightbox', touchNavigation: true, loop: true });
}
})();