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

JenniferXFO 7年前發布 | 13K 次閱讀 CSS3 Android開發 移動開發 JavaScript

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

基于 Promise 的動畫庫

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

JS Bin on jsbin.com

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

JS Bin on jsbin.com

有了 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 接口的方法 ,實現了動畫序列。它的實現雖然簡單,但功能卻是很強大的,用它實現的動畫代碼也很優雅:

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 動畫的主要實現方式。

 

來自:http://web.jobbole.com/90660/

 

 本文由用戶 JenniferXFO 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!