Reputation: 53
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>
Upvotes: 0
Views: 3216
Reputation: 136688
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,
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...
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