前段时间,大名鼎鼎的动画库GSAP
它终于免费了!之前需要付费是阻挡我学习它的唯一理由,现在既然免费了,当然要好好看看让这么多人都推荐的动画库是怎么个事。整体体验下来上手还是比较简单的。众所周知,学习一个库用法的最好方式就是看完文档写Demo
,现在,让我们实践一下吧~
效果图:

前排说明 :下面的具体内容是在React
中进行的~
文档参考
如果有对GSAP
的基础用法还不了解的同学,可以参考下面的文档。不过阅读本文应该只需要看看下面对timeline
的简单介绍就可以简单理解了:
简单介绍 timeline
本文主要用到了GSAP
的时间轴部分,这部分和之前我们熟悉的CSS
动画或者requestAnimationFrame
有些不同,其余用到的部分应该可以做到见字知义,所以这里先简单介绍一下GSAP
的timeline
的语法。
GSAP(GreenSock Animation Platform)的 Timeline 是一个强大的动画编排工具,它允许你将多个动画组合在一起,精确控制它们的时序关系。
1. 什么是 Timeline?
Timeline 是 GSAP 中用于组织和管理多个动画的容器,类似于视频编辑软件中的时间轴。它可以:
- 按顺序、并行或重叠方式播放动画
- 统一控制所有子动画(播放、暂停、倒放、加速等)
- 设置全局属性(如重复次数、延迟时间)
- 创建复杂的动画序列
2. 基本用法
创建 Timeline
javascript
import { gsap } from "gsap";
// 创建一个基本时间轴
const tl = gsap.timeline();
// 创建无限循环的时间轴
const loopingTl = gsap.timeline({
repeat: -1, // 无限循环
repeatDelay: 1, // 每次循环间隔1秒
yoyo: true // 往返播放(正向→反向→正向...)
});
添加动画到 Timeline
使用.to()
、.from()
、.fromTo()
方法添加动画,并指定时间点:
javascript
// 方法1:按顺序添加(自动衔接)
tl.to(element1, { x: 100, duration: 1 })
.to(element2, { y: 50, duration: 0.5 })
.to(element3, { opacity: 0, duration: 0.8 });
// 方法2:指定绝对时间点(从时间轴开始的秒数)
tl.to(element1, { x: 100, duration: 1 }, 0) // 0秒开始
.to(element2, { y: 50, duration: 0.5 }, 0.5) // 0.5秒开始(与第一个动画会有重叠,因为第一个动画执行要1秒,但是这个在0.5秒时就开始了)
.to(element3, { opacity: 0, duration: 0.8 }, 1); // 1秒开始
3. 时间控制技巧
相对时间
使用+=
和-=
指定相对于前一个动画的时间:
javascript
tl.to(element1, { x: 100, duration: 1 })
.to(element2, { y: 50, duration: 0.5 }, "+=0.3"); // 在前一个动画结束后0.3秒开始
特殊标记
<
:表示前一个动画的开始时间点>
:表示前一个动画的结束时间点(等同于+=0
)
javascript
tl.to(element1, { x: 100, duration: 1 })
.to(element2, { y: 50, duration: 0.5 }, "<"); // 与前一个动画同时开始
4. 常用 Timeline 属性
javascri
const tl = gsap.timeline({
repeat: 3, // 重复3次(总共播放4次)
repeatDelay: 0.5, // 每次重复间隔0.5秒
yoyo: true, // 往返播放(正向→反向→正向...)
defaults: { // 为所有子动画设置默认值
duration: 0.5,
ease: "power2.out"
},
onComplete: () => { // 动画完成回调
console.log("Timeline finished!");
}
});
5. 实用方法
javascript
// 控制时间轴
tl.play(); // 播放
tl.pause(); // 暂停
tl.reverse(); // 倒放
tl.restart(); // 重新开始
tl.seek(2); // 跳到2秒位置
tl.timeScale(2); // 加速2倍
// 查询状态
tl.duration(); // 获取总时长
tl.totalTime(); // 获取当前时间点
tl.isActive(); // 检查是否正在播放
6. 应用场景
- 序列动画:如打字效果、卡片翻转动画
- 交互动画:滚动触发的动画、悬停效果
- 复杂过渡:页面切换、组件展开 / 收起
- 同步多元素:让多个元素按特定顺序运动
7. 与 React 结合使用
在 React 中推荐使用@gsap/react
的useGSAP
钩子,不用这个的话,也可以使用React
提供的useLayoutEffect
钩子:
javascript
import { useRef } from 'react';
import { useGSAP } from '@gsap/react';
import gsap from 'gsap';
const MyComponent = () => {
const elementRef = useRef(null);
useGSAP(() => {
const tl = gsap.timeline();
tl.to(elementRef.current, { x: 100, duration: 1 })
.to(elementRef.current, { rotation: 360, duration: 2 });
}, [elementRef]);
return <div ref={elementRef}>Animated Element</div>;
};
总结
Timeline 是 GSAP 的核心特性之一,通过精确控制动画的时序关系,可以创建出流畅、复杂的动画效果。掌握 Timeline 后,你可以轻松实现:
- 顺序播放的动画序列
- 重叠交错的并行动画
- 循环、往返的动态效果
- 带回调和状态管理的交互式动画
结合相对时间和特殊标记,可以更灵活地编排动画,避免手动计算复杂的时间点。
现在废话不多说,直接开始~
整体展示 & 动画拆分
再看图:

