返回目录

从补间到叙事:GSAP 动画实战 · 公开阅读

第14章 React / Vue 中集成 GSAP

在 React 与 Vue 中管理动画、组件生命周期和清理逻辑。

本章小节

难度:★★★☆☆ | 用到的GSAP能力:useGSAP、gsap.context()、ScrollTrigger | 项目类型:React/Vue组件动画


14.1 框架集成的核心挑战

在框架中集成GSAP与在原生HTML中不同——React和Vue通过虚拟DOM和响应式系统管理DOM,不可直接使用document.querySelector。

GSAP 诞生于"直接操作DOM"的时代,而 React 和 Vue 通过虚拟DOM和响应式系统管理DOM。集成时需要处理3个核心问题:

  1. DOM引用:框架不鼓励 document.querySelector,需要用 useRef(React)或 ref(Vue)
  2. 生命周期:动画需要在组件挂载后创建,卸载前清理——不能残留"僵尸动画"
  3. 响应式冲突:Vue的Proxy响应式代理可能与GSAP的动画属性冲突——两者同时操作同一个值会导致竞争

14.2 React 集成

@gsap/react 包

好消息是,GSAP官方专门为React做了一个包:

npm install gsap @gsap/react
import gsap from 'gsap';
import { useGSAP } from '@gsap/react';

// 注册插件(只需一次,在入口文件)
gsap.registerPlugin(useGSAP);

useGSAP 基础用法

import { useRef } from 'react';
import gsap from 'gsap';
import { useGSAP } from '@gsap/react';

function HeroSection() {
  const containerRef = useRef(null);
  const titleRef = useRef(null);
  const buttonRef = useRef(null);

  useGSAP(() => {
    // 动画代码
    gsap.from(titleRef.current, {
      y: 60,
      opacity: 0,
      duration: 0.8,
      ease: 'power3.out'
    });

    gsap.from(buttonRef.current, {
      scale: 0,
      duration: 0.6,
      delay: 0.3,
      ease: 'back.out(1.7)'
    });
  }, { scope: containerRef });  // scope限制选择器范围

  return (
    <div ref={containerRef} className="hero">
      <h1 ref={titleRef}>欢迎回来</h1>
      <button ref={buttonRef}>开始使用</button>
    </div>
  );
}

useGSAP 的3个关键能力:

  1. 自动清理:组件卸载时自动 revert() 所有动画
  2. scope作用域:scope 参数限制 gsap.to('.item') 只在当前组件内查找
  3. 依赖追踪:第二个参数中的 dependencies 可控制动画重新执行

useGSAP = 自动帮你"用完即收",动画来了就管,走了就收拾干净。

对比:useGSAP vs useEffect 手动管理

// ❌ 手动 useEffect:繁琐、易出错
useEffect(() => {
  const ctx = gsap.context(() => {
    gsap.from('.title', { y: 60, opacity: 0 });
    gsap.from('.btn', { scale: 0, delay: 0.3 });
  }, containerRef);

  return () => ctx.revert();   // 手动清理
}, []);

// ✅ useGSAP:自动处理
useGSAP(() => {
  gsap.from('.title', { y: 60, opacity: 0 });
  gsap.from('.btn', { scale: 0, delay: 0.3 });
}, { scope: containerRef });

useGSAP 的清理机制覆盖了4种场景:

场景useEffectuseGSAP
组件卸载需要手动 return () => ctx.revert()自动清理
StrictMode双重挂载动画执行两次自动revert并重新执行
依赖变化需要手动管理cleanupdependencies 自动触发
Fast Refresh(HMR)可能泄漏动画自动revert

14.3 useRef 选择元素

React 中不推荐 document.querySelector——它不受组件作用域限制,可能选中其他组件的元素。

// ❌ 不推荐:全局查询
useGSAP(() => {
  gsap.from('.global-class', { y: 60 });  // 可能选中其他组件的元素
});

// ✅ 推荐1:useRef
const elRef = useRef(null);
useGSAP(() => {
  gsap.from(elRef.current, { y: 60 });
});

// ✅ 推荐2:scope + 选择器(useGSAP的scope限制范围)
useGSAP(() => {
  gsap.from('.item', {
    y: 60,
    stagger: 0.1
  });
}, { scope: containerRef });  // 只在containerRef内查找.item

