用 65 行程式碼實現 JS 動畫序列播放

發表於2017-03-07

最近在給學生上課,上週六的第一堂課是關於 JavaScript 動畫的內容,其中包括一些簡單的動畫,比如勻速或者勻加/減速的運動,也包括複雜一些的組合動畫。而動畫的基本原理,在我之前的文章已經有了詳細的介紹。在這裡,我想談一談的是,我們可以如何針對現代瀏覽器設計更加簡單的 API,來實現動畫的序列播放。

基於 Promise 的動畫庫

所謂的動畫序列,也就是說可以在上一段動畫播放結束之後進行下一段動畫的播放,這樣可以方便用多段動畫實現各種不同的複雜效果。而我們不難想到,要實現這個目的,將動畫介面實現成 Promise 是一個非常好的方案:

JS Bin on jsbin.com

上面這個例子,在支援 async/await 的現代瀏覽器中程式碼非常簡潔和優雅。如果要相容舊的瀏覽器,也並不複雜,只需要針對 es6-promise 做 polyfill 或引入第三方庫即可。再來看一個例子:

JS Bin on jsbin.com

有了 Promise,像這樣的序列運動非常簡單。那麼要實現這個動畫庫,具體該怎麼做呢?

具體實現

其實整個庫實現起來並不複雜,只需要將基礎動畫封裝為 Promise 就可以了。

不過在這裡,為了相容老版本的瀏覽器,我們先對一些基礎函式進行封裝:

我們說動畫是關於時間的函式,因此我們需要一個簡單的獲取時間功能。在新的 requestAnimationFrame 規範中,frame 回撥的引數 timestamp 是一個DOMHighResTimeStamp 物件,它比 Date 的計時要更精確(可以精確到納秒)。因此獲取時間我們優先使用 performance.now(),如果瀏覽器不支援 performance.now(),我們再降級使用 Date.now()。

接下來,我們對 requestAnimationFrame 進行 polyfill:

然後,是具體的 Animator 實現:

Animator 構造的時候可以傳三個引數,第一個是動畫的總時長,第二個是動畫每一幀的 update 事件,在這裡可以改變元素的屬性,從而實現動畫。update 事件回撥提供兩個引數,第一個是 ep,是經過 easing 之後的動畫程式,第二個是 p,是不經過 easing 的動畫程式,ep 和 p 的值都是從 0 開始,到 1 結束。(為什麼要使用 ep 和 p,在前一個動畫教程裡已經說明了。)

Animator 有一個 animate 的物件方法,它返回一個 promise,當動畫播放完成時,它的 promise 被 resolve,使用者還可以在 promise resolve 前呼叫 cancel 方法,這樣它的 promise 會被 reject。

於是這樣,很簡單地我們就通過將 animator 封裝為帶有返回 Promise 介面的方法,實現了動畫序列。它的實現雖然簡單,但功能卻是很強大的,用它實現的動畫程式碼也很優雅:

JS Bin on jsbin.com

我們還提供了一個 ease 方法(0.2.0+版),能夠傳入新的 easing,並返回新的 Animator 物件,這樣我們就可以在原動畫的基礎上擴充套件我們的動畫效果:

JS Bin on jsbin.com

用 CSS3 如何?

的確,許多動畫可以用 CSS3 來實現。不過 JavaScript 動畫與 CSS3 動畫有其不同的特點和使用場景。總體來說, CSS3 動畫適用於任何純展現效果的簡單動畫。雖然它也能提供基本的動畫組合方法(有 animationEnd 時間,但標準化較晚),但操作起來依然不方便,而且還需要 JavaScript 來控制。有些動畫庫用降級的方式,能採用 CSS3 動畫的採用 CSS3 動畫,不能的自動降級為 JavaScript 動畫,這不失為一種好方式,但也有利有弊。因為 CSS3 動畫是繫結為操作元素屬性的,而 JavaScript 更靈活一些。就像我們這個封裝的動畫庫,其實提供的是更底層的 API,操作的只是時間進度,並沒有耦合任何元素、屬性或者其他展示類的東西,因此它完全可以用來操作 DOM、Canvas、SVG、音訊/視訊流甚至是其他非同步動作。另外,如果在動畫過程中需要有其他一些精細的動作處理,也還是應該使用 JavaScript 動畫而不是 CSS3 動畫。

總結

使用 Promise 實現的簡單動畫庫,能夠很好地執行組合的時序動畫,配合 async/await 程式碼實現簡潔且優雅,同時還具有非常好的擴充套件性,能夠組合出非常強大的動畫效果。我相信這將成為未來瀏覽器上 JavaScript 動畫的主要實現方式。

最後,可以訪問 GitHub repo 獲取最新程式碼。

相關文章