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
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;
|
|
|
|
})(); |