I’ve been teaching students recently, and the first class last Saturday was about JavaScript animation, which includes simple animations, such as uniform or uniform acceleration/deceleration, as well as more complex combination animations. The basic principles of animation have been introduced in detail in my previous articles. What I want to talk about here is how we can design simpler apis for modern browsers to implement sequence playback of animations.

Promise based animation library

The so-called animation sequence, that is to say, the next animation can be played after the end of the last animation, which can facilitate the use of multiple animation to achieve a variety of complex effects. It’s not hard to imagine that implementing the animation interface as a Promise is a good solution for this purpose:

let animator = new Animator(2000.function(p){
  let tx = -100 * Math.sin(2 * Math.PI * p),
      ty = -100 * Math.cos(2 * Math.PI * p);

  block.style.transform = 'translate(' 
    + tx + 'px,' + ty + 'px)';     
});

block.addEventListener('click'.async function(evt){
  let i = 0;
  
  //noprotect
  while(1) {await animator.animate()
    block.style.background = ['red'.'green'.'blue'][i++%3]; }});Copy the code

The above example is very concise and elegant in modern browsers that support async/await. To be compatible with older browsers, it’s not that complicated, just polyfill es6-Promise or bring in third-party libraries. Here’s another example:

var a1 = new Animator(1000.function(p){
  var tx = 100 * p;
  block.style.transform = 'translateX(' 
    + tx + 'px)';     
});

var a2 = new Animator(1000.function(p){
  var ty = 100 * p;
  block.style.transform = 'translate(100px,' 
    + ty + 'px)';     
});

var a3 = new Animator(1000.function(p){
  var tx = 100 * (1-p);
  block.style.transform = 'translate(' 
    + tx + 'px, 100px)';     
});

var a4 = new Animator(1000.function(p){
  var ty = 100 * (1-p);
  block.style.transform = 'translateY('  
    + ty + 'px)';     
});


block.addEventListener('click'.async function(){
  await a1.animate();
  await a2.animate();
  await a3.animate();
  await a4.animate();
});
Copy the code

With Promise, sequence movements like this are very simple. So how do you implement this library?

The specific implementation

The entire library isn’t complicated to implement, just encapsulate the base animation as a Promise.

Here, however, we’ll wrap some basic functions for compatibility with older browsers:

function nowtime(){
  if(typeofperformance ! = ='undefined' && performance.now){
    return performance.now();
  }
  return Date.now ? Date.now() : (new Date()).getTime();
}
Copy the code

We say that animation is a function of time, so we need a simple time fetch function. In the new requestAnimationFrame specification, the timestamp parameter to the frame callback is a DOMHighResTimeStamp object, which is more accurate (down to nanoseconds) than Date. So we use performing.now () to get time, and if the browser doesn’t support performing.now (), we demote to date.now ().

Next, we polyfill the requestAnimationFrame:

if(typeof global.requestAnimationFrame === 'undefined') {global.requestAnimationFrame = function(callback){
    return setTimeout(function(){ //polyfill
      callback.call(this, nowtime());
    }, 1000/60);
  }
  global.cancelAnimationFrame = function(qId){
    return clearTimeout(qId); }}Copy the code

Then, there is the concrete Animator implementation:

function Animator(duration, update, easing){
  this.duration = duration;
  this.update = update;
  this.easing = easing;
}

Animator.prototype = {

  animate: function(){

    var startTime = 0,
        duration = this.duration,
        update = this.update,
        easing = this.easing,
        self = this;

    return new Promise(function(resolve, reject){
      var qId = 0;

      function step(timestamp){
        startTime = startTime || timestamp;
        var p = Math.min(1.0, (timestamp - startTime) / duration);

        update.call(self, easing ? easing(p) : p, p);

        if(p < 1.0){
          qId = requestAnimationFrame(step);
        }else{
          resolve(self);
        }
      }

      self.cancel = function(){
        cancelAnimationFrame(qId);
        update.call(self, 0.0);
        reject('User canceled! ');
      }

      qId = requestAnimationFrame(step);
    });
  },
  ease: function(easing){
    return new Animator(this.duration, this.update, easing); }};module.exports = Animator;
Copy the code

The Animator can be constructed by passing three parameters: the first is the total length of the animation, the second is the update event for each frame of the animation, where you can change the attributes of the element to achieve the animation, and the third parameter is easing. The second parameter, the update event callback, provides two parameters: ep, which is the animation process after easing, and P, which is the animation process without easing. Ep and P both start at 0 and end at 1. (Why ep and P are used was explained in the previous animation tutorial.)

Animator has an animate object method that returns a promise. When the animation is finished, its promise will be resolved. The user can also invoke the cancel method before the promise resolve. Then its promise will be rejected.

Thus, we simply implemented the animation sequence by encapsulating the animator as a method with a return Promise interface. Its implementation is simple, but the function is very powerful, using it to achieve the animation code is also very elegant:

var a1 = new Animator(1414.function(p){
  var ty = 200 * p * p;
  block.style.transform = 'translateY(' 
    + ty + 'px)';     
});

var a2 = new Animator(1414.function(p){
  var ty = 200 - 200 * p * (2-p);
  block.style.transform = 'translateY(' 
    + ty + 'px)';     
});

block.addEventListener('click'.async function(){
  
  //noprotect
  while(1) {await a1.animate();
    awaita2.animate(); }});Copy the code

We also provide an ease method (version 0.2.0+) that can pass in new easing and return a new Animator object, so we can extend our animation based on the original animation:

var easeInOutBack = BezierEasing(0.68, -0.55.0.265.1.55);
//easeInOutBack

var a1 = new Animator(2000.function(ep,p){
  var x = 200 * ep;

  block.style.transform = 'translateX(' + x + 'px)';
}, easeInOutBack);

var a2 = a1.ease(p= > easeInOutBack(1 - p)); //reverse a1

block.addEventListener('click'.async function(){
  await a1.animate();
  await a2.animate();
});
Copy the code

How about CSS3?

Indeed, many animations can be implemented using CSS3. However, JavaScript animation and CSS3 animation have different characteristics and use scenarios. In general, CSS3 animations are suitable for any simple animation with pure presentation effects. While it provides basic animation composition methods (with animationEnd times, but later standardized), it is still cumbersome and requires JavaScript to control. Some animation library with degraded way, can use CSS3 animation using CSS3 animation, not automatically degraded to JavaScript animation, this is a good way, but also has advantages and disadvantages. Because CSS3 animations are bound to manipulate element attributes, JavaScript is more flexible. The wrapped animation library, for example, provides a lower-level API that operates only on time and progress without coupling any elements, attributes, or other presentation classes, so it can be used to manipulate DOM, Canvas, SVG, audio/video streams, and even other asynchronous actions. Also, if you need to do some other fine motion processing during the animation process, you should still use JavaScript animations instead of CSS3 animations.

conclusion

The simple animation library, implemented with Promise, is able to execute the combined sequential animation very well. It is concise and elegant with async/await code, and also has very good scalability, which can combine very powerful animation effects. I believe this will be the primary implementation of JavaScript animation on browsers in the future.

Finally, you can access the GitHub repo to get the latest code.