js 实现帧动画原理
img src
div background
和上一个差别不大
Animation.prototype.changeSrc = function (el,imgList) {
var me = this;
var len = imgList.length;
var taskFn;
var type;
if (len) {
taskFn = function (next, time) {
var index = Math.min(time / me.interval | 0 , len-1);
el.src = imageList[index]
//结束时跳到下一个
if (index === len - 1) {
next();
}
}
type = TASK_ASYNC;
} else {
//这里的next就是全局方法 next
taskFn = next;
type = TASK_SYNC;
}
return this._add(taskFn,type)
}
接下来就是具体用timeline的各个接口了。
Animation.prototype.changePosition = function (el, positions, imageUrl) {
var me = this;
var len = position.length;
var taskFn;
var type;
if (len) {
taskFn = function (next, time) {
//next, time 是._asyncTask的时候,传过来的
//如果传入了图片地址,那么修改el的背景图
if (imageUrl) {
el.style.backgoundImage = 'url(' + imageUrl + ')';
}
//如果当前已经执行的回调次数 还没到最大值( 设定中的动画执行次数)
//那么选择当前已经执行的回调次数作为索引(从0开始数,所以可以直接用)
// |0 既 Math.floor
var index = Math.min(time / me.interval | 0, len - 1)
//是这样[ 200 332 , 333 33,]的格式
var position = positions[index].split(' ')
el.style.backgoundPosition = position[0]+'px' +' '+ position[1]+'px'
//动画循环完成了的时候。
if (index === len - 1) {
next();
}
}
type = TASK_ASYNC;
} else {
//这里的next就是全局方法 next
taskFn = next;
type = TASK_SYNC;
}
return this._add(taskFn,type)
}
function next(callback) {
callback && callback()
}
//异步方法是这样的:
Animation.prototype._asyncTask = function(task) {
//设置timeline里面的每帧都调用的回调函数onenterframe。
var enterFrame = function (time) {
var me = this;
var taskFn = task.taskFn;
//回调函数的设置
var next = function () {
//停止当前的
me.timeline.stop()
//执行下一个
me._next()
}
taskFn(next,time)
}
this.timeline.onenterframe = enterFrame;
//通过this.prototype.start传进来的参数Interval传给timeline
this.timeline.start(this.interval)
}
创建Timeline实例,然后用在异步方法里。
function Animation() {
this.timeline = new Timeline();
//来自用户设定的时间间隔
this.interval = 0;
}
Animation.prototype._asyncTask = function(task) {
//设置timeline里面的每帧都调用的回调函数onenterframe。
var enterFrame = function (time) {
var me = this;
var taskFn = task.taskFn;
//回调函数的设置
var next = function () {
//停止当前的
me.timeline.stop()
//执行下一个
me._next()
}
taskFn(next,time)
}
this.timeline.onenterframe = enterFrame;
//通过this.prototype.start传进来的参数Interval传给timeline
this.timeline.start(this.interval)
}
我的笔记代码并不是完全和视频一致的。
这一小节有趣的一点是做了setInterval的迭代式形式。 nextTick函数,其中有requestAnimationFrame(nextTick)。
其中不断更新记录上一帧结束的时间lastTick。
而这样的话,lastTick- startTime /interval 大概就是过了多少帧了。
代码:
function startTimeline(timeline, startTime) {
//设置实例上的数据,储存用
timeline.startTime = startTime
//为用requestAnimationFrame加上的callback.interval
nextTick.interval = timeline.interval
//记录最后一次回调的时间戳
var lastTick = +new Date();
nextTick();
//+new Date() == new Date().getTime()
//其实这是一个迭代形式的setInterval
//每一帧都执行的函数哦
function nextTick() {
//判断如果时间到interval设定了的时间了,就执行回调,
var now = +new Date();
timeline.animationHandler = requestAnimationFrame(nextTick)
if (now - lastTick >= timeline.interval) {
timeline.onenterframe(now - startTime)
//并且更新最后一次回调的时间
lastTick = now;
}
}
}
类:
function Timeline(interval) {
//当前动画的状态。
this.state = STATE_INITIAL;
//当前动画进行时间。
this.startTime = 0;
//每次回调的时间间隔。
this.interval = DEFAULT_INTERVAL;
//setTimeout的ID
this.animationHandler = 0;
//动画开始了多久,暂停的时候储存留待再次开始
this.dur = 0;
}
/**
* 动画停止
*/
Timeline.prototype.stop = function (interval) {
if (this.state !== STATE_START) {
return
}
this.state = STATE_STOP;
//如果动画已经开始了,那么记录一下已经开始多久了。
if (this.stateTime) {
this.dur = +new Date() - this.startTime
}
cancelAnimationFrame(this.animationHandler)
}
/**
* 重新播放动画
*/
Timeline.prototype.restart = function (interval) {
if (this.state !== STATE_START) {
return
}
if (!this.dur || this.interval) {
return
}
this.state = STATE_START
//从停止那一刻算起,开始动画
startTimeline(this, +new Date() - this.dur)
}
这里是定义具体执行异步函数的方法。
首先是处理window.requestAnimationFrame 和window.cancelAnimationFrame的兼容性问题。这里使用的自执行函数,通过 || 返回经过类型转换后为true的值。如果不支持这个方法,就用setTimeout,如果用setTimeout,默认的时间间隔是1000/60毫秒。
代码:
var DEFAULT_INTERVAL = 1000/60
//requestAnimationFrame每17毫秒会刷新一次。
var requestAnimationFrame = (function () {
//浏览器兼容
return window.requestAnimationFrame ||
window.webketRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
//如果不支持,则用setTimeout ,默认为 1000/60 毫秒后
function (callback) {
return window.setTimeout(callback(),callback.interval || DEFAULT_INTERVAL)
}
})()
var cancelAnimationFrame = (function () {
return window.cancelAnimationFrame ||
window.webketCancelAnimationFrame ||
window.mozCancelAnimationFrame ||
window.oCancelAnimationFrame ||
//如果不支持,则用setTimeout ,默认为 1000/60 毫秒后
function (id) {
return window.clearTimeout(id)
}
})()
接下来定义class。
总结:
这一个小节的内容,是把需要的任务方法添加到任务队列,然后将任务按照不同类型处理。
这里需要注意的是,每一个任务函数都要有处理callback的能力,这样才能next;
第一个任务,将任务添加到任务队列,是这样做的。在使用这个库的时候,demo会首先调用Animation.prototype.loadImage()这个方法。而在代码中,并不是直接调用预加载loadImage()方法,而是把这个方法定义为对象中的一个属性,并定义它的类型,然后将完成的对象加入一个列表中,并且返回this。
这个列表就是this.taskQueue.
格式: [{taskFn: taskFn,
type: type}]
Animation.prototype._add 将任务加入到任务队列中,
最终this._add返回this。这个this也被Animation.prototype.loadImage()最终返回。
代码:
Animation.prototype.loadImage = function (imageList) {
var taskFn = function (next) {
//为了保证不影响原对象。使用slice
loadImage(imageList.slice(),next)
}
//设置为同步任务。
var type = TASK_SYNC;
//放到任务队列中,同时返回由this._add()所返回的this
return this._add(taskFn, type);
}
Animation.prototype._add = function (taskFn, type) {
this.taskQueue.push({
taskFn: taskFn,
type: type
})
return this;
}
任务队列taskQueue已经有了。接下来就是执行里面的任务了。
通过调用Animation.prototype.start(),
设置了任务执行状态,以及时间间隔之后
这里调用的是,Animation.prototype._runTasks
_runTasks中,遍历并没有用for 语句,而是通过迭代。
代码:
Animation.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._runTasks();
return this;
}
通过在this上定义一个值为0的index属性,index会在完成任务的时候递增,并且在Index == 任务队列的长度的时候,也就是执行完所有任务的时候,停止执行,释放内存等。
代码:
function Animation() {
//任务队列
this.taskQueue = [];
//控制遍历的计数器
this.index = 0;
//执行状态
this.state = STATE_INITIAL
}
使用this.taskQueue[this.index]的形式取出当前方法。
index每次递增都会重新调动任务队列的执行函数_runTasks
_runTasks里面的逻辑是,
通过之前this._add的时候,设置的方法状态,来确定是异步还是同步。
代码:
Animation.prototype._runTasks = function() {
//所以呢,它应该是接收一个任务队列,然后按照异步或者同步的方式分别执行。
//在这里这个任务队列在实例上的taskQueue
if (!this.taskQueue.length || this.state !== STATE_START ) {
return;
}
//任务执行完毕,释放内存
if (this.index === this.taskQueue.length) {
this.dispose()
return;
}
//由于由this.index控制遍历,所以从当前任务开始,this.index默认为第一个
//这里已经是经过this._add后,对象的形式{taskFn: taskFn, type: type}
var task = this.taskQueue[this.index]
//task可能是同步任务或者异步任务,两种任务执行方式不同
if (task.type === TASK_SYNC) {
this._syncTask(task)
} else if (task.type === TASK_ASYNC) {
this._asyncTask(task)
}
}
这里this.index++,以及_runTasks的再次调用,是在同步或者异步任务执行完毕之后,由负责同步任务或者异步任务的函数来调用的。
代码:
/*执行同步任务的方法 */
Animation.prototype._syncTask = function (task) {
//保留this,因为会在闭包中调用。
var m = this
//执行完毕的回调
var next = function () {
me._next()
}
//取出要执行的方法
var taskFn = task.taskFn;
//执行它,并传入回调。
taskFn(next)
}
/*
*切换到下一个任务的方法
*/
Animation.prototype._next = function () {
this.index++;
//这里是遍历。
this._runTasks();
}
/**
* 异步任务执行方法
*/
Animation.prototype._asyncTask = function(task) {
//未待续完,使用requestAnimationFrame
}
Animation.prototype._runTasks 负责确定极限值的收尾工作,以及确定任务状态并且执行负责同步任务或者负责异步任务的函数。
所有的数据都由实例来负责。在实例上定义。
总结下,大概是这样的逻辑
预加载图片流程
由定义在proptotype上的LoadImage方法把数组交给真正去加载图片的组件模块。该数组为[{src:'imageurl'}] 这样的格式。
经由该模块之后,会返回一个数组,其中标志了哪些图片加载成功,哪些没有。为了这样做的话,就需要给图片分别加一个对应ID。
在这个模块中,会将数组中的对象遍历取出来加载。为了这样做,需要先排除错误数据,。不存在可以遍历的对象,该对象不存在src,以及在Prototype上的属性,如果是string的话,做类型转换成Object。
遍历是用for in 语句。
for (var key in images) {}
在真正的加载过程中,首先要新建Image对象。将其绑定到window上。像前面说的,为Image加上ID,同时数据中的对象也要保存这个ID。
设置onload, onerror的处理后,设置Image对象的src,就会加载了。
而为了全部加载成功之后之后调用callback回调函数,需要进行计数和确定完成的方法。
在这里是遍历的时候count ++ ,每次加载完成,无论失败与否,都会 --count ,
当然失败与否在该对象上进行的标记不同,这里用
status 为loaded 作为成功。status 为error作为失败
最后当没有图片可以加载 的时候,就可以加载回调函数了。
当然还有对加载超时的优化。
这里我觉得比较特别的是,并没有用var timer 而用了 timoutId 这个变量设置setTimeout
这里分为两种情况,所以最开始也设置了一个局部变量作为成功或者失败的标志位。var success
如果加载成功,那么success为true, clearTimeout
如果所有的遍历和加载都完成了,而且success == false 的情况,当然就是setTimeout了
https://github.com/ustbhuangyi/animation/tree/gh-pages/images
js实现帧动画
帧动画原理