设计 Timeline 时间轴来更精确地控制动画

Firefox 偷偷实现了一个 AnimationTimeline,用来为动画提供时间轴。根据文档,它是一个抽象类,被 DocumentTimeline 继承。git

因为是非标准的特性,MDN 的文档里面也没有解释的很清楚,只是说它用来让多个动画共享时间轴,可是具体该怎么用,并无详细的说明。github

今天在这篇文章里,我并不想解释 Firefox 实现的这个 Timeline 该怎么用,而是借着这个 Timeline 的概念进行一些扩展,实现了一个全新的 Timeline 库。让咱们看看若是为动画或者其余依赖于时间的行为设计一个 Timeline,咱们能作什么。markdown

在这里,要说明动画和 Timeline 的关系,我先给你们看一个直观的例子:oop

例 1 - Timeline 与动画动画

在一个场景里有多个动画同时播放,若是我如今想要让全部的动画所有暂停,该怎么办?spa

若是咱们拿到全部的动画实例一个一个暂停,那样固然也是能够的,可是不方便。若是我还要支持快进、慢进又怎么办?总之处理起来会很麻烦。这个时候,咱们的 Timeline 的做用就体现出来了。设计

Timeline,能够想象成虚拟世界里的时间线,咱们将世界分解成许多个相互叠加的平行宇宙,每一个宇宙有本身独立的时间线,一个宇宙里的一切行为都基于当前宇宙的时间线。code

对于上面的动画来讲,它们共享一个独立的时间线,当咱们须要让动画速度改变时,直接改变 timeline 的 playbackRate,控制时间的流逝速度便可。orm

如何作到?

举一些更简单的例子:继承

首先看不使用 Timeline 的一个简单的圆周运动动画:

例 2 - 不使用 Timeline

let startTime = Date.now(), T = 2000

requestAnimationFrame(function update(){
  let p = (Date.now() - startTime) / T

  ball.style.transform = `rotate(${360 * p}deg)`
  requestAnimationFrame(update)
})


复制代码

上面这个例子很简单,就是计算小球转过的角度,而后绘制成圆周运动动画。可是若是咱们想要在不修改小球运动参数的状况下让小球动画加快一倍或者减慢为原先的一半速度,该怎么办呢?咱们把小球运动想象成一个电影,咱们但愿修改播放器的播放速度,并不改变电影里的实际时间。在这时候咱们就须要引入时间轴啦:

例 3 - 原速

let timeline = new Timeline()
let startTime = timeline.currentTime, T = 2000

requestAnimationFrame(function update(){
  let p = (timeline.currentTime - startTime) / T

  ball.style.transform = `rotate(${360 * p}deg)`
  requestAnimationFrame(update)
})


复制代码

上面的例 3 和以前例 2 很是类似,咱们只是把 Date.now() 给换成了 timeline.currentTime,也就是用咱们的 Timeline 取代了系统默认的时间。咱们这么作了以后,能够经过调整 timeline 的参数 playbackRate 来加速或者减速动画!

例 4 - 2 倍速度

let timeline = new Timeline({playbackRate: 2.0})
let startTime = timeline.currentTime, T = 2000

requestAnimationFrame(function update(){
  let p = (timeline.currentTime - startTime) / T

  ball.style.transform = `rotate(${360 * p}deg)`
  requestAnimationFrame(update)
})


复制代码

例 5 - 1/2 速度

let timeline = new Timeline({playbackRate: .5})
let startTime = timeline.currentTime, T = 2000

requestAnimationFrame(function update(){
  let p = (timeline.currentTime - startTime) / T

  ball.style.transform = `rotate(${360 * p}deg)`
  requestAnimationFrame(update)
})


复制代码

例 6 - 2 倍速倒放

let timeline = new Timeline({playbackRate: -2.0})
let startTime = timeline.currentTime, T = 2000

requestAnimationFrame(function update(){
  let p = (timeline.currentTime - startTime) / T

  ball.style.transform = `rotate(${360 * p}deg)`
  requestAnimationFrame(update)
})


复制代码

时间轴与 Timer

上面的例子能够看出,Timeline 所作的事情只不过是根据 playbackRate 独立计算 currentTime,这样咱们全部须要获取时间的地方直接用 timeline.currentTime 取代 Date.now() 便可。不过为了使用方便,咱们的 Timeline 还提供了本身的 timer:

例 7 - 毫秒变秒

let timeline = new Timeline({playbackRate: 0.001})

timeline.setInterval(() => {
  ball.innerHTML = Math.round(timeline.currentTime)
}, 1)


复制代码