把这一节跑一遍:作用域与清理

A/B 具有相同类名却各自被容器限定。React 的 ref 指向 DOM,context 负责收集动画;Vue scoped CSS 只限定样式规则,不能替代 DOM 查询作用域。

实验:作用域与清理

GSAP 3.13 / 实验

亲手调整参数,观察动画差异。点击开启后再播放,不会自动启动动画。

观察与验证:同时创建 A/B,只还原 A。把这个过程对应到组件挂载/卸载,再检查事件回调后来创建的动画是否也进入 context。

14.4 ScrollTrigger 在 React 中的使用

import { useRef } from 'react';
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
import { useGSAP } from '@gsap/react';

gsap.registerPlugin(ScrollTrigger, useGSAP);

function ScrollSection() {
  const sectionRef = useRef(null);
  const cardRefs = useRef([]);

  useGSAP(() => {
    cardRefs.current.forEach((card, i) => {
      gsap.from(card, {
        y: 80,
        opacity: 0,
        duration: 0.8,
        scrollTrigger: {
          trigger: card,
          start: 'top 85%',
          end: 'top 40%',
          scrub: false,
          markers: false
        }
      });
    });
  }, { scope: sectionRef });

  return (
    <div ref={sectionRef} className="scroll-section">
      {[1, 2, 3].map((n) => (
        <div
          key={n}
          ref={el => cardRefs.current[n - 1] = el}
          className="card"
        >
          Card {n}
        </div>
      ))}
    </div>
  );
}

关键点:ScrollTrigger 实例也在 useGSAP 的清理范围内——组件卸载时,相关的 ScrollTrigger 也会被kill。


14.5 Vue 集成

Vue 3 Composition API

<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import gsap from 'gsap';

const containerRef = ref(null);
const titleRef = ref(null);

// onMounted 中创建动画,onUnmounted 中清理
let ctx;

onMounted(() => {
  ctx = gsap.context(() => {
    gsap.from(titleRef.value, {
      y: 60,
      opacity: 0,
      duration: 0.8,
      ease: 'power3.out'
    });
  }, containerRef.value);
});

onUnmounted(() => {
  ctx.revert();    // 清理所有动画
});
</script>

<template>
  <div ref="containerRef" class="hero">
    <h1 ref="titleRef">欢迎回来</h1>
  </div>
</template>

Vue 3 自定义 Hook(Composable)

将GSAP逻辑封装为可复用composable——动画逻辑和组件分离:

// composables/useGsapAnimation.js
import { onMounted, onUnmounted } from 'vue';
import gsap from 'gsap';

export function useGsapAnimation(fn, scopeRef) {
  let ctx;

  onMounted(() => {
    ctx = gsap.context(() => {
      fn(gsap);
    }, scopeRef?.value || scopeRef);
  });

  onUnmounted(() => {
    ctx?.revert();
  });

  return ctx;
}

// 使用
import { useGsapAnimation } from './composables/useGsapAnimation';

const containerRef = ref(null);

useGsapAnimation((gsap) => {
  gsap.from('.title', { y: 60, opacity: 0 });
  gsap.from('.desc', { y: 30, opacity: 0, delay: 0.2 });
}, containerRef);

Vue 2 Options API

<script>
import gsap from 'gsap';

export default {
  data() {
    return {
      ctx: null
    };
  },
  mounted() {
    this.ctx = gsap.context(() => {
      gsap.from(this.$refs.title, {
        y: 60, opacity: 0, duration: 0.8
      });
    }, this.$el);
  },
  beforeDestroy() {
    this.ctx?.revert();
  }
};
</script>

14.6 状态驱动的动画

React:state 变化触发 GSAP 动画

function Counter() {
  const [count, setCount] = useState(0);
  const valueRef = useRef(null);

  // 监听 count 变化,触发动画
  useGSAP(() => {
    gsap.from(valueRef.current, {
      scale: 2,
      color: '#ff6b6b',
      duration: 0.3,
      ease: 'back.out(1.7)'
    });
  }, { dependencies: [count], revertOnUpdate: true });      // count变化时重新执行

  return (
    <div>
      <span ref={valueRef}>{count}</span>
      <button onClick={() => setCount(c => c + 1)}>+1</button>
    </div>
  );
}

Vue:watch + reactive 数据

