Monday, February 18, 2008

Javascript Animation - Part 1 - The Simple Stuff

So I've started work on some new GUI widgets, and realized that many of them would be much easier to do in Flash. But unless I planned hook into the Flash player via fscommand or the getURL method, things would get very heavy very fast. An entirely Flash-based interface is, in my humble opinion, a waste of bandwidth and creates as many problems as it solves - not the least of which is maintainability and scalability. Rebuilding and deploying a large Flash-app is often not a trivial matter, and because the result is a binary, all or nothing file, one cannot easily turn features on and off to suit either the environment or the user's preferences. So with those concerns in mind, I wanted to see what I could do using only JavaScript, CSS and HTML, (and a little JSON and/or XML thrown in for data transfer purposes).

As soon as I started working on the main class that would handle the creation, display and animation of a few simple DIV blocks of content, I realized that I would have to recreate the kind of frame-based system that Flash uses. But since HTML and Javascript were not really intended to be an animation platform, there were a number of conceptual and practical hurdles to overcome. The one I decided to tackle first was to undermine one of the DOM's greatest strengths, and use a more obscure feature to rebuild it to my specs.


The "onpropertychange" Problem

The ability to change HTML element properties using Javascript, and have those changes be reflected instantly on the screen, is one of the most basic tasks a script can perform, and one of the strengths of the system. So overriding that behavior is neither trivial nor without consequence.


For example, if I want to animate an object's position on the screen using Javascript, I have to keep in mind that the human eye sees things at around 24 frames-per-second, meaning that anything faster will not even register. So if I try to do this...



for( var i=0; i<100; i++ )
some_element.style.left = ( findPosX( some_element ) + 1 ) + "px";


/*
NOTE: The method findPosX gets the X position of an element
regardless of the browser. I won't reproduce it here,
but you can get it at QuirksMode.org, a great site for all
things cross-browser.
*/



...the element will simply vanish and reappear instantly 100 pixels to the right . Not exactly a smooth animation. The problem is that each time the loop is executed, and the onpropertychange event is fired for the element we are moving, and the render engine moves the element to the newly specified position - in this case, one pixel to the right. But since the loop executes in a few hundredths of a second, the eye can't see what's happening during each iteration. It is in fact, worse than that, because unless the refresh rate of the screen you are using is fast enough to actually render them all, most of the iterations never even make it to your eye.

So how do we insure that things will happen at a rate that the eye can actually perceive? The answer to make sure each iteration is separated by enough time for both the screen to render the change and your eye to perceive it. That's where setInterval comes in handy.


Using setInterval to slow things down a bit

The native JavaScript method setInterval requires two arguments: a function to execute over and over again, and the number of milliseconds to wait between executions. So for example, I can declare a function that will move the element one pixel to the right...
  function moveElement(){
some_element.style.left = ( findPosX( some_element ) + 1 ) + "px";
}

...and then call that function once a second using setInterval.


  var interval_id = window.setInterval( moveElement, 1000 ); // 1000 milliseconds = 1 second  


Note how setInterval returns a unique id we can use to clear the interval using, you guessed it, clearInterval.


  window.clearInterval( interval_id );

Now the whole point is to make the element move at a rate slow enough to perceive, but fast enough to appear smooth - i.e., as though the element were a real thing moving across our field of vision, so we have to speed things up just a bit. Since 24 frames per/second is the standard for film, that's what we'll use. So if our formula is as follows:


24 frames per/second = 1 frame every 1/24th of a second 
1/24 seconds = 0.04 seconds
0.04 seconds = 40 milliseconds

So at the very minimum, we want to move the element one pixel every 40 milliseconds. But setting the interval to 40ms is, in general, not going to do the job because there are a lot of other factors at work.


Keep everything, not just the animation, moving smoothly

JavaScript is not a threaded language. Most browsers these days do create a thread for the JavaScript interpreter to execute in, but everything that happens in that thread must wait for it's turn. As a result, more than the specified number of milliseconds may pass between executions of our moveElement

method if some other JavaScript code is executed at the same time, e.g., while a user interacts with another element that uses JavaScript to change some property or another. Moreover, the browser will often pause the JavaScript thread to give priority to give some other more important thread a chance - like the one that makes HTTP requests, the one that creates context menus, etc. With this in mind, we are tempted to drop the number of milliseconds between executions down to 1, so we can be sure that the animation does not appear choppy. After all, it would seem that smooth and speedy is better than slow and choppy. But there is a downside. If we ask for the function to be executed every millisecond, we are effectively requesting the interpreter to make everything else a lower priority - including user interaction. So if by clicking the "Do Something" button, we begin our animation AND create and send an XMLHttpRequest, the request process will not have a chance to do its thing until after the animation has completed. The axiom "form follows function" holds just as true in the development of web interfaces as it does in the architecture of buildings, so we do not want to make the user wait for some important functionality just so we can be sure that we have a photorealistic animation. Better to compromise a bit and keep it at 30ms or so. That way other processes have a chance to do their thing.


So here is the code so far. Note that we check to see if the element has moved the correct number of pixels, and if it has, we stop execution. (Please ignore the ugly global variables. We'll get to making this prettier later on.)


  var interval_id; 
var some_element;
var start_position;
var end_position;

start_animation_button.onclick = function(){

// THE ABSOLUTELY POSITIONED ELEMENT WE WANT TO MOVE
some_element = document.getElementById( "some-element" );

// THE ELEMENT'S X POSITION
start_position = findPosX( some_element );

// where we want it to end up: 100 pixels to the right.
end_position = start_position + 100;

// move the element one pixel (at least) once every 30ms
interval_id = window.setInterval( moveElement, 30 );
}

// the method to execute repeatedly
function moveElement(){

// where it is at the moment
var current_left = parseInt( some_element.style.left );

// test if we've reached the destination - if so, stop executing the method
if( current_left >= end_position )
window.clearInterval( interval_id );
}



When the "start_animation_button" is clicked, we initialize a pointer to the element we want to move, and determine the start and end positions. With these three variables, we can test the current position of the element during each execution of the method, and stop execution when the element has moved the specified number of pixels to the right.


 


In the next entry, I will talk more about how to best manage multiple changes to an element during each "frame".

No comments: