GitHub has the source code and Dome, you can download and view the effect of GitHub address

What is frame animation in JS?

It is the animation effect by switching background Position of one image, or by switching SRC of multiple images

Most browsers display at 16.7ms, so use 16.7ms or multiples of 16.7ms for frame animation to avoid frame loss. So we use requestAnimationFrame to perform the animation.

Animation library interface design


Animation (imgList) / / animation classes


External exposed interface:

ChangePosition (ele, Positions, imageUrl) // Change the background-position of an element to achieve frame animation

ChangeUrl (ele, imgList) // Animate frames by changing the URL of an image element

Repeat (times) // The number of times the animation is executed

Wait (time) // Animation wait time

Then (callback) // Customize execution tasks

Start (interval) // Animation starts. Interval indicates that the animation is executed at an interval of 16.7ms by default

Pause () // Animation pauses

Restart () // The animation is re-executed from the last pause


Private interface:

_loadImage(imgList

_add(taskFn, type) // The method is added to the task queue. TaskFn is the task function and type is the task type

_runTask() // Executes the task

_next() // Execute the next task after the current task

_dispose() // Release resources

Define all interfaces first

Var TIMING = 1000/60 // 60 frames per second execution speed is the same as the browser display speed /** ** frame animation class */functionAnimation (imgList) {this._state = 0; This._taskquery = [] // this._index = 0 // img must be loaded before executing the next task. Avoid animation execution when img is not loaded out this._loadimage (imgList)} /** * change element background-position to implement frame animation * @param ele DOM object * @param positions Example background position array: [20 '0'.'40 0'] * @ param imageUrl background picture url * / Animation. The prototype. ChangePosition =function (ele, positions, imageUrl) {
  returnThis} / * * * through changing the URL of image element to achieve Animation to * @ param ele dom object * @ param imglist picture URL array * / Animation. The prototype. ChangeUrl =function (ele, imglist) {
  returnThis} /** * loops through * @paramtimesCycle to perform a task on the number of * / Animation in the prototype. Repeat =function (times) {
  returnThis} /** * wait time for the next task * @param time Wait time */ animation.prototype. wait =function (time) {
  returnThis} /** * custom execution task * @param calback Custom execution task */ animation.prototype. then =function (calback) {
  returnThis} / Animation start * * * * @ param interval Animation execution frequency * / Animation in the prototype. Start =function (interval) {
  this.interval = interval || TIMING
  returnThis} / suspended Animation * * * * / Animation. The prototype. Pause =function () {
  returnThis} / * * * * / Animation Animation from the last time to suspend to perform the prototype. Restart =function() {
  return/ / animation.prototype._loadimage = / / animation.prototype._loadimage =function(imgList) {} /** * Adds the task to the task queue * @param taskFn Executes the task * @paramtypeTask type */ animation.prototype. _add =function(taskFn, type} /** * Executes the current task */ animation.prototype. _runTask =function() {} /** * switch to the next task */ animation.prototype. _next =function() {} /** * Dispose = / animation.prototype._dispose =function() {}Copy the code

So let’s implement _loadImage

Animation.prototype._loadImage = function(imgList) {// Each taskFn takes a next argument, which is passed when the taskFn is executed in _runTask and is used to perform the next operation after the task is completedfunction(next) {// Image load event loadImage(imgList, next)} /** * 0 for non-animation tasks * 1 for animation tasks such as changePosition and changeUrl events */ vartype = 0

  this._add(taskFn, type)}Copy the code

Var loadImage = require(‘./imageLoad’) var loadImage = require(‘./imageLoad’) But that’s going to happen later, so let’s do the easy stuff first.

2. Let’s implement _add and _next first

