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

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



目录
相关文章
|
10月前
|
运维 持续交付 云计算
深入解析云计算中的微服务架构:原理、优势与实践
深入解析云计算中的微服务架构:原理、优势与实践
542 86
|
7月前
|
安全 算法 网络协议
解析:HTTPS通过SSL/TLS证书加密的原理与逻辑
HTTPS通过SSL/TLS证书加密,结合对称与非对称加密及数字证书验证实现安全通信。首先,服务器发送含公钥的数字证书,客户端验证其合法性后生成随机数并用公钥加密发送给服务器,双方据此生成相同的对称密钥。后续通信使用对称加密确保高效性和安全性。同时,数字证书验证服务器身份,防止中间人攻击;哈希算法和数字签名确保数据完整性,防止篡改。整个流程保障了身份认证、数据加密和完整性保护。
|
9月前
|
存储 缓存 算法
HashMap深度解析:从原理到实战
HashMap,作为Java集合框架中的一个核心组件,以其高效的键值对存储和检索机制,在软件开发中扮演着举足轻重的角色。作为一名资深的AI工程师,深入理解HashMap的原理、历史、业务场景以及实战应用,对于提升数据处理和算法实现的效率至关重要。本文将通过手绘结构图、流程图,结合Java代码示例,全方位解析HashMap,帮助读者从理论到实践全面掌握这一关键技术。
292 14
|
6月前
|
机器学习/深度学习 数据可视化 PyTorch
深入解析图神经网络注意力机制:数学原理与可视化实现
本文深入解析了图神经网络(GNNs)中自注意力机制的内部运作原理,通过可视化和数学推导揭示其工作机制。文章采用“位置-转移图”概念框架,并使用NumPy实现代码示例,逐步拆解自注意力层的计算过程。文中详细展示了从节点特征矩阵、邻接矩阵到生成注意力权重的具体步骤,并通过四个类(GAL1至GAL4)模拟了整个计算流程。最终,结合实际PyTorch Geometric库中的代码,对比分析了核心逻辑,为理解GNN自注意力机制提供了清晰的学习路径。
482 7
深入解析图神经网络注意力机制:数学原理与可视化实现
|
6月前
|
机器学习/深度学习 缓存 自然语言处理
深入解析Tiktokenizer:大语言模型中核心分词技术的原理与架构
Tiktokenizer 是一款现代分词工具,旨在高效、智能地将文本转换为机器可处理的离散单元(token)。它不仅超越了传统的空格分割和正则表达式匹配方法,还结合了上下文感知能力,适应复杂语言结构。Tiktokenizer 的核心特性包括自适应 token 分割、高效编码能力和出色的可扩展性,使其适用于从聊天机器人到大规模文本分析等多种应用场景。通过模块化设计,Tiktokenizer 确保了代码的可重用性和维护性,并在分词精度、处理效率和灵活性方面表现出色。此外,它支持多语言处理、表情符号识别和领域特定文本处理,能够应对各种复杂的文本输入需求。
806 6
深入解析Tiktokenizer:大语言模型中核心分词技术的原理与架构
|
7月前
|
机器学习/深度学习 算法 数据挖掘
解析静态代理IP改善游戏体验的原理
静态代理IP通过提高网络稳定性和降低延迟,优化游戏体验。具体表现在加快游戏网络速度、实时玩家数据分析、优化游戏设计、简化更新流程、维护网络稳定性、提高连接可靠性、支持地区特性及提升访问速度等方面,确保更流畅、高效的游戏体验。
189 22
解析静态代理IP改善游戏体验的原理
|
7月前
|
编解码 缓存 Prometheus
「ximagine」业余爱好者的非专业显示器测试流程规范,同时也是本账号输出内容的数据来源!如何测试显示器?荒岛整理总结出多种测试方法和注意事项,以及粗浅的原理解析!
本期内容为「ximagine」频道《显示器测试流程》的规范及标准,我们主要使用Calman、DisplayCAL、i1Profiler等软件及CA410、Spyder X、i1Pro 2等设备,是我们目前制作内容数据的重要来源,我们深知所做的仍是比较表面的活儿,和工程师、科研人员相比有着不小的差距,测试并不复杂,但是相当繁琐,收集整理测试无不花费大量时间精力,内容不完善或者有错误的地方,希望大佬指出我们好改进!
474 16
「ximagine」业余爱好者的非专业显示器测试流程规范,同时也是本账号输出内容的数据来源!如何测试显示器?荒岛整理总结出多种测试方法和注意事项,以及粗浅的原理解析!
|
6月前
|
传感器 人工智能 监控
反向寻车系统怎么做?基本原理与系统组成解析
本文通过反向寻车系统的核心组成部分与技术分析,阐述反向寻车系统的工作原理,适用于适用于商场停车场、医院停车场及火车站停车场等。如需获取智慧停车场反向寻车技术方案前往文章最下方获取,如有项目合作及技术交流欢迎私信作者。
445 2
|
8月前
|
机器学习/深度学习 自然语言处理 搜索推荐
自注意力机制全解析:从原理到计算细节,一文尽览!
自注意力机制(Self-Attention)最早可追溯至20世纪70年代的神经网络研究,但直到2017年Google Brain团队提出Transformer架构后才广泛应用于深度学习。它通过计算序列内部元素间的相关性,捕捉复杂依赖关系,并支持并行化训练,显著提升了处理长文本和序列数据的能力。相比传统的RNN、LSTM和GRU,自注意力机制在自然语言处理(NLP)、计算机视觉、语音识别及推荐系统等领域展现出卓越性能。其核心步骤包括生成查询(Q)、键(K)和值(V)向量,计算缩放点积注意力得分,应用Softmax归一化,以及加权求和生成输出。自注意力机制提高了模型的表达能力,带来了更精准的服务。
10253 46
|
7月前
|
Java 数据库 开发者
详细介绍SpringBoot启动流程及配置类解析原理
通过对 Spring Boot 启动流程及配置类解析原理的深入分析,我们可以看到 Spring Boot 在启动时的灵活性和可扩展性。理解这些机制不仅有助于开发者更好地使用 Spring Boot 进行应用开发,还能够在面对问题时,迅速定位和解决问题。希望本文能为您在 Spring Boot 开发过程中提供有效的指导和帮助。
785 12

推荐镜像

更多
  • DNS