整体元素可以分为上面旋转的圈和下面跳动的字。下面分别拆分下各自的动画步骤。
旋转的圈
仔细看的话,其实可以很明显的发现,动画会先转到大概3/4的位置,然后有一个类似惯性的效果,然后往回收一点,类似一个蓄力的效果,随后会快速的转一圈过去,回到初始位置,起始的时候,也有一个类似蓄力的感觉。
这个加载动画中的旋转圈动画主要通过 GSAP 的时间轴(Timeline)功能实现复杂的旋转效果。
动画核心实现
javascript
if(circleRef.current){
const circleTl = gsap.timeline({ repeat: -1 });
circleTl.to(circleRef.current, {
rotate: -15,
duration: 0.2,
ease: "power1.inOut",
}, 0);
circleTl.to(circleRef.current, {
rotate: 285,
duration: 1,
ease: "power1.inOut",
}, 0.2);
circleTl.to(circleRef.current, {
rotate: 255,
duration: 0.2,
ease: "power1.inOut",
}, 1.2);
circleTl.to(circleRef.current, {
rotate: 720,
duration: 1,
ease: "power1.inOut",
}, 1.45);
}
动画拆解分析
-
时间轴创建:
javascriptconst circleTl = gsap.timeline({ repeat: -1 });
创建一个无限循环的时间轴,使动画持续运行。
-
第一阶段:轻微逆时针旋转:
javascriptcircleTl.to(circleRef.current, { rotate: -15, duration: 0.2, ease: "power1.inOut", }, 0);
- 初始时先逆时针旋转 15 度(rotate: -15)
- 动画时长 0.2 秒
- 缓动函数使用 power1.inOut,使动画开始和结束更平滑
- 动画从时间点 0 开始
-
第二阶段:顺时针大幅旋转:
javascriptcircleTl.to(circleRef.current, { rotate: 285, duration: 1, ease: "power1.inOut", }, 0.2);
- 从 - 15 度顺时针旋转到 285 度(相当于旋转了 300 度)
- 动画时长 1 秒,相对较慢以展示旋转过程
- 从时间点 0.2 秒开始,与第一阶段无缝衔接
- 最终位置 285 度是故意设置的,为下一步动画做准备
-
第三阶段:轻微逆时针回退:
javascriptcircleTl.to(circleRef.current, { rotate: 255, duration: 0.2, ease: "power1.inOut", }, 1.2);
- 从 285 度逆时针回退 30 度到 255 度
- 动画时长 0.2 秒,快速完成回退
- 从时间点 1.2 秒开始,衔接前面动画完成立刻开始,也可以使用
"+=0"
这种方式来表示,表示在前一个动画结束后立即开始。
-
第四阶段:完整两圈旋转:
javascriptcircleTl.to(circleRef.current, { rotate: 720, duration: 1, ease: "power1.inOut", }, 1.45);
- 从 255 度顺时针旋转到 720 度(相当于两整圈)
- 动画时长 1 秒
- 从时间点 1.45 秒开始,与上一阶段有 0.25 秒的重叠,创造平滑过渡
- 720 度等于两圈(360×2),完成后回到初始位置但视觉上形成连续旋转效果
跳动的字
跳动的字是形成一个依次跃起的动画效果,在前一个字母跳起完成,开始下落的同时,后一个字跃起。动画整体等待全部完成后再重复,这种情况利用GSAP
时间轴来进行是非常合适的。
动画核心实现示意
jsx
<div ref={textContainerRef} className={style.textContainer}>
<p className={style.text}>P</p>
<p className={style.text}>L</p>
<p className={style.text}>E</p>
<p className={style.text}>A</p>
<p className={style.text}>S</p>
<p className={style.text}>E</p>
<p className={style.text}></p>
<p className={style.text}>W</p>
<p className={style.text}>A</p>
<p className={style.text}>I</p>
<p className={style.text}>T</p>
</div>
if(textContainerRef.current){
const texts = textContainerRef.current.querySelectorAll('p');
const texttl = gsap.timeline({ repeat: -1, repeatDelay: 0.2 });
const interval = 0.2; // 各字母跳跃时间间隔
const duration = 0.2; // 单个动画持续时间
texts.forEach((text, index)=>{
// 上跳动画
texttl.to(text, {
y: -8, // 上跳距离
duration,
ease: 'power1.inOut',
}, index * interval); // 每个字母按间隔依次开始
// 下落动画
texttl.to(text, {
y: 0,
duration,
ease: 'power1.inOut',
}, index * interval + duration); // 下落开始时间 = 上跳结束时间
});
}
动画拆解分析
-
获取 DOM 元素:
javascriptconst texts = textContainerRef.current.querySelectorAll('p');
获取所有需要动画的字母元素(每个字母被包裹在
<p>
标签中) -
创建时间轴:
javascriptconst texttl = gsap.timeline({ repeat: -1, repeatDelay: 0.2 });
- 创建一个无限循环的时间轴(
repeat: -1
) - 每次循环结束后延迟 0.2 秒再开始下一次循环(
repeatDelay: 0.2
)
- 创建一个无限循环的时间轴(
-
设置关键参数:
javascriptconst interval = 0.2; // 各字母跳跃时间间隔 const duration = 0.2; // 单个动画持续时间
interval
控制相邻字母开始跳动的时间间隔duration
控制每个字母完成一次跳动(上跳 + 下落)所需的时间- 修改这里的参数可以实现另外的动画效果,比如放大间隔的话,可以实现更长更连续的"波浪"。
-
循环创建每个字母的动画:
javascripttexts.forEach((text, index)=>{ // 上跳动画 texttl.to(text, { y: -8, // 上跳距离 duration, ease: 'power1.inOut', }, index * interval); // 下落动画 texttl.to(text, { y: 0, duration, ease: 'power1.inOut', }, index * interval + duration); });
-
上跳动画:
- 将字母向上移动 8px(
y: -8
) - 动画持续 0.2 秒
- 从
index * interval
时间点开始(例如第一个字母 0 秒开始,第二个 0.2 秒开始,以此类推)
- 将字母向上移动 8px(
-
下落动画:
- 将字母移回原位(
y: 0
) - 动画持续 0.2 秒
- 从
index * interval + duration
时间点开始(即上跳动画结束后立即开始下落)
- 将字母移回原位(
-
这种动画设计创造了一种 "波浪式跳动" 的效果:
-
序列启动 :
每个字母按顺序依次开始跳动,形成波浪的 "传播" 效果
-
重叠动画 :
由于每个字母的动画周期是 0.4 秒(上跳 0.2 秒 + 下落 0.2 秒),而间隔只有 0.2 秒,导致相邻字母的动画会有重叠:
- 当第一个字母完成上跳开始下落时
- 第二个字母刚好开始上跳
- 第三个字母准备启动
- ... 依此类推
如果我们用时间线来表示这个动画:
plaintext
时间轴(秒):0.0 0.2 0.4 0.6 0.8 1.0 1.2 ...
字母P: 上 下
字母L: 上 下
字母E: 上 下
字母A: 上 下
可以看到每个字母依次启动,形成一个循环往复的波浪效果。当最后一个字母完成跳动时,第一个字母已经等待了 0.2 秒(repeatDelay
),然后整个序列重新开始。
完整代码
css
/* index.module.css */
.container {
position: relative;
width: 100%;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
background-color: var(--bg-color);
color: var(--color-primary);
}
.circleContainer {
width: 60px;
height: 60px;
display: flex;
justify-content: center;
align-items: center;
}
.circle {
font-size: 36px;
color: var(--color-primary);
}
.textContainer {
position: absolute;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
column-gap: 12px;
bottom: 240px;
}
.text {
width: 24px;
height: 24px;
line-height: 24px;
text-align: center;
color: var(--color-primary);
font-size: 24px;
font-weight: 300;
user-select: none;
}
tsx
import React, { useRef } from 'react';
import { useGSAP } from '@gsap/react';
import gsap from 'gsap';
import { Loading3QuartersOutlined } from '@ant-design/icons';
import style from './index.module.css'
const Loading: React.FC = () => {
const textContainerRef = useRef<HTMLDivElement | null>(null);
const circleRef = useRef<HTMLDivElement | null>(null);
useGSAP(()=>{
if(textContainerRef.current){
const texts = textContainerRef.current.querySelectorAll('p');
const texttl = gsap.timeline({ repeat: -1, repeatDelay: 0.2 });
const interval = 0.2; // 各字母跳跃时间间隔
const duration = 0.2; // 单个动画持续时间
texts.forEach((text, index)=>{
// 分开设置每个字母上跳下落各自的动画
texttl.to(text, {
y: -8, // 距离
duration,
ease: 'power1.inOut',
}, index * interval); // 依次播放
// 下落
texttl.to(text, {
y: 0,
duration,
ease: 'power1.inOut',
}, index * interval + duration); // 下落开始时间 = 上跳结束时间
});
}
if(circleRef.current){
const circleTl = gsap.timeline({ repeat: -1 });
circleTl.to(circleRef.current, {
rotate: -15,
duration: 0.2,
ease: "power1.inOut",
}, 0);
circleTl.to(circleRef.current, {
rotate: 285,
duration: 1,
ease: "power1.inOut",
}, 0.2);
circleTl.to(circleRef.current, {
rotate: 255,
duration: 0.2,
ease: "power1.inOut",
}, 1.2);
circleTl.to(circleRef.current, {
rotate: 720,
duration: 1,
ease: "power1.inOut",
}, 1.45);
}
}, [textContainerRef, circleRef])
return (
<div className={style.container}>
<div ref={circleRef} className={style.circleContainer}>
<Loading3QuartersOutlined className={style.circle} />
</div>
<div ref={textContainerRef} className={style.textContainer}>
<p className={style.text}>P</p>
<p className={style.text}>L</p>
<p className={style.text}>E</p>
<p className={style.text}>A</p>
<p className={style.text}>S</p>
<p className={style.text}>E</p>
<p className={style.text}></p>
<p className={style.text}>W</p>
<p className={style.text}>A</p>
<p className={style.text}>I</p>
<p className={style.text}>T</p>
</div>
</div>
)
}
export default Loading;
整体还是比较简单的~ 其实自己学习这些动画库,主要的卡点不在学习怎么用它上面,而是在自己的脑子实在想不出来什么有意思的动画上......😂