用GSAP实现一个有趣的加载页!

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

效果图:

前排说明 :下面的具体内容是在React中进行的~

文档参考

如果有对GSAP的基础用法还不了解的同学,可以参考下面的文档。不过阅读本文应该只需要看看下面对timeline的简单介绍就可以简单理解了:

简单介绍 timeline

本文主要用到了GSAP的时间轴部分,这部分和之前我们熟悉的CSS动画或者requestAnimationFrame有些不同,其余用到的部分应该可以做到见字知义,所以这里先简单介绍一下GSAPtimeline的语法。

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. 应用场景

  1. 序列动画:如打字效果、卡片翻转动画
  2. 交互动画:滚动触发的动画、悬停效果
  3. 复杂过渡:页面切换、组件展开 / 收起
  4. 同步多元素:让多个元素按特定顺序运动

7. 与 React 结合使用

在 React 中推荐使用@gsap/reactuseGSAP钩子,不用这个的话,也可以使用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);
}

动画拆解分析

  1. 时间轴创建

    javascript 复制代码
    const circleTl = gsap.timeline({ repeat: -1 });

    创建一个无限循环的时间轴,使动画持续运行。

  2. 第一阶段:轻微逆时针旋转

    javascript 复制代码
    circleTl.to(circleRef.current, {
      rotate: -15,
      duration: 0.2,
      ease: "power1.inOut",
    }, 0);
    • 初始时先逆时针旋转 15 度(rotate: -15)
    • 动画时长 0.2 秒
    • 缓动函数使用 power1.inOut,使动画开始和结束更平滑
    • 动画从时间点 0 开始
  3. 第二阶段:顺时针大幅旋转

    javascript 复制代码
    circleTl.to(circleRef.current, {
      rotate: 285,
      duration: 1,
      ease: "power1.inOut",
    }, 0.2);
    • 从 - 15 度顺时针旋转到 285 度(相当于旋转了 300 度)
    • 动画时长 1 秒,相对较慢以展示旋转过程
    • 从时间点 0.2 秒开始,与第一阶段无缝衔接
    • 最终位置 285 度是故意设置的,为下一步动画做准备
  4. 第三阶段:轻微逆时针回退

    javascript 复制代码
    circleTl.to(circleRef.current, {
      rotate: 255,
      duration: 0.2,
      ease: "power1.inOut",
    }, 1.2);
    • 从 285 度逆时针回退 30 度到 255 度
    • 动画时长 0.2 秒,快速完成回退
    • 从时间点 1.2 秒开始,衔接前面动画完成立刻开始,也可以使用"+=0"这种方式来表示,表示在前一个动画结束后立即开始。
  5. 第四阶段:完整两圈旋转

    javascript 复制代码
    circleTl.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); // 下落开始时间 = 上跳结束时间
  });
}

动画拆解分析

  1. 获取 DOM 元素

    javascript 复制代码
    const texts = textContainerRef.current.querySelectorAll('p');

    获取所有需要动画的字母元素(每个字母被包裹在<p>标签中)

  2. 创建时间轴

    javascript 复制代码
    const texttl = gsap.timeline({ repeat: -1, repeatDelay: 0.2 });
    • 创建一个无限循环的时间轴(repeat: -1
    • 每次循环结束后延迟 0.2 秒再开始下一次循环(repeatDelay: 0.2
  3. 设置关键参数

    javascript 复制代码
    const interval = 0.2; // 各字母跳跃时间间隔
    const duration = 0.2; // 单个动画持续时间
    • interval控制相邻字母开始跳动的时间间隔
    • duration控制每个字母完成一次跳动(上跳 + 下落)所需的时间
    • 修改这里的参数可以实现另外的动画效果,比如放大间隔的话,可以实现更长更连续的"波浪"。
  4. 循环创建每个字母的动画

    javascript 复制代码
    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);
    });
    • 上跳动画

      • 将字母向上移动 8px(y: -8
      • 动画持续 0.2 秒
      • index * interval时间点开始(例如第一个字母 0 秒开始,第二个 0.2 秒开始,以此类推)
    • 下落动画

      • 将字母移回原位(y: 0
      • 动画持续 0.2 秒
      • index * interval + duration时间点开始(即上跳动画结束后立即开始下落)

这种动画设计创造了一种 "波浪式跳动" 的效果:

  1. 序列启动

    每个字母按顺序依次开始跳动,形成波浪的 "传播" 效果

  2. 重叠动画

    由于每个字母的动画周期是 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;

整体还是比较简单的~ 其实自己学习这些动画库,主要的卡点不在学习怎么用它上面,而是在自己的脑子实在想不出来什么有意思的动画上......😂

相关推荐
钢铁男儿2 小时前
C# 类和继承(使用基类的引用)
java·javascript·c#
czliutz2 小时前
NiceGUI 是一个基于 Python 的现代 Web 应用框架
开发语言·前端·python
koooo~3 小时前
【无标题】
前端
Attacking-Coder4 小时前
前端面试宝典---前端水印
前端
姑苏洛言6 小时前
基于微信公众号小程序的课表管理平台设计与实现
前端·后端
烛阴7 小时前
比UUID更快更小更强大!NanoID唯一ID生成神器全解析
前端·javascript·后端
Alice_hhu7 小时前
ResizeObserver 解决 echarts渲染不出来,内容宽度为 0的问题
前端·javascript·echarts
charlee448 小时前
解决Vditor加载Markdown网页很慢的问题(Vite+JS+Vditor)
javascript·markdown·cdn·vditor
逃逸线LOF8 小时前
CSS之动画(奔跑的熊、两面反转盒子、3D导航栏、旋转木马)
前端·css
萌萌哒草头将军9 小时前
⚡️Vitest 3.2 发布,测试更高效;🚀Nuxt v4 测试版本发布,焕然一新;🚗Vite7 beta 版发布了
前端