js实现帧动画

简介

帧动画就是在连续的关键帧中分解动画动作,也就是在时间轴上的每帧上逐帧绘制不同的内容,使其连续播放成动画。常见的帧动画方式有GIF,CSS3 animation和js。使用GIF和CSS3 animation的不足是不能灵活地控制动画的暂停和播放(比如点击暂停),也不能灵活地捕捉到动画完成的事件,另外在动画扩展性上js更好。

原理

js实现帧动画的有几种方法。一是用一个img标签去承载图片,定时改变img的src属性,这样显然不好。第二种是把所有动画关键帧绘制在一张图片里,把图片作为元素的background-image,定时改变元素的background-position(雪碧图)。对于只是位移的动画,可以通过js直接改变元素的left或者top值

实践

下面说一下第二种

   var imgUrl = 'xx.png';
   var positions = ['0 -854', '-174 852', 'x y'...];
   var ele = document.getElementById('ele');
   animation(ele, positions, imgUrl);
   function animation(ele, positions, imgUrl){
       ele.style.backgroundImage = 'url(' + imgUrl + ')';
       ele.style.backgroundRepeat = 'no-repeat';
       var index = 0;
       function run(){
          var position = positions[index].split(' ');
          ele.style.backgroundPosition = position[0] + 'px ' + position[1] + 'px';
          index++;
          if(index >= positions.length){
              index = 0;
          }
          setTimeout(run, 80);
       }
       run();
   }

封装

封装一个动画对象我们一般会经过这四步,需求分析,设计编程接口,设计调用方式,最后是代码设计。
首先是需求分析,首先我们要支持图片预加载(图片加载是异步过程,我们要保证动画开始的时候图片已经加载完)。第二个是要支持单组动画控制循环次数(可以不断循环),第三个是可以切换到另一个动画。第四个是动画可以暂停和继续播放,第五个是支持动画完成后的回调函数。
根据以上需求我们设计了这样一个接口

   loadImage(imglist) //预加载图片
   changePosition(ele, position, imageUrl) //通过改变元素的background-position实现动画
   enterFrame(callback) //每一帧动画执行的函数,可以自定义每一帧动画的callback
   repeat(times) //动画重复执行的次数,times为空时表示无限次
   repeatForever() //相当于上面不传参数的情况,这个接口比较友好
   wait(time)  //每个动画执行之后等待的时间
   then(callback) //动画执行完成后的回调函数
   start(interval) //动画开始执行,interval表示动画执行的间隔
   pause() //动画暂停
   restart() //动画从上一次暂停处重新执行
   unmount() //释放资源

接下来我们设计一下调用方式,首先我们希望支持链式调用,如下面的例子

   var animation = require('animation');
   var demo = animation().loadImage(images).changePosition(ele, position, imgUrl).repeat(2).then(function(){
      //所有动画执行后的回调
   })
   demo.start(80);

最后是代码设计,我们把图片预加载,动画执行,动画结束等看成一个任务队列(js的数组)
任务队列中有两种类型的任务,一种是同步执行完成(从预加载到动画执行),另一种是异步定时执行的。
那么我们需要记录当前任务链的索引,在每个任务执行完毕后,通过调用next方法,执行下一个任务,同时更新任务链索引值

代码编写

我们先定义一个基本框架

   function Animation(){   
   }
   Animation.prototype = {
      loadImage: function(imglist){
      },
      //添加一个异步定时任务
      changePosition: function(ele, positions, imageUrl){
      },
      //每一帧动画执行的回调函数
      enterFrame: function(taskFn){
      },
      //在一个任务结束时的回调函数
      then: function(callback){
      },
      start: function(interval){
      },
      //添加一个同步任务,该任务是回退到上一个任务中,实现重复上一个任务的效果
      repeat: function(times){
      },
      //无限循环上一次任务
      repeatForever: function(){
      },
      //设置当前任务结束后到下个任务前的等待时间time
      wait: function(time){
      },
      pause: function(){
      },
      restart: function(){
      },
      unmount: function(){
      }
   }

