携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第11天,点击查看活动详情
什么是帧动画?
所谓帧动画就是在连续的关键帧中分解动画动作,也就是在时间轴上的每帧绘制不同的内容,使其连续播放而成的动画。
由于是一帧一帧的画,所以帧动画具有非常大的灵活性,几乎可以表现任何想表达的内容。
常见的帧动画方式有:
- GIF
- CSS3 animation
- javascript
GIF 和 CSS3 animation实现帧动画的不足:
- 不能灵活的控制动画的播放和暂停(gif css3)
- 不能捕捉到动画完成的事件(gif)
- 不能对帧动画做更加灵活的扩展(gif css3)
js实现帧动画库的原理
js实现帧动画库有两种方式:
- 如果有多张帧图片,用一个image标签去承载图片,定时改变image的src属性(不推荐)
- 把所有动画关键帧绘制在一张图片中,把图片作为元素的background-image,定时改变元素的background-position属性(推荐)
设计帧动画库的步骤
需求分析
- 支持图片预加载
- 支持两种动画播放方式,以及自定义每帧动画
- 支持单组动画控制循环次数(可支持无限次)
- 支持一组动画完成,进行下一组动画
- 支持每个动画完成后有等待时间
- 支持动画的暂停和播放
- 支持动画完成后执行回调函数
编程接口
- loadImage(imglist): 预加载图片
- changePosition(ele, positions, imageUrl): 通过改变元素的background-position实现动画
- changeSrc(ele, imglist): 通过改变image元素的src
- enterFrame(callback): 每一帧动画执行的函数,相当于用户可以自定义每一帧动画的callback,也就是用户来定义动画的逻辑
- repeat(times): 动画重复执行的次数,times为空表示无限次
- repeatForever(): 无限次重复上一次动画,相当于repeat(),更友好的一个接口
- wait(time): 每个动画执行完成后等待的时间
- then(callback): 动画执行完后的回调函数
- start(interval): 动画开始执行,interval表示动画执行的间隔,默认是16.67ms
- pause(): 动画暂停
- restart(): 动画从上一次暂停处开始执行
- dispose(): 释放资源
调用方式
支持链式调用,我们期望用动词的方式来描述接口,调用方式如下:
var animation = require('animation')
var demoAnimation = animation()
.loadImage(images)
.changePosition(ele, positions, images[0])
.repeat(2)
.then(() => {
// 动画执行完成后调用此函数
})
demoAnimation.start(80)
代码设计
- 我们把图片预加载 -> 动画执行 -> 动画结束 等系列操作看成是一条任务链(数组)
任务链的任务分两种: 一种是同步执行完毕的,一种是异步定时执行的(通过定时器或者raf)
-
记录当前任务链的索引
-
每个任务执行完毕后,通过调用
next方法,执行下一个任务,同时更新任务链的索引值
实现链式任务调用
实现步骤
- 定义一个帧动画的类
var DEFAULT_INTERVAL = 1000 / 60
// 初始化状态
var STATE_INITIAL = 0
// 开始状态
var STATE_START = 1
// 停止状态
var STATE_STOP = 2
// 同步任务
var TASK_SYNC = 0
// 异步任务
var TASK_ASYNC = 1
function Animation() {
this.taskQueue = []
this.index = 0
this.state = STATE_INITIAL
}
- 定义添加任务对任务链中的方法
/**
* 添加任务到任务对垒
* @param {*} taskFn 任务方法
* @param {*} type 任务类型
*/
Animation.prototype._add = function (taskFn, type) {
this.taskQueue.push({
taskFn: taskFn,
type: type
})
return this
}
- 添加任务到任务链中
Animation.prototype.loadImage = function (imglist) {
var taskFn = function (next) {
...
}
var type = TASK_SYNC
return this._add(taskFn, type)
}
把图片预加载添加到任务链中,其他的任务都是执行类似的逻辑,即调用this._add(taskFn, type)。
- 定义动画执行的逻辑,通过
next来执行下一个任务,从而执行完任务链中的所有任务
/**
* 开始执行任务,异步定时任务执行
* @param interval 时间间隔
*/
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 || DEFAULT_INTERVAL
this._runTask()
return this
}
this._runTask
Animation.prototype._runTask = function () {
if (!this.taskQueue.length || 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)
}
}
同步任务vs异步任务
// 同步
Animation.prototype._syncTask = function (task) {
var me = this
var next = function () {
// 切换到写一个任务,此时this指向全局window
me._next(task)
}
var taskFn = task.taskFn
taskFn(next)
}
/**
* 执行下一个任务
* @param task 当前任务
*/
Animation.prototype._next = function (task) {
var me = this
this.index++
this._runTask()
}
// 异步
Animation.prototype._asyncTask = function (task) {
var me = this
var taskFn = task.taskFn
var next = function () {
// 停止当前任务
me.stop()
// 执行下一个任务
me._next(task)
}
taskFn(next, time)
}
next在同步任务和异步任务中的执行时机的区别:
- 在同步任务中,执行完同步代码后,直接执行
next
Animation.prototype.then = function (callback) {
var taskFn = function (next) {
callback()
next()
}
var type = TASK_SYNC
return this._add(taskFn, type)
}
- 在异步任务中,需要异步任务完成之后,才能执行
next
在changePosition这个异步任务中,只有遍历完所有的positions才能执行next
if (index === len) {
next()
}
Animation.prototype.changePosition = function (ele, positions, imgUrl) {
var len = positions.length
var taskFn
var type
if (len) {
var me = this
// time是动画从开始到目前的时间,通过这个时间和interval来计算index
taskFn = function (next, time) {
if (imgUrl) {
ele.style.backgroundImage = 'url(' + imgUrl + ')'
}
// 获得当前背景图片的位置索引
// | 0 等于Math.floor
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)
}
总结
通过上面的学习,可以知道怎么设计一个任务链,同时任务链的任务有同步的,也有异步的。这其中的关键点有如下几点:
- 通过一个函数来包裹任务,函数参数就是
next函数,下面演示伪代码:
// 真实的任务
function realTask(next) {
// ....
next()
}
const taskFn = function (next) {
// 真实的任务执行
realTask()
}
// 把任务推倒队列中
this.queue.push({
taskFn
})
- 执行任务
let index = 0
function start () {
let task = this.queue[index]
let next = function () {
index++
// 继续执行
start()
}
task(next)
}
- 对于异步任务,执行
next的时机是在异步任务结束后