视频与原型运动提取 (video-to-gsap)
1. 目的与触发条件
流畅的微交互和电影级运动使高端电商体验与众不同。然而,实现不当的Web动画会导致线程阻塞、丢帧(卡顿)、内存泄漏以及糟糕的交互到下一次绘制(INP)指标。此技能负责提取并实现丝滑的 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容器引用。内部的每个选择器将自动被作用域化,所有时间线、补间和 ScrollTrigger 实例在组件卸载时将被干净地恢复和销毁。 - 这防止了单页应用和 React 19 快速刷新循环中的僵尸事件监听器和内存保留。
规则 3:消除 FOUC(未样式内容闪烁)
- 使用
autoAlpha代替opacity。当不透明度为 0 时,autoAlpha设置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;
}