Files
Agent-n8n/doc/任务/子任务/gsap_examples.md

511 lines
13 KiB
Markdown
Raw Normal View History

# GSAP 动画实现示例
## 核心动画配置
### 1. GSAP 初始化设置
```typescript
// src/utils/gsapConfig.ts
import { gsap } from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
import { ScrollToPlugin } from 'gsap/ScrollToPlugin';
// 注册插件
gsap.registerPlugin(ScrollTrigger, ScrollToPlugin);
// 全局配置
gsap.config({
nullTargetWarn: false,
force3D: true
});
// 默认缓动函数
gsap.defaults({
ease: "power2.inOut",
duration: 0.8
});
```
### 2. 时间轴导航动画
```typescript
// src/components/TimelineNav.tsx
import React, { useEffect, useRef } from 'react';
import { gsap } from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
interface TimelineItem {
id: string;
title: string;
date: string;
description: string;
}
const TimelineNav: React.FC<{ items: TimelineItem[] }> = ({ items }) => {
const timelineRef = useRef<HTMLDivElement>(null);
const progressRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const ctx = gsap.context(() => {
// 进度条动画
gsap.to(progressRef.current, {
height: '100%',
ease: 'none',
scrollTrigger: {
trigger: document.body,
start: 'top top',
end: 'bottom bottom',
scrub: 1,
onUpdate: (self) => {
// 更新当前激活的时间节点
const progress = self.progress;
const activeIndex = Math.floor(progress * items.length);
updateActiveNode(activeIndex);
}
}
});
// 时间节点入场动画
gsap.from('.timeline-node', {
scale: 0,
opacity: 0,
duration: 0.5,
stagger: 0.1,
ease: 'back.out(1.7)'
});
}, timelineRef);
return () => ctx.revert();
}, [items]);
const scrollToSection = (id: string) => {
gsap.to(window, {
duration: 1.2,
scrollTo: {
y: `#${id}`,
offsetY: 100
},
ease: "power3.inOut"
});
};
return (
<nav ref={timelineRef} className="fixed left-8 top-1/2 -translate-y-1/2 z-50">
<div className="relative">
{/* 进度条背景 */}
<div className="absolute left-1/2 -translate-x-1/2 w-1 h-full bg-gray-200" />
{/* 动态进度条 */}
<div
ref={progressRef}
className="absolute left-1/2 -translate-x-1/2 w-1 bg-gradient-to-b from-blue-500 to-purple-600"
style={{ height: '0%' }}
/>
{/* 时间节点 */}
{items.map((item, index) => (
<div
key={item.id}
className="timeline-node relative mb-12 cursor-pointer"
onClick={() => scrollToSection(item.id)}
>
<div className="w-4 h-4 bg-white border-2 border-gray-400 rounded-full transition-all hover:scale-125" />
<span className="absolute left-8 top-1/2 -translate-y-1/2 whitespace-nowrap text-sm">
{item.date}
</span>
</div>
))}
</div>
</nav>
);
};
```
### 3. 内容区块滚动动画
```typescript
// src/components/ContentSection.tsx
import React, { useEffect, useRef } from 'react';
import { gsap } from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
const ContentSection: React.FC<{ data: any; index: number }> = ({ data, index }) => {
const sectionRef = useRef<HTMLElement>(null);
const contentRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const ctx = gsap.context(() => {
// 创建时间线
const tl = gsap.timeline({
scrollTrigger: {
trigger: sectionRef.current,
start: 'top 80%',
end: 'bottom 20%',
toggleActions: 'play none none reverse',
markers: false // 开发时可设为true查看触发点
}
});
// 标题动画
tl.from('.section-title', {
y: 100,
opacity: 0,
duration: 1,
ease: 'power3.out'
});
// 内容交错动画
tl.from('.content-item', {
x: index % 2 === 0 ? -150 : 150,
opacity: 0,
duration: 0.8,
stagger: 0.2,
ease: 'power2.out'
}, '-=0.5');
// 图片视差效果
gsap.to('.parallax-image', {
yPercent: -20,
ease: 'none',
scrollTrigger: {
trigger: sectionRef.current,
start: 'top bottom',
end: 'bottom top',
scrub: 1
}
});
}, sectionRef);
return () => ctx.revert();
}, [index]);
return (
<section ref={sectionRef} id={data.id} className="min-h-screen py-20">
<div ref={contentRef} className="container mx-auto px-8">
<h2 className="section-title text-5xl font-bold mb-8">{data.title}</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
{data.items.map((item: any, i: number) => (
<div key={i} className="content-item">
<div className="overflow-hidden rounded-lg">
<img
src={item.image}
alt={item.title}
className="parallax-image w-full h-64 object-cover"
/>
</div>
<h3 className="text-xl font-semibold mt-4">{item.title}</h3>
<p className="text-gray-600 mt-2">{item.description}</p>
</div>
))}
</div>
</div>
</section>
);
};
```
### 4. 图片查看器动画
```typescript
// src/components/ImageViewer.tsx
import React, { useState, useRef, useEffect } from 'react';
import { gsap } from 'gsap';
const ImageViewer: React.FC<{ images: string[] }> = ({ images }) => {
const [selectedImage, setSelectedImage] = useState<string | null>(null);
const modalRef = useRef<HTMLDivElement>(null);
const imageRef = useRef<HTMLImageElement>(null);
useEffect(() => {
if (selectedImage && modalRef.current && imageRef.current) {
// 模态框背景动画
gsap.fromTo(modalRef.current,
{ opacity: 0 },
{ opacity: 1, duration: 0.3 }
);
// 图片缩放入场
gsap.fromTo(imageRef.current,
{
scale: 0.5,
opacity: 0,
rotation: -10
},
{
scale: 1,
opacity: 1,
rotation: 0,
duration: 0.5,
ease: "back.out(1.7)"
}
);
}
}, [selectedImage]);
const closeViewer = () => {
// 退出动画
const tl = gsap.timeline({
onComplete: () => setSelectedImage(null)
});
tl.to(imageRef.current, {
scale: 0.8,
opacity: 0,
duration: 0.3,
ease: "power2.in"
});
tl.to(modalRef.current, {
opacity: 0,
duration: 0.2
}, '-=0.2');
};
const handleZoom = (scale: number) => {
gsap.to(imageRef.current, {
scale: scale,
duration: 0.4,
ease: "power2.inOut"
});
};
return (
<>
{/* 图片网格 */}
<div className="grid grid-cols-3 gap-4">
{images.map((src, index) => (
<div
key={index}
className="image-thumbnail cursor-pointer overflow-hidden rounded-lg"
onClick={() => setSelectedImage(src)}
onMouseEnter={(e) => {
gsap.to(e.currentTarget.querySelector('img'), {
scale: 1.1,
duration: 0.3
});
}}
onMouseLeave={(e) => {
gsap.to(e.currentTarget.querySelector('img'), {
scale: 1,
duration: 0.3
});
}}
>
<img src={src} alt="" className="w-full h-full object-cover" />
</div>
))}
</div>
{/* 查看器模态框 */}
{selectedImage && (
<div
ref={modalRef}
className="fixed inset-0 bg-black/90 z-50 flex items-center justify-center"
onClick={closeViewer}
>
<img
ref={imageRef}
src={selectedImage}
alt=""
className="max-w-[90%] max-h-[90%] cursor-zoom-in"
onClick={(e) => {
e.stopPropagation();
handleZoom(2);
}}
/>
{/* 控制按钮 */}
<div className="absolute bottom-8 left-1/2 -translate-x-1/2 flex gap-4">
<button
onClick={(e) => { e.stopPropagation(); handleZoom(0.5); }}
className="px-4 py-2 bg-white/20 rounded-lg hover:bg-white/30"
>
缩小
</button>
<button
onClick={(e) => { e.stopPropagation(); handleZoom(1); }}
className="px-4 py-2 bg-white/20 rounded-lg hover:bg-white/30"
>
原始大小
</button>
<button
onClick={(e) => { e.stopPropagation(); handleZoom(2); }}
className="px-4 py-2 bg-white/20 rounded-lg hover:bg-white/30"
>
放大
</button>
</div>
</div>
)}
</>
);
};
```
### 5. 页面加载动画
```typescript
// src/components/LoadingAnimation.tsx
import React, { useEffect, useRef } from 'react';
import { gsap } from 'gsap';
const LoadingAnimation: React.FC<{ onComplete: () => void }> = ({ onComplete }) => {
const loaderRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const tl = gsap.timeline({
onComplete: onComplete
});
// Logo动画
tl.from('.logo-text', {
y: 100,
opacity: 0,
duration: 1,
ease: 'power3.out'
});
// 进度条动画
tl.to('.progress-bar', {
width: '100%',
duration: 2,
ease: 'power2.inOut'
});
// 退出动画
tl.to(loaderRef.current, {
yPercent: -100,
duration: 0.8,
ease: 'power3.inOut'
});
}, [onComplete]);
return (
<div ref={loaderRef} className="fixed inset-0 bg-gradient-to-br from-blue-600 to-purple-700 z-50">
<div className="flex flex-col items-center justify-center h-full">
<h1 className="logo-text text-6xl font-bold text-white mb-8">
订单班展示
</h1>
<div className="w-64 h-1 bg-white/20 rounded-full overflow-hidden">
<div className="progress-bar h-full bg-white rounded-full" style={{ width: '0%' }} />
</div>
</div>
</div>
);
};
```
## 性能优化技巧
### 1. ScrollTrigger 优化
```typescript
// 批量创建 ScrollTrigger
ScrollTrigger.batch('.animate-item', {
onEnter: (batch) => gsap.to(batch, {
opacity: 1,
y: 0,
stagger: 0.15,
overwrite: true
}),
onLeave: (batch) => gsap.to(batch, {
opacity: 0,
y: 100,
stagger: 0.15,
overwrite: true
}),
onEnterBack: (batch) => gsap.to(batch, {
opacity: 1,
y: 0,
stagger: 0.15,
overwrite: true
}),
onLeaveBack: (batch) => gsap.to(batch, {
opacity: 0,
y: -100,
stagger: 0.15,
overwrite: true
})
});
// 刷新 ScrollTrigger
ScrollTrigger.refresh();
```
### 2. 清理和内存管理
```typescript
useEffect(() => {
// 创建上下文
const ctx = gsap.context(() => {
// 所有GSAP动画代码
}, containerRef);
// 清理函数
return () => {
ctx.revert(); // 清理所有动画
ScrollTrigger.getAll().forEach(trigger => trigger.kill());
};
}, []);
```
### 3. 响应式处理
```typescript
useEffect(() => {
// 创建媒体查询
const mm = gsap.matchMedia();
mm.add("(min-width: 768px)", () => {
// 桌面端动画
gsap.to('.desktop-element', {
x: 100,
duration: 1
});
});
mm.add("(max-width: 767px)", () => {
// 移动端动画
gsap.to('.mobile-element', {
y: 50,
duration: 0.8
});
});
return () => mm.revert();
}, []);
```
## 常用缓动函数
```typescript
// GSAP 内置缓动函数
const easings = {
smooth: "power2.inOut", // 平滑过渡
snappy: "power3.out", // 快速停止
bounce: "bounce.out", // 弹跳效果
elastic: "elastic.out(1, 0.3)", // 弹性效果
back: "back.out(1.7)", // 回弹效果
expo: "expo.out", // 指数缓动
slow: "power4.inOut", // 缓慢过渡
custom: "cubic-bezier(0.68, -0.55, 0.265, 1.55)" // 自定义贝塞尔曲线
};
```
## 注意事项
1. **性能考虑**
- 使用 `will-change` CSS属性优化动画元素
- 避免同时动画过多元素
- 使用 `force3D: true` 启用硬件加速
2. **移动端优化**
- 减少移动端的动画复杂度
- 使用 `gsap.matchMedia()` 做响应式处理
- 考虑使用 `reduced-motion` 媒体查询
3. **调试技巧**
- 使用 `markers: true` 查看 ScrollTrigger 触发点
- 使用 GSDevTools 插件进行动画调试
- 控制台使用 `gsap.globalTimeline.timeScale(0.5)` 减慢所有动画
4. **最佳实践**
- 始终使用 `gsap.context()` 管理动画
- 组件卸载时清理所有动画和 ScrollTrigger
- 使用 `overwrite: true` 避免动画冲突