欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

javascript帧动画(实例讲解)

程序员文章站 2022-11-24 23:15:09
前面的话 帧动画就是在“连续的关键帧”中分解动画动作,也就是在时间轴的每帧上逐帧绘制不同的内容,使其连续播放而成的动画。由于是一帧一帧的画,所以帧动画具有非常大的灵活性,...

前面的话

帧动画就是在“连续的关键帧”中分解动画动作,也就是在时间轴的每帧上逐帧绘制不同的内容,使其连续播放而成的动画。由于是一帧一帧的画,所以帧动画具有非常大的灵活性,几乎可以表现任何想表现的内容。本文将详细介绍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方法,执行下一个任务,同时更新任务链索引值

javascript帧动画(实例讲解)

【接口定义】

'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帧动画(实例讲解)就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持。