详细说明: - 能源订单班: 重命名7个图片文件为描述性中文名称 - 能源订单班: 更新markdown文档中的所有图片引用 - 智能开发订单班: 优化图片命名结构 - 化工订单班: 整理图片资源 - 新增SuperDesign食品订单班设计迭代文件 - 新增能源订单班终端模拟数据(energy.ts) - 清理web_frontend冗余文档 图片重命名映射: - Whisk_1ebf7115ee180218c354deb8bff7f3eddr.jpg → 光伏面板室外场景图片.jpg - Whisk_582dc133200b175859e4b322295fb3d1dr.jpg → 光伏面板生成画面.jpg - image.jpg → PLC示意图.jpg - Whisk_b35aa11c60670e38bea44dcd9fe7df5fdr.jpg → 工业机器人图片.jpg - Whisk_028f4b832e3496db8814cd48f050ec03dr.jpg → 机器视觉相机图片.jpg - Whisk_eb381c66f5156a4a74f49102095ae534dr.jpg → 输送与治具.jpg - Mermaid_Chart[...].jpg → Mermaid流程图.jpg 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
511 lines
13 KiB
Markdown
511 lines
13 KiB
Markdown
# 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` 避免动画冲突
|