<script setup>
import { ref, watch } from 'vue';
import gsap from 'gsap';

const count = ref(0);
const valueRef = ref(null);

watch(count, () => {
  gsap.from(valueRef.value, {
    scale: 2,
    color: '#ff6b6b',
    duration: 0.3,
    ease: 'back.out(1.7)'
  });
});
</script>

<template>
  <div>
    <span ref="valueRef">{{ count }}</span>
    <button @click="count++">+1</button>
  </div>
</template>

14.7 SSR 兼容策略

GSAP是纯浏览器端的库,依赖window和document API。SSR环境中(Node.js)这些API不存在,需要在客户端环境中才加载GSAP。

Next.js 中的动态导入

GSAP 依赖浏览器API(window、document),在SSR环境(Node.js)中会报错。

// Next.js App Router (app/page.js)
import dynamic from 'next/dynamic';

// 动态导入:仅在客户端渲染包含GSAP的组件
const HeroAnimation = dynamic(
  () => import('./components/HeroAnimation'),
  { ssr: false }                 // 禁用SSR
);

export default function HomePage() {
  return (
    <div>
      <HeroAnimation />
      {/* 其余SSR内容 */}
    </div>
  );
}
// components/HeroAnimation.jsx — 纯客户端组件
'use client';                    // Next.js App Router 客户端标记

import { useRef } from 'react';
import gsap from 'gsap';
import { useGSAP } from '@gsap/react';

export default function HeroAnimation() {
  const containerRef = useRef(null);

  useGSAP(() => {
    gsap.from('.title', { y: 60, opacity: 0, duration: 0.8 });
  }, { scope: containerRef });

  return (
    <div ref={containerRef}>
      <h1 className="title">Hello</h1>
    </div>
  );
}

Nuxt 中的处理

<!-- Nuxt 3: components/GSAPAnimation.client.vue -->
<!-- .client.vue 后缀自动标记为纯客户端组件 -->

<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import gsap from 'gsap';

const containerRef = ref(null);
// ... 同上
</script>

或在 Nuxt 中使用 ClientOnly 包裹:

<template>
  <ClientOnly>
    <GSAPAnimation />
  </ClientOnly>
</template>

通用条件判断(兼容所有SSR框架)

// utils/gsap-client.js
let gsap = null;

if (typeof window !== 'undefined') {
  gsap = require('gsap');
  require('gsap/ScrollTrigger');
}

export default gsap;
import gsap from '@/utils/gsap-client';

useEffect(() => {
  if (!gsap) return;  // SSR环境:跳过
  const animation = gsap.from('.title', { y: 60 });
  return () => animation.kill();
}, []);

14.8 完整代码 Demo

React 组件

// ============================================
// React Demo: HeroSection.jsx
// 依赖: npm install gsap @gsap/react
// ============================================
import { useRef, useState } from 'react';
import gsap from 'gsap';
import { useGSAP } from '@gsap/react';

gsap.registerPlugin(useGSAP);

// ----- 主Hero组件 -----
export default function HeroSection() {
  const containerRef = useRef(null);
  const titleRef = useRef(null);
  const subtitleRef = useRef(null);
  const ctaRef = useRef(null);
  const [isVisible, setIsVisible] = useState(true);

  useGSAP(() => {
    const tl = gsap.timeline({ defaults: { ease: 'power3.out' } });

    tl.from(titleRef.current, {
      y: 80,
      opacity: 0,
      duration: 0.8
    })
    .from(subtitleRef.current, {
      y: 40,
      opacity: 0,
      duration: 0.6
    }, '-=0.3')
    .from(ctaRef.current, {
      scale: 0,
      duration: 0.7,
      ease: 'elastic.out(1, 0.5)'
    }, '-=0.2');

    return () => tl.kill();  // 清理Timeline
  }, { scope: containerRef });

  // CTA hover 效果
  const handleHover = () => {
    gsap.to(ctaRef.current, {
      scale: 1.05,
      boxShadow: '0 0 30px rgba(108, 99, 255, 0.5)',
      duration: 0.3
    });
  };

  const handleLeave = () => {
    gsap.to(ctaRef.current, {
      scale: 1,
      boxShadow: 'none',
      duration: 0.3
    });
  };

  const handleToggle = () => {
    if (isVisible) {
      gsap.to(containerRef.current, {
        opacity: 0,
        y: -30,
        duration: 0.5,
        onComplete: () => setIsVisible(false)
      });
    } else {
      setIsVisible(true);
      // 注意:useGSAP会在isVisible变为true后,DOM重建后重新执行
    }
  };

  if (!isVisible) return <button onClick={handleToggle}>重新显示</button>;

  return (
    <div ref={containerRef} style={styles.hero}>
      <div style={styles.content}>
        <h1 ref={titleRef} style={styles.title}>
          用GSAP点亮React
        </h1>
        <p ref={subtitleRef} style={styles.subtitle}>
          声明式动画 · 自动清理 · SSR兼容
        </p>
        <button
          ref={ctaRef}
          style={styles.button}
          onMouseEnter={handleHover}
          onMouseLeave={handleLeave}
          onClick={handleToggle}
        >
          探索更多
        </button>
      </div>
    </div>
  );
}

