Dojo動畫原理解析

  dojo中動畫部分分爲兩部分:dojo/_base/fx, dojo/fx。dojo/_base/fx部分是dojo動畫的基石,裏面有兩個底層API:animateProperty、anim和兩個經常使用動畫:fadeIn、fadeOut(相似jQuery中的show、hide)。dojo/fx中有兩個複合動畫:chain(相似jQuery中的動畫隊列)、combine和三個動畫函數:wipeIn、wipeOut、slideTo。html

  dojo中動畫的原理與jquery相似,都是根據setInterval計算當前時間與初試時間差值與總時間的半分比來肯定元素動畫屬性的當前計算值:node

percent = (Date.now() - startTime) / duration;
value = (end - start) * percent + start;

  接下來咱們會一步步的演化,慢慢接近dojo APIjquery

  首先咱們實現一個動畫函數,他接受如下參數:編程

  • node: 動畫元素
  • prop: 動畫屬性
  • start: 動畫起始值
  • end:  動畫結束值
  • duration: 動畫執行時間
  • interval:動畫間隔
function Animate(node, prop, start, end, duration, interval) {
 var startTime = Date.now();
 
 var timer = setInterval(function(){
  var percent = (Date.now() - startTime) / duration;
  percent = percent < 0 ? 0 : percent;
  percent = percent > 1 ? 1 : percent;
  var v = (end - start) * percent + start;
 
  node.style[prop] = v;
 
  if (percent >= 1) {
   clearInterval(timer);
  }
 }, interval);
}
View Code

 示例:數組

  

  dojo中全部的動畫函數都返回一個Animation的實例:Animation擁有一系列的屬性和動畫控制方法,下面咱們簡單的實現play和stop方法:閉包

function Animate(node, prop, start, end, duration, interval/*, delay*/) {
      var timer = null;
      var startTime =0;

      function startTimer() {
        timer = setInterval(function(){
          var percent = (Date.now() - startTime) / duration;
          percent = percent < 0 ? 0 : percent;
          percent = percent > 1 ? 1 : percent;

          var v = (end - start) * percent + start;

          node.style[prop] = isFinite(v) ? v /*+ 'px'*/ : v;

          if (percent >= 1) {
            clearInterval(timer);
            timer = null;
          }
        }, interval);
      }

      function stopTimer() {
        if (timer) {
          clearInterval(timer);
          timer = null;
        }
      }

      return {
        play: function() {
          if (startTime === 0) {
            startTime = Date.now();
          }

          startTimer();
        },
        stop: function() {
          stopTimer();
        }
      }
    }
View Code

  這裏將上文中Animate函數放入startTimer函數中,增長stopTimer用來移除定時器。app

示例:ide

 

  下面咱們要支持延時和暫停功能,實現延遲的方法在於使用setTimeout設置延時啓動時間,對於暫停功能,咱們須要記錄全部的暫停時間,在計算當前百分比時減去全部的暫停時間。函數

function Animate(node, prop, start, end, duration, interval, delay) {
      var timer = null;
      var startTime =0;
      var delayTimer = null;

      var paused = false;
      var pauseStartTime = null;
      var pausedTime = 0;//記錄全部的暫停時間

      function startTimer() {
        timer = setInterval(function(){
          var percent = (Date.now() - startTime - pausedTime) / duration;減去暫停消耗的時間
          percent = percent < 0 ? 0 : percent;
          percent = percent > 1 ? 1 : percent;

          var v = (end - start) * percent + start;

          node.style[prop] = isFinite(v) ? v /*+ 'px'*/ : v;

          if (percent >= 1) {
            stopTimer();
          }
        }, interval);
      }

      function stopTimer() {
        if (timer) {
          clearInterval(timer);
          timer = null;
        }
      }

      function clearDelayTimer() {
        clearTimeout(delayTimer);
        delayTimer = null;
      }

      return {
        play: function() {
          if (startTime === 0) {
            startTime = Date.now();
          }

          if (paused) {
            pausedTime += Date.now() - pauseStartTime;計算暫停時間
            startTimer();
            paused = false;
          } else if (isFinite(delay)) {
            delayTimer = setTimeout(function() {
              clearDelayTimer();
              startTime = Date.now();
              startTimer();
            }, delay); //delay延遲啓動
          } else {
            startTimer();
          }
        },
        pause: function() {
          paused = true;
          if (delayTimer) {
            clearDelayTimer();
          } else {
            stopTimer();
            pauseStartTime = Date.now();記錄本次暫停起始時間
          }
        },
        stop: function() {
          stopTimer();
        }
      }
    }
