从零到一实现一个原生js帧动画库(上)

406 阅读5分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第11天,点击查看活动详情

什么是帧动画?

所谓帧动画就是在连续的关键帧中分解动画动作,也就是在时间轴上的每帧绘制不同的内容,使其连续播放而成的动画。

由于是一帧一帧的画,所以帧动画具有非常大的灵活性,几乎可以表现任何想表达的内容。

常见的帧动画方式有:

  • GIF
  • CSS3 animation
  • javascript

GIF 和 CSS3 animation实现帧动画的不足:

  • 不能灵活的控制动画的播放和暂停(gif css3)
  • 不能捕捉到动画完成的事件(gif)
  • 不能对帧动画做更加灵活的扩展(gif css3)

js实现帧动画库的原理

js实现帧动画库有两种方式:

  • 如果有多张帧图片,用一个image标签去承载图片,定时改变image的src属性(不推荐)
  • 把所有动画关键帧绘制在一张图片中,把图片作为元素的background-image,定时改变元素的background-position属性(推荐)

设计帧动画库的步骤

需求分析

  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表示动画执行的间隔,默认是16.67ms
  10. pause(): 动画暂停
  11. restart(): 动画从上一次暂停处开始执行
  12. dispose(): 释放资源

调用方式

支持链式调用,我们期望用动词的方式来描述接口,调用方式如下:

var animation = require('animation')
var demoAnimation = animation()
    .loadImage(images)
    .changePosition(ele, positions, images[0])
    .repeat(2)
    .then(() => {
        // 动画执行完成后调用此函数
    })

demoAnimation.start(80)

代码设计

  1. 我们把图片预加载 -> 动画执行 -> 动画结束 等系列操作看成是一条任务链(数组)

任务链的任务分两种: 一种是同步执行完毕的,一种是异步定时执行的(通过定时器或者raf)

  1. 记录当前任务链的索引

  2. 每个任务执行完毕后,通过调用next方法,执行下一个任务,同时更新任务链的索引值

image.png

实现链式任务调用

实现步骤

  1. 定义一个帧动画的类
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
}
  1. 定义添加任务对任务链中的方法
/**
 * 添加任务到任务对垒
 * @param {*} taskFn 任务方法
 * @param {*} type 任务类型
 */
Animation.prototype._add = function (taskFn, type) {
  this.taskQueue.push({
    taskFn: taskFn,
    type: type
  })
  return this
}
  1. 添加任务到任务链中
Animation.prototype.loadImage = function (imglist) {
  var taskFn = function (next) {
    ...
  }
  var type = TASK_SYNC
  return this._add(taskFn, type)
}

把图片预加载添加到任务链中,其他的任务都是执行类似的逻辑,即调用this._add(taskFn, type)

  1. 定义动画执行的逻辑,通过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的时机是在异步任务结束后