实现元素沿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,监听滚动进度到达路径关键节点时,控制卡片的显示/隐藏。
代码扩展示例
- 新增卡片引用与触发逻辑(在原组件中添加):
// 在组件顶部添加卡片引用 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 }), }); });
- 在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
相关产品推荐
相关产品推荐