View Code

示例:post

 

  dojo/fx.animateProperty中能夠設置多個動畫屬性,實現方式不難,只須要在每次動畫計算時依次計算各個動畫屬性便可。

function Animate(node, props, duration, interval, delay) {
      var timer = null;
      var startTime =0;
      var delayTimer = null;

      var paused = false;
      var pauseStartTime = null;
      var pausedTime = 0;

      function startTimer() {
        timer = setInterval(function(){
          var percent = (Date.now() - startTime - pausedTime) / duration;
          percent = percent < 0 ? 0 : percent;
          percent = percent > 1 ? 1 : percent;
          for (var p in props) {
            var prop = props[p];
            node.style[p] = ((prop.end - prop.start) * percent + prop.start) + (prop.units ? prop.units : '');
          }

          if (percent >= 1) {
            stopTimer();
          }
        }, interval);
      }

      function stopTimer() {
        if (timer) {
          clearInterval(timer);
          timer = null;
        }
      }

      function clearDelayTimer() {
        clearTimeout(delayTimer);
        delayTimer = null;
      }

      return {
        play: function() {
          if (startTime === 0) {
            startTime = Date.now();
          }

          if (paused) {
            pausedTime += Date.now() - pauseStartTime;
            startTimer();
            paused = false;
          } else if (isFinite(delay)) {
            delayTimer = setTimeout(function() {
              clearDelayTimer();
              startTime = Date.now();
              startTimer();
            }, delay);
          } else {
            startTimer();
          }
        },
        pause: function() {
          paused = true;
          if (delayTimer) {
            clearDelayTimer();
          } else {
            stopTimer();
            pauseStartTime = Date.now();
          }
        },
        stop: function() {
          stopTimer();
        }
      }
    }

    var btnPlay = document.getElementById('btnPlay');
    var n = document.getElementById('anim');
    var anim = Animate(n, {
      opacity: {start: 0.3, end: 1},
      width: {start:50, end: 500, units: 'px'}
    }, 5000, 25, 1000);
    btnPlay.onclick = function() {
      anim.play();
    }

    btnPause = document.getElementById('btnPause');
    btnPause.onclick = function() {
      anim.pause();
    }
