Files
n8n_Demo/doc/任务/子任务/gsap_examples.md
Yep_Q 3b8cb3c568 feat: 完成能源订单班图片重命名和文档整理
详细说明:
- 能源订单班: 重命名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>
2025-10-01 22:06:59 +08:00

13 KiB

GSAP 动画实现示例

核心动画配置

1. GSAP 初始化设置

// 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. 时间轴导航动画

// 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. 内容区块滚动动画

// 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. 图片查看器动画

// 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. 页面加载动画

// 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 优化

// 批量创建 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. 清理和内存管理

useEffect(() => {
  // 创建上下文
  const ctx = gsap.context(() => {
    // 所有GSAP动画代码
  }, containerRef);
  
  // 清理函数
  return () => {
    ctx.revert(); // 清理所有动画
    ScrollTrigger.getAll().forEach(trigger => trigger.kill());
  };
}, []);

3. 响应式处理

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

常用缓动函数

// 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 避免动画冲突