javascript幀動畫

前面的話

  幀動畫就是在「連續的關鍵幀」中分解動畫動做,也就是在時間軸的每幀上逐幀繪製不一樣的內容,使其連續播放而成的動畫。因爲是一幀一幀的畫,因此幀動畫具備很是大的靈活性,幾乎能夠表現任何想表現的內容。本文將詳細介紹javascript幀動畫javascript

 

概述

【分類】html

  常見的幀動畫的方式有三種,包括gif、CSS3 animation和javascriptjava

  git和CSS3 animation不能靈活地控制動畫的暫停和播放、不能對幀動畫作更加靈活地擴展。另外,gif圖不能捕捉動畫完成的事件。因此,通常地,使用javascript來實現幀動畫webpack

【原理】git

  js實現幀動畫有兩種實現方式web

  一、若是有多張幀動畫圖片,能夠用一個image標籤去承載圖片,定時改變image的src屬性(不推薦)編程

  二、把全部的動畫關鍵幀都繪製在一張圖片裏,把圖片做爲元素的background-image,定時改變元素的background-position屬性(推薦)數組

  由於第一種方式須要使用多個HTTP請求,因此通常地推薦使用第二種方式瀏覽器

【實例】dom

  下面是使用幀動畫製做的一個實例

<div id="rabbit" style="width:102px;height:80px"></div> 
<button id="btn">暫停運動</button> 
<script>
var url = 'rabbit-big.png';
var positions = ['0,-854','-174 -852','-349 -852','-524 -852','-698 -852','-873 -848'];
var ele = document.getElementById('rabbit');
var oTimer = null;
btn.onclick = function(){
  if(btn.innerHTML == '開始運動'){
    frameAnimation(ele,positions,url);
    btn.innerHTML = '暫停運動';
  }else{
    clearTimeout(oTimer);
    btn.innerHTML = '開始運動';
  } 
}
frameAnimation(ele,positions,url);
function frameAnimation(ele,positions,url){
  ele.style.backgroundImage = 'url(' + url + ')';
  ele.style.backgroundRepeat = 'no-repeat'; 
  var index = 0;
  function run(){
    var pos = positions[index].split(' ');
    ele.style.backgroundPosition = pos[0] + 'px ' + pos[1] + 'px';
    index++;
    if(index >= positions.length){
      index = 0;
    }
    oTimer = setTimeout(run,80);
  }
  run();
}  
</script>

 

通用幀動畫

  下面來設計一個通用的幀動畫庫

【需求分析】

  一、支持圖片預加載

  二、支持兩種動畫播放方式,及自定義每幀動畫

  三、支持單組動畫控制循環次數(可支持無限次)

  四、支持一組動畫完成,進行下一組動畫

  五、支持每一個動畫完成後有等待時間

  六、支持動畫暫停和繼續播放

  七、支持動畫完成後執行回調函數

【編程接口】

  一、loadImage(imglist)//預加載圖片

  二、changePosition(ele,positions,imageUrl)//經過改變元素的background-position實現動畫

  三、changeSrc(ele,imglist)//經過改變image元素的src

  四、enterFrame(callback)//每一幀動畫執行的函數,至關於用戶能夠自定義每一幀動畫的callback

  五、repeat(times)//動畫重複執行的次數,times爲空時表示無限次

  六、repeatForever()//無限重複上一次動畫,至關於repeat()

  七、wait(time)//每一個動畫執行完成後等待的時間

  八、then(callback)//動畫執行完成後的回調函數

  九、start(interval)//動畫開始執行,interval表示動畫執行的間隔

  十、pause()//動畫暫停

  十一、restart()//動畫從上一交暫停處從新執行

  十二、dispose()//釋放資源

【調用方式】

  支持鏈式調用,用動詞的方式描述接口