接下来我们开始填充具体代码

   //类似常量的写法,在ES6中使用const关键字
   var STATE_INITIAL = 0;
   var STATE_START = 0;
   var STATE_STOP = 0;
   //同步任务
   var TASK_SYNC = 0;
   var TASK_ASYNC = 1;
   //CMD写法,引入imageloader函数(后面会详细讲)
   var loadImage = require('./imageloader');
   var Timeline = require('./timeline');
   //简单的函数封装,执行callback
   function next(callback){
       //如果callback没传进来,undefined就退出
       callback && callback();
   }
   function Animation(){   
       this.taskQueue = [];
       this.index = 0;
       this.state = STATE_INITIAL;
       this.timeline = new Timeline();
   }
   Animation.prototype = {
      //类内部使用
      //添加一个任务到任务队列
      _add: function(taskFn, type){
          this.taskQueue.push({
             taskFn: taskFn,
             type: type
          })
          return this;
      },
      _runTask: function(){
          if(!this.taskQueue || this.state !== STATE_START){
              return;
          }
          if(this.index === this.taskQueue.length){
              this.unmount();
              return;
          }
          var task = this.taskQueue[index];
          if(task.type === TASK_SYNC){
              this._syncTask(task);
          } else {
              this._asyncTask(task);
          }
      },
      _syncTask: function(task){
          var self = this;
          var next = function(){
              //切换到下一个任务
              self._next(task);
          }
          var taskFn = task.taskFn;
          taskFn(next);
      },
      _asyncTask: function(task){
          var self = this;
          //定义每一帧执行的回调函数
          var enterFrame = function(time){
              var taskFn = task.taskFn;
              var next = function(){
                  //停止当前任务
                  self.timeline.stop();
                  self._next(task);
              }
              taskFn(next, time);
          }
          this.timeline.onenterframe = enterFrame;
          this.timeline.start(this.interval);
      },
      _next: function(task){
          this.index++;
          //如果有wait属性,设置等待时间
          var self = this;
          task.wait ? setTimeout(function(){
              self._runTask();
          }, task.wait) : this._runTask();
      },
      loadImage: function(imglist){
         var taskFn = function(next){
             //把next当作callback传入
             loadImage(imglist.slice(), next);
         }
         var type = TASK_SYNC;
         //把this实例传出去,那么在_add方法中也要return this
         return this._add(taskFn, type);
      },
      //添加一个异步定时任务
      changePosition: function(ele, positions, imageUrl){
          var len = positions.length;
          var taskFn;
          var type;
          if(len){
              var self = this;
              taskFn = function(next, time){
                  if(imgUrl){
                     ele.style.backgroundImage = 'url(' + imageUrl + ')';
                  }
                  //获得当前背景图片位置索引
                  // | 0 相当于Math.floor
                  var index = Math.min(time/self.interval | 0, len - 1);
                  var position = positions[index].split(' ');
                  //改变dom对象的背景图片位置
                  ele.style.backgroundPosition = position[0] + 'px ' + position[1] + 'px';
                  if(index === len - 1){
                      next();
                  }
              }
              type = TASK_ASYNC;
          } else {
             taskFn = next;
             type = TASK_SYNC;
          }
          return this._add(taskFn, type);
      },
      //每一帧动画执行的回调函数
      enterFrame: function(taskFn){
          return this._add(taskFn, TASK_ASYNC);
      },
      //在一个任务结束时的回调函数
      then: function(callback){
          var taskFn = function(next){
              callback();
              next();
          }
          var type = TASK_SYNC;
          return this._add(taskFn, type);
      },
      start: function(interval){
          if(this.state === STATE_START){
              return this;
          }
          if(!this.taskQueue.length){
              return this;
          }
          this.state = STATE_START;
          this._runTask();
          return this;
      },
      //添加一个同步任务,该任务是回退到上一个任务中,实现重复上一个任务的效果
      repeat: function(times){
          var self = this;
          var taskFn = function(){
              if(typeof times === 'undefined'){
                  //无限回退到上一个任务
                  self.index--;
                  self._runTask();
                  return;
              }
              if(times){
                  times--;
                  self.index--;
                  self._runTask();      
              } else {
                 //达到重复次数
                 var task = self.taskQueue[self.index];
                 self._next(task);
              }
          }
          var type = TASK_SYNC;
          return this._add(taskFn, type);
      },
      //无限循环上一次任务
      repeatForever: function(){
          return this.repeat();
      },
      //设置当前任务结束后到下个任务前的等待时间
      wait: function(){
          if(this.taskQueue && this.taskQueue.length > 0){
              this.taskQueue[this.taskQueue.length - 1].wait = time;
          }
          return this;
      },
      pause: function(){
          if(this.state === STATE_START){
              this.state = STATE_STOP;
              this.timeline.stop();
              return this;
          }
          return this;
      },
      restart: function(){
          if(this.state === STATE_STOP){
              this.state = STATE_START;
              this.timeline.restart();
              return this;
          }
          return this;
      },
      unmount: function(){
          if(this.state !== STATE_INITIAL){
             this.state = STATE_INITIAL;
             this.taskQueue = null;
             this.timeline.stop();
             this.timeline = null;
             return this;
          }
      }
   }
   module.exports = function(){
      //类似工厂模式
      return new Animation();
   }

