4.2.11

Simple animation algorithm

Let's start with a simple case, fading in an element turning up the opacity on it. It's a good idea to start with the interface (how we want the call to look, ideally). Remember simple is always best:

fadeIn(element);

Then the actual function definition:

function fadeIn(element){

      // how smooth the animation runs
  var increment = 50,
      // how long it takes
      duration = 500,
      // this is important: it normalizes the steps
      scale = duration/increment;

  // Define the steps ahead of time
  for (var i = scale; 0 < i; i--) {

    // we use a function to retain the value of i
    setTimeout((function(i){

      // the function actually executed on timeout
      return function(){
        // Standard browsers
        element.style.opacity = i/scale;
        // Substandard browsers
        element.style.filter = 'alpha(opacity='+(scale*i)+')';
      };

    })(i), increment*i);

  }
}

Above, the scale is 10. The loop will then distribute 10 calls evenly, starting with the last one (500ms) and ending with the first, 10ms in the future.

We then use the scale to transform into the opacity values to be used: 0, .1, .2... 1 for standard browsers and 0-100 for browsers that were developed with little concern for cooperation for the benefit of user experience and the programmers having to implement it.

But this wouldn't be fun without making it a little more reusable, so let's clean it up. A general pattern to apply to functions is to keep track of what is modified —the element in this case— and what can change —increment, duration and property being mutated.

function animate(fn, config){
  var increment = (config||{}).increment || 50,
      duration = (config||{}).duration || 500,
      scale = duration/increment;
  
  for (var i = scale; 0 < i; i--) {
    setTimeout((function(i){
      return function(){ fn(i, scale); };
    })(i), increment*i);
  }
}

Since there are at least two operations to be executed on every step (because of different browser handling), I opted for explicitly passing the "verb" or lambda or visitor function to the animate function. This is how it could be used:

var element = document.getElementById('my_element');
animate(function(i, scale){
  element.style.opacity = i/scale;
  element.style.filter = 'alpha(opacity='+(scale*i)+')';
});
A little trick I used above: since config may not be defined, I make sure there is always a default object or value to work with, so this:
var increment = 50;
if (config && config.increment) {
  increment = config.increment;
}
...turns into this:
var increment = (config || {}).increment || 50;

There are a couple of ways to deal with config objects and defaults, and it depends on how many variables you are trying to set or override. It may make sense for larger configuration objects to extend a default object with the configuration values, for example.

Other modifications would involve using an "easing" function that determines how often the callback is fired and with what increment values. The one above is the linear, the simplest.

No comments: