HyperFrames GSAP 缓动、错峰与函数式动画值:从运动语言到可寻址弹簧物理
【免费下载链接】hyperframesWrite HTML. Render video. Built for agents.项目地址: https://gitcode.com/GitHub_Trending/hy/hyperframes
导读
HyperFrames 是一个"写 HTML、渲染视频"的 seek 驱动(seek-driven)确定性渲染运行时,GSAP 是其中 95% 动效工作的默认动画运行时。本文基于 skills/hyperframes-animation/adapters/gsap-easing-and-stagger.md 展开,系统讲解 HyperFrames 中的 GSAP 缓动(easing)体系、错峰(stagger)编排、函数式动画值(function-based values)以及gsap.matchMedia的预览用法,并深入剖析"烘焙弹簧缓动"(springEase)的闭环解析解与可寻址(seek-safe)原理。读完后你将掌握:如何在单条暂停时间轴上为入场、退场、连续运动挑选正确的缓动"语气";如何用错峰对象把一组元素编排成一个整体节拍;以及如何用无状态的弹簧解析解替代有状态的弹簧积分器,让动画在逐帧 seek 下保持严格确定。
一、先理解 HyperFrames 的运动契约:缓动与错峰为什么如此重要
在进入 GSAP 具体 API 之前,需要先锚定 HyperFrames 的两个核心事实:
- 渲染是 seek 驱动的。每支合成(composition)只有一条
paused: true的时间线,注册到window.__timelines["<composition-id>"],由框架调用seek()驱动播放头;渲染关键动效绝不能调用.play()。因此任何缓动都必须是时间的纯函数——同一时间值每次 seek 都必须产生完全相同的状态。 - 确定性是硬约束。合成中禁止
Math.random()、Date.now()、performance.now()、事件驱动动画以及repeat: -1。这条约束直接决定了下文"为什么不能使用有状态弹簧库"的答案。
GSAP 适配器的底层实现在 packages/core/src/runtime/adapters/gsap.ts:createGsapAdapter的seek方法先对时间线执行pause(),再通过totalTime或seek把播放头定位到精确时间;值得注意的是它先以safeTime + 0.001做一次"微扰"再 seek 到目标时间,以强制 GSAP 3.x 在_tTime相同(脏状态缺失)时也能重新渲染。这套机制要求时间线上的每一个 tween 都是无状态的、可由时间唯一决定的状态函数——这正是本文件所有规则的设计前提。
完整的时间线契约见 skills/hyperframes-animation/adapters/gsap-timeline-and-labels.md 与 skills/hyperframes-animation/SKILL.md 中的 Critical Constraints。
二、内置缓动:家庭、变体与"平滑优于弹跳"的运动准则
2.1 缓动家庭与.in/.out/.inOut变体
GSAP 内置缓动家族包括:power1、power2、power3、power4、back、bounce、circ、elastic、expo、sine、none。每个家族都有.in、.out、.inOut三种变体。
选择口诀:入场用.out,退场用.in,对称移动与连续运动用.inOut。
2.2 常用缓动速查表
| Ease | 适用场景 |
|---|---|
power1.out、power2.out | 次要元素的温和运动(字幕淡入、小幅位移)。不是入场默认值 |
power3.out(house default)、power4.out | 标准的长尾沉降。入场、标题卡、主角揭示 |
sine.inOut | 长、慢、平静的运动。交叉淡化、环境漂移 |
back.out(1.7) | 过冲后沉降。罕见——仅限显式俏皮语域,永不作默认 |
elastic.out(1, 0.3) | 弹簧弹跳。同样仅限俏皮;更推荐烘焙弹簧(见 Spring Eases 一节) |
expo.inOut | 干脆、戏剧化。主角场景之间的快速转场 |
none(线性) | 带时间对位(timed counterpoint)的运镜、机械感运动 |
运动准则:Smooth beats bouncy(平滑优于弹跳)。见 rules/spring-pop-entrance.md:入场默认
power3.out或烘焙的临界阻尼弹簧;back/elastic/bounce这类过冲缓动是罕见、显式俏皮的语域,不是 house style。bouncy 是 agent 生成视频的头号劝退点。
2.3 缓动词汇表:性格与情绪
缓动是"语气"(tone of voice):一支视频如果全程只有一个语气会很无聊,在"低语—正常—重拳"之间变化才有感染力。一份合成应在各节拍上取约3 种缓动性格,但变化要在平滑家族内部按能量分层(sine/power1平静 →power3标准 →power4/expo重拳),不要为求变化而动用过冲。过冲是语域(register),不是调味料;到处用同一个缓动读起来扁平,到处用弹跳读起来廉价——后者是更严重的失败。
完整调色板(每个家族均有.in/.out/.inOut变体):
| 家族 | 性格 | 典型用途 |
|---|---|---|
power1–power4 | 从温和(1)到激进(4)的加速曲线 | 通用。power3 是 house workhorse;power2 用于次要温和运动,power4 用于戏剧性急停 |
back(N) | 过冲后沉降。N 控制越过目标多远(1=轻微,4=狂野) | 罕见——仅显式俏皮语域。保持 N ≤ 2;更推荐 ζ 0.6–0.7 的烘焙弹簧(物理沉降) |
elastic(amp, freq) | 弹簧弹跳。amp=幅度,freq=振荡速度 | 罕见——同上;烘焙弹簧是其物理版本 |
bounce | 球落地式弹跳 | 罕见——仅限物理喜剧语域(东西真的在掉落) |
expo | 极陡加速度曲线(远陡于 power4) | 高级/奢华揭示、戏剧化入场 |
sine | 平滑、有机、无硬边 | 环境浮动、呼吸、Ken Burns、任何循环运动。.inOut用于往复运动 |
circ | 圆形加速度(起始极快、结束极缓,反之亦然) | 运镜、场景转场、轨道运动 |
steps(N) | N 步离散跳变,无插值 | 打字效果、光标闪烁、计数器滴答、复古/数码美学 |
情绪映射:让缓动性格匹配节拍的情感内容。平滑/有机缓动(sine、power1)读起来沉思、漂移;激进减速(power4.out、expo.out)读起来干脆、自信;弹簧过冲(back.out)读起来弹跳、物理——但"弹跳"是语域而非强调工具,只在显式俏皮节拍上使用。分镜的情绪描述应当指引选择哪种性格,而不是套公式。
三、默认值:在时间线作用域声明运动语言
时间线级默认值是把该合成"运动语言"集中记录在一处的最佳实践:
const tl = gsap.timeline({ paused: true, defaults: { duration: 0.6, ease: "power3.out" }, // house settle —— 平滑优于弹跳 });也可以全局设置:
gsap.defaults({ duration: 0.6, ease: "power3.out" });推荐在时间线作用域设置 defaults——它把本合成的运动语言文档化在单个位置,后续每个子 tween 自动继承duration与ease,不必逐行重复。这与 gsap-timeline-and-labels.md 中"用 defaults 代替每行重复 ease/duration"的准则一致。
四、Spring Eases:烘焙物理,天生可寻址
4.1 什么是"iOS 手感"
所谓"iOS 手感"其实是阻尼弹簧的速度曲线,而不是弹跳:快速启动后进入漫长的渐近沉降。优秀的系统动画要么临界阻尼、要么接近临界阻尼——几乎不过冲或完全不过冲。power3.out/expo.out只是近似这条曲线;当你需要精确的曲线、或为罕见的俏皮语域需要物理感过冲时,就把弹簧的闭式解烘焙成函数缓动。
4.2 为什么不能用实时弹簧库
交互式弹簧是有状态积分器(速度逐帧累积),无法确定性 seek——要渲染第 N 帧,必须把第 0…N−1 帧全部模拟一遍。而下面这个闭式解是关于进度的纯函数:无状态、无同步漂移、按构造即 seek-safe。这也是为什么交互类库的弹簧求解器被禁止用于合成。
4.3 springEase 完整实现
// springEase —— 阻尼弹簧精确位置曲线,作为 GSAP ease 使用。 // response ≈ 一次振荡所需秒数(入场取 0.3–0.6) // dampingFraction 1.0 = 临界阻尼——平滑沉降,无过冲(house default) // 0.80–0.85 ≈ iOS 系统语域——约 1–1.5% 过冲,可感而不可见 // 0.60–0.70 = 显式俏皮——约 5–10% 过冲(罕见;取代 back.out) function springEase({ response = 0.5, dampingFraction = 1 } = {}) { const w = (2 * Math.PI) / response; // 无阻尼固有角频率 const z = dampingFraction; let pos; // x(t): 0 → 1,静止启动(v0 = 0) if (z < 1) { const wd = w * Math.sqrt(1 - z * z); pos = (t) => 1 - Math.exp(-z * w * t) * (Math.cos(wd * t) + ((z * w) / wd) * Math.sin(wd * t)); } else if (z > 1) { const wo = w * Math.sqrt(z * z - 1); pos = (t) => 1 - Math.exp(-z * w * t) * (Math.cosh(wo * t) + ((z * w) / wo) * Math.sinh(wo * t)); } else { pos = (t) => 1 - Math.exp(-w * t) * (1 + w * t); } // 沉降时间:曲线最后一次离开目标 ±0.1% 的时刻。 // 固定步长扫描,仅在初始化时运行一次——确定性(无 Math.random / Date.now)。 const EPS = 0.001; const rate = z <= 1 ? z * w : (z - Math.sqrt(z * z - 1)) * w; // 最慢衰减模态 const SCAN = 12 / rate; const N = 4800; let T = SCAN; for (let i = N; i >= 0; i--) { const t = (i / N) * SCAN; if (Math.abs(1 - pos(t)) > EPS) { T = ((i + 1) / N) * SCAN; break; } } const xT = pos(T); return { duration: T, // 用作 tween 的 duration——沉降时间本身就是物理 ease: (p) => pos(p * T) + p * (1 - xT), // 归一化,使 ease(1) === 1 精确成立 }; }实现要点:
- 三种阻尼分支:欠阻尼(
z < 1)用cos/sin振荡项;过阻尼(z > 1)用cosh/sinh双曲项;临界阻尼(z == 1)用(1 + w·t)·e^(−w·t)的经典临界形式。 - 沉降时间扫描:以固定步长(4800 步)扫描曲线最后一次偏离目标超过 ±0.1% 的时刻作为
duration。扫描只在初始化时运行一次,且不含任何随机源,保证确定性。 - 端点归一化:返回的 ease 对进度 p 做
pos(p * T) + p * (1 - xT),确保ease(1) === 1精确成立,符合 GSAP ease 契约。
工程佐证:仓库中与弹簧相关的能力不止这一处。
packages/core/src/parsers/springEase.ts提供 Studio 的单参数弹簧 tokenspring(bounce)的解析与端点归一化求值(evaluateSpringEase),其测试 packages/core/src/parsers/springEase.test.ts 明确断言:弹簧"从 0 出发、可过冲、精确收敛到 1",且对相同输入两次求值结果完全相同(确定性断言),更高 bounce 值产生更多振荡。这与本文 springEase 的"纯函数、确定性、物理沉降"哲学同源。
4.4 使用方式:缓动与时长一起取
duration 必须一并来自 helper——沉降时间本身就是物理的一部分;覆盖 duration 只是给同一曲线重新计时,调速应当通过response参数:
const settle = springEase({ response: 0.4 }); // 临界阻尼 → duration ≈ 0.59s tl.fromTo( "#hero", { scale: 0, opacity: 0 }, { scale: 1, opacity: 1, duration: settle.duration, ease: settle.ease }, 0.2, );4.5 参数对照表
dampingFraction(阻尼比 ζ):
| dampingFraction | 过冲 | 语域 |
|---|---|---|
| 1.0(默认) | 无(单调) | House settle——即power3.out所近似的精确曲线。产品/企业/严肃基调 |
| 0.80–0.85 | 约 1–1.5% | "有生气而不弹跳"——iOS 系统默认语域。过冲可感而不可见 |
| 0.60–0.70 | 约 5–10% | 仅限显式俏皮(与back.out同规则——弹簧的二阶沉降读起来物理,back读起来卡通) |
| < 0.55 | > 12% | 不要用。卡通抖动区 |
response(响应时间):
| response | duration(ζ=1) | 手感 |
|---|---|---|
| 0.25–0.35 | 0.37–0.51s | 紧致急停——芯片、小型 UI |
| 0.35–0.50 | 0.51–0.74s | 标准入场 |
| 0.50–0.70 | 0.74–1.03s | 有重量的主角落地——注意检查t ≤ 0.5s可见性规则 |
4.6 工艺要点(Craft notes)
- ζ=1 vs
power3.out:真实弹簧前置加载更猛(四分之一时刻已走约 67%,power3.out约 58%),且沉降在更长的渐近尾段;最大形状差异约 11%。那条长尾就是"高级感"的来源——当沉降本身即镜头(字标落地、最终定版)时使用它。 - ζ<1 时,过冲曲线只能用在 transform 上——绝不能用于
opacity(会越过 1)或颜色。把 opacity 拆到同一时间线位置的独立power2.outtween 上。 - 准则不变:ζ 低于约 0.8 仍是罕见、显式俏皮的例外(见 rules/spring-pop-entrance.md)。本节的默认值是 ζ=1——真实弹簧物理不是弹跳的许可。
五、Stagger:把一组元素编排成一个节拍
5.1 基本用法
gsap.fromTo(".item", { y: 24, opacity: 0 }, { y: 0, opacity: 1, duration: 0.5, stagger: 0.08 });5.2 对象形式
gsap.fromTo( ".item", { y: 24, opacity: 0 }, { y: 0, opacity: 1, stagger: { each: 0.08, // 每个元素之间的延迟 from: "center", // "start" | "end" | "center" | "edges" | "random" | index amount: 0.6, // 总错峰时长(若同时设置,覆盖 each) grid: "auto", // 用于 2D 错峰 axis: "x" | "y", }, }, );字段速览:
each:相邻元素启动间隔(秒);from:错峰起始方向,支持"start"、"end"、"center"、"edges"、"random",也可直接传索引数值;amount:整个序列的总错峰时长,设置后覆盖each(二者互斥时以amount为准);grid:为二维网格布局启用"自动"行列感知错峰;axis:在主轴(x/y)上限制错峰方向。
5.3 实践准则
- 优先用
stagger,而不是 N 个带手动delay的独立 tween——当目标数量或顺序变化时它依然正确。 - 用
fromTo()而非from(),让起始状态显式(参见 gsap-timeline-and-labels.md 的子合成入场章节)。原因:子合成每次宿主片段可见时都会被重新 seek,gsap.from()在注册时(页面加载)快照起始状态,播放头跳回data-start之前时该快照可能与真实 CSS 状态脱同步;fromTo()显式声明两端,回 seek 永远产生相同起始状态。 - 分组错峰参照 spring-pop-entrance.md 的经验值:
STAGGER取min(0.06, 0.5 / ITEM_COUNT),条目数 3–9,确保ITEM_COUNT × STAGGER ≤ 约 0.5s,让整组在一个节拍内落地;超过 9 个条目错峰会"消失",应改用擦除/扫掠揭示。
六、函数式动画值:按索引/属性/尺寸计算每个元素的值
任何动画属性都可以是函数(index, target, targets) => value:
gsap.to(".item", { x: (i, target, targets) => i * 50, rotation: (i) => (i % 2 === 0 ? 5 : -5), stagger: 0.1, });回调签名:
i:目标索引(0 起);target:当前目标元素;targets:目标数组全集。
适用场景:需要按索引、元素属性或测量尺寸计算逐元素值的地方。相比在循环里逐个构建 tween,函数式动画值更省成本、更地道。注意与 SKILL.md 的约束结合:预计算布局常量,绝不要在 tween 时刻从getBoundingClientRect()推导位置——渲染器并行采样会导致脱同步;坐标应在合成 setup 阶段计算一次并复用。
七、gsap.matchMedia:仅用于预览
matchMedia只在媒体查询匹配时运行 setup,并在不再匹配时自动还原(revert)。它适用于浏览器预览时的不同视口尺寸,以及prefers-reduced-motion处理:
let mm = gsap.matchMedia(); mm.add( { isDesktop: "(min-width: 800px)", reduceMotion: "(prefers-reduced-motion: reduce)", }, (context) => { const { isDesktop, reduceMotion } = context.conditions; gsap.to(".box", { rotation: isDesktop ? 360 : 180, duration: reduceMotion ? 0 : 2, }); }, );重要限制:
matchMedia不能替代按合成实际data-width/data-height渲染——HyperFrames 在固定视口下渲染,媒体查询只对浏览器中的预览体验有意义,不能作为响应式布局的渲染手段。
八、把这些规则放进一条合规的时间线
将本文要点组装成一段符合 HyperFrames 契约的完整示例:
window.__timelines = window.__timelines || {}; const tl = gsap.timeline({ paused: true, defaults: { duration: 0.6, ease: "power3.out" }, // house settle }); // 1. 主角:烘焙的临界阻尼弹簧,duration 来自 helper const settle = springEase({ response: 0.4 }); tl.fromTo( "#hero", { scale: 0, opacity: 0 }, { scale: 1, opacity: 1, duration: settle.duration, ease: settle.ease }, 0.2, ); // 2. 标题:power3.out 长尾沉降 tl.fromTo(".title", { y: 24, opacity: 0 }, { y: 0, opacity: 1 }, "hero+=0.1"); // 3. 卡片组:stagger 成一个节拍,函数式动画值做交替微旋转 tl.fromTo( ".card", { y: 24, opacity: 0, rotation: (i) => (i % 2 === 0 ? 5 : -5) }, { y: 0, opacity: 1, rotation: 0, duration: 0.5, stagger: { each: 0.08, from: "center" } }, "title", ); window.__timelines["main"] = tl; // 键必须等于合成根的>node skills/hyperframes-animation/scripts/animation-map.mjs <composition-dir> \ --out <composition-dir>/.hyperframes/anim-map九、速查与自检清单
- 入场:
.out;退场:.in;对称/连续运动:.inOut。 - 默认缓动:
power3.out(时间线 defaults 中声明一次);重拳用power4.out/expo.out;平静用sine/power1。 - 过冲缓动(
back/elastic/bounce)是显式俏皮语域,不是调味料;需要物理感弹跳时用springEase({ dampingFraction: 0.6–0.7 })。 - 弹簧物理:ζ=1 临界阻尼是 house default;ζ<1 时过冲曲线只上 transform,opacity 拆独立 tween;duration 必须取自 helper。
- 错峰:优先
stagger对象(each/from/amount/grid/axis),组内落地控制在约 0.5s 内。 - 函数式动画值:
(i, target, targets) => value处理逐元素差异,比循环建 tween 更便宜。 - 确定性红线:无
Math.random/Date.now/performance.now,无repeat: -1,fromTo显式两端,paused: true并注册到window.__timelines。 matchMedia仅用于浏览器预览,渲染永远以data-width/data-height为准。
【免费下载链接】hyperframesWrite HTML. Render video. Built for agents.项目地址: https://gitcode.com/GitHub_Trending/hy/hyperframes
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考