一起动手实现一个js帧动画库

GitHub上有源码和dome,自己可以下载和查看效果 GitHub地址

js中什么是帧动画?

就是通过一张图片切换background Position,或者通过多张图片的切换src来进行的动画效果

相当一部分的浏览器的显示频率是16.7ms,所以在进行帧动画的时候选用16.7ms的频率或者是16.7ms的倍数,避免丢帧。所以我们采用requestAnimationFrame来执行动画的操作。

动画库的接口设计


animation(imgList) // animation 类


对外暴露接口:

changePosition(ele, positions, imageUrl) // 改变元素的 background-position 实现帧动画

changeUrl(ele, imgList) // 通过改变图片元素的URL 实现帧动画

repeat(times) // 动画执行的次数 times为空时无限循环

wait(time) // 动画的等待时间

then(callback) // 自定义执行任务

start(interval) // 动画开始执行,interval 表示动画执行的间隔 默认为16.7ms

pause() // 动画暂停

restart() // 动画从上一次暂停处重新执行


私有接口:

_loadImage(imgList) // 预加载图片

_add(taskFn, type) // 方法添加到任务队列中,taskFn为任务函数,type为任务类型

_runTask() // 执行任务

_next() // 当前任务结束后执行下一个任务

_dispose() // 释放资源

将所有的接口先定义完成

 var TIMING = 1000 / 60 // 一秒60帧的执行速度和浏览器的显示速度相同
/**
 * 帧动画类
 */
function Animation (imgList) {
  /**
   * 0为初始状态
   * 1为运动状态
   * 2为暂停状态
   */
  this._state = 0
  // 任务队列,所有事件都被载入这个任务队列中,在start后被依次执行
  this._taskQuery = []
  // 执行当前任务的索引
  this._index = 0
  // 执行任务前要先加载好img后才能执行下一个任,避免动画执行中img还没加载出来
  this._loadImage(imgList)
}

/**
 * 改变元素的 background-position 实现帧动画
 * @param ele dom对象
 * @param positions 背景位置数组 示例: ['20 0', '40 0']
 * @param imageUrl 背景图片url
 */
Animation.prototype.changePosition = function (ele, positions, imageUrl) {
  return this
}

/**
 * 通过改变图片元素的URL 实现帧动画
 * @param ele dom对象
 * @param imglist 图片url数组
 */
Animation.prototype.changeUrl = function (ele, imglist) {
  return this
}

/**
 * 循环执行
 * @param times 循环执行上一个任务的次数
 */
Animation.prototype.repeat = function (times) {
  return this
}

/**
 * 执行下一个任务的等待时长
 * @param time 等待的时长
 */
Animation.prototype.wait = function (time) {
  return this
}

/**
 * 自定义执行任务
 * @param calback 自定义执行任务
 */
Animation.prototype.then = function (calback) {
  return this
}

/**
 * 动画开始执行
 * @param interval 动画执行的频率
 */
Animation.prototype.start = function (interval) {
  this.interval = interval || TIMING
  return this
}

/**
 * 动画暂停
 */
Animation.prototype.pause = function () {
  return this
}

/**
 * 动画从上一次暂停处重新执行
 */
Animation.prototype.restart = function() {
  return this
}


/**
 * 图片预加载
 * @param imgList 预加载的图片数组
 */
Animation.prototype._loadImage = function (imgList) {
}

/**
 * 添加任务到任务队列
 * @param taskFn 执行的任务
 * @param type 任务类型
 */
Animation.prototype._add = function(taskFn, type) {

}

/**
 * 执行当前任务
 */
Animation.prototype._runTask = function () {

}

/**
 * 切换到下一个任务
 */
Animation.prototype._next = function () {

}

/**
 * 释放资源
 */
Animation.prototype._dispose = function () {

}
复制代码

接下来我们来实现_loadImage

Animation.prototype._loadImage = function (imgList) {
// 每个taskFn都会接受一个next参数,在_runTask中执行taskFn的时候会传入,用来在任务执行完成后进行下一个操作
  var taskFn = function (next) {
    // 图片加载事件
    loadImage(imgList, next)
  }
  /**
   * 0为非动画任务
   * 1为动画任务 比如 changePosition 和 changeUrl 事件
   */
  var type = 0

  this._add(taskFn, type)
}
复制代码

1、这里出现了我们未曾定义的事件loadImage,我们新建一个loadimage.js, 把loadinimage引入当前js中吧 var loadImage = require('./imageLoad')。 不过这个事件要待会来实现,我们先把简单的事先做了吧。

2、我们先吧这里用到的_add先实现把,顺便把_next也实现一下把

/**
 * 添加任务到任务队列
 * @param taskFn 执行的任务
 * @param type 任务类型
 */
