Monday, October 1, 2007

Javascript Animation : take it ease-y ! (3,)

Javascript Animation : basics of easing and Javascript : Controlling time entries developed some basics of what is easing and how to apply duration and frames to an animation.

In the first entry, we've seen how to create an easing out that will expand the width of a div quickly at the beginning and slowly at the end.
But we noticed that we didn't have any control over the time the animation will take and the second entry tries to answer this problem in a simple way. We were able to manipulate different properties of the div such as its width, height, opacity, left and top position thru an number of frames and a duration.

But...
We forgot about easing !
Let's see the actual function.

The animation function


function animate(elm,prop,begin,end,duration,fps) {

     if(!duration) duration=1000;
     if(!fps) fps = 12;

     var change       = end-begin;
     var interval     = Math.ceil(1000/fps);
     var totalFrames  = Math.ceil(duration/interval);
     var fixedStep    = change/totalFrames;
            
     for(i=1;i<=totalFrames;i++) {
        (function() {
           var frame=i;
           function inner() {
              var actualValue=begin+fixedStep*frame;

              unit=(elm=='opacity') ? '' : 'px';
              if(window.attachEvent && !unit) { 
                  actualValue*=100; 
                  obj.style.zoom = 1;
                  obj.style.filter = "alpha(opacity=" + actualValue + ")";
              } else {
                  obj.style[elm]  = actualValue+unit; 
              }
           }
           timer = setTimeout(inner,interval*frame);
        })();
    }
}
//example
animate(document.getElementById('bluesquare'),'width',150,400,1500,20);
You can find how this function works with some examples in the Javascript : Controlling time entry.
So, this function is basically adding a fix value to the property over time.
Let's suppose that we want to animate the width property of a div from 150px to 400px in 1.5 seconds with a 12 frames per second base, this is how the change will happen:

begin         = 150;
end           = 400;
change        = end-begin         = 400 - 150 = 250;
interval      = 1000/frames       = 1000/20   = 50;
total frames  = duration/interval = 1500/ 50  = 30;
fix value added per frame         = change/total frames = 250/30 = 8.33333..

So, we'll have a width changing from 150 to 400 pixels within 1500 milliseconds, separated by 30 frames of 50 milliseconds each, adding 8.3334 pixels
This is how then, we were calculating the new value of the width thru each frames :
var actualValue = begin+step*frame;
//first frame
var actualValue = 150 + 8.333*1  = 158.333
//second frame
var actualValue = 150 + 8.333*2  = 166.666
//third frame
var actualValue = 150 + 8.333*3  = 174.999
As you can see, we are adding a fix value multiplied by the actual frame to the original width.
When the function we'll reach the last frame, the width will be equal to 400 pixels

If you look at the calculation, we can see that the only value changing is the frame number, the actual time of the animation.
We can then write the general calculation for this animation like this:
actualValue = 150 + 8.3333*frame;
or...
 v = 150 + 8.333*frame;
//or 
y = 150 +8.333*x;

Noooooooo !!!
...

Fear not !
You shall keep reading, even if it reminds you some of these hours where you felt like everything was going upside-down in your head !
This is going to be very easy!

Linear easing equation : The Graph shows it all


Yes, I used the E-word, this is an equation but a very easy one (I can understand it, to say !)
You've seen that the animation was quite mechanical and indeed, if you take this equation and trace a graph of it, you will get a beautiful straight line !
Check for yourself with this website and enter this :
150+8.333*x
graph in new window
graph in same window

You should see this red line going straight !
In our animation code, y is equal to the value of the width and x is equal to the elapsed time expressed in frames.
So, the width (y) is a function of the time (x).
When looping thru the frames, the only unknown value is the the width that is calculated thanks to the frame.

Why Trying to get some curve ?


Before going further with this equation, we should perhaps remember what is so cool about easing.
Perhaps some of you are happy with the way the actual function perform the interpolation thru A to B, but don't you feel this is too clean ? too predictable ? too mechanical ?
In our last entry about controlling time, we took the example of a runner targeting 120 meters in 15 seconds.
We then calculated the average ratio, our runner should run per second to get from 0 to 120 meters within this 15 seconds.
var ratio = 120/15 = 8m/s 
So, our runner shall run with a constant speed of 8 meters per second...
That's theoretical, mathematical but is it really realistic ?
If we change the meters into kilometers, is our runner being able to keep with a constant speed ?
He will certainly start at a slow speed at the beginning to keep some of his strength and speed up at the very end to be the first
In the meantime, he might be tired, get a sudden strength boosting him up, making its speed going up/down.

