Karlos Margaritos
Karlos Margaritos

Reputation: 25

Improving Canvas draw call Performance

I want to draw quite a few dots. That's what I do:

let canvas = document.getElementById('canvas');
let ctx = canvas.getContext('2d');

function render () {
    window.requestAnimationFrame(render);
    // clear screen
    ctx.clearRect(0, 0, cwidth, cheight);
    for (let p of particles) {
        p.rmove();
        p.render(ctx);
    }
}

render();

The drawing function for my point looks like this:

class Point {

    x: number;
    y: number;

    constructor(x, y) {
        this.x = x;
        this.y = y;
    }

    rmove() {
        this.x += Math.round(Math.random() < 0.5 ? -1 : 1);
        this.y += Math.round(Math.random() < 0.5 ? -1 : 1);
    }

    render (ctx) {
        ctx.fillStyle = "gray";
        ctx.beginPath();
        ctx.rect(this.x, this.y, 1.5,1.5);
        ctx.fill();
        ctx.stroke();
    }
}

Note that I round the values in the rmove() function, as canvas draws points with integer coordinates more quickly.

I'd like to somehow put all these drawing calls together.

Upvotes: 1

Views: 774

Answers (1)

Kaiido
Kaiido

Reputation: 136598

Make your points trace on the given context (could even be a Path2D), and keep the actual drawing for the renderer.

All your points have to do is to make the context moveTo their own coords before tracing the rect.

class Point {
    constructor(x, y) {
        this.x = x;
        this.y = y;
    }

    rmove() {
        this.x += Math.round(Math.random() < 0.5 ? -1 : 1);
        this.y += Math.round(Math.random() < 0.5 ? -1 : 1);
    }

    trace (ctx) {
      ctx.moveTo( this.x, this.y );
      ctx.rect(this.x, this.y, 1.5, 1.5);
    }
}

const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const cwidth = canvas.width = 300;
const cheight = canvas.height = 300;

const particles = Array.from(
  { length: 5000 },
  ()=> new Point( cwidth/2, cheight/2 )
);


function animate () {
  update();
  draw();
  window.requestAnimationFrame(animate);
}
function update() {
  for (let p of particles) {
    p.rmove();
  }
}
function draw() {
  // clear screen
  ctx.clearRect(0, 0, cwidth, cheight);

  // define our single path
  ctx.beginPath();
  for (let p of particles) {
    p.trace(ctx);
  }
  ctx.fillStyle = "gray";
  ctx.stroke(); // OP has it reversed, but then the fill-color is almost not visible
                // (1.5 width - 2*0.5 stroke leaves only 0.5 for the fill => antialiased...
  ctx.fill();
}

window.requestAnimationFrame( animate );
<canvas id="canvas"></canvas>

But this works only because all your particles share the same color. If they didn't, then you'd need a bit more logic:

const colors = ['red', 'green', 'blue', 'cyan', 'magenta', 'yellow'];
class Point {
    constructor(x, y, color=0) {
        this.x = x;
        this.y = y;
        this.color = color;
    }

    rmove() {
        this.x += Math.round(Math.random() < 0.5 ? -1 : 1);
        this.y += Math.round(Math.random() < 0.5 ? -1 : 1);
    }

    trace (ctx) {
      ctx.moveTo( this.x, this.y );
      ctx.rect(this.x, this.y, 1.5, 1.5);
    }
}

const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const cwidth = canvas.width = 300;
const cheight = canvas.height = 300;

const particles = Array.from(
  { length: 5000 },
  ()=> new Point( cwidth/2, cheight/2, (Math.random()*colors.length-1)|0 )
);


function animate () {
  update();
  draw();
  window.requestAnimationFrame(animate);
}
function update() {
  for (let p of particles) {
    p.rmove();
  }
}
function draw() {
  // clear screen
  ctx.clearRect(0, 0, cwidth, cheight);

  // define our single path
  let last_color = -1;
  for (let p of particles) {
    let p_color = p.color;
    if( p_color !== last_color ) {
      paint();
      last_color = p_color;
    }
    p.trace(ctx);
  }
  paint(); // the last
  
  function paint() {
    ctx.fillStyle = colors[ last_color ];
    ctx.strokeStyle = colors[ (last_color + 1) % colors .length ];
    ctx.stroke();
    ctx.fill();
    ctx.beginPath();
  }
}

window.requestAnimationFrame( animate );
<canvas id="canvas"></canvas>

Though doing this, you may very well end up with a lot of drawings, so a final trick which might not work everywhere is to sort your particles by their color. This results in a different graphic since this one color will always be at the top, but it might work in some cases and the performance gain can outfit the downside.

const colors = ['red', 'green', 'blue', 'cyan', 'magenta', 'yellow'];
class Point {
    constructor(x, y, color=0) {
        this.x = x;
        this.y = y;
        this.color = color;
    }

    rmove() {
        this.x += Math.round(Math.random() < 0.5 ? -1 : 1);
        this.y += Math.round(Math.random() < 0.5 ? -1 : 1);
    }

    trace (ctx) {
      ctx.moveTo( this.x, this.y );
      ctx.rect(this.x, this.y, 1.5, 1.5);
    }
}

const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const cwidth = canvas.width = 300;
const cheight = canvas.height = 300;

const particles = Array.from(
  { length: 5000 },
  ()=> new Point( cwidth/2, cheight/2, (Math.random()*colors.length-1)|0 )
);

particles.sort( (a, b) => a.color - b.color );

function animate () {
  update();
  draw();
  window.requestAnimationFrame(animate);
}
function update() {
  for (let p of particles) {
    p.rmove();
  }
}
function draw() {
  // clear screen
  ctx.clearRect(0, 0, cwidth, cheight);

  // define our single path
  let last_color = -1;
  for (let p of particles) {
    let p_color = p.color;
    if( p_color !== last_color ) {
      paint();
      last_color = p_color;
    }
    p.trace(ctx);
  }
  paint(); // the last
  
  function paint() {
    ctx.fillStyle = colors[ last_color ];
    ctx.strokeStyle = colors[ (last_color + 1) % colors .length ];
    ctx.stroke();
    ctx.fill();
    ctx.beginPath();
  }
}

window.requestAnimationFrame( animate );
<canvas id="canvas"></canvas>

And nothing prevents you to generate chunks from these sorted particles so it looks more random.

Upvotes: 2

Related Questions