ビデオ&プロトタイプモーション抽出 (video-to-gsap)
1. 目的とトリガー
流暢なマイクロインタラクションと映画的なモーションは、ハイエンドなコマース体験を差別化します。しかし、Webアニメーションの実装が不適切だと、スレッドのブロッキング、フレームドロップ(ジャンク)、メモリリーク、INP(Interaction to Next Paint)の低下を引き起こします。このスキルは、滑らかな60/120 FPSアニメーションの抽出と実装を統制します。
トリガー:
- ユーザーがビデオクリップ、GIF、またはFigmaモーションプロトタイプを提供した場合。
- カートスライドオーバー、ページトランジション、アコーディオン展開、プロダクトカードホバー、チェックアウト完了の祝福をアニメーション化する場合。
- ScrollTriggerを使用したスクロール駆動アニメーションを作成する場合。
2. モーション分解プロトコル
モーションクリップを分析する際は、体系的に以下を特定します:
- 振り付けシーケンス:
- ステージング / トリガー: どのアクションがモーションを開始するか(ホバー、スクロール、ルート変更、クリック)?
- 予備動作: 主要モーションの前に微妙な逆方向の動きがあるか?
- アクションと移動: どの要素が、どの方向に、どの距離(px、rem、%)動くか?
- 落ち着きとフォロースルー: オーバーシュート(スプリング/エラスティック)するか、スムーズに減速するか(
power3.out、expo.out)?
- タイミング分解:
- 各要素の持続時間(例:ドロワーエントリー:
450ms、背景フェード:300ms)。 - スタガーされた子要素間のスタガーオフセット(例:プロダクトグリッドカード:
0.06sスタガー)。
- 各要素の持続時間(例:ドロワーエントリー:
- イージングシグネチャ:
- リニア: 連続ループ専用(例:マーキー)。
- イーズアウト(
power2.out、expo.out): UIエントランス用(高速エントリー、緩やかなセトリング)。 - イーズイン(
power2.in): UIエグジット用(画面外に加速)。 - イーズインアウト(
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インスタンスは、コンポーネントのアンマウント時にクリーンに元に戻され、killされます。- これにより、シングルページアプリケーションやReact 19の高速リフレッシュサイクルにおけるゾンビイベントリスナーとメモリ保持を防ぎます。
ルール3: FOUC(スタイル未適用コンテンツのフラッシュ)の排除
opacityの代わりにautoAlphaを使用します。autoAlphaは、不透明度が0になるとvisibility: hiddenを設定し、ハイドレーション前のゴーストクリックやレンダリングフラッシュを防ぎます。- または、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">Featured Collection</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;
}