piyush walia
piyush walia

Reputation: 53

Canvas using high CPU in chrome

I am using a Codepen demo but after checking the CPU usage in chrome, it is using approx 100% of CPU. After trying hard, I am not able to figure out the problem as I am not an expert in javascript and canvas. What modifications do I need to make it use less CPU. Codepen Link As per my understanding, the problem is in animating particles or maybe I am wrong.

// Global Animation Setting
window.requestAnimFrame = 
  window.requestAnimationFrame ||
  window.webkitRequestAnimationFrame ||
  window.mozRequestAnimationFrame ||
  window.oRequestAnimationFrame ||
  window.msRequestAnimationFrame ||
  function(callback) {
    window.setTimeout(callback, 1000/60);
};

// Global Canvas Setting
var canvas = document.getElementById('particle');
var ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;


// Particles Around the Parent
function Particle(x, y, distance) {
  this.angle = Math.random() * 2 * Math.PI;
  this.radius = Math.random() ; 
  this.opacity =  (Math.random()*5 + 2)/10;
  this.distance = (1/this.opacity)*distance;
  this.speed = this.distance*0.00003;
  
  this.position = {
    x: x + this.distance * Math.cos(this.angle),
    y: y + this.distance * Math.sin(this.angle)
  };
  
  this.draw = function() {
    ctx.fillStyle = "rgba(255,255,255," + this.opacity + ")";
    ctx.beginPath();
    ctx.arc(this.position.x, this.position.y, this.radius, 0, Math.PI*2, false);
    ctx.fill();
    ctx.closePath();
  }
  this.update = function() {
    this.angle += this.speed; 
    this.position = {
      x: x + this.distance * Math.cos(this.angle),
      y: y + this.distance * Math.sin(this.angle)
    };
    this.draw();
  }
}

function Emitter(x, y) {
  this.position = { x: x, y: y};
  this.radius = 30;
  this.count = 3000;
  this.particles = [];
  
  for(var i=0; i< this.count; i ++ ){
    this.particles.push(new Particle(this.position.x, this.position.y, this.radius));
  }
}


Emitter.prototype = {
  draw: function() {
    ctx.fillStyle = "rgba(0,0,0,1)";
    ctx.beginPath();
    ctx.arc(this.position.x, this.position.y, this.radius, 0, Math.PI*2, false);
    ctx.fill();
    ctx.closePath();    
  },
  update: function() {  
   for(var i=0; i< this.count; i++) {
     this.particles[i].update();
   }
    this.draw(); 
  }
}


var emitter = new Emitter(canvas.width/2, canvas.height/2);

function loop() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  emitter.update();
  requestAnimFrame(loop);
}

loop();
body{background:#000;}
 <canvas id="particle"></canvas>

enter image description here

Upvotes: 0

Views: 3216

Answers (1)

Kaiido
Kaiido

Reputation: 136688

Avoid semi-transparency as much as possible.

Painting with alpha is a CPU killer, avoid blending as much as possible by using solid colors:

// Global Animation Setting
window.requestAnimFrame = 
  window.requestAnimationFrame ||
  window.webkitRequestAnimationFrame ||
  window.mozRequestAnimationFrame ||
  window.oRequestAnimationFrame ||
  window.msRequestAnimationFrame ||
  function(callback) {
    window.setTimeout(callback, 1000/60);
};

// Global Canvas Setting
var canvas = document.getElementById('particle');
var ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;


// Particles Around the Parent
function Particle(x, y, distance) {
  this.angle = Math.random() * 2 * Math.PI;
  this.radius = Math.random() ; 
  this.opacity =  (Math.random()*5 + 2)/10;
  // convert to solid color '#nnnnnn'
  this.color = '#' + Math.floor((this.opacity * 255)).toString(16).padStart(2, 0).repeat(3);
  this.distance = (1/this.opacity)*distance;
  this.speed = this.distance*0.00003;
  
  this.position = {
    x: x + this.distance * Math.cos(this.angle),
    y: y + this.distance * Math.sin(this.angle)
  };
  
  this.draw = function() {
    ctx.fillStyle = this.color;
    ctx.beginPath();
    ctx.arc(this.position.x, this.position.y, this.radius, 0, Math.PI*2, false);
    ctx.fill();
    ctx.closePath();
  }
  this.update = function() {
    this.angle += this.speed; 
    this.position = {
      x: x + this.distance * Math.cos(this.angle),
      y: y + this.distance * Math.sin(this.angle)
    };
    this.draw();
  }
}

function Emitter(x, y) {
  this.position = { x: x, y: y};
  this.radius = 30;
  this.count = 3000;
  this.particles = [];
  
  for(var i=0; i< this.count; i ++ ){
    this.particles.push(new Particle(this.position.x, this.position.y, this.radius));
  }
}


Emitter.prototype = {
  draw: function() {
    ctx.fillStyle = "rgba(0,0,0,1)";
    ctx.beginPath();
    ctx.arc(this.position.x, this.position.y, this.radius, 0, Math.PI*2, false);
    ctx.fill();
    ctx.closePath();    
  },
  update: function() {  
   for(var i=0; i< this.count; i++) {
     this.particles[i].update();
   }
    this.draw(); 
  }
}


var emitter = new Emitter(canvas.width/2, canvas.height/2);

function loop() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  emitter.update();
  requestAnimFrame(loop);
}

