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

实现元素沿SVG路径移动时在视口内垂直居中

解决方案:卡车沿SVG路径移动并保持垂直居中+位置触发卡片

一、核心问题解决:卡车始终垂直居中

关键思路

将容器固定在视口(pin: true),卡车强制垂直居中,通过反向移动SVG路径+动态计算卡车水平位置/旋转,营造沿路径行驶的视觉效果,同时保证卡车不离开视口。

修改后的完整代码

"use client";

import React, { useEffect, useRef } from "react";
import gsap from "gsap";
import { ScrollTrigger } from "gsap/dist/ScrollTrigger";
import { MotionPathPlugin } from "gsap/dist/MotionPathPlugin";

const TruckPathAnimation = () => {
  const containerRef = useRef<HTMLDivElement>(null);
  const truckRef = useRef<HTMLDivElement>(null);
  const pathRef = useRef<SVGPathElement>(null);
  const svgRef = useRef<SVGSVGElement>(null);

  useEffect(() => {
    if (typeof window !== "undefined") {
      gsap.registerPlugin(ScrollTrigger, MotionPathPlugin);

      const path = pathRef.current!;
      const truck = truckRef.current!;
      const container = containerRef.current!;
      const svg = svgRef.current!;

      // 获取路径总长度,用于计算进度对应点
      const pathLength = path.getTotalLength();

      const tl = gsap.timeline({
        scrollTrigger: {
          trigger: container,
          start: "top top",
          end: "+=300vh", // 匹配原300vh的滚动范围
          scrub: 1,
          pin: true, // 固定容器在视口
          anticipatePin: 1,
          markers: true,
          invalidateOnRefresh: true,
          fastScrollEnd: true,
        },
      });

      // 1. SVG路径随滚动反向偏移,配合卡车固定位置
      tl.to(svg, {
        y: "-200vh", // 抵消300vh容器与100vh视口的高度差
        ease: "power1.inOut",
        duration: 1,
      }, 0);

      // 2. 动态计算卡车的水平位置与旋转角度
      tl.to(truck, {
        onUpdate: () => {
          const progress = tl.progress();
          // 获取路径当前进度的坐标点
          const point = path.getPointAtLength(progress * pathLength);
          // 将SVG viewBox的x百分比转换为视口实际x位置
          const viewportX = (point.x / 100) * window.innerWidth;
          // 设置卡车水平位置(居中对齐)
          gsap.set(truck, {
            x: viewportX - truck.offsetWidth / 2,
          });

          // 计算路径切线角度,设置卡车朝向
          const nextPoint = path.getPointAtLength(Math.min((progress + 0.01) * pathLength, pathLength));
          const angle = Math.atan2(nextPoint.y - point.y, nextPoint.x - point.x) * (180 / Math.PI);
          gsap.set(truck, {
            rotate: angle + 90, // 调整偏移匹配卡车初始朝向
          });
        },
        duration: 1,
      }, 0);

      return () => {
        ScrollTrigger.getAll().forEach((trigger) => trigger.kill());
        tl.kill();
      };
    }
  }, []);

  return (
    <div
      ref={containerRef}
      className="relative w-full overflow-hidden"
      style={{ height: "100vh" }} // 容器高度设为视口高度
    >
      {/* SVG路径容器 */}
      <svg
        ref={svgRef}
        className="absolute top-0 left-0 w-full h-full"
        viewBox="0 0 100 100"
        preserveAspectRatio="none"
        style={{ pointerEvents: "none" }}
      >
        <path
          ref={pathRef}
          d="M50,0 L50,20 L70,30 L30,50 L70,70 L30,80 L50,90 L50,100"
          stroke="#ddd"
          strokeWidth="0.1"
          strokeDasharray="1,1"
          fill="none"
        />
      </svg>

      {/* 卡车:固定垂直居中,仅水平位置和旋转变化 */}
      <div
        ref={truckRef}
        className="fixed top-1/2 transform -translate-y-1/2"
        style={{ width: "50px", willChange: "transform" }}
      >
        <img src="/truck.png" alt="Truck" />
      </div>
    </div>
  );
};

export default TruckPathAnimation;

关键修改说明

  • 容器定位:容器设为100vh并开启pin: true,固定在视口不随滚动移动。
  • 卡车固定:用fixed + top-1/2 + -translate-y-1/2强制垂直居中,仅保留水平位置和旋转的动画。
  • 路径反向移动:SVG路径随滚动向上偏移-200vh,抵消滚动视觉差,让卡车看起来沿路径行驶。
  • 动态坐标计算:通过getPointAtLength获取路径对应进度的坐标,转换为视口水平位置;计算切线角度保证卡车朝向与路径一致。

二、添加特定位置触发文本卡片功能

实现思路

创建多个独立的ScrollTrigger,监听滚动进度到达路径关键节点时,控制卡片的显示/隐藏。

代码扩展示例

  1. 新增卡片引用与触发逻辑(在原组件中添加):
// 在组件顶部添加卡片引用
const cardRefs = [
  useRef<HTMLDivElement>(null),
  useRef<HTMLDivElement>(null),
  useRef<HTMLDivElement>(null)
];

// 在useEffect中添加触发逻辑
triggerPoints.forEach((progress, index) => {
  ScrollTrigger.create({
    trigger: container,
    start: `top top+=${progress * 300}vh`, // 对应总滚动长度的进度点
    end: `top top+=${(progress + 0.1) * 300}vh`, // 卡片显示的滚动区间
    toggleActions: "play none reverse none",
    onEnter: () => gsap.to(cardRefs[index].current, { opacity: 1, y: 0, duration: 0.3 }),
    onLeaveBack: () => gsap.to(cardRefs[index].current, { opacity: 0, y: 20, duration: 0.3 }),
  });
});
  1. 在return中添加卡片元素:
{/* 触发卡片容器 */}
<div className="fixed bottom-10 left-1/2 transform -translate-x-1/2 z-10">
  <div ref={cardRefs[0]} className="bg-white p-4 rounded-lg shadow-lg opacity-0 translate-y-20">
    卡车到达第一个关键点!
  </div>
  <div ref={cardRefs[1]} className="bg-white p-4 rounded-lg shadow-lg opacity-0 translate-y-20">
    卡车到达第二个关键点!
  </div>
  <div ref={cardRefs[2]} className="bg-white p-4 rounded-lg shadow-lg opacity-0 translate-y-20">
    卡车即将到达终点!
  </div>
</div>

说明

  • 通过start/end设置触发的滚动区间,对应总滚动长度的百分比。
  • toggleActions控制动画逻辑,滚动到区间显示卡片,离开时隐藏。
  • 用淡入淡出+位移动画提升卡片显示的流畅度。

内容的提问来源于stack exchange,提问作者Veljox Marjanovic

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 11:15:56