Animation.prototype._add = function(taskFn, type) {
  this._taskQuery.push({
    taskFn: taskFn,
    type: type
  })
}
/**
 * 切换到下一个任务
 */
Animation.prototype._next = function () {
  this._index++
  this._runTask()
}
复制代码

是不是很简单。看这里又用到了_runTask,接下来我们分析一下这个函数吧。

3、任务中分为两种任务,一个是非动画任务,一个是动画任务。 那么我们就需要先创建两个方法。在_runTask中判断任务类型来执行对应的方法。 _syncTask和_asyncTask方法。

/**
 * 动画任务
 * @param task 任务对象 {taskFn, type}
 */
Animation.prototype._asyncTask = function (task) {

}

/**
 * 非动画任务
 * @param task 任务对象 {taskFn, type}
 */
Animation.prototype._syncTask = function (task) {

}
复制代码

4、好了我们接下来先看看_runTask的逻辑吧

/**
 * 执行当前任务
 */
Animation.prototype._runTask = function () {
  // 当任务队列没有任务时或者当前不是处于运动状态时就不做任何操作
  if (!this._taskQuery || this._state !== 1) {
    return
  }

  // 当任务全部完成时释放资源
  if (this._index === this._taskQuery.length) {
    this._dispose()
    return
  }

  var task = this._taskQuery[this._index]
  var type = task.type
  if (type === 0) {
    this._syncTask(task)
  } else if (type === 1) {
    this._asyncTask(task)
  }
}
复制代码

5、我们这里用到了_dispose释放资源,这个当然是最好才写的一个方法,_asyncTask和_syncTask当然那最简单的先来练练手。 我们先来看看_syncTask非动画任务来试试

/**
 * 非动画任务
 * @param task 任务对象 {taskFn, type}
 */
Animation.prototype._syncTask = function (task) {
  var _this = this
  var next = function () {
    _this._next()
  }
  var taskFn = task.taskFn
  taskFn(next)
}
复制代码

6、现在写了那么多next有些小伙伴可能有点蒙圈了,怎么都没用过next呀,我有点跑不通逻辑呀。那么我们先来写一个非动画任务then方法来试试

/**
 * 自定义执行任务
 * @param calback 自定义执行任务
 */
Animation.prototype.then = function (calback) {
// 这里的taskFn接受一个next参数
  var taskFn = function (next) {
    calback()
    next() //taskFn在_runTask中被调用了,这里的next就是在_runTask方法中传入的_next方法中
  }
  var type = 0
  // 在_add方法中我们会接受taskFn方法
  this._add(taskFn, type)
  return this
}
复制代码

看到我们的我们的then方法了吗。我来给大家来理一下next吧。其实在_add方法中我们会接受taskFn方法,这里的taskFn又接受一个next参数,taskFn会在_runTask方法中被调用就会传入_next方法那就是这里的next

7、那么_next方法讲明白了,那我们就开始讲repeat(times)方法吧 // 重复执行上一个任务

/**
 * 循环执行
 * @param times 循环执行上一个任务的次数
 */
Animation.prototype.repeat = function (times) {
  var _this = this
  var taskFn = function (next) {
    // times为空 无限循环上一个任务
    if (typeof times === 'undefined') {
      _this._index--
      _this._runTask()
      return
    }
    // times 有数值时
    if (times) {
      times--
      _this._index--
      _this._runTask()
      return
    } else {
      next()
    }
  }
  var type = 0
  this._add(taskFn, type)
  return this
}
复制代码

8、接下来我们在来看看wait方法和start方法吧

/**
 * 执行下一个任务的等待时长
 * @param time 等待的时长
 */
Animation.prototype.wait = function (time) {
  var taskFn = function (next) {
    setTimeout(function () {
      next()
    }, time);
  }
  var type = 0
  this._add(taskFn, type)
  return this
}

/**
 * 动画开始执行
 * @param interval 动画执行的频率
 */
Animation.prototype.start = function (interval) {
  // 本身已经是执行状态或者任务队列里没有任务时不进行操作
  if (this._state === 1 || !this.taskQuery.length) {
    return this
  }
  
  this.interval = interval || TIMING
  this._state = 1
  this._runTask()
  return this
}
复制代码

loadImag

9、现在基本简单的任务也快写完了,那么我们就开始回到第一步中的loadImage方法,那么我们接下来打开loadImage.js来一起完成loadImage方法

/**
 * 图片预加载
 * @param imglist 需要加载的图片数组
 * @param next 加载完成后进入下一个任务
 * @param timeout 图片加载超时时长
 */
