Switch On The Code RSS Button - Click to Subscribe
Aug
3

Javascript and CSS Tutorial - Generic Animation

We have talked about animation in javascript before (notably Sliding Panels), but here we are going to take the concept one step further. This tutorial is about writing a generic animation function - a function which, given an html element, can move and resize that element over a given time period. So, for instance, you could use this function to get sliding panel functionality, or to move some text around on the screen, or pretty much any dynamic movement that you would want to do to a single element on a webpage.

First, lets take a look at a fun little example using this function. Here we have a small red square (which is just a colored div element). You can give the red square a new size in pixels (by setting the values in the height/width textboxes), and a new position (by setting a desired point in in the X/Y textboxes). You can also set the desired speed of the animation, by setting how long you want the animation to take in the Time textbox (which is the amount of time in milliseconds). After setting those values, you can click "Go!", and the square will transition to the new values over the given period of time.


New X Pos: New Y Pos:
New Width: New Height:
Time (Millisec)

Ok, now that your done having fun with the red square, lets take a look at the functionality behind it. Here is the signature of the main method:

function animate(elementID, newLeft, newTop, newWidth,
      newHeight, time, callback)

Its pretty simple, and takes in what you might expect. It takes the id of the element to animate, the new left (X) position, new top (Y) position, new width, new height, and the amount of time for the animation. The last parameter, callback, is the function to run when the animation is complete. So what is in this method? Lets see!

function animate(elementID, newLeft, newTop, newWidth,
      newHeight, time, callback)
{
  var el = document.getElementById(elementID);
  if(el == null)
    return;
 
  var cLeft = parseInt(el.style.left);
  var cTop = parseInt(el.style.top);
  var cWidth = parseInt(el.style.width);
  var cHeight = parseInt(el.style.height);
 
  var totalFrames = 1;
  if(time> 0)
    totalFrames = time/40;

  var fLeft = newLeft - cLeft;
  if(fLeft != 0)
    fLeft /= totalFrames;
 
  var fTop = newTop - cTop;
  if(fTop != 0)
    fTop /= totalFrames;
 
  var fWidth = newWidth - cWidth;
  if(fWidth != 0)
    fWidth /= totalFrames;
 
  var fHeight = newHeight - cHeight;
  if(fHeight != 0)
    fHeight /= totalFrames;
   
  doFrame(elementID, cLeft, newLeft, fLeft,
      cTop, newTop, fTop, cWidth, newWidth, fWidth,
      cHeight, newHeight, fHeight, callback);
}

First off, we get the actual element using the element ID. If there is no element, well, there is nothing to animate, so we return. Next we get the current values for the element's position, height, and width. We have to use the function parseInt because, as you may recall, the values are stored in those style tags with the string 'px' after them. By using parseInt, we get the value and ignore the 'px'. After that, we need to calculate the total number of frames we are going to animate. If a time of 0 or less then 0 is given, then we are just going to have 1 frame. Otherwise, we divide the time by 40. This is just an arbitrary value that means each frame will last 40 milliseconds (which comes out to 25 frames per second). It doesn't matter if the number of frames comes out to a fraction - we are just going to be using that value to calculate the correct amount of movement per frame.

And that is exactly what the rest of the function does - calculate the correct amount of movement per frame. For each of the values (Left, Top, Width, Height) we calculate the difference between the old and the new values. If the difference is not 0, we divide by the total number of frames to get how much that value should move per frame. If the difference is 0, then 0 is the amount we will move per frame. Now that we have all of these values, we call a function called doFrame - which is the code right below:

