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