I guess that almost everything that moves is doing so with a speed that changes over time.
This is something natural and average should be kept for statistics.
Human eyes is used to see things change of speed in their run and that's the way we feel that this has to be 'real'.

Our calculation is everything but 'natural' or even closed to be !
In fact, it will never be but we can make it a little bit more natural by some calculations that will allow us to vary the ratio over time and thus, creating the Ease that will make it live !
With the first calculation, we were going straight to the point.
With easing, we will take a curved path to go to the same point!

Trying to get some life !


Now, it's time to play with this equation a little !
We are trying to add some calculation to the equation and see what we get:
y=150+8.333*x-((20*x)/x);

Don't try to understand why I have done this because I do not even know myself !
It's just an experimentation and you can copy and paste this into the above link and see how it goes!

Start:
End:
Duration:
FPS:
test

You can see the value changing thru each frame.
What is important to notice is that you have the animation going back a little and then keeping its way.
This could be a kind of easing but if you look at the final value, you will see that it doesn't reach the one set !
So it's not exactly what we want...

In fact, we need to keep things related to the frame which multiply the constant.
Remember that we want to go from A to B by taking different paths.
We need therefore to keep things proportional to the main factor on which the value depends on : the time/frame.
By the way, we should perhaps change this constant back to what is was:
var step        = change/totalframes;
var actualValue = begin + step*frame;
//becomes 
var actualValue = begin + (change/totalframes)*frame;
The more constant we have, the more we'll be able to change their value during the process and create different easing type!

Ok, let's implement a basic easing out:
var actualValue=(change*frame/totalframes)*(frame/totalframes)+begin;
You should try to see the corresponding graph with the settings above:
250*((x/30)*(x/30))+150
and the original one:
150+(250/30)*x
You can see that the new equation is curved but more importantly, you can see that the two lines intersect at exactly the same point:
The final width!
We are just taking a different path, that's all.

Start:
End:
Duration:
FPS:
test


If we want to create different easing, we should get the calculation of the actual value outside of the main function:
function animate(obj,elm,begin,end, duration, fps) {

  if(!duration) duration = 1000;
  if(!fps)      fps      = 20;

  begin     = parseFloat(begin);
  end       = parseFloat(end);
  duration  = parseFloat(duration);
  fps       = parseFloat(fps);

  var change      = end-begin;
  var interval    = Math.ceil(1000/fps);
  var totalframes = Math.ceil(duration/interval);
  var step        = change/totalframes;


  for(i=1;i < = totalframes;i++) {
    (function() {
        var frame=i;
        function inner() {
            var increase=easeQuad(begin,change,totalframes,frame);
            unit=(elm=='opacity') ? '' : 'px';
            if(window.attachEvent && !unit) { 
                increase*=100; 
                obj.style.zoom = 1;
                obj.style.filter = "alpha(opacity=" + increase + ")";
            } else {
                obj.style[elm]  = increase+unit; 
            }
       }
       timer = setTimeout(inner,interval*frame);
    })();
  }
}
function easeQuad(begin,change,totalframes,frame) {
 return (change*frame/totalframes)*(frame/totalframes)+begin;
}
By putting the calculation outside, we'll be able to set different easing as callback functions
You will be then able to apply your flavor of easing by adding the function you want to use!

Conclusion


We've seen how to create some easing from the function we created
We don't have that much possibilities for now so we'll see this in an other entry but don't forget to try to find some cool easing and share with everyone!
As for the function, it is still limited as we cannot apply more than one property at a time, it could be nice to be able to change the width, height, left,top and opacity properties in the same time to have something really interesting.
I'll write an other entry about some other functionalities!
Have fun with graphs and equations!
(I've never thought I will say such a thing one day!)

2 comments:

Charles T said...

Interesting read. I will definitely need to go get a math book or peruse a math site. I understood the mechanical version of the equation, but quickly got lost in the mathematics of the curve.

shiriru said...

Thank you for your comment!
I guess the explanations are little bit short...my bad.
Are you talking about the curve in the entry 7 (Bezier curve) or in this entry ?

Hope this is helpful anyway!