1

What Im trying to do is animate circles bouncing off the canvas borders. I can achieve that pretty will with just one ball. But my Home work asks of me to add a new ball every two seconds. Originally I tried this by making new objects listed in an array using a for loop, but had trouble coming up with a way to call "move" method out of each newly created object in an interval. At the end I settled with creating the variable j and setting it to 0 and having it increment by 10 since the interval will execute the function "startAnim" every 10 milliseconds. Then every two seconds have a new object created and then call that objects "move" method.

<!DOCTYPE html>
<html>
<head>
        <title>Some document</title>

<body>
            <h1></h1>
<canvas id="canvas" width = "400" height = "400"></canvas>          
<script src= "https://code.jquery.com/jquery-2.1.0.js"></script>

<script> 
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");

var arrayOfCollor = ["green", "red", "blue","gold","black","purple"];

var ballFunction = function(){
    this.x = 200;
    this.y = 200;
    this.xSpeed = 2;
    this.ySpeed = -3;
    this.radius = 10;
    ctx.fillStyle = arrayOfCollor[Math.floor(Math.random() * 
arrayOfCollor.length)];
}

ballFunction.prototype.draw = function(){
    ctx.beginPath();
    ctx.arc(this.x,this.y, this.radius, 0, Math.PI * 2, false);
    ctx.fill();
}

ballFunction.prototype.move = function(){

ctx.clearRect(0,0,400,400);
ctx.strokeRect(0,0,400,400);
this.x += this.xSpeed;
this.y += this.ySpeed;
this.draw();
if(this.x > 400-this.radius|| this.x < 0 + this.radius){
this.xSpeed = -1* this.xSpeed;
}
if(this.y > 400 - this.radius || this.y < 0 + this.radius){
this.ySpeed = -1* this.ySpeed;
}
}

var j = 0;

function startAnim(){
j+= 10; 
 if( j === 2000){
     var ballOne = new ballFunction
     var ballOneInterval = setInterval(ballOne.move,10)
     }
 if( j === 4000){
     var ballTwo = new ballFunction
     var ballTwoInterval = setInterval(ballTwo.move,10)
    }
 if( j === 6000){
     var ballThree = new ballFunction
     var ballThreeInterval = setInterval(ballThree.move,10)
    }
 if( j === 8000){
     var ballFour = new ballFunction
     var ballFourInterval = setInterval(ballFour.move,10)
   }
if( j === 10000){
    var ballFive = new ballFunction
    var ballFiveInterval = setInterval(ballFive.move,10)
   }
if( j === 12000){
    var ballSix = new ballFunction
    var ballSixinterval = setInterval(ballSix.move,10)
    }
}
var startAnimInt = setInterval(startAnim,10);


</script>
</body>
</html>

I keep receiving that the draw function is not a function at ballFunction.move even though I inserted it in.