【代碼設計】

  一、把圖片預加載 -> 動畫執行 -> 動畫結束等一系列操做當作一條任務鏈。任務鏈包括同步執行和異步定時執行兩種任務

  二、記錄當前任務鏈的索引

  三、每一個任務執行完畢後,經過調用next方法,執行下一個任務,同時更新任務鏈索引值

【接口定義】

'use strict';
/* 幀動畫庫類
 * @constructor
 */
function FrameAnimation(){}

/* 添加一個同步任務,去預加載圖片
 * @param imglist 圖片數組
 */
FrameAnimation.prototype.loadImage = function(imglist){}

/* 添加一個異步定時任務,經過定時改變圖片背景位置,實現幀動畫
 * @param ele dom對象
 * @param positions 背景位置數組
 * @param imageUrl 圖片URL地址
 */
FrameAnimation.prototype.changePosition = function(ele,positions,imageUrl){}

/* 添加一個異步定時任務,經過定時改變image標籤的src屬性,實現幀動畫
 * @param ele dom對象
 * @param imglist 圖片數組
 */
FrameAnimation.prototype.changeSrc = function(ele,imglist){}

/* 添加一個異步定時任務,自定義動畫每幀執行的任務函數
 * @param tastFn 自定義每幀執行的任務函數
 */
FrameAnimation.prototype.enterFrame = function(taskFn){}

/* 添加一個同步任務,在上一個任務完成後執行回調函數
 * @param callback 回調函數
 */
FrameAnimation.prototype.then = function(callback){}

/* 開始執行任務,異步定時任務執行的間隔
 * @param interval
 */
FrameAnimation.prototype.start = function(interval){}

/* 添加一個同步任務,回退到上一個任務,實現重複上一個任務的效果,能夠定義重複的次數
 * @param times 重複次數
 */
FrameAnimation.prototype.repeat = function(times){}

/* 添加一個同步任務,至關於repeat(),無限循環上一次任務
 * 
 */
FrameAnimation.prototype.repeatForever = function(){}

/* 設置當前任務執行結束後到下一個任務開始前的等待時間
 * @param time 等待時長
 */
FrameAnimation.prototype.wait = function(time){}

/* 暫停當前異步定時任務
 * 
 */
FrameAnimation.prototype.pause = function(){}

/* 從新執行上一次暫停的異步定時任務
 * 
 */
FrameAnimation.prototype.restart = function(){}

/* 釋放資源
 * 
 */
FrameAnimation.prototype.dispose = function(){}

 

圖片預加載

  圖片預加載是一個相對獨立的功能,能夠將其封裝爲一個模塊imageloader.js

'use strict';
/**
 * 預加載圖片函數
 * @param    images   加載圖片的數組或者對象
 * @param    callback 所有圖片加載完畢後調用的回調函數
 * @param    timeout  加載超時的時長
 */
