JavaScript Animations, Part II: Real-Time Animations - Doc JavaScript | WebReference

JavaScript Animations, Part II: Real-Time Animations - Doc JavaScript


Real-Time Animations

First of all, let's get rid of the step() and animate() methods from our previous library. We won't be needing them anymore, because we're developing a real-time animation engine. step() and animate() process a given set of coordinates, which must be provided beforehand. Let's take another look at our set of sliding methods:

function slideBy(dx, dy, steps, interval, statement) {
  var fx = this.left();
  var fy = this.top();
  var tx = fx + dx;
  var ty = fy + dy;
  this.slideTo(tx, ty, steps, interval, statement);
}
function slideTo(tx, ty, steps, interval, statement) {
  var fx = this.left();
  var fy = this.top();
  var dx = tx - fx;
  var dy = ty - fy;
  var sx = dx / steps;
  var sy = dy / steps;
  var ar = new Array();
  for (var i = 0; i < steps; i++) {
    fx += sx;
    fy += sy;
    ar[i] = new pos(fx, fy);
  }
  this.path = ar;
  this.statement = (statement) ? statement : null;
  this.animate(interval);
}

We'll replace the steps parameter with increment -- the number of pixels to advance on each frame. In fact, that's the only change we need to apply to the slideBy() method, because slideBy() simply invokes slideTo(). Here's the new version of our slideBy() method:

function slideBy(dx, dy, increment, interval, statement) {
  var fx = this.left();
  var fy = this.top();
  var tx = fx + dx;
  var ty = fy + dy;
  this.slideTo(tx, ty, increment, interval, statement);
}

Now let's take a look at the new version of our slideTo() method:

function slideTo(tx, ty, increment, interval, statement) {
  var fx = this.left();
  var fy = this.top();
  var dx = tx - fx;
  var dy = ty - fy;
  var steps = Math.round(Math.sqrt(dx * dx + dy * dy) / increment);
  var sx = dx / steps;
  var sy = dy / steps;
  if (this.active) return;
  this.active = 1;
  this.lineSlide(0, fx, fy, tx, ty, sx, sy, interval);
}

As you can see, the function's first two statements are identical to those of the slideBy() method. Notice that the number of steps in the animation is required in order to calculate the horizontal and vertical distance per step (sx and sy). The number of steps (before rounding) is equal to the total distance (Math.sqrt(dx * dx + dy * dy), based on Pythagoras) divided by the distance per step (increment). The Math.round() method rounds a number. Since we want a smooth animation, we must make sure the number of frames is an integer.

Notice that the function is terminated if the active property has a true value. We cannot animate the element if it is already in action. If the element isn't moving, we can start animating it, so the active property is set to 1 (which is equivalent to true). After we're sure the element isn't moving, we can start the animation by calling the lineSlide() method:

function lineSlide(num, fx, fy, tx, ty, sx, sy, interval) {
  num++;
  this.moveTo(fx + sx * num, fy + sy * num);
  if (this.left() == tx) {
    this.active = 0;
    if (this.statement) eval(this.statement);
    return;
  }
  this.timer = setTimeout(this.name + ".lineSlide(" + num + -->
", " + fx + ", " + fy + ", " + tx + ", " + ty + ", " + sx + -->
", " + sy + ", " + interval + ")", interval);
}

The function calls itself after interval milliseconds. When the element's horizontal coordinate (this.left()) is equal to the target's horizontal coordinate (tx), the recursion is terminated -- the active property is set to 0, the specified statement is evaluated, and the function is terminated.

Notice that we aren't using the pos() function in our new animation engine. Therefore, we'll slightly modify moveTo() so it rounds the coordinates before moving the element (this isn't required, but it's good style):

function moveTo(x, y) {
  this.element.left = Math.round(x);
  this.element.top = Math.round(y);
}

In Column 18, JavaScript Animations, Part I, we discussed straight lines and circles. Now that we've built a new set of functions for real-time straight line animations, it's time to do the same for circular paths. For your reference, here's the old version of our circle() method:

function circle(radius, angle0, angle1, steps, interval, -->
statement) {
  var dangle = angle1 - angle0;
  var sangle = dangle / steps;
  var x = this.left();
  var y = this.top();
  var cx = x - radius * Math.cos(angle0 * Math.PI / 180);
  var cy = y + radius * Math.sin(angle0 * Math.PI / 180);
  var ar = new Array();
  for (var i = 0; i < steps; i++) {
    angle0 += sangle;
    x = cx + radius * Math.cos(angle0 * Math.PI / 180);
    y = cy - radius * Math.sin(angle0 * Math.PI / 180);
    ar[i] = new pos(x, y);
  }
  this.path = ar;
  this.statement = (statement) ? statement : null;
  this.animate(interval);
}