我们在imgeloader.js中定义一个预加载模块

   //images为加载图片的数组或者对象
   //callback 全部图片加载完毕后的回调函数
   //timeout 加载超时的时长
   function loadImage(images, callback, timeout){
       //图片路径记数器
       var count = 0;
       //全部图片加载成功的tag
       var success = true;
       var timeoutId = 0;
       var isTimeout = false;
       for(var key in images){
           //过滤掉原型上的属性
           if(!images.hasOwnProperty(key)){
               continue;
           }
           var item = images[key];
           //如果item是一个string的时候,当作src处理
           if(typeof item === 'string'){
               //连等表示前两个等于第三个
               item = image[key] = {
                   src: item
               }
           }
           if(!item || !item.src){
               continue;
           }
           count++;
           //设置图片的id
           item.id = '_img_'+ key + getId();
           item.img = window[item.id] = new Image();
           doload(item);
       }
       //如果数组的数据验证失败,count为0,直接调用回调
       //这里是对count任务的同步验证,count还没开始--
       if(!count){
           callback(false);
       } else if(timeout){
           timeoutId = setTimeout(onTimeout, timeout);
       }
       function doload(item){
           item.status = 'loading';
           var img = item.img;
           //加载成功
           img.onload = function(){
              success = success && true;
              item.status = 'loaded';
              done();
           }
           img.onerror = function(){
               item.status = 'error';
               success = false;
               done();
           }
           //发起一个http/https请求
           img.src = item.src;
           //图片加载完成
           function done(){
               img.onload = img.onerror = null;
               try {
                  delete window[item.id];
               } catch(e){
               }
               //如果count为0的时候
               //所有图片加载且没有超时情况,清除计时器且执行回调
               if(!--count && !isTimeout){
                  clearTimeout(timeoutId);
                  callback(success);
               }
           }
           //超时处理函数
           function onTimeout(timeout){
               isTimeout = true;
               callback(false);
           } 
       }
   }
   //_id变量不会污染全局,只是在这个模块闭包中
   var _id = 0;
   function getId(){
       return ++_id;
   }
   module.exports = loadImage;

然后使用一个模块timeline.js来取代定时器,因为在浏览器运行环境中定时器是不准的

   //setTimeout的定时器值推荐最小使用16.7ms的原因(16.7 = 1000 / 60, 即每秒60帧)
   var DEFAULT_INTERVAL = 1000 / 60;
   var STATE_INITIAL = 0;
   var STATE_START = 1;
   var STATE_STOP = 2;
   var requestAnimaitionFrame = (function(){
       return window.requestAnimationFrame ||
           //chrome
           window.webkitRequestAnimationFrame ||
           //firefox
           window.mozRequestAnimationFrame ||
           //opera
           window.oRequestAnimationFrame || 
           function(callback){
               return window.setTimeout(callback, callback.interval || DEFAULT_INTERVAL);
           }
   })()
   var cancelAnimationFrame = (function(){
       //类似上面的兼容
       return window.cancelAnimationFrame ...
   })()
   //时间轴类
   function TimeLine(){
       this.animationHandler = 0;
       this.state = STATE_INITIAL;
   }
   TimeLine.prototype = {
       //时间轴上每一次回调执行的函数
       //time是从动画开始到当前执行的时间
       onenterframe: function(time){
           //在主对象中定义
       },
       //interval 每一次回调的间隔时间
       start: function(interval){
          if(this.state === STATE_START){
              return ;
          }
          this.state = STATE_START;
          this.interval = interval || DEFAULT_INTERVAL;
          //+new Date相当于new Date().getTime()
          startTimeline(this, +new Date());
       },
       stop: function(){
          if(this.state !== STATE_START){
              return;
          }
          this.state = STATE_STOP;
          //记录动画从开始到现在经历的时间
          if(this.startTime){
              this.dur = +new Date() - this.startTime;
          }
          cancelAnimationFrame(this.animationHandler);
       },
       restart: funtion(){
          if(this.state === STATE_START){
              return;
          }
          if(!this.dur || this.interval){
              return;
          }
          this.state = STATE_START;
          //无缝连接动画
          startTimeline(this, +new Date() - this.dur);
       }
   }
   //时间轴动画启动函数
   function startTimeline(timeline, startTime){
       timeline.startTime = startTime;
       nextTick.interval = timeline.interval;
       //记录上一次回调的时间戳,tick的意思是时钟的滴答
       var lastTick = +new Date();
       nextTick();
       //每一帧执行的函数
       function nextTick(){
           var now = +new Date();
           //每17毫秒刷新一次,更精确的定时器
           timeline.animationHandler = requestAnimationFrame(nextTick);
           //如果当前时间与上一次回调的时间戳大于设置的时间间隔,表示这次可以执行回调函数
           if(now - lastTick >= timeline.interval){
               timeline.onenterframe(now - startTime);
               lastTick = now;
           }
       }
   }
   module.exports = Timeline;
  • 5
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值