timeline 提供 setInterval、setTimeout、clearInterval、clearTimeout 四个方法,分别对应 window 的四个相应方法,只不过期间流逝是按照 timeline 的 playbackRate 来的。

currentTime 与 entropy

由于 Timeline 的 playbackRate 是动态的,因此它的 currentTime 也是动态的,结果就是会影响到它的 timer,例如:

例 8 - 时间倒流?

let timeline = new Timeline({originTime: -100, playbackRate: -0.001})

timeline.setInterval(() => {
  ball.innerHTML = Math.round(timeline.currentTime)
}, 1)


复制代码

这个例子咱们让时间倒流,数字每一秒钟减少,看似没有问题,可是,换一种方式看看:

例 9 - 时间倒流的 bug

let timeline = new Timeline({originTime: -100, playbackRate: -0.001})

let count = 100;
timeline.setInterval(() => {
  ball.innerHTML = count--
}, 1)


复制代码

咱们发现定时器其实并无如咱们所指望的那样每一秒钟执行一次。这是由于咱们把 playbackRate 设置为负数,改变了时间箭头的方向。也就是说历史和将来颠倒了,因此 setInterval 并无在 1 秒以后触发,而是当即触发,由于对于 timer 来讲,“将来” 是负时间,而 “1 秒以后” 已是过去了!

咱们作一下修改:

例 10 - 负向 timer

let timeline = new Timeline({originTime: -100, playbackRate: -0.001})

let count = 100;
timeline.setInterval(() => {
  ball.innerHTML = count--
}, -1)


复制代码

因此 playbackRate 若是为负数,那么 timer 的时间也得相应设置为负数。这个很麻烦,容易出错。并且有时候咱们不能保证 timer 必定被触发,好比咱们周期性改变 playbackRate 方向,颇有可能限制时间在一个范围内,那么 timer 可能永远也不会被触发。

有时候咱们须要明确让 timer 在 timeline 等待某个时间以后触发,而无论时间箭头是向前仍是向后,那么咱们就可使用 entropy 这个属性。

entropy 是熵的意思,无论 playbackRate 是正仍是负,entropy 只能增长不能减小。不过 entropy 一样会受到 playbackRate 影响。也就是说 entropy 只和 playbackRate 的绝对值有关,和它的符号无关

因此咱们也能够这么写:

例 11 - 熵与 timer

let timeline = new Timeline({originTime: -100, playbackRate: -0.001})

let count = 100;
timeline.setInterval(() => {
  ball.innerHTML = count--
}, {entropy: 1})


复制代码

entropy 在动态改变 playbackRate 的场景颇有用,它提供了一个单向的时间衡量指标,方便咱们控制动画的速度和流向,例如:

例 12 - 熵控制动画

const T = 2000
let timeline = new Timeline()

timeline.setInterval(function update() {
  ball.innerHTML = Math.round(timeline.currentTime / 100)
  if(timeline.playbackRate < 0){
    ball.style.backgroundColor = 'green'
  } else {
    ball.style.backgroundColor = 'red'
  }
}, {entropy: 100})

speedUp.onclick = function(){
  if(timeline) timeline.playbackRate += 0.2
  rate.innerHTML = timeline.playbackRate.toFixed(1)
}

slowDown.onclick = function(){
  if(timeline) timeline.playbackRate -= 0.2
  rate.innerHTML = timeline.playbackRate.toFixed(1)
}

reverse.onclick = function(){
  if(timeline) timeline.playbackRate = -timeline.playbackRate
  rate.innerHTML = timeline.playbackRate.toFixed(1)
}

pause.onclick = function(){
  if(timeline) timeline.playbackRate = 0
  rate.innerHTML = timeline.playbackRate.toFixed(1)
}


复制代码

时间轴的继承 —— fork

有意思的是,咱们还能够根据当前时间轴建立出相对于当前时间轴的新时间轴,这样的话,咱们能够经过控制父级时间轴来影响全部 fork 出来的子时间轴,也能够控制单个时间轴,这就提供了极大的灵活性。

例 13 - timelien fork

let timeline = new Timeline()

function count(el, timeline, p = Infinity) {
  timeline.setInterval(() => {
    el.innerHTML = Math.round(timeline.currentTime / 1000) % p
  },  {entropy: 1000})
}

count(ball0, timeline)
count(ball1, timeline.fork({playbackRate: 10}), 10)
count(ball2, timeline.fork({playbackRate: 100}), 10)


复制代码

总结

Timeline 是一个能够大大加强对动画控制的辅助类,经过控制动画的时间流速和方向来改变更画进程。要使用功能强大的 Timeline,能够从 GitHub repo 下载。

有任何问题,欢迎讨论~~

相关文章
相关标签/搜索