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:
setTimeout(this.animate, 40);
...will indeed call your animate
function, but with this
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 on window
, which is clearly not what you want.
Instead, you can use a closure:
var me = this;
setTimeout(function() {
me.animate();
}, 40);
That works because the anonymous function we're giving to setTimeout
is a closure over the context in which it's defined, which includes the me
variable we're setting up before defining it. By calling animate
from a property on the object (me.animate()
), we're telling JavaScript to set up this
to be the object during the call.
Some frameworks have methods to create this closure for you (jQuery has jQuery.proxy
, Prototype has Function#bind
), and ECMAScript 5 (about 18 months old) defines a new Function#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.:
this.animate = function animate() { ... };
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 managing this
.
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 for shapes
and a normal for
loop (not for..in
) for looping through the array (read this for why: Myths and realities of for..in
):
var shapes = [
new Shape(0,0,50,50,10)),
new Shape(0,100,100,50,10)),
new Shape(0,200,100,100,10))
];
/**
* AnimationManager class
* animate() runs the animation cycle
*/
var AnimationManager = function(canvas)
{
var canvasWidth = canvas.width(),
canvasHeight = canvas.height(),
ctx = canvas.get(0).getContext('2d'),
running = true, // Really true? Not false?
me = this;
// Set up our public functions
this.start = AnimationManager_start;
this.run = AnimationManager_run;
this.stop = AnimationManager_stop;
/** Start the animations **/
function AnimationManager_start(){
running = true;
animate();
}
/** Allow the animations to run */
function AnimationManager_run(){
running = true;
}
/** Stop the animations from running */
function AnimationManager_stop(){
running = false;
}
/** Internal implementation **/
function animate()
{
if (running)
{
update();
clear();
draw();
}
setTimeout(animate, 40); //25fps
}
/** Update all of the animations */
function update()
{
var i;
for (i = 0; i < shapes.length; ++i) // not for..in
{
shapes[i].moveRight();
}
}
/** Clear the canvas */
function clear()
{
ctx.clearRect(0,0, canvasWidth, canvasHeight);
}
/** Draw all of the updated elements */
function draw()
{
var i;
for (i = 0; i < shapes.length; ++i) // not for..in
{
ctx.fillRect(shapes[i].x, shapes[i].y, shapes[i].w, shapes[i].h);
}
}
}
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With