帧动画就是在连续的关键帧中分解动画动作,也就是在时间轴上的每帧上逐帧绘制不同的内容,使其连续播放成动画。常见的帧动画方式有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;