Wednesday, December 19, 2012

Timer Chaining in JavaScript

Only recently, I was working on a project where I had to implement a feature that is usually implemented using gif. What I had to do was to cause 4 images -  lets say 1, A, B, C overlayed on each other to display sequentially. And then, after the first set of 4 images have been displayed, have a similar sequence repeat, but with only the first image changed, i.e. 2, A, B, C. And then the third set - 3, A, B, C.

Now, since the sample that I had to replicate was made in gif, I presume that creating such an image using an image editing software is pretty easy. My task was to replicate this effect using pure javascript. Moreover, since it was only in the development phase, I had to design for flexibility, since requirements can change at any moment of time. Thankfully, for me, designing for flexibility is a huge turn on. Albeit inititally difficulty, the immense amout of satisfaction that you gain out of it when you see the requirements change and be able to tell your clients that you can implement the changes in like 10 seconds is totally worth the time and energy that you put in during the initial development phases.

For my experiment, the flexibility that I had to incorporate was to be able to change the duration for which each of the 4 images can be displayed if the requirements changed. Had it been a gif file, it wold have been an easy task, just change the time in the application and then it would be ready. Show the client, and if its not what he/she wants, then redo the same.

I wanted to achieve something similar in JavasScript. However instead of a tool I wanted to create a system wherein you can programmatically configure the time duration for each image and then automatically have the effect ripple throughout all the sequences of images.

Also, instead of restricting the system to that of 4 images, I wanted it to be extensible such that it would be able to incorporate any number of images that could be overlaid on top of each other. Also, I wanted the system to behave in a way that incorporates a slightly different functionality - not be restricted to images being overlaid.

So, ultimately, after thinking about all the flexibility that I wanted to incorporate into the system, it turned out that what I wanted to create was a system of timers such that each timer could be chained to each other yet they would be oblivious to characteristics of the next timer. Also, these timers would have a feature that would let them bind the timer ending function for the next timer without knowing what the function does. And the beauty would be that each of these functions can be chained to an infinite level.
So, how did i proceed to make such a system.

First things first.
I designed the system as a phase based system. Since I had 4 images in my sequence, I created a object that saved the properties of 4 phases and references to the functions that would be invoked on the terimination of each function. I called this object a phasedTimeout object, which is essentially nothing but a nested object.

The first thing that I did was - created an object that contains all the values for which i want each of the phases to execute.

var settings = {
  duration1:2000, //Millisecond duration for the first timer
  duration2:300, //Millisecond duration for the second timer.
  duration3:300, //Millisecond duration for the third timer.
  duration4:300, //Millisecond duration for the fourth timer.
 };


In the next part, I define the different phases, and the functions that can be executed at the end of each phase.

var phaseTimeouts =  {
  'phase1':
   {
    duration:settings.duration1,
    end : function(currentPhaseId){
     console.log('Ending phase : '+ currentPhaseId);
    }
   }
  ,
  'phase2':
   {
    duration:settings.duration2,
    end : function(currentPhaseId){
     console.log('Ending phase : '+ currentPhaseId);
    }
   }
  ,
  'phase3':
   {
    duration:settings.duration3,
    end : function(currentPhaseId){
     console.log('Ending phase : '+ currentPhaseId);
    }
   }
  ,
  'phase4':
   {
    duration:settings.duration4,
    end : function(currentPhaseId){
     console.log('Ending phase : '+ currentPhaseId);
    }
   }
 }


And then, finally on window loading, ( because it was part of my requirement to do all of this only after the images on the page have loaded), i created a function that chains together the different phases.

var timeOutSetter = function(){
  
   //Call the terminating function of the previous phase
   phaseTimeouts['phase'+currentPhaseId].end(currentPhaseId);
   
   //If there is anything left in phaseTimeouts array
   //create a new timer and update the new timerId
   var nextPhaseKey = 'phase'+(currentPhaseId+1);
   if(phaseTimeouts[nextPhaseKey]){
    currentPhaseId++;
    console.log('Entering Next Phase : ' + nextPhaseKey);
    currentTimerId = setTimeout(timeOutSetter,phaseTimeouts[nextPhaseKey].duration);
   }
  };


The above function does nothing but chains the different timers together by using the settings from the phaseTimeouts object. The variable phaseTimeouts is a misnomer since it captures more than just the timeout, and can perhaps be used to bind extra functionality.

For my experiment, I toggled the visibility of different images inside the 'end' function of each of the phases. But the fact that  you can have any number of phases and that each phase has a different function that handles the phase end that is independent of each other makes it all the more useful.

Below is the piece of code in its entirety


$(function(){

 var settings = {
  duration1:2000, //Millisecond duration for the first timer
  duration2:300, //Millisecond duration for the second timer.
  duration3:300, //Millisecond duration for the third timer.
  duration4:300, //Millisecond duration for the fourth timer.
 };
 
 var phaseTimeouts =  {
  'phase1':
   {
    duration:settings.duration1,
    end : function(currentPhaseId){
     console.log('Ending phase : '+ currentPhaseId);
    }
   }
  ,
  'phase2':
   {
    duration:settings.duration2,
    end : function(currentPhaseId){
     console.log('Ending phase : '+ currentPhaseId);
    }
   }
  ,
  'phase3':
   {
    duration:settings.duration3,
    end : function(currentPhaseId){
     console.log('Ending phase : '+ currentPhaseId);
    }
   }
  ,
  'phase4':
   {
    duration:settings.duration4,
    end : function(currentPhaseId){
     console.log('Ending phase : '+ currentPhaseId);
    }
   }
 }
 
 //Once all the contents of the window finishes loading, start the slideshow
 $(window).load(function(){
  
  var currentPhaseId = 1;
  var currentTimerId = 0;
  
  var timeOutSetter = function(){
  
   //Call the terminating function of the previous phase
   phaseTimeouts['phase'+currentPhaseId].end(currentPhaseId);
   
   //If there is anything left in phaseTimeouts array
   //create a new timer and update the new timerId
   var nextPhaseKey = 'phase'+(currentPhaseId+1);
   if(phaseTimeouts[nextPhaseKey]){
    currentPhaseId++;
    console.log('Entering Next Phase : ' + nextPhaseKey);
    currentTimerId = setTimeout(timeOutSetter,phaseTimeouts[nextPhaseKey].duration);
   }
  };
  
  var currentTimerId = setTimeout(timeOutSetter,phaseTimeouts['phase1'].duration);
  
 });
 
});


Decoupled designs can be so addictive, is'int it?

Signing Off
Ryan


1 comment:

Ciro S. Costa said...

Thanks for this post! I needed something like that, then after seeing yours, created another: check my gist.

It uses closures so that we don't need to expose some stuff and makes it more compact.

Have a nice day!