Again, we'll replace the steps parameter with increment, specifying the angle incrementation (per step). Since the new version of the method will support endless loops, we must add that option to the method's interface. By specifying an ending angle of null, the method will animate the element in an endless loop (be sure not to specify a statement, because the animation lasts forever). Here's the new version of the function:

function circle(radius, angle0, angle1, full, increment, -->
interval, statement) {
  angle0 = getAngle(angle0);
  if (angle1 != null) {
    angle1 = getAngle(angle1);
    var dangle = 360 * full + ((increment < 0) ? 360 - -->
Math.abs(angle1 - angle0) : Math.abs(angle1 - angle0));
    increment = dangle / Math.round(dangle / increment);
    angle1 = (increment < 0) ? angle0 - dangle : angle0 + dangle;
  }
  var cx = x - radius * Math.cos(angle0 * Math.PI / 180);
  var cy = y + radius * Math.sin(angle0 * Math.PI / 180);
  this.circleSlide(cx, cy, angle0, angle1, increment, -->
interval, statement);
}

Before we discuss this method, take a look at the getAngle() function:

function getAngle(angle) {
  var small = angle % 360;
  if (small 

It converts any angle to a 0-360 degree scale. For example, it converts 540 to 180, and -90 to 270. Note that the % operator returns the remainder of a division operation.

Back to the circle() method. We must make sure the animation is evenly spread, in case the user specifies an angle incrementation that does not equally divide the total angular distance. Remember that if angle1 is null, the animation is an endless loop, so we do not need to calculate its value. However, if it's not null, angle1 represents the ending angle. The statement:

var dangle = 360 * full + ((increment < 0) ? 360 - -->
Math.abs(angle1 - angle0) : Math.abs(angle1 - angle0));

computes the total angular distance, as a positive integer. Since one full circle is equivalent to 360 degrees, we multiply the value of full by 360, and add the remaining distance. In a clockwise rotation (increment < 0) the remaining distance is equal to:

360 - Math.abs(angle1 - angle0)

Likewise, in a counterclockwise rotation (increment > 0) the remaining distance is equal to the following expression:

Math.abs(angle1 - angle0)

The statement:

increment = dangle / Math.round(dangle / increment);

adjusts the value of increment so that it equally divides the entire angular distance. We then set angle1 to the ending angle, accounting for the number of full rotations. For example, it could be 720, -999, or any other integer value. The remaining portion of the function is explained in Column 18, JavaScript Animations, Part I, so let's take a look at the circleSlide() method, which actually moves the element:

function circleSlide(cx, cy, angle0, angle1, increment, -->
interval, statement) {
  angle0 += increment;
  var x = cx + radius * Math.cos(angle0 * Math.PI / 180);
  var y = cy - radius * Math.sin(angle0 * Math.PI / 180);
  this.moveTo(x, y);
  if ((angle1 != null) && (Math.abs(angle1 - angle0) < 0.001)) {
    this.active = 0;
    if (statement) eval(statement);
    return;
  }
  this.timer = setTimeout(this.name + ".circleSlide(" + cx + -->
", " + cy + ", " + angle0 + ", " + angle1 + ", " + increment + -->
", " + interval + ", '" + statement + "')", interval);
}

First, we add the angle incrementation to the angle0 parameter. We then use two trigonometric functions to calculate the new x and y coordinates, and call our moveTo() method to move the element to that position

The animation ends if angle1 isn't null (null specifies an endless loop), and when angle0 is equal to angle1. However, notice that we cannot use the standard == operator, because we are dealing with floating-point (real) numbers. This isn't the place to discuss binary representation of decimal numbers, but you should remember that floating-point calculations are not 100% accurate. Therefore, we must determine if angle0 and angle1 are nearly equal:

if ((angle1 != null) && (Math.abs(angle1 - angle0) < 0.001)) {
  .
  .
  .
}

Feel free to replace 0.001 with any other relatively small number, such as 0.01 and 0.005, as long as it's not 0. If angle0 is (almost) equal to angle1, the animation is terminated, and the specified statement is evaluated. Otherwise, the method's last statement invokes setTimeout() to call itself recursively after interval milliseconds.

http://www.internet.com


Created: May 21, 1998
Revised: May 21, 1998

URL: http://www.webreference.com/js/column19/realtime.html