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。

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

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

 如果您觉得本文对您有帮助,请不要吝啬点击一下推荐!谢谢

时间: 2024-11-13 00:55:25

Dojo动画原理解析的相关文章

Android属性动画完全解析(上)

Android属性动画完全解析(上) 转载:http://blog.csdn.net/guolin_blog/article/details/43536355 在手机上去实现一些动画效果算是件比较炫酷的事情,因此Android系统在一开始的时候就给我们提供了两种实现动画效果的方式,逐帧动画(frame-by-frame animation)和补间动画(tweened animation).逐帧动画的工作原理很简单,其实就是将一个完整的动画拆分成一张张单独的图片,然后再将它们连贯起来进行播放,类似

打造简易NineoldAndroids动画库,深入理解Android动画原理

简介 NineoldAndroids是Github上一个著名的动画库,简单来说,NineOldAndroids是一个向下兼容的动画库,主要是使低于API 11的系统也能够使用View的属性动画. 网上已经有一些文章,介绍了这个库的设计,包括类结构和思想,例如 NineOldAnimations 源码解析 NineoldAndroids动画库源码分析 上面两篇文章都比较详细的介绍了NineoldAndroids的源码,可以说为大家看源码带来很大的方便. 那为什么我还要写这篇文章呢? 我们来看Nin

从Sprite3D理解3D骨骼动画原理

为了能够更好的使用cocos为我们提供的Sprite3D,我和大家分享一下Sprite3D中关于骨骼动画原理的部分,本文使用cocos2d-x 3.2版本,这是cocos首次出现3D骨骼动画的版本,相对与本文写出来时候最新的3.5版本,由于没有其他比如灯光等功能,3D骨骼动画模块读起来要更加的清晰.如果文章有纰漏或者错误的地方,也请大家指教. 目前引擎支持3种动画格式,分别是.obj,.c3b,.c3t,由于.obj没有骨骼,.c3b是二进制,而.c3t是json格式,所以本文就用官方test中

Android属性动画完全解析(中)

转载:http://blog.csdn.net/guolin_blog/article/details/43536355 大家好,在上一篇文章当中,我们学习了Android属性动画的基本用法,当然也是最常用的一些用法,这些用法足以覆盖我们平时大多情况下的动画需求了.但是,正如上篇文章当中所说到的,属性动画对补间动画进行了很大幅度的改进,之前补间动画可以做到的属性动画也能做到,补间动画做不到的现在属性动画也可以做到了.因此,今天我们就来学习一下属性动画的高级用法,看看如何实现一些补间动画所无法实现

Android属性动画完全解析(中),ValueAnimator和ObjectAnimator的高级用法

转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/43536355 大家好,在上一篇文章当中,我们学习了Android属性动画的基本用法,当然也是最常用的一些用法,这些用法足以覆盖我们平时大多情况下的动画需求了.但是,正如上篇文章当中所说到的,属性动画对补间动画进行了很大幅度的改进,之前补间动画可以做到的属性动画也能做到,补间动画做不到的现在属性动画也可以做到了.因此,今天我们就来学习一下属性动画的高级用法,看看如何实现一些补间动画

MyBatis框架中Mapper映射配置的使用及原理解析(七) MapperProxy,MapperProxyFactory

从上文<MyBatis框架中Mapper映射配置的使用及原理解析(六) MapperRegistry> 中我们知道DefaultSqlSession的getMapper方法,最后是通过MapperRegistry对象获得Mapper实例: public <T> T getMapper(Class<T> type, SqlSession sqlSession) { final MapperProxyFactory<T> mapperProxyFactory =

JS 实现无缝滚动动画原理(初学者入)

这段时间在教培训班的学生使用原生javascript实现无缝滚动的动画案例,做了这个原理演示的动画,分享给自学JS的朋友!博主希望对你们有帮助! 在讲解之前先看一下demo: demo:https://224137748.github.io/JS_warehouse/lunbo/domo.HTML源码:https://github.com/224137748/JS_warehouse/blob/master/lunbo/domo.HTML ps: 上面和下面的滚动进度是一致的,上面红色框是为了演

Android中微信抢红包插件原理解析和开发实现

一.前言 自从去年中微信添加抢红包的功能,微信的电商之旅算是正式开始正式火爆起来.但是作为Android开发者来说,我们在抢红包的同时意识到了很多问题,就是手动去抢红包的速度慢了,当然这些有很多原因导致了.或许是网络的原因,而且这个也是最大的原因.但是其他的不可忽略的因素也是要考虑到进去的,比如在手机充电锁屏的时候,我们并不知道有人已经开始发红包了,那么这时候也是让我们丧失了一大批红包的原因.那么关于网络的问题,我们开发者可能用相关技术无法解决(当然在Google和Facebook看来的话,他们

MyBatis框架中Mapper映射配置的使用及原理解析(三) 配置篇 Configuration

从上文<MyBatis框架中Mapper映射配置的使用及原理解析(二) 配置篇 SqlSessionFactoryBuilder,XMLConfigBuilder> 我们知道XMLConfigBuilder调用parse()方法解析Mybatis配置文件,生成Configuration对象. Configuration类主要是用来存储对Mybatis的配置文件及mapper文件解析后的数据,Configuration对象会贯穿整个Mybatis的执行流程,为Mybatis的执行过程提供必要的配