// ----- Feature卡片列表组件 -----
export function FeatureList() {
  const listRef = useRef(null);
  const items = ['高性能', '声明式', '可编排', '响应式'];

  useGSAP(() => {
    gsap.from('.feature-card', {
      y: 60,
      opacity: 0,
      stagger: 0.12,
      duration: 0.6,
      ease: 'back.out(1.4)',
      scrollTrigger: {
        trigger: listRef.current,
        start: 'top 80%'
      }
    });
  }, { scope: listRef });

  return (
    <div ref={listRef} style={styles.featureGrid}>
      {items.map(item => (
        <div key={item} className="feature-card" style={styles.card}>
          <h3>{item}</h3>
          <p style={styles.cardDesc}>
            GSAP为{item}场景提供专业动画支持
          </p>
        </div>
      ))}
    </div>
  );
}

// ========== 内联样式(简化演示) ==========
const styles = {
  hero: {
    height: '100vh',
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'center',
    background: 'linear-gradient(135deg, #0f0f1a, #1a1a2e)',
    color: '#fff',
    textAlign: 'center'
  },
  content: { maxWidth: 600 },
  title: {
    fontSize: 'clamp(2rem, 5vw, 3.5rem)',
    fontWeight: 800,
    marginBottom: '1rem',
    lineHeight: 1.2
  },
  subtitle: {
    fontSize: '1.1rem',
    color: 'rgba(255,255,255,0.6)',
    marginBottom: '2rem'
  },
  button: {
    padding: '0.8rem 2rem',
    fontSize: '1rem',
    fontWeight: 600,
    color: '#fff',
    background: '#6C63FF',
    border: 'none',
    borderRadius: 12,
    cursor: 'pointer'
  },
  featureGrid: {
    display: 'grid',
    gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))',
    gap: '1.5rem',
    padding: '3rem 2rem',
    maxWidth: 900,
    margin: '0 auto'
  },
  card: {
    padding: '2rem',
    background: '#1a1a2e',
    borderRadius: 16,
    color: '#fff',
    border: '1px solid #30363D'
  },
  cardDesc: {
    fontSize: '0.9rem',
    color: 'rgba(255,255,255,0.5)',
    marginTop: '0.5rem'
  }
};

Vue 3 组件

<!-- ============================================ -->
<!-- Vue 3 Demo: HeroSection.vue + FeatureList.vue -->
<!-- 依赖: npm install gsap -->
<!-- ============================================ -->

<!-- HeroSection.vue -->
<script setup>
import { ref, computed } from 'vue';
import gsap from 'gsap';
import { useGsapAnimation } from './composables/useGsapAnimation';

const containerRef = ref(null);
const titleRef = ref(null);
const subtitleRef = ref(null);
const ctaRef = ref(null);
const isVisible = ref(true);

// 动画composable
useGsapAnimation(() => {
  const tl = gsap.timeline({ defaults: { ease: 'power3.out' } });

  tl.from(titleRef.value, {
    y: 80,
    opacity: 0,
    duration: 0.8
  })
  .from(subtitleRef.value, {
    y: 40,
    opacity: 0,
    duration: 0.6
  }, '-=0.3')
  .from(ctaRef.value, {
    scale: 0,
    duration: 0.7,
    ease: 'elastic.out(1, 0.5)'
  }, '-=0.2');
}, containerRef);

