HyperFrames 中的 Web Animations API(WAAPI)适配器:确定性渲染下的原生 Keyframes 动效实战
2026/9/10 0:52:28 网站建设 项目流程

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()currentTimedocument.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(...)创建原生浏览器动画时,适配器会在每一帧渲染时执行两件事:

  1. 调用document.getAnimations()枚举当前文档中的所有动画;
  2. 把每个动画的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 一节):

  • 在组合初始化阶段同步创建动画:不要在asyncsetTimeoutPromise回调里创建动画(与 skills/hyperframes-animation/SKILL.md 中复述的核心约束一致),否则适配器可能错过发现时机;
  • 使用有限的durationiterations:时长推断依赖有限的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 适配器最合适的三类场景:

  1. 轻量 DOM 动效:CSS keyframes 表达力不足、又不想为几行动画引入 GSAP 依赖时;
  2. 由结构化数据生成的动画:数据驱动、批量生成 keyframes;
  3. 可用 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基线换算成组合相对秒数;若endTimeInfinity/NaN(无限迭代),返回unbounded: true标记(waapi.ts)。

结论:

  • 只要每个element.animate()都使用有限的durationiterations,根元素上的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修改渲染关键 DOMfinished由真实时间驱动,与逐帧 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)按以下逻辑判定:

  1. 若组合既没有 GSAP timeline,也没有data-duration,且没有任何 CSS/WAAPI/Lottie/Three.js 动画信号——运行时完全无法确定时长,渲染必然失败于"Composition has zero duration",报错并给出修复提示;
  2. 若检测到usesWaapi(正则匹配.animate(...)的数组字面量、对象字面量或变量 keyframes 三种调用形式)且动画有限——不是错误,运行时可以从.animate()推断时长,data-duration在此处是可选的;
  3. 仅当存在无限 CSS 动画(animation-iteration-count: infinite)等无界信号时,才强制要求显式声明data-duration

也就是说:符合本文契约(有限duration+ 有限iterations+ 同步创建)的 WAAPI 组合可以顺利通过 lint,这正是契约设计的目的。相关测试见 composition.test.ts。

运行时行为细节:源码与测试交叉印证

以下行为均能从 waapi.ts 与其测试 waapi.test.ts 中直接印证,帮助你在排障时理解适配器的边界:

行为源码位置测试验证
seek 将秒换算为毫秒并 clamp 负值到 0seek(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 的resolveAdapterDurationFloorSecondsgetSafeTimelineDurationSeconds,以及适配器内的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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询