/** * Entrance effects. * * Visible content is the baseline: without this script every block simply stands on the page. * With it, a block that carries an entrance class waits out of sight until it scrolls into * view, then comes in - the way the Flatsome animations behave. * * Whether the browser preference "reduce motion" counts is the website's decision, made under * Einstellungen and delivered as a class on . Without `motion-respect` the effects play * for everyone; many systems report the preference without anyone having chosen it, and a site * whose animations never show is not what the person building it asked for. */ export function initMotion(doc = document) { const view = doc.defaultView; const html = doc.documentElement; const preference = view.matchMedia('(prefers-reduced-motion: reduce)'); const reduced = () => html.classList.contains('motion-respect') && preference.matches; const animations = new Set(); const seen = new WeakSet(); // Blocks hidden while they wait. Kept so they can be shown again the moment the effects are // switched off - a block must never stay invisible because of a preference. const hidden = new Set(); const start = { fade: { opacity: 0 }, rise: { opacity: 0, transform: 'translateY(40px)' }, sink: { opacity: 0, transform: 'translateY(-40px)' }, left: { opacity: 0, transform: 'translateX(-56px)' }, right: { opacity: 0, transform: 'translateX(56px)' }, zoom: { opacity: 0, transform: 'scale(.88)' }, flip: { opacity: 0, transform: 'perspective(800px) rotateX(-18deg)' }, blur: { opacity: 0, filter: 'blur(8px)' }, }; const nameOf = node => Object.keys(start).find(name => node.classList.contains(`fx-entrance-${name}`)); const nodes = [...doc.querySelectorAll('[class*="fx-entrance-"]')].filter(nameOf); const hide = node => { if (!seen.has(node) && !hidden.has(node)) { hidden.add(node); node.style.opacity = '0'; } }; const unhide = node => { if (hidden.delete(node)) node.style.opacity = ''; }; const show = node => { if (seen.has(node) || reduced()) return; seen.add(node); unhide(node); const name = nameOf(node); const delay = node.classList.contains('fx-delay-long') ? 600 : node.classList.contains('fx-delay-medium') ? 300 : node.classList.contains('fx-delay-short') ? 150 : 0; // The block's own transform and filter, if any, are the end state - the entrance is // composed in front of them rather than replacing them. 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; if (first.filter && finish.filter !== 'none') first.filter += ' ' + finish.filter; // `backwards` holds the start state through the delay, so a delayed block does not flash // up and vanish before its turn. const animation = node.animate([first, finish], { duration: 750, delay, easing: 'cubic-bezier(.2,.7,.2,1)', fill: 'backwards' }); animations.add(animation); animation.finished.then(() => animations.delete(animation), () => animations.delete(animation)); }; /** A block the reader has already scrolled past: shown at once, no entrance for nobody. */ const passed = node => { seen.add(node); unhide(node); observer.unobserve(node); }; const observer = new view.IntersectionObserver(entries => { for (const entry of entries) { if (entry.isIntersecting) { show(entry.target); observer.unobserve(entry.target); } else if (entry.boundingClientRect.bottom < 0) passed(entry.target); else hide(entry.target); } }, { threshold: 0, rootMargin: '0px 0px -40px 0px' }); // A jump - an anchor link, a fast scroll, a restored position - can carry the reader past a // block without it ever intersecting. Nothing that is above the viewport may stay hidden. let sweeping = false; const sweep = () => { sweeping = false; for (const node of [...hidden]) if (node.getBoundingClientRect().bottom < 0) passed(node); }; const onScroll = () => { if (!sweeping) { sweeping = true; view.requestAnimationFrame(sweep); } }; const syncPreference = () => { if (reduced()) { observer.disconnect(); animations.forEach(animation => animation.cancel()); [...hidden].forEach(unhide); } 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); view.addEventListener('scroll', onScroll, { passive: true }); syncPreference(); return () => { observer.disconnect(); animations.forEach(animation => animation.cancel()); [...hidden].forEach(unhide); preference.removeEventListener('change', syncPreference); view.removeEventListener('scroll', onScroll); }; } if (!document.querySelector('#editor')) initMotion();