-1

I have the following constructor and prototype:

function drawCircles() {
    $.each(circles, function() {
        this.draw(context);
    });
}

Circle.prototype.draw = function {
    // Drawing the circle using HTML5 canvas tag
}

circles is an array. How do I draw all of the circles with intervals like 100ms between each circle when rendering?

I tried the following but it returns Uncaught TypeError: this.draw is not a function

function drawCircles() {
    // context.clearRect(0, 0, screenW, screenH);
    var interval;
    $.each(circles, function(index) {
        setTimeout(function(){
            this.draw(context);
        }, interval);
        interval += 500;
    });
}
Albert Einstein
  • 7,472
  • 8
  • 36
  • 71
Dylan Hsiao
  • 131
  • 7
  • You could use `setTimeout()` to wait 100ms before calling the next draw. https://www.w3schools.com/jsref/met_win_settimeout.asp – NewToJS Sep 05 '17 at 01:30
  • it would not work inside an $.each() function. it's already reiterating the array? – Dylan Hsiao Sep 05 '17 at 01:32
  • Probably a duplicate of https://stackoverflow.com/questions/5202403/how-to-add-pause-between-each-iteration-of-jquery-each – Ray Toal Sep 05 '17 at 01:36
  • Related: [How to access the correct `this` inside a callback?](https://stackoverflow.com/q/20279484/218196) – Felix Kling Sep 05 '17 at 01:44

2 Answers2

0

You have to use an asynchronous approach for this (rather than a for-loop).

You can do this by making a closure. For example:

function drawCircles() {             // main function
  var i = 0, me = this;              // parent scope: iterator, self
  if (!circles.length) return;       // no circles to draw

  (function loop() {                 // inner loop
    var circle = circles[i++];       // get circle and increment iterator
    me.draw(circle, context);        // draw
    if (i >= circles.length) i = 0;  // reset iterator
    setTimeout(loop, 100);           // wait 100 ms, call inner loop
  })();                              // self-invoked
}

You can use flag or cancel the timeout to stop the loop.

function CircleCascade(context) {
  this.circles = [];
  this.context = context;
}
CircleCascade.prototype = {
  generate: function(count) {
    for(var i = 0; i < count; i++) {
      this.circles.push({
        x: Math.random() * this.context.canvas.width,
        y: Math.random() * this.context.canvas.height,
        r: Math.random() * 100 + 20,
        c: "hsl(" + (360 * Math.random()) + ",50%,50%)"
      })
    }
  },
  
  drawCircles: function() {            // main function
    var i = 0, me = this;              // parent scope: iterator, self
    if (!me.circles.length) return;    // no circles to draw

    (function loop() {                 // inner loop
      var circle = me.circles[i++];    // get circle and increment iterator
      me.draw(circle, me.context);     // draw
      if (i >= me.circles.length) i=0; // reset iterator
      setTimeout(loop, 100);           // wait 100 ms, call inner loop
    })();                              // self-invoked
  },
  
  draw: function(circle, context) {
    context.beginPath();
    context.fillStyle = circle.c;
    context.arc(circle.x, circle.y, circle.r, 0, 6.28);
    context.fill();
  }
};

var ctx = c.getContext("2d");
var cc = new CircleCascade(ctx);
cc.generate(50);
cc.drawCircles();
<canvas id=c width=600 height=400></canvas>
  • thank you, this is pretty advanced for my level. I put it in and this shows 'Uncaught TypeError: me.draw is not a function at loop' – Dylan Hsiao Sep 05 '17 at 01:47
  • @DylanHsiao the example assumed your code was working :) (ie. that `this` referenced the parent object with the methods shown in the question). –  Sep 05 '17 at 01:53
  • @DylanHsiao added a running example (Show code snippet). Adopt as needed –  Sep 05 '17 at 02:07
0

Binding a function to self

You can bind a function to its own reference and then call it directly from the setTimeout function.

First bind the draw function in the Circle constructor.

function Circle(settings){
    Object.assign(this,settings);
    this.draw = Circle.prototype.draw.bind(this);
}

Construct the prototype as normal

Circle.prototype = {
    draw(){
        ctx.beginPath();
        ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2);
        ctx.stroke();
    }
}

Then to draw the circles at intervals you can pass the bound draw function directly to the setTimeout function.

const circleInterval = 100;
function drawCircles(){
    circles.forEach((circle, i) => setTimeout(circle.draw, (i + 1) * circleInterval));
}

Example

//==========================================================
// helper functions randI for random integer and setOf creates an array
const randI = (min, max = min + (min = 0)) => (Math.random() * (max - min) + min) | 0;
const setOf = (count, cb = (i)=>i) => {var a = [],i = 0; while (i < count) { a.push(cb(i ++)) } return a };
//==========================================================
// get canvas an set example constants
const w = canvas.width = innerWidth;
const h = canvas.height = innerHeight;
const ctx = canvas.getContext("2d");
const circleCount = 1500;
const circleInterval = 100;
//==========================================================
function Circle(settings) {
  Object.assign(this, settings);
  this.draw = Circle.prototype.draw.bind(this);
}
Circle.prototype = {
  draw() {
    ctx.beginPath();
    ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2);
    ctx.stroke();
  }
}
//==========================================================
// create circle array
const circles = setOf(circleCount, () => new Circle({ x: randI(w), y: randI(h), r: randI(20, 100)}));

//==========================================================
// draw the circles at intervals
function drawCircles() {
  circles.forEach((circle, i) => setTimeout(circle.draw, (i + 1) * circleInterval));
}
drawCircles()
canvas {
  position: absolute;
  top: 0px;
  left: 0px;
}
<canvas id="canvas"></canvas>
Blindman67
  • 51,134
  • 11
  • 73
  • 136