HyperFrames 中的 Web Animations API(WAAPI)适配器:确定性渲染下的原生 Keyframes 动效实战
【免费下载链接】hyperframesWrite HTML. Render video. Built for agents.项目地址: https://gitcode.com/GitHub_Trending/hy/hyperframes
导读
HyperFrames 的核心理念是"写 HTML、渲染视频、为 Agent 而生":页面上的任意 DOM 动效都必须能被运行时逐帧精确回放,才能产出确定性的视频。本文基于skills/hyperframes-animation/adapters/waapi.md展开,讲解如何在不依赖 GSAP 的前提下,使用浏览器原生 Web Animations API(element.animate()、currentTime、document.getAnimations())编写可被 HyperFrames 确定性渲染的动画,并深入其waapi运行时适配器的源码(packages/core/src/runtime/adapters/waapi.ts)与 lint 规则,揭示"时长自动推断、逐帧 seek、动画基线锚定"的底层原理。读完本文,你将掌握 WAAPI 适配器的使用契约、三种核心编写模式、合成时长的推断规则,以及一套可直接复制运行的 HTML 组合示例。
WAAPI 适配器是什么
HyperFrames 通过waapi运行时适配器(Runtime Deterministic Adapter)来驱动 Web Animations API 动画。当你在组合(composition)里调用element.animate(...)创建原生浏览器动画时,适配器会在每一帧渲染时执行两件事:
- 调用
document.getAnimations()枚举当前文档中的所有动画; - 把每个动画的
currentTime设置为 HyperFrames 当前时间(毫秒),然后立即pause()暂停它。
对应源码见 createWaapiAdapter 的seek实现:const timeMs = Math.max(0, (Number(ctx.time) || 0) * 1000)将秒换算为毫秒,随后对每个被追踪的动画写入animation.currentTime = localTimeMs并调用animation.pause()。
这种设计意味着:你写的 WAAPI 动画本身是"活的",但渲染时由 HyperFrames 全权接管时间轴——浏览器自己的时钟、requestAnimationFrame、定时器统统不参与,动画状态完全由currentTime的寻址值决定,从而保证每一帧画面可复现。
编写契约(Contract)
要让 WAAPI 动画在 HyperFrames 中稳定渲染,必须遵守以下契约(即原文档的 Contract 一节):
- 在组合初始化阶段同步创建动画:不要在
async、setTimeout或Promise回调里创建动画(与 skills/hyperframes-animation/SKILL.md 中复述的核心约束一致),否则适配器可能错过发现时机; - 使用有限的
duration和iterations:时长推断依赖有限的endTime,无限迭代会让推断失去依据(详见下文"合成时长"一节); - 使用
fill: "both":让被 seek 到的状态在动画时间范围外也能持久保持,否则回退到动画之前的帧时元素会回到初始状态; - 创建动画后暂停它,或交给适配器在首次 seek 时暂停:适配器会在 seek 时统一
pause(),但显式animation.pause()更符合确定性原则; - 避免用回调(
animation.finished等)和 Promise 驱动渲染关键状态:这些异步信号依赖真实时间推进,与逐帧 seek 的渲染模型冲突。
从源码看,适配器通过snapshotAnimations()包装document.getAnimations()(带 try/catch 防御,waapi.ts),并对每个动画维护一个baselinesWeakMap,记录"组合时间 ↔ 动画时间"的锚点,这是后续 seek 定位与时长推断的基础。
基础模式:单元素 Keyframes 动画
原文档给出的基础示例(保持原文可运行性,补充注释):
<div id="orb" class="clip orb">document.querySelectorAll(".token").forEach((token, index) => { const animation = token.animate( [ { transform: "translateY(24px)", opacity: 0 }, // 起始:下移 + 透明 { transform: "translateY(0)", opacity: 1 }, // 结束:归位 + 不透明 ], { duration: 620, delay: index * 80, // 每项错峰 80ms easing: "cubic-bezier(0.2, 0, 0, 1)", fill: "both", iterations: 1, }, ); animation.pause(); });这种"由结构化数据生成动画"的模式正是 WAAPI 的强项:不需要引入动画库,数据数组与动画参数一一对应,适合从 JSON/数据模型批量生成动效。
适用场景(Good Uses)
WAAPI 适配器最合适的三类场景:
- 轻量 DOM 动效:CSS keyframes 表达力不足、又不想为几行动画引入 GSAP 依赖时;
- 由结构化数据生成的动画:数据驱动、批量生成 keyframes;
- 可用 keyframes + delay + offset 表达的简单时间线:不需要 GSAP timeline 那样的复杂编排与标签系统。
相对地,SKILL.md 的运行时选择建议是:GSAP 仍是 95% 动效工作的默认方案(时间线编排、变换、缓动、交错),WAAPI 适合"无需 GSAP 依赖的原生 keyframes"这一细分场景(skills/hyperframes-animation/SKILL.md)。多个运行时可以在同一组合中共存,HyperFrames 会一次性 seek 所有运行时注册的实例。
合成时长(Composition Duration):自动推断原理
渲染引擎必须知道合成总时长才能决定捕获多少帧。GSAP 时间线会自动上报时长,而纯 WAAPI 组合没有 timeline 对象,因此运行时从每个动画的effect.getComputedTiming().endTime推断时长,并以动画创建时刻相对组合起点的时间为基准进行偏移。
对应的实现链路在 packages/core/src/runtime/init.ts:
- 每个非 GSAP 适配器可暴露
getInferredDurationSeconds(),resolveAdapterDurationFloorSeconds()汇总所有适配器上报的最大有限结束时间; getSafeTimelineDurationSeconds()取"媒体时长下限、作者声明的data-duration、适配器推断时长"三者的最大值,作为最终安全时长;- WAAPI 适配器的
inferAnimationEndSeconds读取animation.effect?.getComputedTiming()?.endTime(毫秒),加上该动画的compositionTimeMs基线换算成组合相对秒数;若endTime是Infinity/NaN(无限迭代),返回unbounded: true标记(waapi.ts)。
结论:
- 只要每个
element.animate()都使用有限的duration和iterations,根元素上的data-duration就是可选的——这正是契约强制有限迭代的深层原因; - 无限
iterations没有有限的endTime,无法自动推断。此时必须在根元素[data-composition-id]上显式添加data-duration="<秒>",否则npx hyperframes lint会报root_composition_missing_duration_source错误; - 有趣的是,推断逻辑对"无限动画"是宽容的:若组合里同时存在一个有限动画和一个无限动画,
getInferredDurationSeconds仍会返回有限动画的结束时间作为有效时长信号(对应测试 waapi.test.ts);只有当所有动画都无限时才会返回null。
动态发现与基线锚定:适配器会对Element.prototype.animate打补丁(installAnimateHook),使初始化之后新创建的动画也能被立即追踪,并为每个动画记录compositionTimeMs/animationTimeMs基线,保证"中途才出现的动画"也能从它出现时的组合时间点正确起步(waapi.ts);seek 时本地时间为baseline.animationTimeMs + max(0, timeMs - baseline.compositionTimeMs)。测试用例"anchors newly discovered WAAPI animations"验证了这一行为(waapi.test.ts)。
应避免的做法(Avoid)
原文档列出的红线,逐条给出原因:
- 无限
iterations:没有有限endTime,时长无法自动推断(见上文),lint 会报错; - 依赖
animation.finished修改渲染关键 DOM:finished由真实时间驱动,与逐帧 seek 模型冲突,状态不可复现; - 用
requestAnimationFrame、定时器或performance.now()跑独立时钟:确定性渲染要求唯一时间源是 HyperFrames 的时间轴; - 动画布局属性(
width/height/top/left):能由 transform 和 opacity 表达的运动应尽量用它们,布局属性会触发重排、破坏性能与确定性(SKILL.md 的核心约束同样禁止布局属性 tween); - 假设 clip 本地起始时间是自动的:WAAPI 适配器 seek 的是文档级动画时间,clip 偏移必须用
delay建模,或把动画挂在由 HyperFrames 时序控制可见性的元素上。
验证:lint 与 check
编辑完 WAAPI 组合后,原文档推荐执行:
npx hyperframes lint npx hyperframes check其中lint负责静态规则检查。关于root_composition_missing_duration_source,lint 规则源码(packages/lint/src/rules/composition.ts)按以下逻辑判定:
- 若组合既没有 GSAP timeline,也没有
data-duration,且没有任何 CSS/WAAPI/Lottie/Three.js 动画信号——运行时完全无法确定时长,渲染必然失败于"Composition has zero duration",报错并给出修复提示; - 若检测到
usesWaapi(正则匹配.animate(...)的数组字面量、对象字面量或变量 keyframes 三种调用形式)且动画有限——不是错误,运行时可以从.animate()推断时长,data-duration在此处是可选的; - 仅当存在无限 CSS 动画(
animation-iteration-count: infinite)等无界信号时,才强制要求显式声明data-duration。
也就是说:符合本文契约(有限duration+ 有限iterations+ 同步创建)的 WAAPI 组合可以顺利通过 lint,这正是契约设计的目的。相关测试见 composition.test.ts。
运行时行为细节:源码与测试交叉印证
以下行为均能从 waapi.ts 与其测试 waapi.test.ts 中直接印证,帮助你在排障时理解适配器的边界:
| 行为 | 源码位置 | 测试验证 |
|---|---|---|
| seek 将秒换算为毫秒并 clamp 负值到 0 | seek(L149-L178) | "seek clamps negative time to 0" |
每次 seek 都对动画执行currentTime写入 +pause() | 同上 | "seek pauses and sets currentTime on all animations"(期望 2.5s → 2500ms) |
动画finish/cancel后自动从追踪集合移除 | trackAnimation(L67-L81) | "drops finished lazy-tracked animations" |
空发现后不逐帧全局扫描(性能优化),仅靠animate钩子捕获新动画 | seek条件!didDiscover \|\| animations.size > 0 | "does not rescan document animations on every seek when discover found none" |
缺少document.getAnimations时优雅降级不抛错 | snapshotAnimations(L25-L32) | "handles missing getAnimations API" |
revert恢复被替换的Element.prototype.animate原函数 | revert(L192-L216) | "revert restores the Element.animate hook" |
其中"空发现后跳过逐帧扫描"是一条值得注意的性能设计:document.getAnimations()在 Chromium 中即使返回空数组也相当昂贵,而renderSeek每帧都会调用适配器,因此在发现阶段为空时会跳过全局扫描,直到作者代码通过被 hook 的Element.animate创建动画为止(源码注释 waapi.ts)。
进一步阅读
- 适配器源码:packages/core/src/runtime/adapters/waapi.ts
- 时长自动推断:packages/core/src/runtime/init.ts 的
resolveAdapterDurationFloorSeconds与getSafeTimelineDurationSeconds,以及适配器内的getInferredDurationSeconds(waapi.ts) - 运行时适配器接口定义:packages/core/src/runtime/types.ts(
getInferredDurationSeconds的类型契约) - 测试用例:packages/core/src/runtime/adapters/waapi.test.ts
- lint 规则:packages/lint/src/rules/composition.ts 与对应测试 packages/lint/src/rules/composition.test.ts
- 组合结构、数据属性与确定性渲染契约:见
skills/hyperframes-core;WAAPI 在整个动画技能体系中的定位见 skills/hyperframes-animation/SKILL.md(运行时选择一节) - 官方 Web Animations API 与
Animation.currentTime的规范细节可查阅 MDN 对应条目
【免费下载链接】hyperframesWrite HTML. Render video. Built for agents.项目地址: https://gitcode.com/GitHub_Trending/hy/hyperframes
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考