function doFrame(eID, cLeft, nLeft, fLeft,
      cTop, nTop, fTop, cWidth, nWidth, fWidth,
      cHeight, nHeight, fHeight, callback)
{
   var el = document.getElementById(eID);
   if(el == null)
     return;

  cLeft = moveSingleVal(cLeft, nLeft, fLeft);
  cTop = moveSingleVal(cTop, nTop, fTop);
  cWidth = moveSingleVal(cWidth, nWidth, fWidth);
  cHeight = moveSingleVal(cHeight, nHeight, fHeight);

  el.style.left = Math.round(cLeft) + 'px';
  el.style.top = Math.round(cTop) + 'px';
  el.style.width = Math.round(cWidth) + 'px';
  el.style.height = Math.round(cHeight) + 'px';
 
  if(cLeft == nLeft && cTop == nTop && cHeight == nHeight
    && cWidth == nWidth)
  {
    if(callback != null)
      callback();
    return;
  }
   
  setTimeout( 'doFrame("'+eID+'",'+cLeft+','+nLeft+','+fLeft+','
    +cTop+','+nTop+','+fTop+','+cWidth+','+nWidth+','+fWidth+','
    +cHeight+','+nHeight+','+fHeight+','+callback+')', 40);
}

This function is where all the animation actually happens. First we translate the element id into the actual element again. We do this instead of passing the element because between frames the element could have disappeared - other javascript code may have run and changed the page. Then we calculate the new values for Left, Top, Width, and Height using a function called moveSingleVal, which I will explain in a moment. It then sets the element's left, top, width, and height style fields to the appropriate values. Here we have to round the current values because you can only set an element's position/size using integers, and the current values might not be (if the amount moved per frame is not an integer). We also have to add the string 'px' onto the values, so that they are interpreted correctly in the style tags.

We then check to see if the element has reached its final position/size by checking the current values against the desired new values. If they are all equal, we do not need to animate anymore - so we check to see if the callback function exists. If it does, call it, otherwise, return. If the element is not in its final position, use a setTimeout call to call doFrame again in 40 milliseconds. The setTimeout might look a little weird, but that is because we have to create a string that javascript will evaluate and run in the future - if we had just written out the doFrame function call, javascript would evaluate it at that moment. One thing to note here - while we ask here for setTimeout to wait 40 milliseconds, on slow computers it can potentially be a lot more than that. What this means is that there is always the potential for the animation to take longer then the time given in the initial animate call.

Ok, lets take a look at moveSingleVal:

function moveSingleVal(currentVal, finalVal, frameAmt)
{
  if(frameAmt == 0 || currentVal == finalVal)
    return finalVal;
 
  currentVal += frameAmt;
  if((frameAmt> 0 && currentVal>= finalVal)
    || (frameAmt <0 && currentVal <= finalVal))
  {
    return finalVal;
  }
  return currentVal;
}

This function calculates a new value for a frame given the current value, the final desired value, and the amount to move in a frame. If the frame amount is 0 or we are already at the final desired values, then we don't need to do any work, so we return the final value. Otherwise, we add the frame amount onto the current value. Now, if we are moving in a positive direction (the frame amount is positive), when the current value is greater than (or equal to) the final value, we have finished moving and should return the final value. But if we are moving in a negative direction, we have reached the final value when the current value is less than (or equal to) the new value. So that is why there are two cases to that if statement for returning the final value. If neither of those are met, we are still moving, so we return the modified current value.

That actually about covers everything. Here is the code all together in a single block for your ease of reading:

function animate(elementID, newLeft, newTop, newWidth,
      newHeight, time, callback)
{
  var el = document.getElementById(elementID);
  if(el == null)
    return;
 
  var cLeft = parseInt(el.style.left);
  var cTop = parseInt(el.style.top);
  var cWidth = parseInt(el.style.width);
  var cHeight = parseInt(el.style.height);
 
  var totalFrames = 1;
  if(time> 0)
    totalFrames = time/40;

  var fLeft = newLeft - cLeft;
  if(fLeft != 0)
    fLeft /= totalFrames;
 
  var fTop = newTop - cTop;
  if(fTop != 0)
    fTop /= totalFrames;
 
  var fWidth = newWidth - cWidth;
  if(fWidth != 0)
    fWidth /= totalFrames;
 
  var fHeight = newHeight - cHeight;
  if(fHeight != 0)
    fHeight /= totalFrames;
   
  doFrame(elementID, cLeft, newLeft, fLeft,
      cTop, newTop, fTop, cWidth, newWidth, fWidth,
      cHeight, newHeight, fHeight, callback);
}

