用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;

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

相关推荐
崔庆才丨静觅9 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby606110 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了10 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅10 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅10 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅11 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment11 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅11 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊11 小时前
jwt介绍
前端
爱敲代码的小鱼11 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax