188 lines
5.5 KiB
JavaScript
188 lines
5.5 KiB
JavaScript
const MOTION_QUERY = '(prefers-reduced-motion: reduce)';
|
|
|
|
function prefersReducedMotion() {
|
|
return window.matchMedia(MOTION_QUERY).matches;
|
|
}
|
|
|
|
function enableEnhancement() {
|
|
if (prefersReducedMotion() || typeof IntersectionObserver !== 'function') {
|
|
document.documentElement.removeAttribute('data-motion');
|
|
return false;
|
|
}
|
|
|
|
document.documentElement.dataset.motion = 'enhance';
|
|
return true;
|
|
}
|
|
|
|
function activatePageOpen(enhance) {
|
|
document.querySelectorAll('[data-motion="page-open"]').forEach((node) => {
|
|
if (!enhance) {
|
|
node.classList.add('is-active');
|
|
return;
|
|
}
|
|
|
|
requestAnimationFrame(() => {
|
|
requestAnimationFrame(() => node.classList.add('is-active'));
|
|
});
|
|
});
|
|
}
|
|
|
|
function initializeMotionDelays() {
|
|
const beatOrder = new Map([
|
|
['seal', 0],
|
|
['heading', 0],
|
|
['title', 1],
|
|
['media', 2],
|
|
['cta', 3],
|
|
]);
|
|
|
|
document.querySelectorAll('[data-motion="page-open"]').forEach((page) => {
|
|
page.querySelectorAll('[data-motion-beat]').forEach((beat, index) => {
|
|
const namedIndex = beatOrder.get(beat.dataset.motionBeat);
|
|
beat.style.setProperty('--motion-index', String(Math.min(namedIndex ?? index, 3)));
|
|
});
|
|
});
|
|
|
|
document.querySelectorAll('[data-reveal-group]').forEach((group) => {
|
|
const nodes = Array.from(group.querySelectorAll('[data-reveal]'))
|
|
.filter((node) => node.closest('[data-reveal-group]') === group);
|
|
|
|
nodes.forEach((node, index) => {
|
|
node.style.setProperty('--motion-index', String(Math.min(index, 3)));
|
|
});
|
|
});
|
|
}
|
|
|
|
function observeReveals(enhance) {
|
|
const nodes = Array.from(document.querySelectorAll('[data-reveal]'));
|
|
if (nodes.length === 0) {
|
|
return;
|
|
}
|
|
|
|
if (!enhance || typeof IntersectionObserver !== 'function') {
|
|
nodes.forEach((node) => node.classList.add('is-revealed'));
|
|
return;
|
|
}
|
|
|
|
const observer = new IntersectionObserver(
|
|
(entries) => {
|
|
entries.forEach((entry) => {
|
|
if (!entry.isIntersecting) {
|
|
return;
|
|
}
|
|
|
|
entry.target.classList.add('is-revealed');
|
|
observer.unobserve(entry.target);
|
|
});
|
|
},
|
|
{
|
|
rootMargin: '0px 0px -12% 0px',
|
|
threshold: 0.2,
|
|
},
|
|
);
|
|
|
|
nodes.forEach((node) => observer.observe(node));
|
|
}
|
|
|
|
function setupChapterIndex() {
|
|
const index = document.querySelector('[data-chapter-index]');
|
|
const progress = document.querySelector('[data-chapter-progress] span');
|
|
const chapters = Array.from(document.querySelectorAll('[data-chapter]'));
|
|
|
|
if (!index || chapters.length === 0) {
|
|
return;
|
|
}
|
|
|
|
const links = Array.from(index.querySelectorAll('a[href^="#"]'));
|
|
let framePending = false;
|
|
|
|
const setActive = (id) => {
|
|
links.forEach((link) => {
|
|
const isCurrent = link.getAttribute('href') === `#${id}`;
|
|
if (isCurrent) {
|
|
link.setAttribute('aria-current', 'true');
|
|
} else {
|
|
link.removeAttribute('aria-current');
|
|
}
|
|
});
|
|
};
|
|
|
|
const updateProgress = (ratio) => {
|
|
if (!progress) {
|
|
return;
|
|
}
|
|
|
|
const clamped = Math.min(1, Math.max(0, ratio));
|
|
progress.style.setProperty('--chapter-progress', `${(clamped * 100).toFixed(2)}%`);
|
|
};
|
|
|
|
const sync = () => {
|
|
const marker = window.scrollY + Math.min(window.innerHeight * 0.35, 280);
|
|
let current = chapters[0];
|
|
|
|
chapters.forEach((chapter) => {
|
|
if (chapter.offsetTop <= marker) {
|
|
current = chapter;
|
|
}
|
|
});
|
|
|
|
const heading = current.querySelector('[id$="-heading"]') || document.getElementById(`${current.dataset.chapter}-heading`);
|
|
const headingId = heading?.id
|
|
|| current.getAttribute('aria-labelledby')
|
|
|| `${current.dataset.chapter}-heading`;
|
|
|
|
setActive(headingId);
|
|
|
|
const doc = document.documentElement;
|
|
const max = Math.max(1, doc.scrollHeight - window.innerHeight);
|
|
updateProgress(window.scrollY / max);
|
|
};
|
|
|
|
const scheduleSync = () => {
|
|
if (framePending) {
|
|
return;
|
|
}
|
|
|
|
framePending = true;
|
|
requestAnimationFrame(() => {
|
|
framePending = false;
|
|
sync();
|
|
});
|
|
};
|
|
|
|
setActive(chapters[0].getAttribute('aria-labelledby') || 'hero-heading');
|
|
sync();
|
|
|
|
window.addEventListener('scroll', scheduleSync, { passive: true });
|
|
window.addEventListener('resize', scheduleSync);
|
|
}
|
|
|
|
function bootMotion() {
|
|
initializeMotionDelays();
|
|
const enhance = enableEnhancement();
|
|
activatePageOpen(enhance);
|
|
observeReveals(enhance);
|
|
setupChapterIndex();
|
|
|
|
window.matchMedia(MOTION_QUERY).addEventListener('change', (event) => {
|
|
if (event.matches) {
|
|
document.documentElement.removeAttribute('data-motion');
|
|
document.querySelectorAll('[data-motion="page-open"]').forEach((node) => {
|
|
node.classList.add('is-active');
|
|
});
|
|
document.querySelectorAll('[data-reveal]').forEach((node) => {
|
|
node.classList.add('is-revealed');
|
|
});
|
|
return;
|
|
}
|
|
|
|
document.documentElement.dataset.motion = 'enhance';
|
|
});
|
|
}
|
|
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', bootMotion, { once: true });
|
|
} else {
|
|
bootMotion();
|
|
}
|