Wednesday, November 7, 2007

Javascript Animation : Strategy and Factory Pattern at the rescue (8)

In just 7 entries, we've seen how to start with a basic easing effect dealing with 2 or 3 css attributes at a time and end up with a rich animation function able to deal with time, frames,manipulate several properties at once in a cross browser way, animate colors, create several easing in/out effects and deviate the path thru a bezier curve! but...

The main drawbacks is that we were adding new possibilities directly into the animate function with the help of if...else statements...
If it may be a good work around for a time and for few properties, when the css properties you want to handle grow up, that new css properties pop up, having to add if... else if... else statements in the core function can become cumbersome:
- you may have to handle many new css attributes
- you will need to add more and more if statements, making the function hardly readable - even if you use just 10 percents of the function capacities, you will have to load the entire function - if the code has been working for a while, adding directly in the core can bring bugs, unexpected chaining effects...

In other word, the code will be a mess!!

Here comes the strategy pattern associated with the factory pattern to the rescue !
we will see how to create an expandable function that can be plugged from outside to add new properties like margin or padding thanks to this pattern!

Design pattern what ?


Design patterns are common solutions to common problems.
We have seen above what kind of problems our program could lead to in the long run.
In fact, our program is a solution to the problem but unfortunately not the better one!
The gang of four, authors of design patterns, have brought to us many solutions to many common problems.Solutions thought and re-thought, try and retried to end up to be a 'design pattern'. If you want, a kind of solution template for a recurrent problem !
They all relate to object oriented programming but we will see that we can use the concept behind the patterns in a procedural way too.

Strategy pat... what ?


We have seen that we had to change our effect calculation strategy according to the value we got:
- we had a specific calculation strategy for a simple numeric value like 12px or 1 that could be resumed to simple numbers (12 instead of 12px) or
- we had an other specific calculation strategy for a string that contains the numeric value in a different format like the colors #FF00FF or rgb(255,0,255)!
In order to face this divergence we just added an if statement to handle numeric values and colors.
By doing so, we were aware that the function was becoming messy... hard to read, hard to maintain and not expandable as we could wish.
Let's say that we know want to manipulate margin or padding...
We can see their value structure:
margin:12px;
margin:12px 1px;
margin:12px 1px 5px;
margin:12px 1px 5px 8px;

The same for the padding!
We face 4 different values in one attribute and the number of value changes the sub attribute they relate to !
We could do this at first:
if(numeric) {
//calculation
}
else if (color) {
//calculation 
}
else if (multi) {
//calculation
}
I have shorten up the code but I guess that you can see that if else if else statements (even if disguised under a switch statement!!) is far to be the most expandable way of doing it!
Is there a way to change of strategy automagicly without even having to specify it?
Well, there is!

The Strategy pattern

