The excellent book DOM Scripting by Jeremy Keith includes and example of how to animate a simple page element around the screen using setTimeout() to move the element incrementally every n milliseconds. There's nothing especially remarkable about the example. It seems straightforward enough, but I ran into problems when I tried to extend it.
I would like to be able to chain movements so I could run the page element around the edges of a rectangle as shown below.
I found the movements seemed to conflict with one another and only one movement occurs. This post explores setTimeout(), the reason for the problem I found and how to produce the animation sequence I want.
Starting with a simple page that shows the first movement in the rectangle. The important elements are highlighted bold.
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<link rel="stylesheet" href="styles/typography.css" type="text/css" media="screen" charset="utf-8"/>
<script type="text/javascript" charset="utf-8" src="scripts/addLoadEvent.js"></script>
<script type="text/javascript" charset="utf-8" src="scripts/animate.js"></script>
<title>
Animation Example
</title>
</head>
<body>
<p id="message">Whee!</p>
</body>
</html>
The JavaScript is based on the example from the book.
addLoadEvent.js (manages a queue of calls to window.onload)
function addLoadEvent(func) {
var oldOnLoad = window.onload;
if (typeof window.onload != 'function') {
window.onload = func;
} else {
window.onload = function () {
oldOnLoad();
func();
}
}
}
animate.js
addLoadEvent(animateMessagePosition);
function animateMessagePosition() {
if (!document.getElementById ||
!document.getElementById("message")
) return false;
var elem = document.getElementById("message");
positionElement(elem, 10, 10);
moveMessage(elem, 250, 10);
function positionElement(elem, left, top) {
elem.style.position = "absolute";
elem.style.left = left + "px";
elem.style.top = top + "px";
}
function moveMessage(elem, xtarget, ytarget) {
if (!setTimeout) return false;
var xpos = parseInt(elem.style.left);
var ypos = parseInt(elem.style.top);
if (xpos == xtarget && ypos == ytarget) return true;
if (xpos < xtarget) xpos++;
if (xpos > xtarget) xpos--;
if (ypos < ytarget) ypos++;
if (ypos > ytarget) ypos--;
positionElement(elem, xpos, ypos);
setTimeout(function () {moveMessage(elem, xtarget, ytarget)}, 5);
}
This works fine. The "whee!" text moves across the screen. Let's add the 4 movements we want in sequence and uncomment the second step.
function animateMessagePosition() {
if (!document.getElementById ||
!document.getElementById("message")
) return false;
var elem = document.getElementById("message");
positionElement(elem, 10, 10);
moveMessage(elem, 250, 10);
moveMessage(elem, 250, 250);
// moveMessage(elem, 10, 250);
// moveMessage(elem, 10, 10);
}
The second movement is ignored!
The problem occurs because setTimeout() does not block the execution our script It simply registers a callback function and a timeout value in the JavaScript runtime and returns control to the calling script. The callback runs when timeout expires. John Resig describes JavaScript timers in much more detail.
This behaviour is OK for a single call to moveMessage, but adding the second call means that two timeouts are ticking down at the same time and the first one registered undoes the changes made by the second one. We can demonstrate this happening by adding a debug trace to the page.
First, I added an extra div to the markup
...
<body>
<p id="message">Whee!</p>
<div id="statusMessages" style="position: absolute; left: 10px; top: 260px;">
<h3>Status Messages:</h3>
</div>
</body>
...
and a new JavaScript function which writes a position as (x,y) co-ordinates into the div
function writePosition(left, top) {
if (!document.createElement ||
!document.getElementById ||
!document.getElementById("statusMessages") ||
!document.createTextNode ||
!document.body.appendChild
) return false;
var msgNode = document.createElement("p");
var textNode = document.createTextNode("position: (" + left + "," + top + ")");
msgNode.appendChild(textNode);
var statusMessages = document.getElementById("statusMessages");
statusMessages.appendChild(msgNode);
}
finally, I updated the function that performs the movements to write a debug line
function positionElement(elem, left, top) {
elem.style.position = "absolute";
elem.style.left = left + "px";
elem.style.top = top + "px";
writePosition(left, top);
}
Refreshing the page starts the animation and displays a list of all the movements made. Here are the first few positions shown:
The first line is the initial position. Then the first call to moveMessage kicks in an shifts the x position by 1. Next the second call starts and moves the y position by 1. However, when the first call picks up again, it resets the y position and increments the x position. Each call looks at the current position of the message and shifts it's x and y position to move it closer to the target. This means that the message is actually wobbling up and down the y axis by 1 pixel as it moves right but first call eventually gets the message to x == 250. At that point, the 2 function calls enter an infinite tug of war where the first call cannot complete because the second call keeps moving the message 1 pixel down the y axis!
How do we fix it?
The moveMessage() function manages it's own callbacks once it starts. I like this design because JavaScript programs run in a single thread and it's important not to block the thread while animating because that would lock the page. There's no reason to change the design pattern. We can fix the problem by make moveMessage() call the next animation when it is completed. This is achieved by passing a callback function to moveMessage that runs once the animation is completed.
function moveMessage(elem, xtarget, ytarget, nextMove) {
if (!setTimeout) return false;
var xpos = parseInt(elem.style.left);
var ypos = parseInt(elem.style.top);
if (xpos == xtarget && ypos == ytarget) {
if (typeof nextMove == 'function') nextMove();
return true;
}
if (xpos < xtarget) xpos++;
if (xpos > xtarget) xpos--;
if (ypos < ytarget) ypos++;
if (ypos > ytarget) ypos--;
positionElement(elem, xpos, ypos);
setTimeout(function () {moveMessage(elem, xtarget, ytarget, nextMove)}, 10);
}
And where it is called...
function animateMessagePosition() {
if (!document.getElementById ||
!document.getElementById("message")
) return false;
var elem = document.getElementById("message");
positionElement(elem, 10, 10);
moveMessage(elem, 250, 10, function () {moveMessage(elem, 250, 250)});
// moveMessage(elem, 10, 250);
// moveMessage(elem, 10, 10);
}
nextMove is an optional argument to moveMethod and if it is undefined the animation sequence will simply end.
This works but there is still a problem. The call to moveMessage is becoming a little hard to understand and it will become a tangled mess of round and curly brackets if the remaining movements in the sequence were to be added. Really, I would like to be able to setup a chain of animations and then run it. Something like this would do the trick...
sequence = createAsyncSequence();
sequence.add(moveMessage, [elem, 250, 10]);
sequence.add(moveMessage, [elem, 250, 250]);
sequence.add(moveMessage, [elem, 10, 250]);
sequence.add(moveMessage, [elem, 10, 10]);
sequence.run();
Before I wrote this, I noticed that the moveMessage function could move any element, while the variable 'elem' points to our message. We should refactor these naming problems out before they become confusing. Here are the updated functions
function animateMessagePosition() {
if (!document.getElementById ||
!document.getElementById("message")
) return false;
var message = document.getElementById("message");
positionElement(message, 10, 10);
sequence = createAsyncSequence();
sequence.add(moveElement, [message, 250, 10]);
sequence.add(moveElement, [message, 250, 250]);
sequence.add(moveElement, [message, 10, 250]);
sequence.add(moveElement, [message, 10, 10]);
sequence.run();
}
function moveElement(elem, xtarget, ytarget, nextMove) {
if (!setTimeout) return false;
var xpos = parseInt(elem.style.left);
var ypos = parseInt(elem.style.top);
if (xpos == xtarget && ypos == ytarget) {
if (typeof nextMove == 'function') nextMove();
return true;
}
if (xpos < xtarget) xpos++;
if (xpos > xtarget) xpos--;
if (ypos < ytarget) ypos++;
if (ypos > ytarget) ypos--;
positionElement(elem, xpos, ypos);
setTimeout(function () {moveElement(elem, xtarget, ytarget, nextMove)}, 10);
}
Now, I can write createAsyncSequence()
function createAsyncSequence() {
var my = {};
var seq = [];
var emptyEntry = {
name : "",
args : [],
};
emptyEntry.toFunc = function () {
var that = this;
return function () {that.name.apply(null, that.args)};
}
my.add = function (name, args) {
var entry = Object.create(emptyEntry);
entry.args = args;
if (seq.length > 0) seq[seq.length - 1].args.push(entry.toFunc());
seq.push(entry);
};
my.run = function () {
if(seq.length > 0) {
return seq[0].toFunc() ();
} else {
return true;
}
};
return my;
}
Notice the call to Object.create. This function creates a new object based on an existing object prototype. It is added at the top of the source file.
if (typeof Object.create !== 'function') {
Object.create = function(o) {
var F = function () {};
F.prototype = o;
return new F();
}
}
Now, let's dig into the code that creates the asynchronous sequence. First, we create an empty object that we will build and return, and an empty array to hold the sequence of calls
function createAsyncSequence() {
var my = {};
var seq = [];
My strategy is to store the data needed to run the sequence of calls in an array and convert it to actual function calls as and when they are needed. We, therefore, need a private class to hold the items in the sequence. We do this by creating an empty object that we can clone using prototype inheritance. Strictly speaking, it's not necessary to create a prototype object in this case because we could attach attributes to an empty object later. However, I think it makes the code clearer by stating explicitly what attributes this object holds so I have decided to create it anyway.
var emptyEntry = {
name : "",
args : [],
};
We also need a function to convert our entry objects into function calls.
emptyEntry.toFunc = function () {
var that = this;
return function () {that.name.apply(null, that.args)};
}
Now we have the plumbing in place, we add two methods to the async sequence object we are building. One to add items to the sequence and the other to run the sequence. First the add method.
my.add = function (name, args) {
var entry = Object.create(emptyEntry);
entry.args = args;
if (seq.length > 0) seq[seq.length - 1].args.push(entry.toFunc());
seq.push(entry);
};
The critical line is highlighted in bold. Remember that moveElement() takes an optional argument called nextMove that holds a function to run after the movement is completed. The my.add method appends the 'functionized' version of the entry we are adding as the final argument to the previous method call in the sequence. The run method simply needs to start the first movement in the sequence and the chain of methods will look after itself.
Notice also the call to Object.create described above.
Here is the run method. It simply converts the first entry to a function and then runs it.
my.run = function () {
if(seq.length > 0) {
return seq[0].toFunc() ();
} else {
return true;
}
};
Finally, we return the asynchronous sequence object.
Well, that's covered a fair amount for now. The next step is to add a way to stop the animation mid-flight when the user clicks a button but I'll leave that until part 2.