Dojo动画原理解析

本文涉及的产品
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
简介:

 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。

  dojo中动画的原理与jquery类似,都是根据setInterval计算当前时间与初试时间差值与总时间的半分比来确定元素动画属性的当前计算值:


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

 接下来我们会一步步的演化,慢慢接近dojo API

  首先我们实现一个动画函数,他接受以下参数:

  • 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);
}

 示例:

  

  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();
        }
      }
    }

 这里将上文中Animate函数放入startTimer函数中,增加stopTimer用来移除定时器。

示例:

 

  下面我们要支持延时和暂停功能,实现延迟的方法在于使用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();
        }
      }
    }

示例:

 

  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();
    }


  翻看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;
        }
      }
    }

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;
    }

  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]);

 在此有兴趣的读者可以自行实现。



目录
相关文章
|
1月前
|
存储 算法 Java
解析HashSet的工作原理,揭示Set如何利用哈希算法和equals()方法确保元素唯一性,并通过示例代码展示了其“无重复”特性的具体应用
在Java中,Set接口以其独特的“无重复”特性脱颖而出。本文通过解析HashSet的工作原理,揭示Set如何利用哈希算法和equals()方法确保元素唯一性,并通过示例代码展示了其“无重复”特性的具体应用。
41 3
|
1月前
|
C++
【C++】深入解析C/C++内存管理:new与delete的使用及原理(二)
【C++】深入解析C/C++内存管理:new与delete的使用及原理
|
1月前
|
编译器 C++ 开发者
【C++】深入解析C/C++内存管理:new与delete的使用及原理(三)
【C++】深入解析C/C++内存管理:new与delete的使用及原理
|
1月前
|
存储 C语言 C++
【C++】深入解析C/C++内存管理:new与delete的使用及原理(一)
【C++】深入解析C/C++内存管理:new与delete的使用及原理
|
19天前
|
算法 Java 数据库连接
Java连接池技术,从基础概念出发,解析了连接池的工作原理及其重要性
本文详细介绍了Java连接池技术,从基础概念出发,解析了连接池的工作原理及其重要性。连接池通过复用数据库连接,显著提升了应用的性能和稳定性。文章还展示了使用HikariCP连接池的示例代码,帮助读者更好地理解和应用这一技术。
32 1
|
24天前
|
数据采集 存储 编解码
一份简明的 Base64 原理解析
Base64 编码器的原理,其实很简单,花一点点时间学会它,你就又消除了一个知识盲点。
66 3
|
6天前
|
存储 供应链 物联网
深入解析区块链技术的核心原理与应用前景
深入解析区块链技术的核心原理与应用前景
|
6天前
|
存储 供应链 安全
深度解析区块链技术的核心原理与应用前景
深度解析区块链技术的核心原理与应用前景
15 0
|
1月前
|
前端开发 Java 应用服务中间件
21张图解析Tomcat运行原理与架构全貌
【10月更文挑战第2天】本文通过21张图详细解析了Tomcat的运行原理与架构。Tomcat作为Java Web开发中最流行的Web服务器之一,其架构设计精妙。文章首先介绍了Tomcat的基本组件:Connector(连接器)负责网络通信,Container(容器)处理业务逻辑。连接器内部包括EndPoint、Processor和Adapter等组件,分别处理通信、协议解析和请求封装。容器采用多级结构(Engine、Host、Context、Wrapper),并通过Mapper组件进行请求路由。文章还探讨了Tomcat的生命周期管理、启动与停止机制,并通过源码分析展示了请求处理流程。
|
1月前
|
搜索推荐 Shell
解析排序算法:十大排序方法的工作原理与性能比较
解析排序算法:十大排序方法的工作原理与性能比较
51 9

推荐镜像

更多