1. 简介
本节非规范性。本文档介绍了一种新的原语,它提供了 Web 动画的可扩展性,并在 Web 上实现了高性能的交互式过程动画。有关详细的理由和动机,请参见 [解释器] 和 [原则]。
动画 Worklet API 提供了一种创建脚本动画的方法,这些动画控制一组 动画效果。该 API 设计使得用户代理可以在其专用线程中运行此类动画,从而与主线程提供一定程度的性能隔离。
1.1. 与 Web 动画 API 的关系
本节非规范性。在 动画 Worklet
执行上下文中运行的动画在主 JavaScript 执行上下文中暴露了来自 Web 动画规范的 动画
接口。这意味着它们可以使用相同的 Web 动画 API 从主线程进行控制和检查。
2. 动画 Worklet
动画 Worklet 是一个Worklet
,负责与自定义动画相关的所有类。Worklet 可以通过 animationWorklet
属性访问。
animationWorklet
的 worklet 全局范围类型 是 AnimationWorkletGlobalScope
。
AnimationWorkletGlobalScope
表示 animationWorklet
的全局执行上下文。
[Exposed =Window ]partial namespace CSS { [SameObject ]readonly attribute Worklet ; };
animationWorklet
3. 动画师
动画师 代表了在动画线程上运行的动画实例。
动画师通过唯一的名称进行识别,并根据当前的输入时间确定动画如何推进其关键帧效果。动画师 实例存在于 AnimationWorkletGlobalScope
中,每个实例都与一个 WorkletAnimation
实例相关联。动画师只能通过构造 WorkletAnimation
来实例化。
支持两种动画师类型:无状态动画师
和 有状态动画师
,每个提供不同的状态管理策略。
3.1. 无状态动画师接口
此接口表示一个无状态动画师。此类动画师不依赖于存储在实例或全局范围内的任何本地状态。实际上,无状态动画师
的动画函数可以被视为纯函数,期望在相同的输入下产生相同的输出。
[Exposed =AnimationWorklet ,Global =AnimationWorklet ,(
Constructor optional any )]
options interface { };
StatelessAnimator
class FooAnimatorextends StatelessAnimator{ constructor( options) { // 当一个新的动画师被实例化时调用。 } animate( currentTime, effect) { // 动画帧逻辑在此。 } }
注意: 无状态使动画工作流能够执行优化,例如并行生成多个动画帧,并进行非常低成本的拆卸和设置。强烈推荐使用无状态动画师,以启用此类优化。
3.2. 有状态动画师接口
此接口表示一个有状态的动画师。该动画师可以有本地状态,并且动画工作流保证只要有状态动画师履行如下所述的接口要求,它的状态将被维护。
动画 Worklet
维护着一组可能存在于不同线程或进程中的 WorkletGlobalScope
。
动画 worklet 可能会暂时终止某个全局范围(例如,为了节省资源),或在不同全局范围之间移动一个正在运行的 动画师实例(例如,如果其效果只能在某一特定线程中发生变化)。动画 worklet
保证有状态动画师实例的状态即使在不同的全局范围中重新生成时也能保持。
维护状态的基本机制是动画 worklet 快照通过 state
函数暴露的本地状态,并将其实例化,以便在稍后可能在不同全局范围内重新生成动画师实例时,将其传递给构造函数。迁移动画师实例 算法详细说明了这一过程。
用户定义的有状态动画师需履行以下要求:其 state 函数返回一个对象,表示其状态,该状态可使用结构化序列化算法进行序列化;同时,构造函数也能根据传递给它的同一对象重新创建其状态。
[Exposed =AnimationWorklet ,Global =AnimationWorklet ,(
Constructor optional any ,
options optional any )]
state interface {
StatefulAnimator any (); };
state
class BarAnimatorextends StatefulAnimator{ constructor( options, state) { // 当一个新的动画师被实例化时(无论是首次还是重新生成后)。 this . currentVelocity= state? state. velocity: 0 ; } animate( currentTime, effect) { // 动画帧逻辑在此,可以依赖 this.currentVelocity。 this . currentVelocity+= 0.1 ; } state() { // 返回的对象应使用结构化克隆算法进行序列化。 return { velocity: this . currentVelocity; } } }
3.3. 动画师定义
动画师定义 是一个结构,它根据 AnimationWorkletGlobalScope
的需要描述作者定义的自定义动画。
它包括:
-
一个 动画师名称 <标识符>。
-
一个 类构造函数,它是一个
AnimatorInstanceConstructor
回调函数 类型。 -
一个 有状态标志。
3.4. 注册动画师定义
AnimationWorkletGlobalScope
具有一个动画师名称到动画师定义的映射。
当调用 registerAnimator(name, animatorCtor)
时,映射会被填充。
[Exposed =AnimationWorklet ,Global =AnimationWorklet ]interface :
AnimationWorkletGlobalScope WorkletGlobalScope {void registerAnimator (DOMString ,
name AnimatorInstanceConstructor ); };
animatorCtor callback =
AnimatorInstanceConstructor any (any ,
options optional any );
state
当在AnimationWorkletGlobalScope
中调用 registerAnimator(name, animatorCtor)
方法时,用户代理必须运行以下步骤:
-
如果 name 作为键存在于动画师名称到动画师定义的映射中,抛出 NotSupportedError 并中止所有这些步骤。
-
如果 IsConstructor(animatorCtor) 的结果为 false, 抛出 TypeError 并中止所有这些步骤。
-
令 prototype 为调用 Get(animatorCtor, "prototype") 的结果。
-
如果 SameValue(prototype,
无状态动画师
) 为 true,则将 stateful 设置为 false,否则如果 SameValue(prototype,有状态动画师
) 为 true,则将 stateful 设置为 true,否则抛出 TypeError 并中止所有这些步骤。 -
令 animateValue 为调用 Get(prototype, "animate") 的结果。
-
令 animate 为 转换 animateValue 为 函数 回调函数 类型的结果。如果抛出异常,请重新抛出异常并中止这些步骤。
-
令 definition 为新的 动画师定义,其中包括:
-
将键值对 (name - definition) 添加到 动画师名称到动画师定义的映射 中。
4. 动画师实例
动画师实例
是一个结构,
描述了在AnimationWorkletGlobalScope
中完全实现的自定义动画实例。
它引用了一个动画师定义,并拥有特定实例状态,如动画效果和时间线。它包括:
-
一个动画师名称。
-
一个动画请求标志。
-
一个动画师效果,它是一个 动画效果。
-
一个动画师当前时间,它是对应的 worklet 动画 的当前时间。
-
一个动画师时间线,它是一个 时间线。
-
一个动画师序列化选项,它是一个可序列化的对象。
一个有状态动画师实例 是一个 动画师实例,其对应的 动画师定义 的 有状态标志 为 true。
4.1. 创建动画师实例
每个动画师实例
存在于AnimationWorkletGlobalScope
中。
每个AnimationWorkletGlobalScope
具有一个动画师实例集。当用户代理在AnimationWorkletGlobalScope
作用域中构造一个新的动画师实例时,该集合将被填充。每个动画师实例对应于文档作用域中的一个 worklet 动画。
为给定的name、timeline、effect、serializedOptions、serializedState和workletGlobalScope,用户代理必须运行以下步骤以创建一个新的动画师实例:
-
令definition为在workletGlobalScope的动画师名称到动画师定义映射中查找name的结果。
如果definition不存在,终止以下步骤。
-
令animatorCtor为definition的类构造函数。
-
令options为结构化反序列化(serializedOptions)的结果。
-
令state为结构化反序列化(serializedState)的结果。
-
令animatorInstance为使用«options、state»作为参数构造animatorCtor的结果。如果抛出异常,重新抛出异常并终止所有这些步骤。
-
在animatorInstance上设置以下内容:
-
将animatorInstance添加到workletGlobalScope的动画师实例集。
4.2. 运行动画师
当用户代理希望生成一个新的动画帧时,如果任何动画师实例的 关联动画请求标志为帧请求,则用户代理必须为当前帧运行动画师。
注意:用户代理不需要在每个视觉帧上运行动画。可以推迟到后续帧生成动画帧。这允许用户代理根据其策略提供不同的服务级别。
当用户代理想要在给定的workletGlobalScope中运行动画器时,它必须遍历workletGlobalScope中的所有动画器实例,并迭代workletGlobalScope的动画器实例集。对于每个这样的实例,用户代理必须执行以下步骤:
-
令animatorName为instance的动画师名称
-
令definition为在workletGlobalScope的动画师名称到动画师定义映射中查找animatorName的结果。
如果definition不存在,则中止以下步骤。
-
如果instance的动画请求标志为当前帧, 或者instance所属的效果不会在当前帧的可视视口内可见,用户代理可以中止所有后续步骤。
-
令animateFunction为definition的动画函数。
-
令currentTime为instance的动画师当前时间。
-
令effect为instance的动画师效果。
-
调用animateFunction,参数为«currentTime,effect», 并以instance作为回调的 this 值。
需要明确说明如果 animateFunction 抛出异常会发生什么。至少需要说明关键帧的 localTime 值会被忽略,以避免不正确的部分更新。
4.3. 移除动画师实例
为给定的instance和workletGlobalScope移除一个动画师实例,用户代理必须执行以下步骤:
-
将instance从workletGlobalScope的动画师实例集中移除。
4.4. 迁移动画师实例
迁移过程允许有状态动画师实例迁移到不同的
WorkletGlobalScope
而不丢失其本地状态。
要将一个动画师实例迁移从一个WorkletGlobalScope
迁移到另一个,给定instance、sourceWorkletGlobalScope和
destinationWorkletGlobalScope,用户代理必须执行以下步骤:
-
令serializedState未定义。
-
在sourceWorkletGlobalScope上排队一个任务,执行以下步骤:
-
令animatorName为instance的动画师名称
-
令definition为在sourceWorkletGlobalScope的动画师名称到动画师定义映射中查找animatorName的结果。
如果definition不存在,则中止以下步骤。
-
令stateful为definition的有状态标志。
-
如果stateful为false,则中止以下步骤。
-
令stateFunction为instance的状态函数。
-
令state为调用stateFunction的结果,调用时,instance作为回调的 this 值。如果抛出任何异常,请重新抛出异常并中止 以下步骤。
-
将serializedState设置为结构化序列化(state)的结果。 如果抛出任何异常,则中止以下步骤。
-
执行移除动画师实例的过程,给定 instance和sourceWorkletGlobalScope。
-
-
等待上述任务完成。如果任务中止,停止以下步骤。
如果动画师状态 getter 抛出异常,用户代理将移除动画师,但不会重新创建它。 这实际上移除了动画师实例。
4.5. 请求动画帧
每个动画师实例 关联一个动画请求标志。它必须是 帧请求或当前帧。它最初设置为当前帧。不同的情况可能会导致动画请求标志被 设置为帧请求。 包括以下情况:
-
动画师对应的当前时间的变化Worklet 动画
§4.2 运行动画师将动画师上的动画请求标志重置为 当前帧。
5. Web 动画集成
5.1. Worklet 动画
Worklet 动画是一种 动画,它将动画播放委托给 一个动画师实例。它控制其对应的动画师实例的生命周期和播放状态。作为一个动画,worklet 动画具有动画效果和 时间线。但是,不同于其他动画,worklet 动画的 当前时间不会直接决定动画效果的 本地时间 (通过其继承时间)。相反,关联的 动画师实例直接控制动画效果的 本地时间。请注意,这意味着时间线的当前时间不能完全决定 动画的输出。
Worklet 动画
除了Animation
接口外,还具有以下属性:
-
一个动画师名称,用于标识其 动画师定义。
-
一个序列化选项,这是一个可序列化对象, 用于构建新的动画师实例。
动画的当前时间输入到动画师实例中,生成动画效果的本地时间值。如果动画师实例在并行全局作用域中运行,实现还可以选择使用本地时间值生成最终效果值,并并行更新视觉效果。
5.2. 创建 Worklet 动画
[Exposed =Window ,Constructor (DOMString ,
animatorName optional (AnimationEffect or sequence <AnimationEffect >)?=
effects null ,optional AnimationTimeline ?,
timeline optional any )]
options interface :
WorkletAnimation Animation {readonly attribute DOMString ; };
animatorName
WorkletAnimation(animatorName, effects, timeline, options)
使用以下过程创建一个新的WorkletAnimation
对象。
-
令workletAnimation为一个新的
WorkletAnimation
对象。 -
运行设置动画时间线的过程,传递timeline作为新的时间线,或者,如果未提供timeline参数, 则传递与Window关联的 默认文档时间线。
-
根据以下条件匹配第一个结果,令effect为对应的结果。
- 如果effects是一个
AnimationEffect
对象, -
令effect为effects。
- 如果effects是一个
AnimationEffect
对象的列表, -
令effect为一个新的
WorkletGroupEffect
,其子级设置为effects。 - 否则,
-
令effect为未定义。
- 如果effects是一个
-
运行设置动画目标效果的过程,传递effect作为新的效果。
-
令serializedOptions为结构化序列化(options)的结果。 重新抛出任何异常。
-
将workletAnimation的序列化选项设置为serializedOptions。
-
将workletAnimation的动画师名称设置为animatorName。
5.3. Worklet 动画时间模型
本节描述了worklet 动画的时间模型与其他 动画的不同之处。
除了现有条件下何时将动画视为 就绪, worklet 动画 只有在以下条件也为真时,才会被视为就绪:
-
用户代理已完成创建worklet 动画对应的 动画师实例 所需的任何设置。
如§5.1 Worklet 动画中所述,
worklet 动画的
当前时间不会决定其
动画效果的
本地时间。相反,关联的
动画师实例
直接控制动画效果的本地时间。这意味着动画效果的本地时间由可能在并行执行上下文中的
WorkletGlobalScope
控制。
以下是上述语义的几个含义:
-
设置当前时间或开始时间的 worklet 动画 不一定会更改其输出,但可能会更改动画的播放状态。
-
同样,调用
finish()
或更新worklet 动画的 播放速率只会更改动画的 播放状态,可能不会更改输出。 -
使用
getComputedTiming()
查询动画效果的本地时间时,可能会返回陈旧的信息,如果 动画师实例 正在并行执行上下文中运行。
如果 Worklet 动画在并行 worklet 执行上下文中执行,则应定期将其动画效果的最新状态同步回主 JavaScript 执行上下文。从并行 worklet 执行上下文到主 JavaScript 执行上下文的 效果值同步必须在文档生命周期中 运行动画帧回调之前进行。 请注意,由于此动画模型的异步性质,当主 JavaScript 执行上下文中的脚本读取由 Worklet 动画 动画的目标属性时,可能会看到陈旧的值, 而与当前用于生成用户可见的视觉帧的值不同。这类似于异步滚动时读取主 JavaScript 执行上下文中的滚动偏移量的效果。
为动画师实例在其 动画 currentTime 发生变化时(例如,通过 reverse()、finish() 或 playbackRate 更改)提供适当的机制进行通知。 这样它可以做出适当的反应。 <https://github.com/w3c/css-houdini-drafts/issues/811>
5.4. 与动画师实例的交互
一个worklet 动画在任何时间最多对应一个动画师实例,并且可能没有当前对应的动画师实例。对于worklet 动画,其播放状态决定了对应的动画师实例。
为了给定workletAnimation的关联动画师实例, 用户代理必须执行以下步骤:
-
如果workletAnimation已经有一个对应的动画师实例,则中止后续步骤。
-
令workletGlobalScope为与workletAnimation关联的
AnimationWorkletGlobalScope
。 -
如果该过程成功,则将生成的动画师实例设置为与workletAnimation对应。
为了给定workletAnimation的取消关联动画师实例, 用户代理必须执行以下步骤:
当给定的workletAnimation的播放状态变为 待处理、运行或暂停时, 执行关联动画师实例给定workletAnimation的过程。
当给定的workletAnimation的播放状态变为 空闲或已完成时, 执行取消关联动画师实例给定workletAnimation的过程。
当为给定的workletAnimation调用设置动画的目标效果的过程时,执行设置动画师实例给定workletAnimation的过程。
当为给定的workletAnimation调用设置动画的时间线的过程时,执行设置动画师实例给定workletAnimation的过程。
5.5. 滚动时间线
本节非规范性。ScrollTimeline
是一个提议添加到Web动画API的新概念。它定义了一个动画时间线,其时间值取决于滚动容器的滚动位置。Worklet动画
可以有一个滚动时间线,从而根据滚动偏移驱动它们的脚本效果。
注意: 访问输入:我们有兴趣暴露除滚动以外的其他用户输入(例如,触摸/指针输入)给这些动画,以便作者可以创建无卡顿的输入驱动动画,而这些动画在当今并不太可能实现。我们仍在努力找出实现这一目标的正确抽象和机制。
5.6. WorkletGroupEffect
WorkletGroupEffect
是一种组效果,它允许其子效果的本地时间单独变更。
当一个WorkletGroupEffect
被设置为动画效果,并与Worklet动画
关联时,
对应的动画师实例可以直接控制子效果的本地时间。这允许单个worklet动画协调多个效果——参见§8.2
示例2:视差背景。中的一个使用场景示例。
[Exposed =AnimationWorklet ]interface {
WorkletGroupEffect sequence <AnimationEffect >(); }; [
getChildren Exposed =AnimationWorklet ]partial interface AnimationEffect { // 用于Animation Worklet范围内驱动效果。attribute double ; };
localTime
为了将t值设置为effect的localTime
属性,
用户代理应执行以下第一个匹配条件对应的操作:
- 如果effect没有父组,
-
将effect的本地时间设置为t。
- 如果effect有父组,且父组类型为
WorkletGroupEffect
, -
将效果开始时间设置为(父级的变换时间- t)。注意这实际上设置了effect的本地时间为t。
- 否则
-
抛出一个异常,指出子效果时间只能由其父组控制。
上述接口暴露了作为web-animation-2一部分提议的组效果的保守子集。我们应该将其移到对web动画的增量规范中。<https://github.com/w3c/csswg-drafts/issues/2071>
5.7. 效果堆栈和组合顺序
与其他动画一样,worklet动画参与了效果堆栈。worklet动画没有特定的动画类,这意味着它与其他由JavaScript创建的Web动画具有相同的组合顺序。
6. 安全性考虑
这些特性没有已知的安全问题。
7. 隐私考虑
这些特性没有已知的隐私问题。
8. 示例
8.1. 示例1:Twitter 头部。
一个展示Twitter个人资料头部效果的示例,其中两个元素(头像和头部)根据滚动偏移同步更新。// 在文档范围内。< div id = 'scrollingContainer' > < div id = 'header' style = 'height: 150px' ></ div > < div id = 'avatar' >< img ></ div > </ div > < script > await CSS. animationWorklet. addModule( 'twitter-header-animator.js' ); const animation= new WorkletAnimation( 'twitter-header' , [ new KeyframeEffect( $avatar, /* 向上滚动时缩放 */ [{ transform: 'scale(1)' }, { transform: 'scale(0.5)' }], { duration: 1000 , iterations: 1 }), new KeyframeEffect( $header, /* 向上滚动时透明度降低 */ [{ opacity: 0 }, { opacity: 0.8 }], { duration: 1000 , iterations: 1 })], new ScrollTimeline({ scrollSource: $scrollingContainer, orientation: 'block' , timeRange: 1000 , startScrollOffset: 0 , endScrollOffset: $header. clientHeight})); animation. play(); // 由于此动画使用的是组效果,同一动画实例可以通过不同的句柄访问:$avatarEl.getAnimations()[0], $headerEl.getAnimations()[0] </ script >
// 在AnimationWorkletGlobalScope内。 registerAnimator( 'twitter-header' , class HeaderAnimatorextends StatelessAnimator{ constructor( options) { this . timing_= new CubicBezier( 'ease-out' ); } animate( currentTime, effect) { const scroll= currentTime; // 滚动值在[0, 1000]范围内 // 通过单独设置其子效果的本地时间来驱动输出组效果。 effect. children[ 0 ]. localTime= scroll; effect. children[ 1 ]. localTime= this . timing_( clamp( scroll, 0 , 500 )); } }); function clamp( value, min, max) { return Math. min( Math. max( value, min), max); }
8.2. 示例2:视差背景。
一个简单的视差背景示例。< style > . parallax { position : fixed ; top : 0 ; left : 0 ; opacity : 0.5 ; } </ style > < div id = 'scrollingContainer' > < div id = "slow" class = "parallax" ></ div > < div id = "fast" class = "parallax" ></ div > </ div > < script > await CSS. animationWorklet. addModule( 'parallax-animator.js' ); const scrollTimeline= new ScrollTimeline({ scrollSource: $scrollingContainer, orientation: 'block' , timeRange: 1000 }); const scrollRange= $scrollingContainer. scrollHeight- $scrollingContainer. clientHeight; const slowParallax= new WorkletAnimation( 'parallax' , new KeyframeEffect( $parallax_slow, [{ 'transform' : 'translateY(0)' }, { 'transform' : 'translateY(' + - scrollRange+ 'px)' }], { duration: 1000 }), scrollTimeline, { rate: 0.4 } ); slowParallax. play(); const fastParallax= new WorkletAnimation( 'parallax' , new KeyframeEffect( $parallax_fast, [{ 'transform' : 'translateY(0)' }, { 'transform' : 'translateY(' + - scrollRange+ 'px)' }], { duration: 1000 }), scrollTimeline, { rate: 0.8 } ); fastParallax. play(); </ script >
// 在 AnimationWorkletGlobalScope 内。 registerAnimator( 'parallax' , class ParallaxAnimatorextends StatelessAnimator{ constructor( options) { this . rate_= options. rate; } animate( currentTime, effect) { effect. localTime= currentTime* this . rate_; } });