View Code

  翻看dojo代碼(dojo/_base/fx line:567)咱們會發現,在進行動畫以前dojo對一些動畫屬性作了預處理:

  • 針對width/height動畫時,元素自己inline狀態的處理
  • 對於Opacity的處理,IE8如下在style中設置濾鏡
  • 對於顏色動畫的處理

 

  下面咱們進行的是事件點的添加,在dojo的實際源碼中,回調事件的實現是經過實例化一個dojo/Evented對象來實現的,dojo/Evented是dojo整個事件驅動編程的基石,凡是擁有回調事件的對象都是它的實例。dojo/Evented的核心是dojo/on和dojo/aspect, 這兩部分的解釋能夠看一下個人這幾篇文章:

  Javascript事件機制兼容性解決方案

  dojo/aspect源碼解析

  Javascript aop(面向切面編程)之around(環繞)

    這裏咱們將事件回調掛載到實例上

 function Animate(node, props, duration, interval, delay, callbacks) {
      var timer = null;
      var startTime =0;
      var delayTimer = null;
      var percent = null;

      var stopped = false;
      var ended = false;

      var paused = false;
      var pauseStartTime = null;
      var pausedTime = 0;

      function startTimer() {
        timer = setInterval(function(){
          if (!percent) {
            callbacks.onBegin ? callbacks.onBegin() : null;
          }

          percent = (Date.now() - startTime - pausedTime) / duration;
          percent = percent < 0 ? 0 : percent;
          percent = percent > 1 ? 1 : percent;
          for (var p in props) {
            var prop = props[p];
            node.style[p] = ((prop.end - prop.start) * percent + prop.start) + (prop.units ? prop.units : '');
          }

          callbacks.onAnimate ? callbacks.onAnimate() : null;

          if (percent >= 1) {
            stopTimer();
            ended = true;
            callbacks.onEnd ? callbacks.onEnd() : null;
          }
        }, interval);
      }

      function stopTimer() {
        if (timer) {
          clearInterval(timer);
          timer = null;
        }
      }

      function clearDelayTimer() {
        clearTimeout(delayTimer);
        delayTimer = null;
      }

      return {
        play: function() {
          if (ended) {
            return;
          }
          if (startTime === 0) {
            startTime = Date.now();

            callbacks.beforeBegin ? callbacks.beforeBegin() : null;
          }

          if (paused) {
            pausedTime += Date.now() - pauseStartTime;
            startTimer();
            paused = false;
          } else if (isFinite(delay)) {
            delayTimer = setTimeout(function() {
              clearDelayTimer();
              startTime = Date.now();
              startTimer();
            }, delay);
          } else {
            startTimer();
          }

          callbacks.onPlay ? callbacks.onPlay() : null;
        },
        pause: function() {
          paused = true;
          if (delayTimer) {
            clearDelayTimer();
          } else {
            stopTimer();
            pauseStartTime = Date.now();
          }

          callbacks.onPause ? callbacks.onPause() : null;
        },
        stop: function() {
          stopTimer();
          stopped = true;
          callbacks.onStop ? callbacks.onStop() : null;
        }
      }
    }
View Code

示例:

 

  dojo/fx中最重要的兩個函數就是chain和combine,chain函數容許咱們一次執行一系列動畫與jQuery中動畫隊列的功能相似。因爲每一個Animation實例都擁有onEnd事件,因此chain函數的實現原理就是在每一個動畫結束後,調用下一個動畫的play函數。要模仿這個功能關鍵是若是在onEnd函數執行後綁定play函數。dojo中使用aspect.after方法,這裏咱們簡單實現:爲Function.prototype添加after方法:

Function.prototype.after = function(fn) {
      var self = this;
      return function() {
        var results = self.apply(this, arguments);
        fn.apply(this, [results]);
      }
    }

  還有一個問題就是,上文中利用閉包的實現方式,全部對象的play方法都共享一套變量,在多個實例時有很大問題,因此從如今開始咱們使用對象方式構造Animate類。

示例:

 

  下一步就是combine,combine容許多個動畫聯動。combine的實現原理比較簡單,依次調用Animation數組中的各對象的方法便可。

Function.prototype.after = function(fn) {
      var self = this;
      return function() {
        var results = self.apply(this, arguments);
        fn.apply(this, [results]);
      }
    }

    function Animate(node, props, duration, interval, delay) {
      this.node = node;
      this.props = props;
      this.duration = duration;
      this.interval = interval;
      this.delay = delay;

      this.timer = null;
      this.startTime = 0;
      this.delayTimer = null;
      this.percent = null;

      this.stopped = false;
      this.ended = false;

      this.paused = false;
      this.pauseStartTime = null;
      this.pausedTime = 0;
    }

    Animate.prototype._startTimer = function() {
      var self = this;
      this.timer = setInterval(function() {
        if (!self.percent) {
          self.onBegin ? self.onBegin() : null;
        }

        var percent = (Date.now() - self.startTime - self.pausedTime) / self.duration;
        percent = percent < 0 ? 0 : percent;
        percent = percent > 1 ? 1 : percent;

        self.percent = percent;

        for (var p in self.props) {
          var prop = self.props[p];
          self.node.style[p] = ((prop.end - prop.start) * percent + prop.start) + (prop.units ? prop.units : '');
        }

        self.onAnimate ? self.onAnimate() : null;

        if (self.percent >= 1) {
          self._stopTimer();
          self.ended = true;
          self.onEnd ? self.onEnd() : null;
        }
      }, this.interval);
    };

    Animate.prototype._stopTimer = function() {
      if (this.timer) {
        clearInterval(this.timer);
        this.timer = null;
      }
    };

    Animate.prototype._clearDelayTimer = function() {
      clearTimeout(this._delayTimer);
      this._delayTimer = null;
    };

    Animate.prototype.play = function() {
      if (this.ended) {
        return;
      }

      if (this.startTime === 0) {
        this.startTime = Date.now();

        this.beforeBegin ? this.beforeBegin() : null;
      }

      if (this.paused) {
        this.pausedTime += Date.now() - this.pauseStartTime;
        this._startTimer();
        this.paused = false;
      } else if (isFinite(this.delay)) {
        var self = this;
        this._delayTimer = setTimeout(function() {
          self._clearDelayTimer();
          self.startTime = Date.now();
          self._startTimer();
        }, this.delay);
      } else {
        this._startTimer();
      }

      this.onPlay ? this.onPlay() : null;
    };

    Animate.prototype.pause = function() {
      this.paused = true;
      if (this._delayTimer) {
        this._clearDelayTimer();
      } else {
        this._stopTimer();
        this.pauseStartTime = Date.now();
      }

      this.onPause ? this.onPause() : null;
    };

    Animate.prototype.stop = function() {
      this._stopTimer();
      this.stopped = true;
      this.onStop ? this.onStop() : null;
    }

    var btnPlay = document.getElementById('btnPlay');
    var n = document.getElementById('anim');
    var anim1 = new Animate(n, {
      opacity: {start: 0, end: 1},
      width: {start:50, end: 500, units: 'px'}
    }, 5000, 25, 1000);
    var anim2 = new Animate(n, {
      // opacity: {start: 1, end: 0.3},
      height: {start:50, end: 500, units: 'px'}
    }, 5000, 25, 1000);

    var anim3 = new Animate(n, {
      opacity: {start: 1, end: 0.3},
      height: {start:500, end: 50, units: 'px'}
    }, 5000, 25, 1000);

    var anim = combine([anim1, anim2]);
    // anim = chain([anim, anim3]);

    btnPlay.onclick = function() {
      anim.play();
    }

    btnPause = document.getElementById('btnPause');
    btnPause.onclick = function() {
      anim.pause();
    }

    function combine(anims) {
      var anim = {
        play: function() {
          for (var i = 0, len = anims.length; i < len; i++) {
            anims[i].play();
          }
        },
        pause: function() {
          for (var i = 0, len = anims.length; i < len; i++) {
            anims[i].pause();
          }
        },
        stop: function() {
          for (var i = 0, len = anims.length; i < len; i++) {
            anims[i].stop();
          }
        }
      };

      return anim;
    }

    function chain(anims) {
      var index = 0;
      for (var i = 0, len = anims.length; i < len; i++) {
        var a1 = anims[i];
        var a2 = anims[i + 1];
        if (a2) {
          a1.onEnd = a1.onEnd ? a1.onEnd.after(function() {
            index++;
            anims[index].play();
          }) : (function() {}).after(function() {
            index++;
            anims[index].play();
          });
        }
      }

      var anim = {
        play: function() {
          anims[index].play();
        },
        pause: function() {
          anims[index].pause();
        },
        stop: function() {
          anims[index].stop();
        }
      };

      return anim;
    }
View Code

  示例:

  dojo中chain、combine兩個函數返回的對象跟Animation擁有一樣的方法和屬性,這也意味着利用這兩個函數咱們能夠構造出更復雜的動畫:

var anim1 = new Animate(n, {
      opacity: {start: 0, end: 1},
      width: {start:50, end: 500, units: 'px'}
    }, 5000, 25, 1000);
    var anim2 = new Animate(n, {
      // opacity: {start: 1, end: 0.3},
      height: {start:50, end: 500, units: 'px'}
    }, 5000, 25, 1000);

    var anim3 = new Animate(n, {
      opacity: {start: 1, end: 0.3},
      height: {start:500, end: 50, units: 'px'}
    }, 5000, 25, 1000);

    var anim = combine([anim1, anim2]);
    anim = chain([anim, anim3]);

  在此有興趣的讀者能夠自行實現。

 

 若是您以爲本文對您有幫助,請不要吝嗇點擊一下推薦!謝謝

相關文章
相關標籤/搜索