const handleHover = () => {
  gsap.to(ctaRef.value, {
    scale: 1.05,
    boxShadow: '0 0 30px rgba(108, 99, 255, 0.5)',
    duration: 0.3
  });
};

const handleLeave = () => {
  gsap.to(ctaRef.value, {
    scale: 1,
    boxShadow: 'none',
    duration: 0.3
  });
};

const handleToggle = () => {
  isVisible.value = !isVisible.value;
};
</script>

<template>
  <button v-if="!isVisible" class="toggle-btn" @click="handleToggle">
    重新显示
  </button>

  <div v-else ref="containerRef" class="hero">
    <div class="hero-content">
      <h1 ref="titleRef" class="hero-title">用GSAP点亮Vue</h1>
      <p ref="subtitleRef" class="hero-subtitle">
        声明式动画·自动清理·SSR兼容
      </p>
      <button
        ref="ctaRef"
        class="cta-button"
        @mouseenter="handleHover"
        @mouseleave="handleLeave"
        @click="handleToggle"
      >
        探索更多
      </button>
    </div>
  </div>
</template>

<style scoped>
.hero {
  height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  background: linear-gradient(135deg, #0f0f1a, #1a1a2e);
  color: #fff;
  text-align: center;
}

.hero-content {
  max-width: 600px;
}

.hero-title {
  font-size: clamp(2rem, 5vw, 3.5rem);
  font-weight: 800;
  margin-bottom: 1rem;
  line-height: 1.2;
}

.hero-subtitle {
  font-size: 1.1rem;
  color: rgba(255,255,255,0.6);
  margin-bottom: 2rem;
}

.cta-button {
  padding: 0.8rem 2rem;
  font-size: 1rem;
  font-weight: 600;
  color: #fff;
  background: #6C63FF;
  border: none;
  border-radius: 12px;
  cursor: pointer;
}

.toggle-btn {
  display: block;
  margin: 2rem auto;
  padding: 0.6rem 1.5rem;
  font-size: 1rem;
  background: #1a1a2e;
  color: #fff;
  border: 1px solid #6C63FF;
  border-radius: 8px;
  cursor: pointer;
}
</style>


<!-- FeatureList.vue -->
<script setup>
import { ref } from 'vue';
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
import { useGsapAnimation } from './composables/useGsapAnimation';

gsap.registerPlugin(ScrollTrigger);

const listRef = ref(null);
const items = ['高性能', '声明式', '可编排', '响应式'];

useGsapAnimation(() => {
  gsap.from('.feature-card', {
    y: 60,
    opacity: 0,
    stagger: 0.12,
    duration: 0.6,
    ease: 'back.out(1.4)',
    scrollTrigger: {
      trigger: listRef.value,
      start: 'top 80%'
    }
  });
}, listRef);
</script>

<template>
  <div ref="listRef" class="feature-grid">
    <div
      v-for="item in items"
      :key="item"
      class="feature-card"
    >
      <h3>{{ item }}</h3>
      <p>GSAP为{{ item }}场景提供专业动画支持</p>
    </div>
  </div>
</template>

<style scoped>
.feature-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 1.5rem;
  padding: 3rem 2rem;
  max-width: 900px;
  margin: 0 auto;
}

.feature-card {
  padding: 2rem;
  background: #1a1a2e;
  border-radius: 16px;
  color: #fff;
  border: 1px solid #30363D;
}

.feature-card p {
  font-size: 0.9rem;
  color: rgba(255,255,255,0.5);
  margin-top: 0.5rem;
}
</style>

14.9 技术解析

gsap.context() 的工作机制

const ctx = gsap.context(() => {
  // 在这个回调中创建的所有动画和ScrollTrigger
  // 都会被ctx记录
  gsap.to('.a', { x: 100 });
  gsap.from('.b', { opacity: 0, scrollTrigger: {} });
  const tl = gsap.timeline();
  tl.to('.c', { y: 50 });
}, scopeElement);

// 清理:一键kill所有动画 + 所有ScrollTrigger
ctx.revert();

// revert() 还支持"可恢复":
// revert() 后调用 ctx.kill() → 永久销毁
// revert() 后重新创建 → 重新记录

gsap.context() 是框架集成的基石——它还原该 context 内记录的动画,不会自动清理全部应用资源;事件监听、定时器和绘制资源仍需显式处理。

