anjia / blog

博客,积累与沉淀
107 stars 4 forks source link

CSS Animation Worklet #24

Open anjia opened 5 years ago

anjia commented 5 years ago

简介

CSS Animation Worklet API 是 CSS Houdini 的一部分。关于 Houdini 的介绍,可查看 #23。

这个 API 扩展了 Web 动画堆栈。具体来说:

举几个例子,大家来感受下什么是 有状态 的动画。

比如 Chrome 的顶部地址栏(点此链接查看动画效果),它的显隐不仅取决于滚动位置,还取决于滚动方向。i.e. 当上拉页面向下滚动时,它就隐藏了;当下拉页面向上滚动时,它又回来了,且不管是否有没有滚动到页面的顶部。

比如视差滚动,目前在 Web 上实现不太容易,详见 Performant Parallaxing

比如自定义滚动条样式,让猫作为滚动条,要么需要我们自己监听滚动事件,然后还要确保动画流畅又不耗性能;要么实现起来不容易

有了 Animation Worklet API,我们就可以非常直接且简单地控制此类动画效果了。

https://developers.google.com/web/updates/2018/10/animation-worklet

anjia commented 5 years ago

以下代码均需在 Chrome Canary 中打开,并在 chrome://flags 里开启 Experimental Web Platform features。记得开启后,重启下浏览器

首先,我们看下 Animation Worklet 是怎么扩展 timelines 的。来看个例子

控制动画的时间线

最终的效果是:

源码见 src/css-animation-worklet/demo1.helloworld

在 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赋给了effect.localTime,这样动画就动起来了。

anjia commented 5 years ago

接下来,我们继续改造代码,来看看在 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;
  }
});

完整代码见 src/css-animation-worklet/demo2

anjia commented 5 years ago

有状态的动画

之前我们有提到,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 的实现上。所以,咱们这里只重点介绍思路。

anjia commented 5 years ago

上面介绍的动画都是 time-driven 的,下面我们来看个 scroll-driven 的动画实例。

滚动驱动的动画

它的使用非常简单:

具体代码如下:

在 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,当滚动到底部(或右侧)时,currentTimetimeRange

在 my_aw.js 里

registerAnimator('scrollDriven', class {
  animate(currentTime = 0, effect) {
    effect.localTime = currentTime;
  }
});

最终的效果是:当滚动文本框时,红色的色块也会跟着动。如下:

完整代码见 src/css-animation-worklet/demo3.scroll