/** * Add the task to the task queue * @param taskFn Executes the task * @paramtypeTask type */ animation.prototype. _add =function(taskFn, type) {
  this._taskQuery.push({
    taskFn: taskFn,
    type: type} /** * switch to the next task */ animation.prototype. _next =function () {
  this._index++
  this._runTask()
}
Copy the code

Isn’t that easy? Now that _runTask is used again, let’s look at this function.

3, the task is divided into two tasks, one is non-animation task, the other is animation task. So we need to create two methods first. Determine the task type in _runTask to execute the corresponding method. _syncTask and _asyncTask methods.

/** ** @param task task object {taskFn,type}
 */
Animation.prototype._asyncTask = function(task) {} /** * @param task task object {taskFn,type}
 */
Animation.prototype._syncTask = function (task) {

}
Copy the code

4. Let’s look at the logic of _runTask first

/** * Executes the current task */ animation.prototype. _runTask =function() {// Do nothing when there is no work in the queue or when the current state is not in motionif(! this._taskQuery || this._state ! = = 1) {return} // Release resources when the task is completeif (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)
  }
}
Copy the code

5, here we use _dispose to release resources, this is of course the best method to write, _asyncTask and _syncTask, of course, the easiest one to practice first. Let’s start with the _syncTask non-animated task

/** * non-animated task * @param task task object {taskFn,type}
 */
Animation.prototype._syncTask = function (task) {
  var _this = this
  var next = function () {
    _this._next()
  }
  var taskFn = task.taskFn
  taskFn(next)
}
Copy the code

6, Now write so much next some friends may be a little confused, how did not use next ah, I can not run a logical ah. So let’s try writing a non-animated task then method

/** * @param calback */ animation.prototype. then =function(calback) {// taskFn takes a next parameter var taskFn =function(next) {calback() next() //taskFn is called in _runTask, where next is passed in _next in _runTasktype= 0 // In the _add method we will accept taskFn this._add(taskFn,type)
  return this
}
Copy the code

See our our then method. Let me clarify next for you. In fact, in the _add method we’re going to take the taskFn method, and the taskFn here is going to take a next parameter, and the taskFn is going to be called in the _runTask method and pass in the _next method which is next here

Repeat (times) {// repeat(times) {// repeat(times)

/**
 * 循环执行
 * @param timesCycle to perform a task on the number of * / Animation in the prototype. Repeat =function (times) {
  var _this = this
  var taskFn = function (next) {
    // timesInfinite loop for empty last taskif (typeof times= = ='undefined') {
      _this._index--
      _this._runTask()
      return} / /timesWhen a numericalif (times) {
      times--
      _this._index--
      _this._runTask()
      return
    } else {
      next()
    }
  }
  var type = 0
  this._add(taskFn, type)
  return this
}
Copy the code

8. Let’s look at wait and start methods

/** * Wait time for next task * @param time Wait time */ animation.prototype. wait =function (time) {
  var taskFn = function (next) {
    setTimeout(function () {
      next()
    }, time);
  }
  var type = 0
  this._add(taskFn, type)
  returnThis} / Animation start * * * * @ param interval Animation execution frequency * / Animation in the prototype. Start =function(interval) {// The operation is not performed when it is already in the executing state or there is no task in the task queueif(this._state === 1 || ! this.taskQuery.length) {return this
  }
  
  this.interval = interval || TIMING
  this._state = 1
  this._runTask()
  return this
}
Copy the code

loadImag

9. Now that the basic task is almost finished, let’s go back to the loadImage method in step 1. Let’s open loadImage

/** ** image preload * @param imglist image array to load * @param next after loading the next task * @param timeout image loading timeout */functionLoadImage (imglist, next, timeout) { Go to the next task. Var state_array = [] // Whether timeout occurs var isTimeout =false

  for (var key inImglist) {// Filter properties on propertyif(! imglist.hasOwnProperty(key)) {continue
    }

    var item = imglist[key]

    if(typeof item === 'string') {item = {SRC: item}} // imglist[key] does not exist or typeof item! = ='string'Skip this dataif(! item || ! item.src) {continue} item.image = new image ()function (item) {}
}
Copy the code

10. What do we do with the doimg method

// Load the imagefunctionDoimg (item) {var img = item.image img. SRC = item.src // Whether loading times out item.isTimeout =false

    img.onload = function() {// Very important, if you do not print img, the frame animation that switches the image URL to perform will request the new image resource console.log(img) item.status ='loaded'
      done()
    }

    img.onerror = function () {
      item.status = 'error'
      done()} // Whether timeout occursif(timeout) {// Timeout timer item.timeoutId = 0 item.timeoutId =setTimeout(onTimeout, Timeout)} // Callback after loadingfunction done() {img.onload = img.onError = null // If no timeout is executed, because the timeout has already been executed onceif(! item.isTimeout) { state_array.push(item.status)if(state_array.length === imglist.length) {next()}}} // Load timeoutfunction onTimeout() {
      item.isTimeout = true
      state_array.push('error')
      if (state_array.length === imglist.length) {
        next()
      }
    }
  }
Copy the code

Module. Exports = loadImage

11. Now that we have the loadImage method done, let’s go to the Timeline class, the method that executes the animation task. Let’s create a new Timeline. Var Timeline = require(‘./ Timeline ‘)

Timeline class

The Timeline class is used to perform animation methods

External exposed interface

Start (interval) // The animation starts. Interval Specifies the interval for each callback

Stop () // Animation pauses

Restart () // Continue playing

