name: GSAP动画最佳实践 description: GSAP动画最佳实践,用于网页设计 - 滚动触发器、性能优化、可访问性、响应式动画和测试集成。在WordPress或任何Web项目中实现或审查动画时使用。 allowed-tools: 阅读、写作、编辑、Bash、Glob、Grep
GSAP动画最佳实践
使用GSAP(GreenSock动画平台)实现专业、可访问且性能优越的动画的全面指南。
核心原则
1. 性能优先
- 仅动画
transform和opacity(GPU加速) - 避免动画
width、height、top、left、margin、padding - 谨慎使用
will-change - 目标是在所有设备上达到60fps
2. 始终可访问
- 尊重
prefers-reduced-motion - 确保内容在没有JavaScript的情况下可见
- 不要将关键内容隐藏在动画后面
- 为长时间动画提供跳过/暂停控制
3. 渐进增强
- 内容必须在没有动画的情况下工作
- 动画增强而不是替代功能
- 在禁用动画时进行测试
GSAP设置
安装
<!-- CDN(WordPress推荐)-->
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/ScrollTrigger.min.js"></script>
<!-- 可选插件 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/ScrollSmoother.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/SplitText.min.js"></script>
WordPress Enqueue
function theme_enqueue_gsap() {
// GSAP核心
wp_enqueue_script(
'gsap',
'https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js',
array(),
'3.12.5',
true
);
// ScrollTrigger
wp_enqueue_script(
'gsap-scrolltrigger',
'https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/ScrollTrigger.min.js',
array('gsap'),
'3.12.5',
true
);
// 主题动画
wp_enqueue_script(
'theme-animations',
get_theme_file_uri('/assets/js/animations.js'),
array('gsap', 'gsap-scrolltrigger'),
filemtime(get_theme_file_path('/assets/js/animations.js')),
true
);
}
add_action('wp_enqueue_scripts', 'theme_enqueue_gsap');
动画模式
1. 滚动时淡入
// 基本淡入
gsap.from('.fade-in', {
opacity: 0,
y: 50,
duration: 1,
stagger: 0.2,
scrollTrigger: {
trigger: '.fade-in',
start: 'top 80%',
toggleActions: 'play none none none'
}
});
2. 交错元素
// 卡片逐个出现
gsap.from('.card', {
opacity: 0,
y: 100,
duration: 0.8,
stagger: {
amount: 0.6,
from: 'start'
},
ease: 'power2.out',
scrollTrigger: {
trigger: '.cards-container',
start: 'top 75%'
}
});
3. 平行视差效果
// 图片上的微妙平行视差
gsap.to('.parallax-image', {
yPercent: -20,
ease: 'none',
scrollTrigger: {
trigger: '.parallax-section',
start: 'top bottom',
end: 'bottom top',
scrub: true
}
});
4. 文本揭示(逐行)
// 需要SplitText插件(Club GreenSock)
// 或使用CSS替代方案
// CSS替代方案 - 将每一行包裹在span中
gsap.from('.reveal-line', {
opacity: 0,
y: '100%',
duration: 0.8,
stagger: 0.1,
ease: 'power3.out',
scrollTrigger: {
trigger: '.text-reveal',
start: 'top 80%'
}
});
5. 幕布/遮罩揭示
// 通过滑动遮罩揭示图像
gsap.to('.curtain-mask', {
scaleX: 0,
transformOrigin: 'right center',
duration: 1.2,
ease: 'power4.inOut',
scrollTrigger: {
trigger: '.curtain-container',
start: 'top 70%'
}
});
6. 英雄动画时间线
// 复杂的英雄序列
const heroTL = gsap.timeline({
defaults: { ease: 'power3.out' }
});
heroTL
.from('.hero-bg', { scale: 1.2, duration: 1.5 })
.from('.hero-title', { opacity: 0, y: 100, duration: 1 }, '-=1')
.from('.hero-subtitle', { opacity: 0, y: 50, duration: 0.8 }, '-=0.5')
.from('.hero-cta', { opacity: 0, y: 30, duration: 0.6 }, '-=0.3');
可访问性
尊重减少运动
// 检查用户偏好
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
// 选项1:禁用所有动画
if (prefersReducedMotion) {
gsap.globalTimeline.timeScale(0);
ScrollTrigger.getAll().forEach(st => st.kill());
}
// 选项2:简化动画
const animationConfig = prefersReducedMotion
? { duration: 0, stagger: 0 }
: { duration: 1, stagger: 0.2 };
gsap.from('.element', {
opacity: 0,
y: prefersReducedMotion ? 0 : 50,
...animationConfig
});
CSS回退
/* 确保没有JS时内容可见 */
.fade-in {
opacity: 1;
transform: translateY(0);
}
/* 只有在动画将运行时才隐藏 */
.js .fade-in {
opacity: 0;
transform: translateY(50px);
}
/* 在CSS中也尊重减少运动 */
@media (prefers-reduced-motion: reduce) {
.js .fade-in {
opacity: 1;
transform: none;
}
}
添加JS类到HTML
// 在脚本开始时添加
document.documentElement.classList.add('js');
响应式动画
断点感知动画
// 创建响应式动画
const mm = gsap.matchMedia();
mm.add('(min-width: 1024px)', () => {
// 桌面动画
gsap.from('.hero-image', {
x: 100,
opacity: 0,
duration: 1.2
});
return () => {
// 断点变化时清理
};
});
mm.add('(max-width: 1023px)', () => {
// 移动动画(更简单)
gsap.from('.hero-image', {
opacity: 0,
duration: 0.8
});
});
刷新调整大小时
// 调整大小时重新计算ScrollTrigger
let resizeTimer;
window.addEventListener('resize', () => {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(() => {
ScrollTrigger.refresh();
}, 250);
});
性能优化
1. 仅使用变换属性
// 好 - GPU加速
gsap.to('.element', {
x: 100, // transform: translateX
y: 50, // transform: translateY
rotation: 45, // transform: rotate
scale: 1.2, // transform: scale
opacity: 0.5
});
// 坏 - 引起布局/绘制
gsap.to('.element', {
left: 100, // 触发布局
width: '200px', // 触发布局
marginTop: 50 // 触发布局
});
2. 批量相似动画
// 对许多相似元素使用批量
ScrollTrigger.batch('.card', {
onEnter: batch => gsap.to(batch, {
opacity: 1,
y: 0,
stagger: 0.1
}),
start: 'top 85%'
});
3. 杀死未使用的ScrollTriggers
// 导航(SPA)或组件卸载时清理
function cleanup() {
ScrollTrigger.getAll().forEach(st => st.kill());
gsap.killTweensOf('*');
}
4. 延迟初始化
// 仅初始化可见部分的动画
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
initSectionAnimations(entry.target);
observer.unobserve(entry.target);
}
});
}, { rootMargin: '100px' });
document.querySelectorAll('.animated-section').forEach(section => {
observer.observe(section);
});
ScrollTrigger最佳实践
1. 适当的开始/结束点
// 避免常见错误
ScrollTrigger.create({
trigger: '.section',
start: 'top 80%', // 当触发器顶部击中视口顶部的80%时
end: 'bottom 20%', // 当触发器底部击中顶部的20%时
markers: true, // 调试仅 - 生产中删除!
});
2. 小心地固定部分
// 固定可能导致布局问题
ScrollTrigger.create({
trigger: '.pinned-section',
start: 'top top',
end: '+=100%',
pin: true,
pinSpacing: true, // 通常希望这是真的
anticipatePin: 1 // 有助于移动设备
});
3. 处理图像加载
// 图像加载前等待计算位置
ScrollTrigger.config({
ignoreMobileResize: true
});
window.addEventListener('load', () => {
ScrollTrigger.refresh();
});
// 或在延迟图像加载后刷新
document.querySelectorAll('img[loading="lazy"]').forEach(img => {
img.addEventListener('load', () => ScrollTrigger.refresh());
});
测试集成
视觉QA兼容性
要使视觉-qa技能正确捕获动画:
// 暴露函数以立即完成所有动画
window.completeAllAnimations = function() {
gsap.globalTimeline.progress(1);
ScrollTrigger.getAll().forEach(st => {
st.scroll(st.end);
});
};
// 或跳过动画以进行屏幕截图
if (window.location.search.includes('skip-animations')) {
gsap.globalTimeline.timeScale(100);
}
Playwright测试
// 在Playwright测试中
await page.evaluate(() => {
if (window.completeAllAnimations) {
window.completeAllAnimations();
}
});
await page.waitForTimeout(500);
await page.screenshot({ path: 'screenshot.png', fullPage: true });
常见动画库
可重用的动画类
// animations.js - 可重用的动画库
const Animations = {
// 初始化所有动画
init() {
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
return;
}
this.fadeIn();
this.slideIn();
this.parallax();
this.textReveal();
},
fadeIn() {
gsap.utils.toArray('[data-animate="fade-in"]').forEach(el => {
gsap.from(el, {
opacity: 0,
y: 50,
duration: 0.8,
scrollTrigger: {
trigger: el,
start: 'top 85%',
once: true
}
});
});
},
slideIn() {
gsap.utils.toArray('[data-animate="slide-left"]').forEach(el => {
gsap.from(el, {
opacity: 0,
x: -100,
duration: 1,
scrollTrigger: {
trigger: el,
start: 'top 80%',
once: true
}
});
});
gsap.utils.toArray('[data-animate="slide-right"]').forEach(el => {
gsap.from(el, {
opacity: 0,
x: 100,
duration: 1,
scrollTrigger: {
trigger: el,
start: 'top 80%',
once: true
}
});
});
},
parallax() {
gsap.utils.toArray('[data-parallax]').forEach(el => {
const speed = el.dataset.parallax || 0.2;
gsap.to(el, {
yPercent: -100 * speed,
ease: 'none',
scrollTrigger: {
trigger: el.parentElement,
start: 'top bottom',
end: 'bottom top',
scrub: true
}
});
});
},
textReveal() {
gsap.utils.toArray('[data-animate="text-reveal"]').forEach(el => {
const lines = el.querySelectorAll('.line');
gsap.from(lines, {
opacity: 0,
y: '100%',
duration: 0.8,
stagger: 0.1,
scrollTrigger: {
trigger: el,
start: 'top 80%',
once: true
}
});
});
},
// 动态内容后刷新
refresh() {
ScrollTrigger.refresh();
},
// SPA导航清理
destroy() {
ScrollTrigger.getAll().forEach(st => st.kill());
gsap.killTweensOf('*');
}
};
// 在DOM就绪时初始化
document.addEventListener('DOMContentLoaded', () => Animations.init());
HTML用法
<!-- 淡入 -->
<div data-animate="fade-in">内容</div>
<!-- 从左侧滑入 -->
<div data-animate="slide-left">内容</div>
<!-- 平行视差(0.2 = 20%速度) -->
<img data-parallax="0.3" src="image.jpg">
<!-- 文本揭示(需要行包装) -->
<div data-animate="text-reveal">
<div class="line">第一行</div>
<div class="line">第二行</div>
</div>
调试
启用标记
ScrollTrigger.defaults({
markers: true // 显示开始/结束标记
});
日志动画事件
gsap.to('.element', {
x: 100,
onStart: () => console.log('动画开始'),
onComplete: () => console.log('动画完成'),
onUpdate: self => console.log('进度:', self.progress())
});
检查问题
// 列出所有ScrollTriggers
console.log('ScrollTriggers:', ScrollTrigger.getAll());
// 检查元素是否存在
const el = document.querySelector('.animated-element');
if (!el) console.warn('未找到动画目标!');
清单
发布前
- [ ] 移除所有
markers: true - [ ] 测试
prefers-reduced-motion: reduce - [ ] 在移动设备上测试(真实设备,不仅仅是DevTools)
- [ ] 在DevTools性能选项卡中检查性能
- [ ] 在目标设备上验证60fps
- [ ] 没有JavaScript时内容可见
- [ ] 图像延迟加载前ScrollTrigger刷新
- [ ] 没有布局抖动(避免动画布局属性)
视觉QA集成
- [ ] 屏幕截图前完成动画
- [ ] 全页滚动触发所有动画
- [ ] 屏幕截图捕获最终动画状态
- [ ] 在所有视口大小下测试