:root {
--duration-extra-long: 600ms;
--ease-out-slow: cubic-bezier(0, 0, 0.3, 1);
}
/* 仅在用户未开启“减少运动”时启用动画(晕动症用户友好) */
@media (prefers-reduced-motion: no-preference) {
.scroll-trigger:not(.scroll-trigger--offscreen).animate--slide-in {
animation: slideIn var(--duration-extra-long) var(--ease-out-slow) forwards;
animation-delay: calc(var(--animation-order) * 75ms);
}
@keyframes slideIn {
from {
transform: translateY(2rem);
opacity: 0.01;
}
to {
transform: translateY(0);
opacity: 1;
}
}
}
// ❌ 性能差,频繁触发
window.addEventListener('scroll', checkVisibility);
// ✅ 高性能,浏览器底层优化
const observer = new IntersectionObserver(callback, options);
const SCROLL_ANIMATION_TRIGGER_CLASSNAME = 'scroll-trigger';
const SCROLL_ANIMATION_OFFSCREEN_CLASSNAME = 'scroll-trigger--offscreen';
function onIntersection(entries, observer) {
entries.forEach((entry, index) => {
const el = entry.target;
if (entry.isIntersecting) {
// 进入视口:移除 offscreen 类,允许动画播放
el.classList.remove(SCROLL_ANIMATION_OFFSCREEN_CLASSNAME);
// 若为级联元素,动态设置顺序(兜底)
if (el.hasAttribute('data-cascade')) {
el.style.setProperty('--animation-order', index + 1);
}
// 只触发一次,停止监听
observer.unobserve(el);
} else {
// 离开视口:加上 offscreen 类,禁用动画
el.classList.add(SCROLL_ANIMATION_OFFSCREEN_CLASSNAME);
}
});
}
function initScrollAnimations(root = document) {
const triggers = root.querySelectorAll(`.${SCROLL_ANIMATION_TRIGGER_CLASSNAME}`);
if (!triggers.length) return;
const observer = new IntersectionObserver(onIntersection, {
rootMargin: '0px 0px -50px 0px', // 元素进入视口 50px 后才触发
threshold: [0, 0.25, 0.5, 0.75, 1.0],
});
triggers.forEach((el) => observer.observe(el));
}
// 页面加载完成后启动
document.addEventListener('DOMContentLoaded', () => {
initScrollAnimations();
});