The strategy pattern allows to change the behavior of part of an algorythm in a class or in our case in a function!
Basically we are going to put outside of the function each strategy in order to keep our core function clean and easy to maintain.
By putting outside the calculation, we will also be able to add new strategies very easily!
Let's see the actual function first:
function animate(elm,props, duration, fps,easing) {

 duration = (duration) ? parseFloat(duration) : 1000;
 fps      = (fps)      ? parseFloat(fps)      : 20;
 easing   = (easing)   ? easing               : linearEase;

 var interval    = Math.ceil(1000/fps);
 var totalframes = Math.ceil(duration/interval);

 for(i=1;i <= totalframes;i++) {
   (function() {
      var frame=i;
      displacement=function() {
          for(var prop in props){
              if(!/olor/.test(prop)) {
                  var begin = props[prop].start*100;
                  var end   = props[prop].end*100;
                  var bezier = props[prop].bezier*100;
                  var actualDisplacement=
                  easing(frame, begin, end-begin, totalframes);
                  if(bezier) 
                    actualDisplacement=
                    QuadBezier(frame,actualDisplacement,end,bezier,totalframes);
                    setStyle(elm,prop,actualDisplacement/100;
                  } else {
                    var b = hexStr2rgbArray(props[prop].start);
                    var e = hexStr2rgbArray(props[prop].end);
                    var rgb=[];
                    for(j=0;j<3;j++) 
                    rgb.push(parseInt(easing(frame, b[j], e[j]-b[j], totalframes)));
                    setStyle(elm,prop,'rgb('+rgb.join(',')+')');  
        }
   }
       }
       timer = setTimeout(generalProperty,interval*frame);
   })();   
 }
}

huh, it's a pretty long function!
We first do some initialisation then some calculation for the entire function then we calculate the animation within a loop.
In order to change our calculation strategy according to the property, we check to see if we have a color and if not we switch to the other one.
we see that this won't work with the margin property anymore...
In order to add the margin,padding calculation strategy within the function we will have to change the way we identify the type of the incoming property, which could lead to errors and eventually, a function that you won't be able to maintain and understand.
Therefore, let's do a simple thing: put outside each strategy!

Identifying the strategies


We have two ways of calculating the property : one for the numeric values and one for the strings that contains an hexadecimal color code.
We can then create two helpers functions:
function NumericStrategy(totalframes,frame,prop,props,easing) {
    var begin  = props[prop].start*100;
    var end    = props[prop].end*100;
    var bezier = props[prop].bezier*100;
    var actualDisplacement=easing(frame, begin, end-begin,totalframes);
    if(bezier) {
      actualDisplacement=QuadBezier(frame,actualDisplacement,end,bezier,totalframes);
    }
    setStyle(elm,prop,actualDisplacement/100;
}
function ColorStrategy(totalframes,frame,prop,props,easing) {
   var b = hexStr2rgbArray(props[prop].start);
   var e = hexStr2rgbArray(props[prop].end);
   var rgb=[];
   for(j=0;j<3;j++) {
        rgb.push(parseInt(easing(frame, b[j], e[j]-b[j], totalframes)));
   }
   setStyle(elm,prop,'rgb('+rgb.join(',')+')');
}
The numeric function will be in charge of working with numbers and therefore allow to handle many css properties : height, width, opacity, font-size,top,left...
We consider a number being a unique decimal number followed by its unity.
width:120px,
top:-100px,
opacity:.9
All these properties can be resumed to a unique number.
A margin or padding property is not considered a unique number but a property composed of serveral numbers, that the above function could handle individually.
The Color function is in charge of calculating the value for a string written in an hexadecimal way and defining a color.
This one is rather specific to one type of property but we can use it for color, background-color.
For now, there isn't that big changes, we have just put outside the calculation for each strategy, making the core function a little bit easier to understand but still unable to deal with margin or padding.
Before we go ahead, I shall show you how the strategy pattern does really look like as defined and how it will be used in practice in general:
var prop={
    color:{start:'#FFFFFF',end:'#0000FF'},
    'background-color':{start:'#FF0000',end:'#00FF00'}
};
animate(elm,prop,1500,25,easeInOutBounce,ColorStrategy);
var prop={
    width:{start:'150px',end:'20px'},
    top:{start:'0px',end:'-100px'},
    opacity:{start:0,end:1}
};
animate(elm,prop,1500,25,easeInOutBounce,NumericStrategy);
In that case, there won't be any switch of strategy in the function like we used to do at the beginning, the function will just apply the strategy given as a parameter in the function, like we did for the easing functions.
This is the usual basic way of implementing the strategy pattern but as you can see this is not very useful!
Now you can only animate one type of property at a time and you have to be aware of the strategy to apply...This is not what I will call a user friendly programming interface!
We need to keep the interface easy to use and hide all the process from the end user: the user shouldn't have to know that we change our strategy to calculate the property and the user should obviously not even know the existence of the 2 strategy functions!
In general, in a object oriented programming concept, this is what we call encapsulation or putting everything in a box that hides all the process to the end user.
In this regard, a function is the core, the basic tool you can use to create encapsulation!
instead of writing again and again the same calculation everywhere in your program, you put the calculation in a function, give it an easy to understand name and here you go, you encapsulate the process!
Our first approach, which was to switch the strategy in the function was a good way of keeping encapsulation.
So let's go back a little and keep the encapsulation by having the switching process between the strategies in the function, by using a simple if... else statement:
if(/[0-9]+/.test(parseInt(start)) && !/\s/.test(start)) {
 NumericStrategy(totalframes,frame,prop,props,easing);
} else {
 ColorStrategy(totalframes,frame,prop,props,easing);
}
We have seen that looking for a property containing the word color to switch between numeric or color strategy is not going to work with margin and padding.
Instead we are going to change our split strategy!
We will be looking for what we consider as being a numeric value and everything else! We first check to see if the parseInt value result in a number.
If parseInt can't find a number, it will send back the string,NaN, which stands for Not a Number.
But the margin will be considered as a numeric value so we need to look for any space in the string, if there is, we know that it's not something that our numeric function will be able to handle so we let the ColorStrategy does its works.
This is a little bit naive as it might in fact be the margin or padding or even worse, something we didn't think about!! and our function will crash...
But for now, let's go back to the end user and see how it works:
var prop={
    color:{start:'#FFFFFF',end:'#0000FF'},
    'background-color':{start:'#FF0000',end:'#00FF00'}
    width:{start:'150px',end:'20px'},
    top:{start:'0px',end:'-100px'},
    opacity:{start:0,end:1}
};
animate(elm,prop,1500,25,easeInOutBounce);
As you can see, this is a little bit easier for the end user!
It doesn't have to worry about the type of the property he/she used and therefore, keep the process very easy.
We have gain in the background an easier function to read and debug as we have put outside the calculation. let's see how the function looks like now:
function animate(elm,props, duration, fps,easing) {

      var duration    = (duration) ? parseFloat(duration) : 1000;
      var fps         = (fps)      ? parseFloat(fps)      : 20;
      var easing      = (easing)   ? easing               : easeOutBounce;
      var interval    = Math.ceil(1000/fps);
      var totalframes = Math.ceil(duration/interval);
      var Animator    = new Animate();

      for(var i=1;i <= totalframes;i++) {
         (function() {
     var frame=i;
            var setAnimation=function() {
     for(var prop in props){
                       var start=props[prop].start;
                       if(/[0-9]+/.test(parseInt(start)) && !/\s/.test(start)) {
                           NumericStrategy(totalframes,frame,prop,props,easing);
                       } else {
                           ColorStrategy(totalframes,frame,prop,props,easing);
                       }
     }
            }
            var timer = setTimeout(setAnimation,interval*frame);
        })();   
     }
}
As you can see, the function is shorter and as we have put outside the calculation into functions with easy to understand name, we can almost understand what's going on without having to think too much!
We can now change the numeric or color function without fearing to create bugs in the core function!
But if maintainability has been improved, our function is still unable to deal with margin or padding!
If we want to do so, we will need to go in the function, change our if... else statements, with all the problemes this could bring.
We should put this switch between strategy outside and use a function to wrap this, a function that will be in charge of building the right calculation, the right strategy, a kind of factory!

The factory pattern


You've been using the factory pattern all the time without even knowing it!
If we go back to the source and think of what is a factory, we could define it as something taking a raw element or several raw elements to output something that we can use.
When you write a function that changes an hexadecimal color string into an array of 3 numeric values, you are creating a factory: it takes a raw string that you can't use and change it into something usable and valuable for you!
In our case, the factory will be in charge of defining the proper strategy for the css element it receives and call it to have our final product in every case: a number to which we can apply a computation and therefore create an animation.
The definition of our strategy factory is very easy for now, we just wrap the if else statements in a function:
function strategyFactory(totalframes,frame,prop,props,easing) {
     var start=props[prop].start;
     if(/[0-9]+/.test(parseInt(start)) && !/\s/.test(start)) {
         NumericStrategy(totalframes,frame,prop,props,easing);
     } else {
         ColorStrategy(totalframes,frame,prop,props,easing);
     }
}
This is one way of implementing the factory pattern in a procedural way, the basic and easy way.
Now, we will have to change the strategyFactory instead of the animate function, which is a little improvement but can we make things easier?
Of course we can!
Increasing the number of if... else statements, turning them into a switch statement to make it a little bit more readable won't change the fact that you will have to change the function everytime a new property you didn't think of appears.
If someone wants to add a new property, he/she will have to create a function and then go into the strategyFactory so that it can handle the new feature.
In short, nobody will try to extend your function!!

What will be a factory without a chart?


Every factories follow a chart : i need to have a tree with this property of this weight and length in order to create that desk. I need to use this particular stone in order to create a ring...
Here, our factory just required us to pass the proper arguments to the function to do its job and choose the right strategy
We are going to add a new chart!
The strategies should be named with the name of the css property they cover, changed in camelCase with the first letter in uppercase!
What does it mean?
if you want to implement the background-color css property, your function shall be called BackgroundColor, if you want to create a function that handles the margin property, it has to be called Margin...
By creating a naming convention and thus extending our chart, we will be able to have an intelligent factory able to call the required function without endless if... else statements!!
Let's see the function with the new chart:
function strategyFactory(totalframes,frame,prop,props,easing) {
     var start=props[prop].start;
     if(/[0-9]+/.test(parseInt(start)) && !/\s/.test(start)) {
         NumericStrategy(totalframes,frame,prop,props,easing);
         return true;
     }
     strategy=camelize(firstToUpperCase(prop));
     if(window[strategy]) { 
        window[strategy](totalframes,frame,prop,props,easing);
        return true;
     }
     return false;
}
function firstToUpperCase(val){
     return val.replace(/^([a-z])/,function(m,l) { return l.toUpperCase();})
}
???
You said there won't be any if else statements !!
No, in fact we are going to keep one and only one if statement in order to make things easier to develop! Let's see what's going on so that you understand why we keep this if:
The first part of the function is trying to determine if we are dealing with a unique number.
As you know, each css property should have a function that handles the calculation strategy but simple numerical css properties are numerous: top, left, width, height, opacity, border-radius,margin-top,margin-left,padding-top...
If we follow our new chart, we will have to create a function for each of them:
function Top(totalframes,frame,prop,props,easing) {
       NumericStrategy(totalframes,frame,prop,props,easing);
}
function Left(totalframes,frame,prop,props,easing) {
       NumericStrategy(totalframes,frame,prop,props,easing);
}
function Width(totalframes,frame,prop,props,easing) {
       NumericStrategy(totalframes,frame,prop,props,easing);
}
function Height(totalframes,frame,prop,props,easing) {
       NumericStrategy(totalframes,frame,prop,props,easing);
}
function MarginTop(totalframes,frame,prop,props,easing) {
       NumericStrategy(totalframes,frame,prop,props,easing);
}
I am not going to write them all but as you can see this a fair amount of wrapper functions to write. We can think of a unique numerical value as being the generality and everything else as being the exception, therefore this is what we do!
If we don't have a number, we change the css property in camelCase and then change the first letter into uppercase.
we need to call the function on the window object,that is the global space, holding all the functions and variables.
We then see if the function exists and apply it.
If the function doesn't exist we just retun false.
Now, we will be able to add new css properties without touching the factory and the core function!
It will be very easy for users to contribute to the function by just creating a function that has a name following the chart !
Now, we need to create the functions we are aware of that deal with one color formatted in hexadecimal:
function Color(totalframes,frame,prop,props,easing) {
   var b = hexStr2rgbArray(props[prop].start);
   var e = hexStr2rgbArray(props[prop].end);
   var rgb=[];
   for(j=0;j<3;j++) {
        rgb.push(parseInt(easing(frame, b[j], e[j]-b[j], totalframes)));
   }
   setStyle(elm,prop,'rgb('+rgb.join(',')+')');
}
function backgroundColor(totalframes,frame,prop,props,easing) {
      Color(totalframes,frame,prop,props,easing);
}
The little drawback is that we need to create a wrapper around the Color function for the background-color but we will be able to use the Color function in many other cases, we will see !

Conclusion


We have seen in this entry how we could create an expandable animation function that delegates strategies to outside functions.
We have seen how to use the strategy pattern associated with the factory pattern to get the most of it in a procedural way!
- The main function, animate is now smaller, which will create less overhead when using it
- The main function doesn't need to be aware of the css properties that exists as it has been abstracted in sub functions.
- Our animate function, abstracted of css properties, can now be extended as will by other users by following a simple naming convention. - we can load only the strategies we need for an animation if we want! (if you don't animate colors, don't import the color strategy!) - the animate function can be extended with more elements like events without fearing to create a monster function!
We have seen how we can use the concepts behind design patterns generally used for object oriented programming in a procedural manner but we have several problems with the procedural approach:
- All our functions is in the global name space, the window name space.Therefore, conflicts with other libraries that could use the same naming conventions could appear and create hard to track bugs.
- we can't share a state between functions easily We will see in the next entry how we can simulate namespaces in javascript thru objects and how we can extend the number of css properties our function can handle with Margin and Padding functions !

No comments: