設計 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 下載。

有任何問題,歡迎討論~~

相關文章
相關標籤/搜索