loop();
body{background:#000;}
<canvas id="particle"></canvas>

But that's still not enough,

Avoid painting as much as possible.

The paint operations are really slow on a canvas (compared to non-paint ones) and should be avoided as much as possible. To do this, you can sort your particles by color and draw them by stack of single Path objects, but this requires that we round up a bit the opacity value (done when solidifying the color).

// Global Animation Setting
window.requestAnimFrame = 
  window.requestAnimationFrame ||
  window.webkitRequestAnimationFrame ||
  window.mozRequestAnimationFrame ||
  window.oRequestAnimationFrame ||
  window.msRequestAnimationFrame ||
  function(callback) {
    window.setTimeout(callback, 1000/60);
};

// Global Canvas Setting
var canvas = document.getElementById('particle');
var ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;


// Particles Around the Parent
function Particle(x, y, distance) {
  this.angle = Math.random() * 2 * Math.PI;
  this.radius = Math.random() ; 
  this.opacity =  (Math.random()*5 + 2)/10;
  // convert to solid color '#nnnnnn'
  this.color = '#' + Math.floor((this.opacity * 255)).toString(16).padStart(2, 0).repeat(3);
  this.distance = (1/this.opacity)*distance;
  this.speed = this.distance*0.00003;
  
  this.position = {
    x: x + this.distance * Math.cos(this.angle),
    y: y + this.distance * Math.sin(this.angle)
  };
  
  this.draw = function() {
    // here we remove everything but the 'arc' operation and a moveTo
    // no paint
    ctx.moveTo(this.position.x + this.radius, this.position.y);
    ctx.arc(this.position.x, this.position.y, this.radius, 0, Math.PI*2, false);
  }
  this.update = function() {
    this.angle += this.speed; 
    this.position = {
      x: x + this.distance * Math.cos(this.angle),
      y: y + this.distance * Math.sin(this.angle)
    };
    // 'update' should not 'draw'
//    this.draw();
  }
}

function Emitter(x, y) {
  this.position = { x: x, y: y};
  this.radius = 30;
  this.count = 3000;
  this.particles = [];
  
  for(var i=0; i< this.count; i ++ ){
    this.particles.push(new Particle(this.position.x, this.position.y, this.radius));
  }
  // sort our particles by color (opacity = color)
  this.particles.sort(function(a, b) {
    return a.opacity - b.opacity;
  });
}


Emitter.prototype = {
  draw: function() {
    ctx.fillStyle = "rgba(0,0,0,1)";
    ctx.beginPath();
    ctx.arc(this.position.x, this.position.y, this.radius, 0, Math.PI*2, false);
    ctx.fill();
    // draw our particles in batches
    var particle, color;
    ctx.beginPath();
    for(var i=0; i<this.count; i++) {
      particle = this.particles[i];
      if(color !== particle.color) {
        ctx.fill();
        ctx.beginPath();
        ctx.fillStyle = color = particle.color;
      }
      particle.draw();
    }
    ctx.fill(); // fill the last batch
    
    
  },
  update: function() {  
   for(var i=0; i< this.count; i++) {
     this.particles[i].update();
   }
    this.draw(); 
  }
}


var emitter = new Emitter(canvas.width/2, canvas.height/2);

function loop() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  emitter.update();
  requestAnimFrame(loop);
}

