429 lines
11 KiB
JavaScript
429 lines
11 KiB
JavaScript
/**
|
||
* 动画助手 - 处理滚动触发动画和交互动画
|
||
*/
|
||
|
||
class AnimationHelper {
|
||
constructor() {
|
||
this.animatedElements = [];
|
||
this.isInitialized = false;
|
||
this.observer = null;
|
||
this.init();
|
||
}
|
||
|
||
/**
|
||
* 初始化动画助手
|
||
*/
|
||
init() {
|
||
if (this.isInitialized) return;
|
||
|
||
// 设置Intersection Observer
|
||
this.setupIntersectionObserver();
|
||
|
||
// 添加滚动事件监听
|
||
this.addScrollListeners();
|
||
|
||
// 监听路由变化
|
||
this.setupRouteListener();
|
||
|
||
// 初始化页面加载动画
|
||
this.initPageLoadAnimations();
|
||
|
||
this.isInitialized = true;
|
||
}
|
||
|
||
/**
|
||
* 设置Intersection Observer用于滚动触发动画
|
||
*/
|
||
setupIntersectionObserver() {
|
||
if (!window.IntersectionObserver) {
|
||
// 降级处理:直接显示所有元素
|
||
this.showAllElements();
|
||
return;
|
||
}
|
||
|
||
this.observer = new IntersectionObserver((entries) => {
|
||
entries.forEach(entry => {
|
||
if (entry.isIntersecting) {
|
||
this.animateElement(entry.target);
|
||
}
|
||
});
|
||
}, {
|
||
threshold: 0.1,
|
||
rootMargin: '0px 0px -50px 0px'
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 添加滚动事件监听
|
||
*/
|
||
addScrollListeners() {
|
||
// 监听滚动事件,用于视差效果
|
||
window.addEventListener('scroll', this.handleScroll.bind(this), { passive: true });
|
||
|
||
// 监听窗口大小变化
|
||
window.addEventListener('resize', this.handleResize.bind(this), { passive: true });
|
||
}
|
||
|
||
/**
|
||
* 设置路由监听
|
||
*/
|
||
setupRouteListener() {
|
||
// 监听 popstate 事件(浏览器前进后退)
|
||
window.addEventListener('popstate', this.handleRouteChange.bind(this));
|
||
|
||
// 监听自定义路由变化事件
|
||
window.addEventListener('routeChange', this.handleRouteChange.bind(this));
|
||
|
||
// 监听页面可见性变化
|
||
document.addEventListener('visibilitychange', this.handleVisibilityChange.bind(this));
|
||
}
|
||
|
||
/**
|
||
* 处理滚动事件
|
||
*/
|
||
handleScroll() {
|
||
this.updateParallaxEffects();
|
||
this.updateScrollProgress();
|
||
}
|
||
|
||
/**
|
||
* 处理窗口大小变化
|
||
*/
|
||
handleResize() {
|
||
// 重新计算动画元素位置
|
||
this.recalculateAnimations();
|
||
}
|
||
|
||
/**
|
||
* 处理路由变化
|
||
*/
|
||
handleRouteChange() {
|
||
// 延迟重置动画,确保DOM已更新
|
||
setTimeout(() => {
|
||
this.resetAnimations();
|
||
this.initPageLoadAnimations();
|
||
}, 100);
|
||
}
|
||
|
||
/**
|
||
* 处理页面可见性变化
|
||
*/
|
||
handleVisibilityChange() {
|
||
if (!document.hidden) {
|
||
// 页面重新可见时,重置动画状态
|
||
setTimeout(() => {
|
||
this.resetAnimations();
|
||
this.initPageLoadAnimations();
|
||
}, 100);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 初始化页面加载动画
|
||
*/
|
||
initPageLoadAnimations() {
|
||
// 为页面加载动画添加延迟
|
||
setTimeout(() => {
|
||
this.animatePageLoad();
|
||
}, 100);
|
||
}
|
||
|
||
/**
|
||
* 页面加载动画
|
||
*/
|
||
animatePageLoad() {
|
||
// 标题动画
|
||
const titles = document.querySelectorAll('.section-title.animate-on-scroll');
|
||
titles.forEach((title, index) => {
|
||
setTimeout(() => {
|
||
title.classList.add('visible');
|
||
}, index * 200);
|
||
});
|
||
|
||
// 内容动画
|
||
const contentElements = document.querySelectorAll('.animate-stagger');
|
||
contentElements.forEach((container, containerIndex) => {
|
||
const children = container.children;
|
||
Array.from(children).forEach((child, childIndex) => {
|
||
setTimeout(() => {
|
||
child.style.opacity = '1';
|
||
child.style.transform = 'translateY(0)';
|
||
}, (containerIndex * 300) + (childIndex * 100));
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 为元素添加滚动触发动画
|
||
*/
|
||
addScrollAnimation(element, options = {}) {
|
||
if (!element) return;
|
||
|
||
const defaultOptions = {
|
||
threshold: 0.1,
|
||
rootMargin: '0px 0px -50px 0px',
|
||
animationClass: 'animate-in',
|
||
delay: 0
|
||
};
|
||
|
||
const config = { ...defaultOptions, ...options };
|
||
|
||
// 添加动画类
|
||
element.classList.add('scroll-animate');
|
||
|
||
// 设置延迟
|
||
if (config.delay > 0) {
|
||
element.style.transitionDelay = `${config.delay}s`;
|
||
}
|
||
|
||
// 添加到观察列表
|
||
if (this.observer) {
|
||
this.observer.observe(element);
|
||
}
|
||
|
||
this.animatedElements.push(element);
|
||
}
|
||
|
||
/**
|
||
* 为多个元素添加交错动画
|
||
*/
|
||
addStaggerAnimation(container, options = {}) {
|
||
if (!container) return;
|
||
|
||
const defaultOptions = {
|
||
delay: 0.1,
|
||
staggerDelay: 0.1,
|
||
animationClass: 'visible'
|
||
};
|
||
|
||
const config = { ...defaultOptions, ...options };
|
||
|
||
// 添加交错动画类
|
||
container.classList.add('animate-stagger');
|
||
|
||
// 为子元素设置初始状态
|
||
const children = Array.from(container.children);
|
||
children.forEach((child, index) => {
|
||
child.style.opacity = '0';
|
||
child.style.transform = 'translateY(30px)';
|
||
child.style.transitionDelay = `${config.delay + (index * config.staggerDelay)}s`;
|
||
});
|
||
|
||
// 添加到观察列表
|
||
if (this.observer) {
|
||
this.observer.observe(container);
|
||
}
|
||
|
||
this.animatedElements.push(container);
|
||
}
|
||
|
||
/**
|
||
* 触发元素动画
|
||
*/
|
||
animateElement(element) {
|
||
if (!element) return;
|
||
|
||
// 添加可见类
|
||
if (element.classList.contains('scroll-animate')) {
|
||
element.classList.add('animate-in');
|
||
} else if (element.classList.contains('animate-stagger')) {
|
||
element.classList.add('visible');
|
||
}
|
||
|
||
// 触发自定义事件
|
||
element.dispatchEvent(new CustomEvent('animationStart', {
|
||
detail: { element, type: 'scroll' }
|
||
}));
|
||
}
|
||
|
||
/**
|
||
* 手动触发动画
|
||
*/
|
||
triggerAnimation(element, animationClass = 'animate-in') {
|
||
if (!element) return;
|
||
|
||
element.classList.add(animationClass);
|
||
|
||
// 触发自定义事件
|
||
element.dispatchEvent(new CustomEvent('animationTrigger', {
|
||
detail: { element, animationClass }
|
||
}));
|
||
}
|
||
|
||
/**
|
||
* 重置元素动画状态
|
||
*/
|
||
resetAnimation(element) {
|
||
if (!element) return;
|
||
|
||
element.classList.remove('animate-in', 'visible');
|
||
element.style.transitionDelay = '';
|
||
|
||
// 重置样式
|
||
if (element.classList.contains('scroll-animate')) {
|
||
element.style.opacity = '0';
|
||
element.style.transform = 'translateY(50px)';
|
||
} else if (element.classList.contains('animate-stagger')) {
|
||
const children = Array.from(element.children);
|
||
children.forEach(child => {
|
||
child.style.opacity = '0';
|
||
child.style.transform = 'translateY(30px)';
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 更新视差效果
|
||
*/
|
||
updateParallaxEffects() {
|
||
const parallaxElements = document.querySelectorAll('[data-parallax]');
|
||
const scrolled = window.pageYOffset;
|
||
|
||
parallaxElements.forEach(element => {
|
||
const speed = parseFloat(element.dataset.parallax) || 0.5;
|
||
const yPos = -(scrolled * speed);
|
||
element.style.transform = `translateY(${yPos}px)`;
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 更新滚动进度
|
||
*/
|
||
updateScrollProgress() {
|
||
const progressElements = document.querySelectorAll('[data-scroll-progress]');
|
||
const scrolled = window.pageYOffset;
|
||
const maxScroll = document.documentElement.scrollHeight - window.innerHeight;
|
||
const progress = (scrolled / maxScroll) * 100;
|
||
|
||
progressElements.forEach(element => {
|
||
if (element.dataset.scrollProgress === 'bar') {
|
||
element.style.width = `${progress}%`;
|
||
} else if (element.dataset.scrollProgress === 'number') {
|
||
element.textContent = Math.round(progress);
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 重新计算动画
|
||
*/
|
||
recalculateAnimations() {
|
||
// 重新观察所有元素
|
||
this.animatedElements.forEach(element => {
|
||
if (this.observer) {
|
||
this.observer.unobserve(element);
|
||
this.observer.observe(element);
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 重置所有动画状态
|
||
*/
|
||
resetAnimations() {
|
||
// 重置所有动画元素的类
|
||
this.animatedElements.forEach(element => {
|
||
if (element.classList.contains('scroll-animate')) {
|
||
element.classList.remove('animate-in');
|
||
} else if (element.classList.contains('animate-stagger')) {
|
||
element.classList.remove('visible');
|
||
// 重置子元素状态
|
||
const children = Array.from(element.children);
|
||
children.forEach((child, index) => {
|
||
child.style.opacity = '0';
|
||
child.style.transform = 'translateY(30px)';
|
||
});
|
||
}
|
||
});
|
||
|
||
// 重置所有带有动画类的元素
|
||
const allAnimatedElements = document.querySelectorAll('.animate-on-scroll, .animate-stagger, .scroll-animate');
|
||
allAnimatedElements.forEach(element => {
|
||
element.classList.remove('visible', 'animate-in');
|
||
|
||
// 重置 transform 和 opacity
|
||
if (element.classList.contains('animate-on-scroll')) {
|
||
element.style.opacity = '0';
|
||
element.style.transform = 'translateY(30px)';
|
||
}
|
||
});
|
||
|
||
// 重新观察所有元素
|
||
this.recalculateAnimations();
|
||
}
|
||
|
||
/**
|
||
* 显示所有元素(降级处理)
|
||
*/
|
||
showAllElements() {
|
||
const scrollElements = document.querySelectorAll('.scroll-animate, .animate-stagger');
|
||
scrollElements.forEach(element => {
|
||
if (element.classList.contains('scroll-animate')) {
|
||
element.classList.add('animate-in');
|
||
} else if (element.classList.contains('animate-stagger')) {
|
||
element.classList.add('visible');
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 添加鼠标悬停动画
|
||
*/
|
||
addHoverAnimation(element, animationType = 'scale') {
|
||
if (!element) return;
|
||
|
||
const animations = {
|
||
scale: 'hover-scale',
|
||
lift: 'hover-lift',
|
||
rotate: 'hover-rotate',
|
||
glow: 'hover-glow',
|
||
bounce: 'hover-bounce'
|
||
};
|
||
|
||
const animationClass = animations[animationType] || 'hover-scale';
|
||
element.classList.add(animationClass);
|
||
}
|
||
|
||
/**
|
||
* 添加点击动画
|
||
*/
|
||
addClickAnimation(element, animationType = 'pulse') {
|
||
if (!element) return;
|
||
|
||
element.addEventListener('click', () => {
|
||
element.classList.add(`animate ${animationType}`);
|
||
|
||
// 动画结束后移除类
|
||
setTimeout(() => {
|
||
element.classList.remove(`animate ${animationType}`);
|
||
}, 1000);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 销毁动画助手
|
||
*/
|
||
destroy() {
|
||
if (this.observer) {
|
||
this.observer.disconnect();
|
||
}
|
||
|
||
window.removeEventListener('scroll', this.handleScroll);
|
||
window.removeEventListener('resize', this.handleResize);
|
||
window.removeEventListener('popstate', this.handleRouteChange);
|
||
window.removeEventListener('routeChange', this.handleRouteChange);
|
||
document.removeEventListener('visibilitychange', this.handleVisibilityChange);
|
||
|
||
this.animatedElements = [];
|
||
this.isInitialized = false;
|
||
}
|
||
}
|
||
|
||
// 创建全局实例
|
||
window.animationHelper = new AnimationHelper();
|
||
|
||
// 导出类(如果使用模块系统)
|
||
if (typeof module !== 'undefined' && module.exports) {
|
||
module.exports = AnimationHelper;
|
||
}
|