用 65 行代碼實現 JS 動畫序列播放
最近在給學生上課,上周六的第一堂課是關于 JavaScript 動畫的內容,其中包括一些簡單的動畫,比如勻速或者勻加/減速的運動,也包括復雜一些的組合動畫。而動畫的基本原理,在我之前的文章已經有了詳細的介紹。在這里,我想談一談的是,我們可以如何針對現代瀏覽器設計更加簡單的 API,來實現動畫的序列播放。
基于 Promise 的動畫庫
所謂的動畫序列,也就是說可以在上一段動畫播放結束之后進行下一段動畫的播放,這樣可以方便用多段動畫實現各種不同的復雜效果。而我們不難想到,要實現這個目的,將動畫接口實現成 Promise 是一個非常好的方案:
上面這個例子,在支持 async/await 的現代瀏覽器中代碼非常簡潔和優雅。如果要兼容舊的瀏覽器,也并不復雜,只需要 針對 es6-promise 做 polyfill 或引入 第三方庫 即可。再來看一個例子:
有了 Promise,像這樣的序列運動非常簡單。那么要實現這個動畫庫,具體該怎么做呢?
具體實現
其實整個庫實現起來并不復雜,只需要將基礎動畫封裝為 Promise 就可以了。
不過在這里,為了兼容老版本的瀏覽器,我們先對一些基礎函數進行封裝:
function nowtime(){ if(typeof performance !== 'undefined' && performance.now){ return performance.now(); } return Date.now ? Date.now() : (new Date()).getTime(); }
我們說 動畫是關于時間的函數 ,因此我們需要一個簡單的獲取時間功能。在新的 requestAnimationFrame 規范 中,frame 回調的參數 timestamp 是一個 DOMHighResTimeStamp 對象,它比 Date 的計時要更精確(可以精確到納秒)。因此獲取時間我們優先使用 performance.now(),如果瀏覽器不支持 performance.now(),我們再降級使用 Date.now()。
接下來,我們對 requestAnimationFrame 進行 polyfill:
if(typeof global.requestAnimationFrame === 'undefined'){ global.requestAnimationFrame = function(callback){ return setTimeout(function(){ //polyfill callback.call(this, nowtime()); }, 1000/60); } global.cancelAnimationFrame = function(qId){ return clearTimeout(qId); } }
然后,是具體的 Animator 實現:
function Animator(duration, update, easing){ this.duration = duration; this.update = update; this.easing = easing; } Animator.prototype = { animate: function(){ var startTime = 0, duration = this.duration, update = this.update, easing = this.easing, self = this; return new Promise(function(resolve, reject){ var qId = 0; function step(timestamp){ startTime = startTime || timestamp; var p = Math.min(1.0, (timestamp - startTime) / duration); update.call(self, easing ? easing(p) : p, p); if(p < 1.0){ qId = requestAnimationFrame(step); }else{ resolve(self); } } self.cancel = function(){ cancelAnimationFrame(qId); update.call(self, 0, 0); reject('User canceled!'); } qId = requestAnimationFrame(step); }); }, ease: function(easing){ return new Animator(this.duration, this.update, easing); } }; module.exports = 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 接口的方法 ,實現了動畫序列。它的實現雖然簡單,但功能卻是很強大的,用它實現的動畫代碼也很優雅:
我們還提供了一個 ease 方法(0.2.0+版),能夠傳入新的 easing,并返回新的 Animator 對象,這樣我們就可以在原動畫的基礎上擴展我們的動畫效果:
用 CSS3 如何?
的確,許多動畫可以用 CSS3 來實現。不過 JavaScript 動畫與 CSS3 動畫有其不同的特點和使用場景。總體來說, CSS3 動畫適用于任何純展現效果的簡單動畫。雖然它也能提供基本的動畫組合方法(有 animationEnd 時間,但標準化較晚),但操作起來依然不方便,而且還需要 JavaScript 來控制。有些動畫庫用降級的方式,能采用 CSS3 動畫的采用 CSS3 動畫,不能的自動降級為 JavaScript 動畫,這不失為一種好方式,但也有利有弊。因為 CSS3 動畫是綁定為操作元素屬性的,而 JavaScript 更靈活一些。就像我們這個封裝的動畫庫,其實提供的是更底層的 API,操作的只是 時間 和 進度 ,并沒有耦合任何元素、屬性或者其他展示類的東西,因此它完全可以用來操作 DOM、Canvas、SVG、音頻/視頻流甚至是其他異步動作。另外,如果在動畫過程中需要有其他一些精細的動作處理,也還是應該使用 JavaScript 動畫而不是 CSS3 動畫。
總結
使用 Promise 實現的簡單動畫庫,能夠很好地執行組合的時序動畫,配合 async/await 代碼實現簡潔且優雅,同時還具有非常好的擴展性,能夠組合出非常強大的動畫效果。我相信這將成為未來瀏覽器上 JavaScript 動畫的主要實現方式。
來自:http://web.jobbole.com/90660/