useGSAP vs useEffect 的差异

React StrictMode 在开发环境额外执行 setup→cleanup→setup,检查副作用是否能正确还原;不是生产环境固定挂载两次。useGSAP 提供 context 管理和同构 effect,手动使用 effect + scoped context + revert 也能正确处理。

useGSAP(() => {
  gsap.to('.box', { x: active ? 100 : 0 });
}, { scope: container, dependencies: [active], revertOnUpdate: true });

依赖更新时需要重新创建并还原旧动画,应显式写 revertOnUpdate:true。默认不表示每次依赖更新都自动还原。事件回调中后来创建的动画,用 contextSafe 纳入 context;监听本身仍要 removeEventListener。参见 @gsap/react 文档。

14.10 常见陷阱

陷阱1:StrictMode下动画重复执行

症状:开发环境动画播放两次,或者动画状态错乱。

原因:React 18 StrictMode 在开发环境下会双重挂载组件,导致useEffect中的动画创建两次。

解决:

  • 首选:使用 useGSAP(自动处理)
  • 次选:手动使用 gsap.context()
// 次选方案:手动context
useEffect(() => {
  const ctx = gsap.context(() => {
    gsap.from('.el', { y: 60 });
  });

  return () => ctx.revert();
}, []);

陷阱2:Vue响应式代理与GSAP冲突

Vue 的 reactive Proxy 允许数值属性写入,不能说它天然阻止 GSAP 动画。ref({x:0}).value 通常也会被转为响应式对象,并不自动变成普通对象。

动画响应式数值可能使组件每帧更新;如果同一属性还被 watch、CSS 或另一个动画改变,会产生竞争。仅用于视觉过渡的属性可直接动画 DOM,或使用普通对象 + 按需同步。需要响应式数字显示时,直接 Tween reactive 数值也可以,但应测量更新成本。

const score = reactive({ value: 0 });
let tween;
onMounted(() => { tween = gsap.to(score, { value:100, duration:1 }); });
onBeforeUnmount(() => tween?.kill());

使用 ref、普通对象还是 reactive,应由数据用途决定。

陷阱3:SSR中GSAP未注册导致报错

GSAP 核心能处理普通对象;涉及 DOM 的动画和浏览器插件应在客户端生命周期内初始化。遇到 window/document 错误,检查实际报错模块、导入形式和插件初始化时机。

Next.js 的 "use client" 声明客户端组件边界,并不取消首次页面的服务端预渲染。DOM 操作放在 effect/useGSAP 中;确实只能在浏览器加载的演示,在客户端组件里使用 dynamic(...,{ssr:false})。

'use client';
import dynamic from 'next/dynamic';
const BrowserOnlyScene = dynamic(() => import('./BrowserOnlyScene'), { ssr:false });

Nuxt 的 .client.vue 和 ClientOnly 则按对应框架机制处理。

陷阱4:Vue scoped样式与GSAP选择器

Vue scoped 会给 CSS 选择器附加 data-v 属性,元素原有 class 仍存在。gsap.to('.my-class',...) 仍能匹配,真正的问题是它可能跨组件选到同名元素。

<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import gsap from 'gsap';
const container = ref(null);
let ctx;
onMounted(() => { ctx = gsap.context(() => gsap.to('.my-class',{x:100}), container.value); });
onBeforeUnmount(() => ctx?.revert());
</script>
<template><div ref="container"><div class="my-class">内容</div></div></template>

使用 ref 或 scoped context 限定目标,解决实例隔离;不要把 CSS scoped 与 DOM 查询范围混为一谈。

实战篇结语

五章实战项目覆盖了GSAP在现代前端开发中的核心应用场景:

  • 第10章:Timeline + SplitText + 微交互 → 炫酷首页
  • 第11章:ScrollTrigger.pin + 数据动画 + 视差 → 滚动叙事
  • 第12章:SVG动画 + 对象属性动画 + 交互切换 → 数据可视化
  • 第13章:Three.js对象动画 + 相机飞行 + ScrollTrigger驱动3D → 3D交互
  • 第14章:useGSAP + gsap.context + SSR兼容 → 框架集成

GSAP的强大不在于它能做什么(CSS也能做动画),而在于它用什么方式做——声明式、可编排、可控制、可清理。