gman
  • 100,619
  • 31
  • 269
  • 393
  • If I may, your code is... in need of a complete refactor. Never run an interval with less than 50ms. Never nest setInterval calls. Your whole startAnim should get replaced by a single called function which will call `setTimeout(addNewBall, t1); setTimeout(addNewBall, t2)...`. You'll avoid this never ending interval incrementing the poor `j` until it blows up. Never use setInterval to control a graphic animation, instead run one requestAnimationFrame powered loop, which will go inside an Array of Balls to move all the elements at every frame. – Kaiido Jul 21 '19 at 02:34
  • thanks for the advice, do you have any answers for me as to why "this" looses its binding in the setInterval method, and in general looses its context at all ? – David Pineda Jul 22 '19 at 22:54
  • `this` in most *methods* (a function attached to an Object) refers to the the Object it's attached to when called. `setInterval` is a method of `window` object, so there `this` will be `window`. To circumvent that, you can generate a bound *function* by calling `Function.prototype.bind`, or use an Arrow function (`_=>{ }`) that have no binding scope at call. – Kaiido Jul 22 '19 at 23:39
  • how do we know that certain methods belong to the window object? and what if I was to assighn a setInterval inside a function or another object, will setInterval still be the windows method ? – David Pineda Jul 23 '19 at 00:18
  • Everything you access as `foo` without a variable `foo` in the scope is mapped to `globalThis.foo` in javascript. In a browser, globalThis is window. So any non constructor function you can access globally is actually a window's method. – Kaiido Jul 23 '19 at 00:30
  • i just dont understand how the ballOne.move method looses reference to the object constructor. since ballOne.move is not an independent function – David Pineda Jul 23 '19 at 00:56
  • setInterval passes a method of an object as a plain old function. I think I established that in my head. But now the question is why does it pass the method of an object as a plain old function? therefore erasing the original object reference of "this" and setting it to the window object? – David Pineda Jul 23 '19 at 01:34
  • The problem is "how your function is being called". When using setInterval, the newBall's method is just a function and not a method anymore. It is being called internally by setInterval's routine. Normally, `this` should be undefined in such a case (at least in strict mode), but `setInterval` and some other methods of window will bind that function to window. [Here is a fiddle with a few cases](https://jsfiddle.net/vmnof3c7/) [edited the fiddle with more cases] – Kaiido Jul 23 '19 at 01:45
  • thank you for helping me out! god bless – David Pineda Jul 23 '19 at 01:59

3 Answers3

1

This is one of several common problems people run into with the this reference in JS. What this actually refers to is determined at runtime, and depends on how the function is called - not on how/where it was defined. The problem you have here is that you are using setInterval:

setInterval(ballOne.move,10)

which of course results in the JS engine calling ballOne.move itself, once every 10 milliseconds. But unfortunately, the way the engine will call it is the same as it will any old "plain function" it is passed - it has no way of knowing that the function it is calling is supposed to "belong" to the ballOne object, and therefore its this reference won't refer to ballOne, but instead to the global (window) object.

You can fix this by using the bind method of JS functions, to create a new version which will always have the specified this reference. That is, you can replace the above line with:

setInterval(ballOne.move.bind(ballOne),10)

and similarly for the other setInterval calls. This will of course quickly get repetitious, so it's probably better to fix this in the constructor, by adding the line:

this.move = this.move.bind(this);

Note that, although your question has nothing to do with React, this is what ReactJS recommends to do in Component classes, for exactly the same reason - to make sure the this reference is always the object intended.

Robin Zigmond
  • 17,805
  • 2
  • 23
  • 34
  • I used setInterval with one ball, creating a new object using the "ballFunction" constructor, and then calling that new objects "move" method, and it worked just fine. how come using the Setinterval method here created the promblem you stated up above. Why does "this" no longer refer to the object constructor? – David Pineda Jul 20 '19 at 16:18
  • I tried to explain in the answer, but without going into too much detail. I recommend you read [this](https://github.com/getify/You-Dont-Know-JS/blob/master/this%20%26%20object%20prototypes/ch2.md#implicitly-lost) - and the whole chapter of which its a part (although the link is to the part which explains exactly the situation you have) - because it's by far the best and most comprehensive explanation of `this` in JS that I've ever seen. – Robin Zigmond Jul 20 '19 at 16:24
1

The problem is that you pass the ballFunction method to setInterval as the first parameter and hence its context is lost and the this inside the function will be the window object. You can pass a function calling your move:

<!DOCTYPE html>
<html>
<head>
        <title>Some document</title>

<body>
            <h1></h1>
<canvas id="canvas" width = "400" height = "400"></canvas>          
<script src= "https://code.jquery.com/jquery-2.1.0.js"></script>

<script> 
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");

var arrayOfCollor = ["green", "red", "blue","gold","black","purple"];

var ballFunction = function(){
    this.x = 200;
    this.y = 200;
    this.xSpeed = 2;
    this.ySpeed = -3;
    this.radius = 10;
    ctx.fillStyle = arrayOfCollor[Math.floor(Math.random() * 
arrayOfCollor.length)];
}

ballFunction.prototype.draw = function(){
    ctx.beginPath();
    ctx.arc(this.x,this.y, this.radius, 0, Math.PI * 2, false);
    ctx.fill();
}

ballFunction.prototype.move = function(){

ctx.clearRect(0,0,400,400);
ctx.strokeRect(0,0,400,400);
this.x += this.xSpeed;
this.y += this.ySpeed;
this.draw();
if(this.x > 400-this.radius|| this.x < 0 + this.radius){
this.xSpeed = -1* this.xSpeed;
}
if(this.y > 400 - this.radius || this.y < 0 + this.radius){
this.ySpeed = -1* this.ySpeed;
}
}

var j = 0;

function startAnim(){
j+= 10; 
 if( j === 2000){
     var ballOne = new ballFunction
     var ballOneInterval = setInterval(function() {ballOne.move();},10)
     }
 if( j === 4000){
     var ballTwo = new ballFunction
     var ballTwoInterval = setInterval(function() {ballTwo.move();},10)
    }
 if( j === 6000){
     var ballThree = new ballFunction
     var ballThreeInterval = setInterval(function() {ballThree.move();},10)
    }
 if( j === 8000){
     var ballFour = new ballFunction
     var ballFourInterval = setInterval(function() {ballFour.move();},10)
   }
if( j === 10000){
    var ballFive = new ballFunction
    var ballFiveInterval = setInterval(function() {ballFive.move();},10)
   }
if( j === 12000){
    var ballSix = new ballFunction
    var ballSixinterval = setInterval(function() {ballSix.move();},10)
    }
}
var startAnimInt = setInterval(startAnim,10);


</script>
</body>
</html>
Lajos Arpad
  • 64,414
  • 37
  • 100
  • 175
0

The this inside the move function isn't referencing to the ballFunction object, but to the window.

Use something like:

var ballOne = new ballFunction
var ballOneInterval = setInterval(ballOne.move.bind(ballOne),10)

Instead of:

var ballOne = new ballFunction
var ballOneInterval = setInterval(ballOne.move,10)

for all balls.

Istador
  • 163
  • 7