You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Next.js的onClick事件中启停GSAP动画并操作tl.current

问题核心原因

你代码中最大的问题是连续三次给tl.current重新赋值,每次新建GSAP时间线都会覆盖掉上一次存储的实例,最终tl.current仅保留了最后一个.singleProject:nth-child(4)的动画实例,所以你操作tl.current的时候只能控制最后一个项目的动画,甚至出现不符合预期的表现。

修复方案

  1. 仅初始化一次主时间线,后续所有子动画都通过时间线的add()方法加入主时间线统一管理
  2. 点击第一个.imgWrapper时,调用GSAP内置的paused()方法实现状态切换:该方法不传参时返回当前是否为暂停状态,传入布尔值即可直接设置状态,取反即可实现暂停/播放切换

修正后代码

import { useRef, useEffect, useState } from 'react';
import gsap from 'gsap';

export const Projects = () => {
  const projects = useRef();
  const tl = useRef();
  const q = gsap.utils.selector(projects);

  useEffect(() => {
    // 仅初始化一次主时间线,不要重复赋值覆盖
    tl.current = gsap.timeline({ paused: false });

    // 把每个项目的动画作为子时间线加入主时间线
    tl.current.add(
      gsap
        .timeline()
        .from(q(".singleProject:nth-child(2)"), {
          y: 2000,
          ease: "none",
          opacity: 1,
          duration: 20,
          repeat: -1,
        })
        .to(q(".singleProject:nth-child(2)"), {
          y: -2000,
          ease: "none",
          opacity: 1,
          duration: 20,
          repeat: -1,
        })
    );

    tl.current.add(
      gsap
        .timeline()
        .from(q(".singleProject:nth-child(3)"), {
          y: 2000,
          ease: "none",
          opacity: 1,
          duration: 20,
          repeat: -1,
          delay: 1,
        })
        .to(q(".singleProject:nth-child(3)"), {
          y: -2000,
          ease: "none",
          opacity: 1,
          duration: 20,
          repeat: -1,
          delay: 1,
        })
    );

    tl.current.add(
      gsap
        .timeline()
        .from(q(".singleProject:nth-child(4)"), {
          y: 2000,
          ease: "none",
          opacity: 1,
          duration: 20,
          repeat: -1,
          delay: 2,
        })
        .to(q(".singleProject:nth-child(4)"), {
          y: -2000,
          ease: "none",
          opacity: 1,
          duration: 20,
          repeat: -1,
          delay: 2,
        })
    );

    // 组件卸载时清空动画避免内存泄漏
    return () => {
      tl.current.kill();
    };
  }, []);

  const [animate_1, setAnimate_1] = useState(false);
  const [animate_2, setAnimate_2] = useState(false);
  const [animate_3, setAnimate_3] = useState(false);

  // 动画切换函数
  const toggleAnimation = () => {
    if (!tl.current) return;
    // 切换暂停状态:取反当前paused值即可
    tl.current.paused(!tl.current.paused());
  };

  return (
    <section
      ref={projects}
      id="projects"
      className={`${animate_1 && "animated_1"} ${animate_2 && "animated_2"} ${
        animate_3 && "animated_3"
      }
       w_100 h_100vh flex flexColumn flexAlignCenter flexCenter p4em`}
    >
      <div className="projectsLoading flex flexCenter flexAlignCenter w_100 h_100">
        <h2>LOADING PROJECTS</h2>
      </div>
      <div className={`singleProject w_100 flex flexStart`}>
        {/* 绑定切换函数 */}
        <div className="imgWrapper" onClick={toggleAnimation}>
          <img src={"https://www.aimanalmureish.com/img/lego.jpg"} alt="lego" />
        </div>
      </div>
      <div className="singleProject w_100 flex flexEnd">
        <div className="imgWrapper">
          <img
            onClick={() => {
              setAnimate_2(!animate_2);
            }}
            src={"https://www.aimanalmureish.com/img/jordan.png"}
            alt="jordan"
          />
        </div>
      </div>
      <div className="singleProject w_100 flex flexStart">
        <div className="imgWrapper">
          <img
            onClick={() => setAnimate_3(!animate_3)}
            src={"https://www.aimanalmureish.com/img/ferrari.png"}
            alt="ferrari"
          />
        </div>
      </div>
    </section>
  );
};

额外说明

如果你后续需要单独控制每个项目的动画,可以将三个时间线存储在数组类型的ref中,例如const tls = useRef([]),创建每个子时间线时存入对应索引,操作时直接调取对应索引的实例即可。

内容的提问来源于stack exchange,提问作者Aiman Almureish

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.03 02:36:05