function loadImage(images,callback,timeout){
  //加載完成圖片的計數器
  var count = 0;
  //所有圖片加載成功的標誌位
  var success = true;
  //超時timer的id
  var timeoutId = 0;
  //是否加載超時的標誌位
  var isTimeout = false;
  //對圖片數組(或對象)進行遍歷
  for(var key in images){
    //過濾prototype上的屬性
    if(!images.hasOwnProperty(key)){
      continue;
    }
    //得到每一個圖片元素
    //指望格式是object:{src:xxx}
    var item = images[key];
    if(typeof item === 'string'){
      item = images[key] = {
        src:item
      };
    }
    //若是格式不知足指望,則丟棄此條數據,進行下一次遍歷
    if(!item || !item.src){
      continue;
    }
    //計數+1
    count++;
    //設置圖片元素的id
    item.id = '__img__' + key + getId();
    //設置圖片元素的img,它是一個Image對象
    item.img = window[item.id] = new Image();
    doLoad(item);
  }
  //遍歷完成若是計數爲0,則直接調用callback
  if(!count){
    callback(success);
  }else if(timeout){
    timeoutId = setTimeout(onTimeout,timeout);
  }

  /**
   * 真正進行圖片加載的函數
   * @param   item 圖片元素對象
   */
  function doLoad(item){
    item.status = 'loading';
    var img = item.img;
    //定義圖片加載成功的回調函數
    img.onload = function(){
      success = success && true;
      item.status = 'loaded';
      done();
    }
    //定義圖片加載失敗的回調函數
    img.onerror = function(){
      success = false;
      item.status = 'error';
      done();
    }
    //發起一個http(s)請求
    img.src = item.src;
    /**
     * 每張圖片加載完成的回調函數
     */
    function done(){
      img.onload = img.onerror = null;
      try{
        delete window[item.id];
      }catch(e){

      }
      //每張圖片加載完成,計數器減1,當全部圖片加載完成,且沒有超時的狀況,清除超時計時器,且執行回調函數
      if(!--count && !isTimeout){
        clearTimeout(timeoutId);
        callback(success);
      }
    }
  }
  /**
   * 超時函數
   */
  function onTimeout(){
    isTimeout = true;
    callback(false);
  }
}
var __id = 0;
function getId(){
  return ++__id;
}
module.exports = loadImage;

 

時間軸

  在動畫處理中,是經過迭代使用setTimeout()實現的,可是這個間隔時間並不許確。下面,來實現一個時間軸類timeline.js

'use strict';

var DEFAULT_INTERVAL = 1000/60;

//初始化狀態
var STATE_INITIAL = 0;
//開始狀態
var STATE_START = 1;
//中止狀態
var STATE_STOP = 2;

var requestAnimationFrame = (function(){
  return window.requestAnimationFrame || window.webkitRequestAnimationFrame|| window.mozRequestAnimationFrame || window.oRequestAnimationFrame  || function(callback){
          return window.setTimeout(callback,(callback.interval || DEFAULT_INTERVAL));
        }
})();

var cancelAnimationFrame = (function(){
  return window.cancelAnimationFrame || window.webkitCancelAnimationFrame || window.mozCancelAnimationFrame || window.oCancelAnimationFrame   || function(id){
          return window.clearTimeout(id);
        }  
})();
/**
 * 時間軸類
 * @constructor
 */
function Timeline(){
  this.animationHandler = 0;
  this.state = STATE_INITIAL;
}
/**
 * 時間軸上每一次回調執行的函數
 * @param   time 從動畫開始到當前執行的時間
 */
Timeline.prototype.onenterframe = function(time){

}
/**
 * 動畫開始
 * @param interval 每一次回調的間隔時間
 */
Timeline.prototype.start = function(interval){
  if(this.state === STATE_START){
    return;
  }
  this.state = STATE_START;
  this.interval = interval || DEFAULT_INTERVAL;
  startTimeline(this,+new Date());
}

/**
 * 動畫中止
 */
Timeline.prototype.stop = function(){
  if(this.state !== STATE_START){
    return;
  }
  this.state = STATE_STOP;
  //若是動畫開始過,則記錄動畫從開始到如今所經歷的時間
  if(this.startTime){
    this.dur = +new Date() - this.startTime;
  }
  cancelAnimationFrame(this.animationHandler);
}

/**
 * 從新開始動畫
 */
Timeline.prototype.restart = function(){
  if(this.state === STATE_START){
    return;
  }
  if(!this.dur || !this.interval){
    return;
  }
  this.state = STATE_START;
  //無縫鏈接動畫
  startTimeline(this,+new Date()-this.dur);
}

/**
 * 時間軸動畫啓動函數
 * @param   timeline  時間軸的實例
 * @param   startTime 動畫開始時間戳          
 */
function startTimeline(timeline,startTime){
  //記錄上一次回調的時間戳
  var lastTick = +new Date();
  timeline.startTime = startTime;
  nextTick.interval = timeline.interval;
  nextTick();
  /**
   * 每一幀執行的函數
   */
  function nextTick(){
    var now = +new Date();
    timeline.animationHandler = requestAnimationFrame(nextTick);
    //若是當前時間與上一次回調的時間戳大於設置的時間間隔,表示這一次能夠執行回調函數
    if(now - lastTick >= timeline.interval){
      timeline.onenterframe(now - startTime);
      lastTick = now;
    }
  }
}
module.exports = Timeline;

 

動畫類實現

  下面是動畫類animation.js實現的完整代碼

'use strict';

var loadImage = require('./imageloader');
var Timeline = require('./timeline');
//初始化狀態
var STATE_INITIAL = 0;
//開始狀態
var STATE_START = 1;
//中止狀態
var STATE_STOP = 2;
//同步任務
var TASK_SYNC = 0;
//異步任務
var TASK_ASYNC = 1;

/**
 * 簡單的函數封裝,執行callback
 * @param   callback 執行函數
 */
function next(callback){
  callback && callback();
}
/* 幀動畫庫類
 * @constructor
 */
function FrameAnimation(){
  this.taskQueue = [];
  this.index = 0;
  this.timeline = new Timeline();
  this.state = STATE_INITIAL;
}

/* 添加一個同步任務,去預加載圖片
 * @param imglist 圖片數組
 */
FrameAnimation.prototype.loadImage = function(imglist){
  var taskFn = function(next){
    loadImage(imglist.slice(),next);
  };
  var type = TASK_SYNC;
  return this._add(taskFn,type);
}

/* 添加一個異步定時任務,經過定時改變圖片背景位置,實現幀動畫
 * @param ele dom對象
 * @param positions 背景位置數組
 * @param imageUrl 圖片URL地址
 */
FrameAnimation.prototype.changePosition = function(ele,positions,imageUrl){
  var len = positions.length;
  var taskFn;
  var type;
  if(len){
    var me = this;
    taskFn = function(next,time){
      if(imageUrl){
        ele.style.backgroundImage = 'url(' + imageUrl + ')';
      }
      //得到當前背景圖片位置索引
      var index = Math.min(time/me.interval|0,len);
      var position = positions[index-1].split(' ');
      //改變dom對象的背景圖片位置
      ele.style.backgroundPosition = position[0] + 'px ' + position[1] + 'px';
      if(index === len){
        next();
      }
    }
    type = TASK_ASYNC;
  }else{
    taskFn = next;
    type = TASK_SYNC;
  }
  return this._add(taskFn,type);
}

/* 添加一個異步定時任務,經過定時改變image標籤的src屬性,實現幀動畫
 * @param ele dom對象
 * @param imglist 圖片數組
 */
FrameAnimation.prototype.changeSrc = function(ele,imglist){
  var len = imglist.length;
  var taskFn;
  var type;
  if(len){
    var me = this;
    taskFn = function(next,time){
      //得到當前背景圖片位置索引
      var index = Math.min(time/me.interval|0,len);
      //改變image對象的背景圖片位置
      ele.src = imglist[index-1];
      if(index === len){
        next();
      }
    }
    type = TASK_ASYNC;
  }else{
    taskFn = next;
    type = TASK_SYNC;
  }
  return this._add(taskFn,type);  
}

/* 添加一個異步定時任務,自定義動畫每幀執行的任務函數
 * @param tastFn 自定義每幀執行的任務函數
 */
FrameAnimation.prototype.enterFrame = function(taskFn){
  return this._add(taskFn,TASK_ASYNC);
}

/* 添加一個同步任務,在上一個任務完成後執行回調函數
 * @param callback 回調函數
 */
FrameAnimation.prototype.then = function(callback){
  var taskFn = function(next){
    callback(this);
    next();
  };
  var type  = TASK_SYNC;
  return this._add(taskFn,type);
}

/* 開始執行任務,異步定義任務執行的間隔
 * @param interval
 */
