Let's Make a Framework: Animations Part 6

08 Jul 2010 | By Alex Young | Tags frameworks tutorials animation lmaf

Welcome to part 20 of Let’s Make a Framework, the ongoing series about building a JavaScript framework. This part continues looking at JavaScript animations.

If you haven’t been following along, these articles are tagged with lmaf. The project we’re creating is called Turing and is available on GitHub: turing.js.

Movement Helper

Continuing on from last week with helpers, I would like to be able to do this:

turing.anim.move(element, duration, { x: '100px', y: '100px' });

And the element should move 100 pixels across and down from its current position. The API we’ve built over the last few weeks will do this for relative positioned elements with the following code:

anim.animate(element, duration, { 'left': options.x, 'top': options.y });

Using an absolute positioned element will cause it to jump to the first point in the animation. That means we need to move absolute elements relative to their original location. I’ll come back to this later.

Chained API

Ideally animation calls, whether they be from helpers or animate, should work when chained. Chaining calls should create a sequence of animations:

turing.anim.chain(element)
  .highlight()
  .move(1000, { x: '100px', y: '100px' })
  .animate(2000, { height: '0px' });

It would be useful to be able to pause animations, too:

turing.anim.chain(element)
  .highlight()
  .pause(2000)
  .move(1000, { x: '100px', y: '100px' })
  .animate(2000, { width: '1000px' })
  .fadeOut(2000)
  .pause(2000)
  .fadeIn(2000)
  .animate(2000, { width: '20px' })

I’ve used as similar technique to the Enumerable library’s Chainer class (part 5). This isn’t reused here but it could be made more generic.

The basic principle is to wrap each chainable method in a function that can correctly sequence the animation calls. The anim object contains all of the relevant methods; this object can be iterated over:

for (methodName in anim) {
  (function(methodName) {
    var method = anim[methodName];
    // ...    
  })(methodName);
}

The anonymous function causes the methodName variable to get bound to the scope. The Chainer class needs to keep track of the element we’re animating, and the current position in time:

Chainer = function(element) {
  this.element = element;
  this.position = 0;
};

Then as the animation progresses, we need to update the position:

for (methodName in anim) {
  (function(methodName) {
    var method = anim[methodName];
    Chainer.prototype[methodName] = function() {
      this.position += current position;
    };
  })(methodName);
}

setTimeout can be used to schedule the animations:

setTimeout(function() {
  method.apply(null, args);
}, this.position);

This will not block, so all of the chained animations will be started at around the same time. Since position is incremented with each animation’s duration, the animations should fire at approximately the right time.

The only other thing left to do is progress the arguments passed to the function to insert the element.

Putting it Together

Chainer = function(element) {
  this.element = element;
  this.position = 0;
};

for (methodName in anim) {
  (function(methodName) {
    var method = anim[methodName];
    Chainer.prototype[methodName] = function() {
      var args = Array.prototype.slice.call(arguments);
      args.unshift(this.element);
      this.position += args[1] || 0;
      setTimeout(function() {
        method.apply(null, args);
      }, this.position);
      return this;
    };
  })(methodName);
}

anim.chain = function(element) {
  return new Chainer(element);
};

Conclusion

Now chaining has been added it’s apparent that the animation framework has some warts: in particular, movement animations will start from start positions. I’ll look at fixing these issues over the coming weeks.


blog comments powered by Disqus