function loadImage (imglist, next, timeout) {
  // 全部图片加载完成后的状态,state_array的长度等于imglist的长度时,进入next下一个任务
  var state_array = []
  // 是否超时
  var isTimeout = false

  for (var key in imglist) {
    // 过滤property上的属性
    if (!imglist.hasOwnProperty(key)) {
      continue
    }

    var item = imglist[key]

    if(typeof item === 'string') {
      item = {
        src: item
      }
    }
    // imglist[key]不存在或者typeof item !== 'string'时跳过这条数据
    if (!item || !item.src) {
      continue
    }

    item.image = new Image()
    // 加载图片
    doimg(item)
    
  }
  function (item) {}
}
复制代码

10、那么我们接下来看看doimg方法该怎么写

// 加载图片
  function doimg(item) {
    var img = item.image
    img.src = item.src
    // 加载是否超时
    item.isTimeout = false

    img.onload = function () {
      // 非常重要,如果不打印img,切换图片url来执行的帧动画会请求新的图片资源
      console.log(img)
      item.status = 'loaded'
      done()
    }

    img.onerror = function () {
      item.status = 'error'
      done()
    }
    // 是否超时
    if (timeout) {
      // 超时定时器
      item.timeoutId = 0
      item.timeoutId = setTimeout(onTimeout, timeout)
    }
    // 加载完成后的回调
    function done() {
      img.onload = img.onerror = null
      // 如果没有超时执行,因为超时的时候已经执行过一次了
      if (!item.isTimeout) {
        state_array.push(item.status)
        if (state_array.length === imglist.length) {
          next()
        }
      }
    }

    // 加载超时
    function onTimeout() {
      item.isTimeout = true
      state_array.push('error')
      if (state_array.length === imglist.length) {
        next()
      }
    }
  }
复制代码

当然最后记得把loadImage暴露出来module.exports = loadImage

11、我们loadImage方法已经完成了,那么接下来就是Timeline类,对动画任务进行执行的方法,我们新建一个timeline.js吧。将他进入animation.js中var Timeline = require('./timeline')

Timeline类

Timeline类用来执行动画方法

对外暴露接口

start(interval) // 动画开始,interval 每一次回调的间隔时间

stop() // 动画暂停

restart() // 继续播放

onenterframe(time) // 每一帧执行的函数,该方法不定义内容,给外部重写。time 从动画开始到当前执行的时间

1、首先我们要先定义requestAnimationFrame,这个用来反复执行动画方法。至于为什么用这个文章开头已经解释过了

// 一秒60帧与显示屏刷新频率同步
var TIMEOUT = 1000 / 60

var requestAnimationFrame = (function () {
  return window.requestAnimationFrame ||
    window.webkitRequestAnimationFrame ||
    function (callback) {
      return window.setTimeout(callback, TIMEOUT);
    }
})()

var cancelAnimationFrame = (function () {
  return window.cancelAnimationFrame ||
    window.webkitCancelAnimationFrame ||
    function (id) {
      return window.clearTimeout(id);
    }
})()
复制代码

2、我们来书写所有方法

/**
 * 时间轴类 Timeline
 */

function Timeline() {
    /**
    * 0表示初始状态
    * 1表示播放状态
    * 2表示暂停状态
    */
  this._state = 0
  // 定时器id
  this.animationHandle = 0
}

/**
 * 时间轴上每一次回调执行的函数
 * @param time 从动画开始到当前执行的时间
 */
Timeline.prototype.onenterframe = function (time) {
}

/**
 * 动画开始
 * @param interval 每一次回调的间隔时间
 */
Timeline.prototype.start = function (interval) {
}

/**
 * 动画停止
 */
Timeline.prototype.stop = function () {
}
/**
 * 重新执行
 */
Timeline.prototype.restart = function () {
}

/**
 * 节流函数 对节流函数不理解的朋友可以去看我的防抖和节流,就知道什么是节流了
 * @param timeline timeline对象
 * @param startTime  动画开始的时间
 */
function startTimeline(timeline, startTime) {
}



module.exports = Timeline

复制代码

3、我们先来完成start方法吧

/**
 * 动画开始
 * @param interval 每一次回调的间隔时间
 */
Timeline.prototype.start = function (interval) {
  if (this._state === 1) {
    return this
  }
  this._state = 1
  this.interval = interval || TIMEOUT
  startTimeline(this, +new Date())
}
复制代码

4、接下来我们看看startTimeline干了什么

/**
 * 节流函数 对节流函数不理解的朋友可以去看我的防抖和节流,就知道什么是节流了
 * @param timeline timeline对象
 * @param startTime  动画开始的时间
 */
function startTimeline(timeline, startTime) {
  // 记录这一次开始的时间
  timeline.startTime = startTime

  // 记录节流函数上一次执行的时间
  var lastTime = +new Date()
  nextTask()

  function nextTask(){
    var now = +new Date()
    timeline.animationHandle = requestAnimationFrame(nextTask)
    if (now - lastTime >= timeline.interval) {
      lastTime = now
      // 执行动画的总时长
      timeline.onenterframe(now - startTime)
    }
  }
}
复制代码