loop();
body{background:#000;}
<canvas id="particle"></canvas>

That's better but not yet perfect...

Finally, be clever about YOUR animation.

In your animation, the opacity defines the distance. That is, the particles that are farther from the center are the most transparent ones. This exactly defines what a radial gradient is.

We can thus reduce our paint operations to two. Yes, only two paints for 3000 particles, using a radial-gradient and a bit of compositing, we can first draw all the particles in a single shot, and then apply the gradient as a mask which will apply its color only where there were already something painted. We can even keep the transparency.

// Global Animation Setting
window.requestAnimFrame = 
  window.requestAnimationFrame ||
  window.webkitRequestAnimationFrame ||
  window.mozRequestAnimationFrame ||
  window.oRequestAnimationFrame ||
  window.msRequestAnimationFrame ||
  function(callback) {
    window.setTimeout(callback, 1000/60);
};

// Global Canvas Setting
var canvas = document.getElementById('particle');
var ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;


// Particles Around the Parent
function Particle(x, y, distance) {
  this.angle = Math.random() * 2 * Math.PI;
  this.radius = Math.random() ; 
  this.opacity =  (Math.random()*5 + 2)/10;
  this.distance = (1/this.opacity)*distance;
  this.speed = this.distance*0.00003;
  
  this.position = {
    x: x + this.distance * Math.cos(this.angle),
    y: y + this.distance * Math.sin(this.angle)
  };
  
  this.draw = function() {
    // still no paint here
    ctx.moveTo(this.position.x + this.radius, this.position.y);
    ctx.arc(this.position.x, this.position.y, this.radius, 0, Math.PI*2, false);
  }
  this.update = function() {
    this.angle += this.speed; 
    this.position = {
      x: x + this.distance * Math.cos(this.angle),
      y: y + this.distance * Math.sin(this.angle)
    };
    this.draw();
  }
}

function Emitter(x, y) {
  this.position = { x: x, y: y};
  this.radius = 30;
  this.count = 3000;
  this.particles = [];
  
  for(var i=0; i< this.count; i ++ ){
    this.particles.push(new Particle(this.position.x, this.position.y, this.radius));
  }
  // a radial gradient that we will use as mask
  // in particle.constructor
  // opacities go from 0.2 to 0.7
  // with a distance range of [radius, 1 / 0.2 * this.radius]
  this.grad = ctx.createRadialGradient(x, y, this.radius, x, y, 1 / 0.2 * this.radius);
  this.grad.addColorStop(0, 'rgba(255,255,255,0.7)');
  this.grad.addColorStop(1, 'rgba(255,255,255,0.2)');
}


Emitter.prototype = {
  draw: function() {
    ctx.fillStyle = "rgba(0,0,0,1)";
    ctx.beginPath();
    ctx.arc(this.position.x, this.position.y, this.radius, 0, Math.PI*2, false);
    ctx.fill();
    ctx.closePath();    
  },
  update: function() {
   ctx.beginPath(); // one Path
   ctx.fillStyle = 'black'; // a solid color
   for(var i=0; i< this.count; i++) {
     this.particles[i].update();
   }
   ctx.fill(); // one paint
   // prepare the composite operation
   ctx.globalCompositeOperation = 'source-in';
   ctx.fillStyle = this.grad; // our gradient
   ctx.fillRect(0,0,canvas.width, canvas.height); // cover the whole canvas
   // reset for next paints (center arc and next frame's clearRect)
   ctx.globalCompositeOperation = 'source-over';
   this.draw(); 
  }
}

var emitter = new Emitter(canvas.width/2, canvas.height/2);

function loop() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  emitter.update();
  requestAnimFrame(loop);
}

loop();
body{background:#000;}
<canvas id="particle"></canvas>

Upvotes: 11

Related Questions