嗨咿,我是 illumi!
前端要做「跑馬燈」(marquee) 時,可能會用 CSS animation:
@keyframes marquee {
from { transform: translateX(0); }
to { transform: translateX(-50%); }
}
雖然簡單,但缺點也很明顯:
所以一起用 GSAP + React Hook (useGSAP
),寫一個可控性更強的跑馬燈吧~
需要安裝:
npm install gsap @gsap/react
然後在 React 裡面可以直接引入:
import { useGSAP } from "@gsap/react";
import gsap from "gsap";
先把跑馬燈的結構寫好:
<div ref={marqueeRef} className="w-full overflow-hidden">
<div ref={trackRef} className="flex gap-4">
{loopList.map((item, index) => (
<div key={index} className="shrink-0">
<TemplateCard {...item} />
</div>
))}
</div>
</div>
重點:
marqueeRef
):負責「顯示範圍」,超出就隱藏 (overflow-hidden
)。trackRef
):實際會被平移的元素。loopList
:我們把清單複製一份 [...list, ...list]
,這樣才能無縫銜接。小補充:
在 Figma 做跑馬燈 prototype 也是 套一層外層,再把裡面那層移動,將整體往左和整體往右的兩個元件連上, 用 After 觸發, prototype 中就會自動移動了!
在 useGSAP
裡面加上動畫:
useGSAP(
() => {
if (!templateList.length || !trackRef.current) return;
const track = trackRef.current;
const distance = track.scrollWidth / 2; // 一半距離
gsap.to(track, {
x: -distance, // 向左移動
duration: 15, // 跑完整個距離的時間
repeat: -1, // 無限循環
ease: "none", // 線性移動
modifiers: {
// 每次位移超過 distance,就取餘數,形成無縫循環
x: (x) => `${parseFloat(x) % distance}px`,
},
scrollTrigger: {
trigger: marqueeRef.current,
start: "top bottom",
end: "bottom top",
toggleActions: "play none none pause", // 當畫面滑走就暫停
},
});
},
{ scope: marqueeRef, dependencies: [templateList] }
);
這裡有幾個關鍵點:
distance = scrollWidth / 2
因為我們複製了一份清單,所以一半距離剛好可以無縫循環。
modifiers
GSAP 提供 modifiers
,可以在每一幀修改動畫數值。
這裡我們用 % distance
,讓它一直 loop,不會突然「跳回去」。
ScrollTrigger
讓跑馬燈在滑到畫面外時自動暫停,避免浪費效能。
最後只要在跑馬燈上面加一個跑動的角色,就有種跑跑步機的感覺了!會比單純跑馬燈更有可愛小巧思!
// components/TemplateLoop.tsx
import { useEffect, useRef, useState } from "react";
import { useGSAP } from "@gsap/react";
import gsap from "gsap";
import TemplateCard from "./TemplateCard";
export default function TemplateLoop() {
const [templateList, setTemplateList] = useState([
{ id: "1", title: "挑戰 A" },
{ id: "2", title: "挑戰 B" },
{ id: "3", title: "挑戰 C" },
]);
const marqueeRef = useRef<HTMLDivElement>(null);
const trackRef = useRef<HTMLDivElement>(null);
// GSAP 跑馬燈效果
useGSAP(
() => {
if (!templateList.length || !trackRef.current) return;
const track = trackRef.current;
const distance = track.scrollWidth / 2;
gsap.to(track, {
x: -distance,
duration: 15,
repeat: -1,
ease: "none",
modifiers: {
x: (x) => `${parseFloat(x) % distance}px`,
},
scrollTrigger: {
trigger: marqueeRef.current,
start: "top bottom",
end: "bottom top",
toggleActions: "play none none pause",
},
});
},
{ scope: marqueeRef, dependencies: [templateList] }
);
const loopList = [...templateList, ...templateList];
return (
<div ref={marqueeRef} className="w-full overflow-hidden">
<div ref={trackRef} className="flex gap-4">
{loopList.map((item, index) => (
<div key={`${item.id}-${index}`} className="shrink-0">
<TemplateCard title={item.title} />
</div>
))}
</div>
</div>
);
}
gsap.to()
即時改變 duration
。hover
事件 gsap.killTweensOf(track)
來暫停。懶得做的寶們可以用之前說的ReactBits 元件:Logo loop
好噠~ 今天做到這裡,明天再見啦~
或是大家把跑馬燈發揮成什麼樣子也可以貼在留言給我看!