javascript帧动画(实例讲解)
前端之家 收集整理的这篇文章主要介绍了
javascript帧动画(实例讲解) ,
前端之家 小编觉得挺不错的,现在分享给大家,也给大家做个参考。
前面的话
帧动画就是在“连续的关键帧”中分解动画动作,也就是在时间轴的每帧上逐帧绘制不同的内容 ,使其连续播放而成的动画。由于是一帧一帧的画,所以帧动画具有非常大的灵活性,几乎可以表现任何想表现的内容 。本文将详细介绍javascript帧动画
概述
【分类 】
常见的帧动画的方式有三种,包括 gif、CSS3 animation和javascript
git和CSS3 animation不能灵活地控制动画的暂停和播放、不能对帧动画做更加灵活地扩展。另外,gif图不能捕捉动画完成的事件。所以,一般地,使用javascript来实现帧动画
【原理】
js实现帧动画有两种实现方式
1、如果有多张帧动画图片 ,可以用一个image标签 去承载图片 ,定时改变image的src属性 (不推荐)
2、把所有的动画关键帧都绘制在一张图片 里,把图片 作为元素的background-image,定时改变元素的background-position属性 (推荐)
因为第一种方式需要使用多个HTTP请求,所以一般地推荐使用第二种方式
【实例】
下面是使用帧动画制作的一个实例
通用帧动画
下面来设计一个通用的帧动画库
【需求分析】
1、支持 图片 预加载
2、支持 两种动画播放方式,及自定义 每帧动画
3、支持 单组动画控制循环次数 (可支持 无限次)
4、支持 一组动画完成,进行下一组动画
5、支持 每个动画完成后有等待时间
6、支持 动画暂停和继续播放
7、支持 动画完成后执行 回调函数
【编程接口】
1、loadImage(imglist)//预加载 图片
2、changePosition(ele,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方法 ,执行下一个任务,同时更新任务链索引值
【接口定义】
/*
添加 一个同步任务,去
预加载 图片
@param imglist 图片 数组
*/
FrameAnimation.prototype.loadImage = function(imglist){}
/* 添加 一个异步定时任务,通过定时改变图片 背景位置,实现帧动画
@param ele dom对象
@param positions 背景位置数组
@param imageUrl 图片 URL地址
*/
FrameAnimation.prototype.changePosition = function(ele,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
预加载 图片 函数
@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
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实现的完整代码
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,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打包为一个文件
下面是一个代码 实例,通过创建的帧动画库实现博客 开始的动画效果
<
Meta charset="UTF-8">
Document