function doFrame(eID, cLeft, nLeft, fLeft,
      cTop, nTop, fTop, cWidth, nWidth, fWidth,
      cHeight, nHeight, fHeight, callback)
{
   var el = document.getElementById(eID);
   if(el == null)
     return;

  cLeft = moveSingleVal(cLeft, nLeft, fLeft);
  cTop = moveSingleVal(cTop, nTop, fTop);
  cWidth = moveSingleVal(cWidth, nWidth, fWidth);
  cHeight = moveSingleVal(cHeight, nHeight, fHeight);

  el.style.left = Math.round(cLeft) + 'px';
  el.style.top = Math.round(cTop) + 'px';
  el.style.width = Math.round(cWidth) + 'px';
  el.style.height = Math.round(cHeight) + 'px';
 
  if(cLeft == nLeft && cTop == nTop && cHeight == nHeight
    && cWidth == nWidth)
  {
    if(callback != null)
      callback();
    return;
  }
   
  setTimeout( 'doFrame("'+eID+'",'+cLeft+','+nLeft+','+fLeft+','
    +cTop+','+nTop+','+fTop+','+cWidth+','+nWidth+','+fWidth+','
    +cHeight+','+nHeight+','+fHeight+','+callback+')', 40);
}

function moveSingleVal(currentVal, finalVal, frameAmt)
{
  if(frameAmt == 0 || currentVal == finalVal)
    return finalVal;
 
  currentVal += frameAmt;
  if((frameAmt> 0 && currentVal>= finalVal)
    || (frameAmt <0 && currentVal <= finalVal))
  {
    return finalVal;
  }
  return currentVal;
}

There is one other thing you might find interesting, though. The callback function has a lot of uses, because there are many cases where you might want to trigger something at the end of an animation. But where it is a lot of fun is animation sequences. For instance, at the end of one animation call, the callback can actually be to another animation call. Right below, we have some code that does just that:

function AroundAndAround()
{ animate("ex2Box",10,10,35,25,100,step2); }

function step2()
{ animate("ex2Box",486,10,35,25,2000,step3); }

function step3()
{ animate("ex2Box",496,10,25,35,100,step4); }

function step4()
{ animate("ex2Box",496,205,25,35,1000,step5); }

function step5()
{ animate("ex2Box",486,215,35,25,100,step6); }

function step6()
{ animate("ex2Box",10,215,35,25,2000,step7); }

function step7()
{ animate("ex2Box",10,205,25,35,100,step8); }

function step8()
{ animate("ex2Box",10,10,25,35,1000,step9); }

function step9()
{ animate("ex2Box",10,10,35,25,100,step2); }

You can see the result of this animation sequence below, when you hit the "Go!" button. It essentially animates the square around the box in an infinite loop:




Hope you enjoyed this tutorial, and can put this generic javascript animation function to good use. If you have any questions or comments, feel free to leave them below.



Posted in CSS, Javascript, All Tutorials by The Tallest |

4 Responses

  1. DevTop Says:

    Neat, thanks for sharing! At first your demo didn’t work. Turns out that IE7 apparently does not support JavaScript in its XML feed mode… I had to view your article in a regular browser window, where of course your demo works great.

  2. Yuji Kiriki Says:

    Marveleus work! Its nice that the animation doesn’t blinks…

  3. Anonymous Says:

    slideExample2(’examplePanel2′, this);

  4. Anil Dhiman Says:

    When I insert any asp control inside the Div, the div doesn’t collapse totally,

    It takes the width of the control and adjusts it height to the controls height

    please suggest

Leave a Comment

Please note: Comment moderation is enabled and may delay your comment. There is no need to resubmit your comment.

Powered by WP Hashcash