Is it possible to use setTimout() within a JavaScript object?
Currently the animation method call is running once, it seems that the setTimeout() isn't doing its job. I have managed to get it working, but in a really hackish method of having a function outside of the class which uses the setTimeout. I'd like to make the animation loop a job for the AnimationManager class. If you can see any bad practice, or where i'm going wrong.. please give me a heads up!
JavaScript:
var AnimationManager = function(canvas)
{
this.canvas = canvas;
this.canvasWidth = canvas.width();
this.canvasHeight = canvas.height();
this.ctx = canvas.get(0).getContext('2d');
this.running = true;
this.start = function start(){
this.running = true;
this.animate();
}
/** Allow the animations to run */
this.run = function run(){
this.running = false;
}
/** Stop the animations from running */
this.stop = function stop(){
this.running = false;
}
this.animate = function animate()
{
if(this.running)
{
this.update();
this.clear();
this.draw();
}
setTimeout(this.animate, 40); //25 fps
}
/** Update all of the animations */
this.update = function update()
{
for(var i in shapes)
{
shapes[i].moveRight();
}
}
/** Clear the canvas */
this.clear = function clear()
{
this.ctx.clearRect(0,0, this.canvasWidth, this.canvasHeight);
}
/** Draw all of the updated elements */
this.draw = function draw()
{
for(var i in shapes)
{
this.ctx.fillRect(shapes[i].x, shapes[i].y, shapes[i].w, shapes[i].h);
}
}
}
JavaScript within the index page, which demonstrates how i'd like the AnimationManager to work:
<script type="text/javascript">
$(document).ready(function() {
var canvas = $('#myCanvas');
var am = new AnimationManager(canvas);
am.start();
//If true play the animation
var startButton = $("#startAnimation");
var stopButton = $("#stopAnimation");
stopButton.hide();
//Toggle between playing the animation / pausing the animation
startButton.click(function()
{
$(this).hide();
stopButton.show();
am.run();
});
stopButton.click(function()
{
$(this).hide();
startButton.show();
am.stop();
});
});
</script>
Here's the working code, thanks to T.J. Crowder for fix + interesting blog post: Double-take
Solution: Changes in code are marked with //#########
var shapes = new Array();
shapes.push(new Shape(0,0,50,50,10));
shapes.push(new Shape(0,100,100,50,10));
shapes.push(new Shape(0,200,100,100,10));
/**
* AnimationManager class
* animate() runs the animation cycle
*/
var AnimationManager = function(canvas)
{
this.canvas = canvas;
this.canvasWidth = canvas.width();
this.canvasHeight = canvas.height();
this.ctx = canvas.get(0).getContext('2d');
this.running = true;
var me = this; //#################################Added this in
this.start = function(){
this.running = true;
this.animate();
}
/** Allow the animations to run */
this.run = function(){
this.running = true;
}
/** Stop the animations from running */
this.stop = function(){
this.running = false;
}
this.animate = function()
{
if(this.running)
{
this.update();
this.clear();
this.draw();
}
//###################### Now using me.animate()
setTimeout(function(){
me.animate();
}, 40); //25 fps
}
/** Update all of the animations */
this.update = function()
{
for(var i in shapes)
{
shapes[i].moveRight();
}
}
/** Clear the canvas */
this.clear = function()
{
this.ctx.clearRect(0,0, this.canvasWidth, this.canvasHeight);
}
/** Draw all of the updated elements */
this.draw = function()
{
for(var i in shapes)
{
this.ctx.fillRect(shapes[i].x, shapes[i].y, shapes[i].w, shapes[i].h);
}
}
}
The problem with the code is that in JavaScript,
this
is set (in the normal case) by how a function is called, not where it's defined. This is different than some other languages you might be used to such as Java or C#. So this line:...will indeed call your
animate
function, but withthis
set to the global object (window
, on browsers). So all of those properties you're accessing (this.running
, etc.) will not be looking at your object, but rather looking for those properties onwindow
, which is clearly not what you want.Instead, you can use a closure:
That works because the anonymous function we're giving to
setTimeout
is a closure over the context in which it's defined, which includes theme
variable we're setting up before defining it. By callinganimate
from a property on the object (me.animate()
), we're telling JavaScript to set upthis
to be the object during the call.Some frameworks have methods to create this closure for you (jQuery has
jQuery.proxy
, Prototype hasFunction#bind
), and ECMAScript 5 (about 18 months old) defines a newFunction#bind
feature for JavaScript that does it. But you can't rely on it yet in browser-based implementations.More discussion and solutions here: You must remember
this
Possibly off-topic: In your code, you're using a lot of named function expressions. E.g.:
Named function expressions don't work correctly on IE prior to, I think, IE9. IE will actually create two completely separate functions (at two separate times). More here: Double-take
Update and a bit off-topic, but since all of your functions are defined as closures within your
AnimateManager
constructor anyway, there's no reason for anything you don't want to be public to be public, and you can completely get rid of issues managingthis
.Here's the "solution" code from your updated question, making use of the closures you're already defining to avoid
this
entirely other than when defining the public functions. This also uses array literal notation forshapes
and a normalfor
loop (notfor..in
) for looping through the array (read this for why: Myths and realities offor..in
):Each object created via
new AnimationManager
will get its own copy of the local variables within the constructor, which live on as long as any of the functions defined within the constructor is referenced anywhere. Thus the variables are truly private, and instance-specific. FWIW.