Files
KantSites/public/assets/motion.js
T
kantarci cff468167b
CMS quality / php (8.2) (push) Canceled after 0s
CMS quality / php (8.4) (push) Canceled after 0s
CMS quality / browser (push) Canceled after 0s
CMS quality / mysql (push) Canceled after 0s
Use standard form buttons and resume entrance effects in 1.10.1
2026-09-14 15:22:42 +02:00

45 lines
2.7 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' });
const syncPreference = () => {
if (preference.matches) {
observer.disconnect();
animations.forEach(animation => animation.cancel());
} else {
// Reduction may have been active at startup or enabled during this visit.
// Resume observing unseen content when movement is allowed again.
nodes.forEach(node => { if (!seen.has(node)) observer.observe(node); });
}
};
preference.addEventListener('change', syncPreference);
syncPreference();
return () => { observer.disconnect(); animations.forEach(animation => animation.cancel()); preference.removeEventListener('change', syncPreference); };
}
if (!document.querySelector('#editor')) initMotion();