blog
blog copied to clipboard
CSS Animation Worklet
简介
CSS Animation Worklet API 是 CSS Houdini 的一部分。关于 Houdini 的介绍,可查看 #23。
这个 API 扩展了 Web 动画堆栈。具体来说:
- 它扩展了 timelines,让网页开发人员能编排动画效果
- 当然,这点没有什么特别惊艳的,因为我们已经有类似的技术了
- 比如 CSS transition、CSS animation、requestAnimationFrame、Web Animations API
- 这类动画都是 stateless(无状态的)和 time-driven(时间驱动的)
- 它可以控制 stateful(有状态) 的动画效果了。这是驱动动画的一种新方式
- eg. scroll-driven 滚动驱动的动画
- eg. 还有其它形式的,还在赶来的路上... 敬请期待
举几个例子,大家来感受下什么是 有状态 的动画。
比如 Chrome 的顶部地址栏(点此链接查看动画效果),它的显隐不仅取决于滚动位置,还取决于滚动方向。i.e. 当上拉页面向下滚动时,它就隐藏了;当下拉页面向上滚动时,它又回来了,且不管是否有没有滚动到页面的顶部。
比如视差滚动,目前在 Web 上实现不太容易,详见 Performant Parallaxing。
比如自定义滚动条样式,让猫作为滚动条,要么需要我们自己监听滚动事件,然后还要确保动画流畅又不耗性能;要么实现起来不容易。
有了 Animation Worklet API,我们就可以非常直接且简单地控制此类动画效果了。
https://developers.google.com/web/updates/2018/10/animation-worklet
以下代码均需在 Chrome Canary 中打开,并在 chrome://flags 里开启 Experimental Web Platform features。记得开启后,重启下浏览器
首先,我们看下 Animation Worklet 是怎么扩展 timelines 的。来看个例子
控制动画的时间线
最终的效果是:
在 index.html 里
<div id="demo1"></div>
<script>
if('animationWorklet' in CSS) {
async function init() {
await CSS.animationWorklet.addModule('my_aw.js'); // 加载 Animation Worklet
new WorkletAnimation(
'hellworld', // aw的名字,在my_aw.js里定义的
new KeyframeEffect(
document.querySelector('#demo1'),
[
{
transform: 'translateX(0)'
},
{
transform: 'translateX(500px)'
}
],
{
delay: 2000,
duration: 5000,
iterations: Number.POSITIVE_INFINITY
}
),
document.timeline
).play();
}
init();
}else{
console.warn('您的浏览器暂不支持 Animation Worklet');
}
</script>
每个文档都有个document.timeline
,从文档初始化时开始,它从0计时,存储文档存在的毫秒数。文档的所有动画,都和这个 timeline 有关。
当调用animation.play()
时,动画就用 timeline 的currentTime
值作为它的开始时间startTime
。在上面的代码里,我们设置了delay: 2000
,这意味着当 timeline 到startTime+2000ms
时该动画就开始执行。之后,引擎会让指定的元素在规定的时间duration: 5000
内,按照代码里给定的关键帧序列,从第一个关键帧执行到最后一个。这样,当 timeline 到startTime+2000ms+5000ms
时,动画就恰好执行到了最后一个关键帧。然后再跳到第一个关键帧,开始动画的下一个迭代,如此往复,因为我们设置了iterations: Number.POSITIVE_INFINITY
。在此期间,timeline 控制着我们的整个动画过程。
在 my_aw.js 里
// 定义了一个名字是 hellworld 的 Animation Worklet
registerAnimator('hellworld', class {
animate(currentTime, effect) {
effect.localTime = currentTime;
}
});
animate()
函数,浏览器在渲染每一帧时,就会调用它。
- 参数
currentTime
,是动画 timeline 的当前时间 - 参数
effect
,是当前正在处理的效果
在函数体内,我们只是简单的将currentTime
赋给了effect.localTime
,这样动画就动起来了。
接下来,我们继续改造代码,来看看在 Animation Worklet 里还能做什么。
自定义时间线
在上面的示例中,我们只是通过effect.localTime = currentTime
让动画线性地动起来了。当然,我们也可以自定义任意时间线。比如:
animate(currentTime, effect) {
let minIn = -1, maxIn = 1,
minOut = 0, maxOut = 3000; // 映射到时间范围[minOut, maxOut]
let v = Math.sin(currentTime * 2 * Math.PI / maxOut); // Math.sin()
effect.localTime = (v - minIn)/(maxIn - minIn) * (maxOut - minOut) + minOut;
}
运行的效果是:
传参数
Animation Worklet 也支持传参数。具体代码如下:
在 index.html 里
new WorkletAnimation(
'sin',
new KeyframeEffect(...),
document.timeline,
{ maxOut: 5000 } // 1.传参
).play();
在 my_aw.js 里
registerAnimator('sin', class {
// 2.接收参数 options
constructor(options = {}) {
this.maxOut = options.maxOut || 3000;
}
animate(currentTime, effect) {
let minIn = -1, maxIn = 1,
minOut = 0,
maxOut = this.maxOut; // this.maxOut
let v = Math.sin(currentTime * 2 * Math.PI / maxOut);
effect.localTime = (v - minIn)/(maxIn - minIn) * (maxOut - minOut) + minOut;
}
});
有状态的动画
之前我们有提到,Animation Worklet(简称 AW)想要解决的关键问题之一就是有状态的动画。也就是说它要能保持住状态。
但是,Worklet 的核心功能之一是它们可以迁移到不同的线程,甚至可以被销毁以节省资源,这样就会破坏 AW 的状态。
为了防止状态丢失,AW 提供了一个钩子,在 Worklet 被销毁之前调用,在那儿可以返回状态对象。等下次再重新创建时,AW 的构造函数会接收到这个状态对象(初始创建时,值是 undefined)。
具体代码如下:
registerAnimator('randomspin', class {
// 2. 接收状态参数,第二个参数 state
constructor(options = {}, state = {}) {
this.direction = state.direction || (Math.random() > 0.5 ? 1 : -1);
}
animate(currentTime, effect) {
effect.localTime = 2000 + this.direction * (currentTime % 2000); // this.direction
}
// 1. 钩子函数 destroy(),返回想要保存的状态信息
destroy() {
return {
direction: this.direction
};
}
});
最终的效果及代码见 animation-worklet-state。每次刷新页面时,动画的方向都是重新生成的。为了让再次刷新页面之后动画的运动方向不变,我们保存了this.direction
状态。
注意,生命周期的钩子函数
destory()
已经被 getter 方法取代了,但这种变化还没有反映在规范里或者 Chrome 的实现上。所以,咱们这里只重点介绍思路。
上面介绍的动画都是 time-driven 的,下面我们来看个 scroll-driven 的动画实例。
滚动驱动的动画
它的使用非常简单:
- 使用时,把之前的
document.timeline
换成new ScrollTimeline()
- 注册时,直接
effect.localTime = currentTime
具体代码如下:
在 index.html 里
new WorkletAnimation(
'scrollDriven',
new KeyframeEffect(...),
// document.timeline, // 不要它了,换成 ScrollTimeline
new ScrollTimeline({
scrollSource: document.querySelector('#scroll-area'),
orientation: "vertical", // "horizontal" or "vertical".
timeRange: 3000
})
).play();
ScrollTimeline
用的不是 time,而是 scrollSource 滚动位置,来设置 AW 里的currentTime
。当滚动到顶部(或左侧)时,currentTime
是0,当滚动到底部(或右侧)时,currentTime
是timeRange
。
在 my_aw.js 里
registerAnimator('scrollDriven', class {
animate(currentTime = 0, effect) {
effect.localTime = currentTime;
}
});
最终的效果是:当滚动文本框时,红色的色块也会跟着动。如下: