javascript帧动画(实例讲解)
前面的话
帧动画就是在“连续的关键帧”中分解动画动作,也就是在时间轴的每帧上逐帧绘制不同的内容,使其连续播放而成的动画。由于是一帧一帧的画,所以帧动画具有非常大的灵活性,几乎可以表现任何想表现的内容。本文将详细介绍javascript帧动画
概述
【分类】
常见的帧动画的方式有三种,包括gif、css3 animation和javascript
git和css3 animation不能灵活地控制动画的暂停和播放、不能对帧动画做更加灵活地扩展。另外,gif图不能捕捉动画完成的事件。所以,一般地,使用javascript来实现帧动画
【原理】
js实现帧动画有两种实现方式
1、如果有多张帧动画图片,可以用一个image标签去承载图片,定时改变image的src属性(不推荐)
2、把所有的动画关键帧都绘制在一张图片里,把图片作为元素的background-image,定时改变元素的background-position属性(推荐)
因为第一种方式需要使用多个http请求,所以一般地推荐使用第二种方式
【实例】
下面是使用帧动画制作的一个实例
<div id="rabbit" ></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>
通用帧动画
下面来设计一个通用的帧动画库
【需求分析】
1、支持图片预加载
2、支持两种动画播放方式,及自定义每帧动画
3、支持单组动画控制循环次数(可支持无限次)
4、支持一组动画完成,进行下一组动画
5、支持每个动画完成后有等待时间
6、支持动画暂停和继续播放
7、支持动画完成后执行回调函数
【编程接口】
1、loadimage(imglist)//预加载图片
2、changeposition(ele,positions,imageurl)//通过改变元素的background-position实现动画
3、changesrc(ele,imglist)//通过改变image元素的src
4、enterframe(callback)//每一帧动画执行的函数,相当于用户可以自定义每一帧动画的callback
5、repeat(times)//动画重复执行的次数,times为空时表示无限次
6、repeatforever()//无限重复上一次动画,相当于repeat()
7、wait(time)//每个动画执行完成后等待的时间
8、then(callback)//动画执行完成后的回调函数
9、start(interval)//动画开始执行,interval表示动画执行的间隔
10、pause()//动画暂停
11、restart()//动画从上一交暂停处重新执行
12、dispose()//释放资源
【调用方式】
支持链式调用,用动词的方式描述接口
【代码设计】
1、把图片预加载 -> 动画执行 -> 动画结束等一系列操作看成一条任务链。任务链包括同步执行和异步定时执行两种任务
2、记录当前任务链的索引
3、每个任务执行完毕后,通过调用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" ></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" ></div> <div id="rabbit2" ></div> <div id="rabbit3" ></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>
以上这篇javascript帧动画(实例讲解)就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持。