用 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/