Onenterframe (time) // The function executed on each frame. This method does not define the content and is overridden externally. Time Time from the start of the animation to the current execution

1. First we need to define requestAnimationFrame, which is used to execute animation methods repeatedly. As for why this article has been explained at the beginning

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);
    }
})()
Copy the code

2. Let’s write all the methods

/** ** Timeline */function Timeline() {/** * 0 indicates the initial state * 1 indicates the playback state * 2 indicates the pause state */ this._state = 0 // timer ID this.animationHandle = 0} /** * The function executed on the timeline for each callback * @ param time starting from the animation to the currently executing time * / Timeline. The prototype. Onenterframe =function(time) {} /** * animation start * @param interval Interval for each callback */ timeline.prototype. start =function(interval) {} /** * Animation stop */ timeline.prototype. stop =function() {} / implementation of * * * to * / Timeline in the prototype. Restart =function* @param timeline timeline object * @param startTime animation startTime */function startTimeline(timeline, startTime) {
}



module.exports = Timeline

Copy the code

3. Let’s finish the start method first

/** ** animation start * @param interval Interval for each callback */ timeline.prototype. start =function (interval) {
  if (this._state === 1) {
    return this
  }
  this._state = 1
  this.interval = interval || TIMEOUT
  startTimeline(this, +new Date())
}
Copy the code

4. What does the startTimeline do

* @param timeline timeline object * @param startTime animation startTime */functionstartTimeline(timeline, StartTime = startTime // Record the time when the throttle function was last executed var lastTime = +new Date() nextTask()function nextTask(){
    var now = +new Date()
    timeline.animationHandle = requestAnimationFrame(nextTask)
    if(now-lastTime >= timeline.interval) {lastTime = now // Total duration of animation execution timeline. onEnterFrame (now-startTime)}}}Copy the code

5. It’s time to stop the animation

/** * Animation stop */ timeline.prototype. stop =function () {
  if(this._state ! = = 1) {return this
  }
  this._state = 2
  ifStartTime = +new Date() - this.startTime cancelAnimationFrame(this.animationHandle)}} (this.startTime) {this.startTime = +new Date() - this.startTime cancelAnimationFrame(this.animationHandle)}}Copy the code

Let’s start our animation again from the last frame

/ * * * re-execute * / Timeline. Prototype. Restart =function () {
  if (this._state === 1) {
    return this
  }
  if(! this.dur) {returnThis} this._state = 1 //startTimeline +new Date() -startTime to get the total execution time. This.dur = +new Date() - (+new Date() - this.dur) startTimeline(this, +new Date() - this.dur)}Copy the code

That completes our Timeline class. Var Timeline = require(‘./ Timeline ‘) and instantiate this.timeline = new Timeline() in the animation class.

Animation asyncTask (asyncTask, asyncTask, task

/** ** @param task task object {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)
}
Copy the code

8. Now let’s define an animation method and try changePosition

/** * by changing the background of the image, Implement Animation to * @ param Element {} ele * @ param Array positions attach * @ param {String} imageUrl * / Animation in the 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
}
Copy the code

10. Let’s try it out first and then write down the following interfaces: webpack.config.js

module.exports = {
  entry: {
    animation: './src/animation.js'
  },
  output: {
    path: __dirname + '/build',
    filename: '[name].js',
    library: 'animation',
    libraryTarget: 'umd'}}Copy the code

Execute the command

npm -g webpack

webpack

11. Create a dome folder named index.html. Image line search

<! 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'.'240'.'360']
    var rabbitEle = document.querySelector('#rabbit')
    var rabbit = animation(['./timg.jpg'])
    .changePosition(rabbitEle, positions, './timg.jpg')
    .repeat(10)

    rabbit.start(100)
  </script>
</body>
</html>
Copy the code

12. ChangeUrl uses multiple images to switch animations

/ * * * through changing the URL of image element to achieve Animation to * @ param ele dom object * @ param imglist picture URL array * / Animation. The 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
}
Copy the code

Pause, restart, and _dispose are three final methods

Suspended Animation / * * * * / Animation. The prototype. Pause =function () {
  if(this._state ! = = 1) {return this
  }
  this._state = 2
  this.timeline.stop()
  returnThis} / * * * * / Animation Animation from the last time to suspend to perform the prototype. Restart =function () {
  if(this._state ! = = 2) {return this
  }
  this._state = 1
  this.timeline.restart()
  returnThis} /** * dispose = / animation.prototype._dispose =function () {
  if(this._state ! == STATE_INITTAL) { this._state = STATE_INITTAL this.taskQuery = null this.timeline.stop() this.timeline = null } }Copy the code

We’re done