36 lines
2.4 KiB
JavaScript
36 lines
2.4 KiB
JavaScript
/** Visible content is the baseline. Enhance only when it enters the viewport. */
|
|
export function initMotion(doc = document) {
|
|
const view = doc.defaultView;
|
|
const preference = view.matchMedia('(prefers-reduced-motion: reduce)');
|
|
const animations = new Set();
|
|
const seen = new WeakSet();
|
|
const start = {
|
|
fade: { opacity: .2 }, rise: { transform: 'translateY(28px)' }, sink: { transform: 'translateY(-28px)' },
|
|
left: { transform: 'translateX(-36px)' }, right: { transform: 'translateX(36px)' },
|
|
zoom: { transform: 'scale(.92)' }, flip: { transform: 'perspective(800px) rotateX(-16deg)' }, blur: { filter: 'blur(6px)' },
|
|
};
|
|
const nodes = [...doc.querySelectorAll('[class*="fx-entrance-"]')].filter(node => Object.keys(start).some(name => node.classList.contains(`fx-entrance-${name}`)));
|
|
const show = node => {
|
|
if (seen.has(node) || preference.matches) return;
|
|
seen.add(node);
|
|
const name = Object.keys(start).find(name => node.classList.contains(`fx-entrance-${name}`));
|
|
const delay = node.classList.contains('fx-delay-long') ? 600 : node.classList.contains('fx-delay-medium') ? 300 : node.classList.contains('fx-delay-short') ? 150 : 0;
|
|
// Fade itself is optional; direction, zoom and flip keep text at full contrast.
|
|
const style = view.getComputedStyle(node);
|
|
const finish = { opacity: style.opacity, transform: style.transform, filter: style.filter };
|
|
const first = { ...start[name] };
|
|
if (first.transform && finish.transform !== 'none') first.transform += ' ' + finish.transform;
|
|
const animation = node.animate([first, finish], { duration: 650, delay, easing: 'cubic-bezier(.2,.7,.2,1)', fill: 'none' });
|
|
animations.add(animation); animation.finished.then(() => animations.delete(animation), () => animations.delete(animation));
|
|
};
|
|
const observer = new view.IntersectionObserver(entries => {
|
|
for (const entry of entries) if (entry.isIntersecting) { show(entry.target); observer.unobserve(entry.target); }
|
|
}, { threshold: 0, rootMargin: '0px 0px -24px 0px' });
|
|
if (!preference.matches) nodes.forEach(node => observer.observe(node));
|
|
const reduce = () => { if (preference.matches) { observer.disconnect(); animations.forEach(animation => animation.cancel()); } };
|
|
preference.addEventListener('change', reduce);
|
|
return () => { observer.disconnect(); animations.forEach(animation => animation.cancel()); preference.removeEventListener('change', reduce); };
|
|
}
|
|
|
|
if (!document.querySelector('#editor')) initMotion();
|