FrameAnimation.prototype.start = function(interval){
  if(this.state === STATE_START){
    return this; 
  }
  //若是任務鏈中沒有任務,則返回
  if(!this.taskQueue.length){
    return this;
  }
  this.state = STATE_START;
  this.interval = interval;
  this._runTask();
  return this;
    
}

/* 添加一個同步任務,回退到上一個任務,實現重複上一個任務的效果,能夠定義重複的次數
 * @param times 重複次數
 */
FrameAnimation.prototype.repeat = function(times){
  var me = this;
  var taskFn = function(){
    if(typeof times === 'undefined'){
      //無限回退到上一個任務
      me.index--;
      me._runTask();
      return;
    }
    if(times){
      times--;
      //回退
      me.index--;
      me._runTask();
    }else{
      //達到重複次數,跳轉到下一個任務
      var task = me.taskQueue[me.index];
      me._next(task);
    }
  }
  var type = TASK_SYNC;
  return this._add(taskFn,type);
}

/* 添加一個同步任務,至關於repeat(),無限循環上一次任務
 * 
 */
FrameAnimation.prototype.repeatForever = function(){
  return this.repeat();
}

/* 設置當前任務執行結束後到下一個任務開始前的等待時間
 * @param time 等待時長
 */
FrameAnimation.prototype.wait = function(time){
  if(this.taskQueue && this.taskQueue.length > 0){
    this.taskQueue[this.taskQueue.length - 1].wait = time;
  }
  return this;
}

/* 暫停當前異步定時任務
 * 
 */
FrameAnimation.prototype.pause = function(){
  if(this.state === STATE_START){
    this.state = STATE_STOP;
    this.timeline.stop();
    return this;
  }
  return this;
}

/* 從新執行上一次暫停的異步定時任務
 * 
 */
FrameAnimation.prototype.restart = function(){
  if(this.state === STATE_STOP){
    this.state = STATE_START;
    this.timeline.restart();
    return this;
  }
  return this;  
}

/* 釋放資源
 * 
 */
FrameAnimation.prototype.dispose = function(){
  if(this.state !== STATE_INITIAL){
    this.state = STATE_INITIAL;
    this.taskQueue = null;
    this.timeline.stop();
    this.timeline = null;
    return this;
  }
  return this;    
}

/**
 * 添加一個任務到任務隊列
 * @param taskFn 任務方法
 * @param type   任務類型
 * @private
 */
FrameAnimation.prototype._add = function(taskFn,type){
  this.taskQueue.push({
    taskFn:taskFn,
    type:type
  });
  return this;
}

/**
 * 執行任務
 * @private
 */
FrameAnimation.prototype._runTask = function(){
  if(!this.taskQueue || this.state !== STATE_START){
    return;
  }
  //任務執行完畢
  if(this.index === this.taskQueue.length){
    this.dispose();
    return;
  }
  //得到任務鏈上的當前任務
  var task = this.taskQueue[this.index];
  if(task.type === TASK_SYNC){
    this._syncTask(task);
  }else{
    this._asyncTask(task);
  }
}

/**
 * 同步任務
 * @param  task 執行的任務對象
 * @private
 */
FrameAnimation.prototype._syncTask = function(task){
  var me = this;
  var next = function(){
    //切換到下一個任務
    me._next(task);
  }
  var taskFn = task.taskFn;
  taskFn(next);
}

/**
 * 異步任務
 * @param  task 執行的任務對象
 * @private
 */
FrameAnimation.prototype._asyncTask = function(task){
  var me = this;
  //定義每一幀執行的回調函數
  var enterframe = function(time){
    var taskFn = task.taskFn;
    var next = function(){
      //中止當前任務
      me.timeline.stop();
      //執行下一個任務
      me._next(task);
    };
    taskFn(next,time);
  }
  this.timeline.onenterframe = enterframe;
  this.timeline.start(this.interval);
}

/**
 * 切換到下一個任務,支持若是當前任務須要等待,則延時執行
 * @private
 */
FrameAnimation.prototype._next = function(task){
  this.index++;
  var me = this;
  task.wait ? setTimeout(function(){
    me._runTask();
  },task.wait) :  this._runTask();
}

module.exports = function(){
  return new FrameAnimation();
}

 

webpack配置

  因爲animation幀動畫庫的製做中應用了AMD模塊規範,但因爲瀏覽器層面不支持,須要使用webpack進行模塊化管理,將animation.js、imageloader.js和timeline.js打包爲一個文件

module.exports = {
  entry:{
    animation:"./src/animation.js"
  },
  output:{
    path:__dirname + "/build",
    filename:"[name].js",
    library:"animation",
    libraryTarget:"umd",
  }
}

  下面是一個代碼實例,經過建立的幀動畫庫實現博客開始的動畫效果

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Document</title>
</head>
<body>
<div id="rabbit" style="width:102px;height:80px;background-repeat:no-repeat"></div> 
<script src="../build/animation.js"></script> 
<script>var imgUrl = 'rabbit-big.png';
var positions = ['0,-854','-174 -852','-349 -852','-524 -852','-698 -852','-873 -848'];
var ele = document.getElementById('rabbit');
var animation = window.animation;
var repeatAnimation = animation().loadImage([imgUrl]).changePosition(ele,positions,imgUrl).repeatForever();
repeatAnimation.start(80); 
</script>
</body>
</html>

 

更多實例

  除了能夠實現兔子推車的效果,還可使用幀動畫實現兔子勝利和兔子失敗的效果

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<style>
div{position:absolute;width:102px;height:80px;background-repeat:no-repeat;}  
</style>
</head>
<body>
<div id="rabbit1" style="left:10px;top:50px;"></div>
<div id="rabbit2" style="left:120px;top:50px;"></div>
<div id="rabbit3" style="left:230px;top:50px;"></div> 
<script type="text/javascript" src="http://sandbox.runjs.cn/uploads/rs/26/ddzmgynp/animation.js"></script>
<script>
var baseUrl = 'http://7xpdkf.com1.z0.glb.clouddn.com/runjs/img/';
var images = ['rabbit-big.png','rabbit-lose.png','rabbit-win.png'];
for(var i = 0; i < images.length; i++){
  images[i] = baseUrl + images[i];
}
var rightRunningMap = ["0 -854", "-174 -852", "-349 -852", "-524 -852", "-698 -851", "-873 -848"];
var leftRunningMap = ["0 -373", "-175 -376", "-350 -377", "-524 -377", "-699 -377", "-873 -379"];
var rabbitWinMap = ["0 0", "-198 0", "-401 0", "-609 0", "-816 0", "0 -96", "-208 -97", "-415 -97", "-623 -97", "-831 -97", "0 -203", "-207 -203", "-415 -203", "-623 -203", "-831 -203", "0 -307", "-206 -307", "-414 -307", "-623 -307"];
var rabbitLoseMap = ["0 0", "-163 0", "-327 0", "-491 0", "-655 0", "-819 0", "0 -135", "-166 -135", "-333 -135", "-500 -135", "-668 -135", "-835 -135", "0 -262"];

var animation = window.animation;
function repeat(){
  var repeatAnimation = animation().loadImage(images).changePosition(rabbit1, rightRunningMap, images[0]).repeatForever();
  repeatAnimation.start(80); 
}
function win() {
  var winAnimation = animation().loadImage(images).changePosition(rabbit2, rabbitWinMap, images[2]).repeatForever();
  winAnimation.start(200);
}
function lose() {
  var loseAnimation = animation().loadImage(images).changePosition(rabbit3, rabbitLoseMap, images[1]).repeatForever();
  loseAnimation.start(200);
}
repeat();
win();
lose();
</script>
</body>
</html>
相關文章
相關標籤/搜索