You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

386 lines
14 KiB

/**
* 滚动触发动画系统 JavaScript
* 基于 GSAP ScrollTrigger 的动画控制器
* 配合 scroll-animations.css 使用
*/
(function() {
'use strict';
// 动画系统配置
const ScrollAnimations = {
// 默认配置
config: {
// 触发位置(元素进入视口的百分比)
triggerStart: 'top 80%',
triggerEnd: 'bottom 20%',
// 是否启用标记(调试用)
markers: false,
// 默认动画持续时间
duration: 0.8,
// 默认缓动函数
ease: 'power2.out',
// 是否启用批量处理
batch: true,
// 批量处理间隔
batchInterval: 0.1,
// 是否在元素离开视口时反转动画
toggleActions: 'play none none reverse'
},
// 动画类型映射
animationTypes: {
'fade-in-up': {
from: { opacity: 0, y: 50 },
to: { opacity: 1, y: 0 }
},
'fade-in-down': {
from: { opacity: 0, y: -50 },
to: { opacity: 1, y: 0 }
},
'fade-in-left': {
from: { opacity: 0, x: -50 },
to: { opacity: 1, x: 0 }
},
'fade-in-right': {
from: { opacity: 0, x: 50 },
to: { opacity: 1, x: 0 }
},
'scale-in': {
from: { opacity: 0, scale: 0.8 },
to: { opacity: 1, scale: 1 }
},
'rotate-in': {
from: { opacity: 0, rotation: -10, scale: 0.9 },
to: { opacity: 1, rotation: 0, scale: 1 }
},
'fade-scale-up': {
from: { opacity: 0, y: 30, scale: 0.95 },
to: { opacity: 1, y: 0, scale: 1 }
},
'blur-in': {
from: { opacity: 0, filter: 'blur(10px)', y: 20 },
to: { opacity: 1, filter: 'blur(0px)', y: 0 }
}
},
// 初始化函数
init: function() {
// 等待 GSAP 和 ScrollTrigger 加载完成
this.waitForGSAP(() => {
this.setupScrollTrigger();
this.initAnimations();
this.setupStaggerAnimations();
this.setupTextReveal();
this.bindEvents();
});
},
// 等待 GSAP 加载
waitForGSAP: function(callback) {
if (typeof gsap !== 'undefined' && typeof ScrollTrigger !== 'undefined') {
callback();
} else {
// 监听 GSAP 准备就绪事件
document.addEventListener('gsapReady', callback);
// 备用方案:轮询检查
let attempts = 0;
const checkGSAP = () => {
attempts++;
if (typeof gsap !== 'undefined' && typeof ScrollTrigger !== 'undefined') {
callback();
} else if (attempts < 50) {
setTimeout(checkGSAP, 100);
} else {
this.initCSSFallback();
}
};
setTimeout(checkGSAP, 100);
}
},
// 设置 ScrollTrigger
setupScrollTrigger: function() {
gsap.registerPlugin(ScrollTrigger);
// 设置全局配置
ScrollTrigger.config({
autoRefreshEvents: "visibilitychange,DOMContentLoaded,load"
});
// 启用批量处理以提高性能
ScrollTrigger.batch(".scroll-animate", {
onEnter: (elements) => {
elements.forEach(element => {
this.animateElement(element, 'enter');
});
},
onLeave: (elements) => {
if (this.config.toggleActions.includes('reverse')) {
elements.forEach(element => {
this.animateElement(element, 'leave');
});
}
},
start: this.config.triggerStart,
end: this.config.triggerEnd
});
},
// 初始化动画
initAnimations: function() {
// 为每种动画类型创建 ScrollTrigger
Object.keys(this.animationTypes).forEach(animationType => {
const elements = document.querySelectorAll(`.${animationType}`);
if (elements.length > 0) {
this.createScrollTrigger(elements, animationType);
}
});
},
// 创建 ScrollTrigger
createScrollTrigger: function(elements, animationType) {
const animationConfig = this.animationTypes[animationType];
elements.forEach((element, index) => {
// 设置初始状态
gsap.set(element, animationConfig.from);
// 获取延迟时间
const delay = this.getElementDelay(element, index);
// 创建动画
const animation = gsap.to(element, {
...animationConfig.to,
duration: this.config.duration,
ease: this.config.ease,
delay: delay,
scrollTrigger: {
trigger: element,
start: this.config.triggerStart,
end: this.config.triggerEnd,
toggleActions: this.config.toggleActions,
markers: this.config.markers,
onEnter: () => {
element.classList.add('animate-active');
this.dispatchEvent(element, 'scrollAnimateEnter');
},
onLeave: () => {
if (this.config.toggleActions.includes('reverse')) {
element.classList.remove('animate-active');
this.dispatchEvent(element, 'scrollAnimateLeave');
}
}
}
});
// 存储动画实例到元素上
element._scrollAnimation = animation;
});
},
// 设置交错动画
setupStaggerAnimations: function() {
const staggerContainers = document.querySelectorAll('.stagger-container');
staggerContainers.forEach(container => {
const items = container.querySelectorAll('.stagger-item');
if (items.length > 0) {
// 设置初始状态
gsap.set(items, { opacity: 0, y: 30 });
// 创建交错动画
gsap.to(items, {
opacity: 1,
y: 0,
duration: 0.6,
ease: this.config.ease,
stagger: this.config.batchInterval,
scrollTrigger: {
trigger: container,
start: this.config.triggerStart,
end: this.config.triggerEnd,
toggleActions: this.config.toggleActions,
onEnter: () => {
container.classList.add('animate-active');
}
}
});
}
});
},
// 设置文字揭示动画
setupTextReveal: function() {
const textElements = document.querySelectorAll('.text-reveal');
textElements.forEach(element => {
// 将文字包装在 span 中
this.wrapTextInSpans(element);
const spans = element.querySelectorAll('span');
// 设置初始状态
gsap.set(spans, { opacity: 0, y: '100%' });
// 创建文字动画
gsap.to(spans, {
opacity: 1,
y: '0%',
duration: 0.6,
ease: this.config.ease,
stagger: 0.05,
scrollTrigger: {
trigger: element,
start: this.config.triggerStart,
end: this.config.triggerEnd,
toggleActions: this.config.toggleActions
}
});
});
},
// 包装文字到 span 中
wrapTextInSpans: function(element) {
const text = element.textContent;
const words = text.split(' ');
element.innerHTML = words.map(word =>
`<span style="display: inline-block; overflow: hidden;"><span>${word}</span></span>`
).join(' ');
},
// 获取元素延迟时间
getElementDelay: function(element, index) {
// 检查是否有延迟类
const delayClasses = [
'animate-delay-100', 'animate-delay-200', 'animate-delay-300',
'animate-delay-400', 'animate-delay-500', 'animate-delay-600',
'animate-delay-700', 'animate-delay-800'
];
for (let delayClass of delayClasses) {
if (element.classList.contains(delayClass)) {
return parseInt(delayClass.split('-')[2]) / 1000;
}
}
// 如果启用批量处理,返回基于索引的延迟
return this.config.batch ? index * this.config.batchInterval : 0;
},
// 动画元素
animateElement: function(element, direction) {
if (direction === 'enter') {
element.classList.add('animate-active');
} else {
element.classList.remove('animate-active');
}
},
// 分发自定义事件
dispatchEvent: function(element, eventName) {
const event = new CustomEvent(eventName, {
detail: { element: element },
bubbles: true
});
element.dispatchEvent(event);
},
// CSS 回退方案
initCSSFallback: function() {
// 使用 Intersection Observer 作为回退
if ('IntersectionObserver' in window) {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('animate-active');
}
});
}, {
threshold: 0.1,
rootMargin: '0px 0px -20% 0px'
});
// 观察所有动画元素
Object.keys(this.animationTypes).forEach(animationType => {
const elements = document.querySelectorAll(`.${animationType}`);
elements.forEach(element => observer.observe(element));
});
}
},
// 绑定事件
bindEvents: function() {
// 窗口大小改变时刷新 ScrollTrigger
window.addEventListener('resize', () => {
if (typeof ScrollTrigger !== 'undefined') {
ScrollTrigger.refresh();
}
});
// 页面可见性改变时暂停/恢复动画
document.addEventListener('visibilitychange', () => {
if (typeof gsap !== 'undefined') {
if (document.hidden) {
gsap.globalTimeline.pause();
} else {
gsap.globalTimeline.resume();
}
}
});
},
// 公共 API
api: {
// 刷新所有 ScrollTrigger
refresh: function() {
if (typeof ScrollTrigger !== 'undefined') {
ScrollTrigger.refresh();
}
},
// 启用调试模式
enableDebug: function() {
ScrollAnimations.config.markers = true;
if (typeof ScrollTrigger !== 'undefined') {
ScrollTrigger.refresh();
}
},
// 禁用调试模式
disableDebug: function() {
ScrollAnimations.config.markers = false;
if (typeof ScrollTrigger !== 'undefined') {
ScrollTrigger.refresh();
}
},
// 手动触发元素动画
triggerAnimation: function(element) {
if (element && element._scrollAnimation) {
element._scrollAnimation.play();
}
}
}
};
// 等待 DOM 加载完成后初始化
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
ScrollAnimations.init();
});
} else {
ScrollAnimations.init();
}
// 将 API 暴露到全局
window.ScrollAnimations = ScrollAnimations.api;
})();