5、我们该让动画停下来了

/**
 * 动画停止
 */
Timeline.prototype.stop = function () {
  if (this._state !== 1) {
    return this
  }
  this._state = 2
  if (this.startTime) {
    // dur 总执行的时长
    this.dur = +new Date() - this.startTime
    cancelAnimationFrame(this.animationHandle)
  }
}
复制代码

6、让我们的动画接着上一帧重新跑起来吧

/**
 * 重新执行
 */
Timeline.prototype.restart = function () {
  if (this._state === 1) {
    return this
  }
  if (!this.dur) {
    return this
  }
  this._state = 1
  //startTimeline中会对传入的值进行 +new Date() - startTime 拿到执行总时长,也就是要加上之前的总时长this.dur = +new Date() - (+new Date() - this.dur)
  startTimeline(this, +new Date() - this.dur)
}
复制代码

我们的Timeline类就这样完成了。把Timeline类引入到animation.js中把var Timeline = require('./timeline')同时在Animation类中实例化一下把this.timeline = new Timeline()

7、接下来我们看看怎么写一下animation中的动画执行方法_asyncTask

/**
 * 动画任务
 * @param task 任务对象 {taskFn, type}
 */
Animation.prototype._asyncTask = function (task) {
  var _this = this
  
  function enterframe(time) {
    var taskFn = task.taskFn
    function next () {
      _this.timeline.stop()
      _this._next()
    }
    taskFn(next, time)
  }

  this.timeline.onenterframe = enterframe
  this.timeline.start(this.interval)
}
复制代码

8、接下来我们定义一个动画方法试试吧changePosition

/**
 * 通过改变图片背景位置,实现帧动画
 * @param {Element} ele 
 * @param {Array} positions 
 * @param {String} imageUrl 
 */
Animation.prototype.changePosition = function (ele, positions, imageUrl) {
  var len = positions.length
  var taskFn
  var type
  var _this = this
  if (len) {
    taskFn = function (next, time) {
      if (imageUrl) {
        ele.style.backgroundImage = 'url(' + imageUrl + ')'
      }
      var index = Math.min(time / _this.interval | 0, len)
      var position = positions[index - 1].split(' ')

      ele.style.backgroundPosition = position[0] + 'px ' + position[1] + 'px'

      if (index === len) {
        next()
      }
    }
    type = 1
    this._add(taskFn, type)
  }

  return this
}
复制代码

10、我们先试试能不能跑起来后再进行写下面的几个接口吧 webpack.config.js

module.exports = {
  entry: {
    animation: './src/animation.js'
  },
  output: {
    path: __dirname + '/build',
    filename: '[name].js',
    library: 'animation',
    libraryTarget: 'umd'
  }
}
复制代码

执行命令

npm -g webpack

webpack

11、新建一个dome文件夹index.html。图片行寻找

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
  <style>
    #rabbit{
      width: 600px;
      height: 100px;
    }
  </style>
</head>
<body>
  <div id="rabbit"></div>
  <script src="../build/animation.js"></script>
  <script>
    var positions = ['120 0', '240 0', '360 0']
    var rabbitEle = document.querySelector('#rabbit')
    var rabbit = animation(['./timg.jpg'])
    .changePosition(rabbitEle, positions, './timg.jpg')
    .repeat(10)

    rabbit.start(100)
  </script>
</body>
</html>
复制代码

12、changeUrl利用多张图片切换的动画

/**
 * 通过改变图片元素的URL 实现帧动画
 * @param ele dom对象
 * @param imglist 图片url数组
 */
Animation.prototype.changeUrl = function (ele, imglist) {
  var len = imglist.length
  var taskFn
  var type
  var _this = this
  if (len) {
    taskFn = function (next, time) {
      
      var index = Math.min(time / _this.interval | 0, len - 1)
      var imageUrl = imglist[index]
      ele.style.backgroundImage = 'url(' + imageUrl + ')'

      if (index === len - 1) {
        next()
      }
    }
    type = 1
    this._add(taskFn, type)
  }
  return this
}
复制代码

13、pause、restart、_dispose三个最后的方法

/**
 * 动画暂停
 */
Animation.prototype.pause = function () {
  if (this._state !== 1) {
    return this
  }
  this._state = 2
  this.timeline.stop()
  return this
}

/**
 * 动画从上一次暂停处重新执行
 */
Animation.prototype.restart = function () {
  if (this._state !== 2) {
    return this
  }
  this._state = 1
  this.timeline.restart()
  return this
}
/**
 * 释放资源
 */
Animation.prototype._dispose = function () {
  if (this._state !== STATE_INITTAL) {
    this._state = STATE_INITTAL
    this.taskQuery = null
    this.timeline.stop()
    this.timeline = null
  }
}
复制代码

我们完成了

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值