影片與原型動作提取 (video-to-gsap)
1. 目的與觸發條件
流暢的微互動與電影級動畫是高端商務體驗的關鍵。然而,實作不佳的網頁動畫會導致執行緒阻塞、掉幀 (卡頓)、記憶體洩漏以及較差的「與下一次繪製的互動」(INP)。此技能負責提取並實作順暢的 60/120 FPS 動畫。
觸發條件:
- 當使用者提供影片片段、GIF 或 Figma 動作原型時。
- 當需要為購物車滑出、頁面轉場、手風琴展開、產品卡片懸停或結帳完成慶祝添加動畫時。
- 當使用 ScrollTrigger 建立滾動驅動動畫時。
2. 動作解構協定
分析動作片段時,系統性地識別:
- 編排順序:
- 起始 / 觸發: 什麼動作啟動了動畫 (懸停、滾動、路由變更、點擊)?
- 預備動作: 主要動作之前是否有微小的反向移動?
- 動作與移動: 哪些元素移動、朝哪個方向、移動多遠 (px、rem 或 %)?
- 安定與跟隨: 它是否會超調 (彈簧/彈性) 或平穩減速 (
power3.out、expo.out)?
- 時間分解:
- 每個元素的持續時間 (例如,抽屜進入:
450ms,背景淡入:300ms)。 - 交錯子元素之間的延遲偏移量 (例如,產品網格卡片:
0.06s交錯)。
- 每個元素的持續時間 (例如,抽屜進入:
- 緩動特徵:
- Linear:嚴格用於連續循環 (例如跑馬燈)。
- Ease-out (
power2.out、expo.out):用於 UI 進入 (快速進入,平穩安定)。 - Ease-in (
power2.in):用於 UI 退出 (加速離開螢幕)。 - Ease-in-out (
power3.inOut):用於保持在視野中的狀態轉換。
3. 效能與硬體加速標準
規則 1:僅對複合屬性進行動畫處理
- 允許:
transform(x、y、xPercent、yPercent、scale、rotation、skew) 與opacity(或 GSAPautoAlpha)。 - 嚴格禁止: 對佈局或繪製屬性 (
width、height、top、left、right、bottom、margin、padding、border-width) 進行動畫處理。這些屬性會強制瀏覽器 CPU 在每個影格重新計算佈局幾何並重繪像素,導致 INP 惡化與掉幀。
規則 2:使用範圍化 useGSAP 實現零記憶體洩漏
- 始終使用官方的
@gsap/react套件與useGSAP鉤子。 - 提供一個
scope容器 ref。內部的每個選擇器都將被自動範圍化,並且當元件卸載時,所有時間軸、補間動畫和 ScrollTrigger 實例都將被乾淨地還原和銷毀。 - 這能防止單頁應用程式和 React 19 快速重新整理循環中的殭屍事件監聽器和記憶體殘留。
規則 3:消除 FOUC (未設定樣式內容的閃爍)
- 使用
autoAlpha而非opacity。當透明度降到 0 時,autoAlpha會設定visibility: hidden,防止在 Hydration 之前出現幽靈點擊與渲染閃爍。 - 或者,宣告 CSS 預設隱藏狀態,並使用
gsap.fromTo來顯示它們。
4. 生產實作藍圖
"use client";
import { useRef } from "react";
import gsap from "gsap";
import { useGSAP } from "@gsap/react";
import { ScrollTrigger } from "gsap/ScrollTrigger";
// 確保外掛安全註冊
if (typeof window !== "undefined") {
gsap.registerPlugin(useGSAP, ScrollTrigger);
}
interface ProductShowcaseProps {
readonly items: Array<{ id: string; title: string; price: string }>;
}
export function ProductShowcase({ items }: ProductShowcaseProps) {
const containerRef = useRef<HTMLDivElement | null>(null);
useGSAP(
() => {
// 硬體加速的進入時間軸
const tl = gsap.timeline({
defaults: {
ease: "expo.out",
duration: 0.8,
},
});
tl.from(".showcase-header", {
autoAlpha: 0,
y: 30,
clearProps: "all", // 動畫後清理內聯樣式
}).from(
".product-card",
{
autoAlpha: 0,
y: 40,
scale: 0.96,
stagger: 0.08,
duration: 0.7,
clearProps: "transform,opacity",
},
"-=0.4" // 無縫重疊
);
},
{ scope: containerRef } // 範圍化清理邊界
);
return (
<section ref={containerRef} className="w-full py-16 px-6">
<div className="showcase-header max-w-4xl mx-auto text-center mb-12">
<h2 className="text-h1 font-bold text-content-primary">精選系列</h2>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 max-w-7xl mx-auto">
{items.map((item) => (
<article
key={item.id}
className="product-card will-change-transform bg-surface-elevated rounded-lg p-6 border border-border-subtle"
>
<h3 className="text-h3 font-semibold text-content-primary">{item.title}</h3>
<p className="text-content-secondary mt-2">{item.price}</p>
</article>
))}
</div>
</section>
);
}
5. 減少動作合規性
始終尊重偏好減少動作的使用者:
const { contextSafe } = useGSAP({ scope: containerRef });
// 檢查無障礙偏好
const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (prefersReducedMotion) {
// 立即呈現最終靜止狀態,無需動畫
gsap.set([".showcase-header", ".product-card"], { autoAlpha: 1, y: 0, scale: 1 });
return;
}