I have written a small class to generate and animate one of those trendy particle webs. I plan to use this as the main background of my personal website, so naturally I'm concerned about the performance.
Currently, Chrome's task manager shows it sucking up anywhere between 10% CPU and 20% CPU. I'd like to lower that as much as possible without sacrificing quality. Chrome's profiler shows that the main bottlenecks are the canvas stroke and arc, followed by other canvas draw calls. I'm already batching those calls - even to a dedicated canvas, but have seen no results.
Here's a jsfiddle, and I've also included the code below:
//Helper functions
//Extend an object's properties to a default object
Object.defineProperty(Object.prototype, "extend", {
value: function(defaults) {
for (var prop in defaults)
if (this.hasOwnProperty(prop)) defaults[prop] = this[prop];
return defaults;
}
});
function ParticleRenderer(element, initSettings) {
"use strict";
//Globals
var ctxArc, ctxLine, req,
localSettings = {},
particles = [];
//Default settings that initSettings or newSettings will be merged into
var defaultSettings = {
particles: 100, //Number of particles to render
connectDistance: element.width / element.height * 20, //Maximum distance for particles to be "connected" with a line
frozen: false, //Will not animate if frozen is true
fill: "rgba(170,230,200,1)", //Colour of circles
stroke: "rgba(170,200,230,1)" //Colour of lines
};
//Particle object used as a structure
var Particle = function(ops) {
this.id = ops.id;
this.x = ops.x;
this.y = ops.y;
this.size = ops.size || 3;
this.vx = ops.vx || rand(-1, 1) / 10;
this.vy = ops.vy || rand(-1, 1) / 10;
};
//Clear both canvasses entirely and redraw
function animate() {
ctxArc.clearRect(0, 0, element.width, element.height);
ctxLine.clearRect(0, 0, element.width, element.height);
drawFrame();
req = requestAnimationFrame(animate);
}
//Draws all particles and lines between
function drawFrame() {
var j, dist, p, p2, opacity,
maxDist = localSettings.connectDistance,
i = particles.length;
//Begin drawing circle paths
ctxArc.beginPath();
while (i--) {
p = particles[i];
//Move particle (except mouse-controller particle)
if (i != 0) {
p.x += p.vx;
p.y += p.vy;
}
//Keep particle in-frame by wrapping it to 0
p.y = p.y > element.height ? 0 : p.y;
p.x = p.x > element.width ? 0 : p.x;
//Look through all particles
j = particles.length;
while (j--) {
p2 = particles[j];
//Ignore self
if (p.id == p2.id) break;
//Get distance to other particle
dist = distance(p, p2);
if (dist <= maxDist) {
//Draw faded line to other particle
opacity = 1 - dist / maxDist;
ctxLine.beginPath();
ctxLine.moveTo(p.x, p.y);
ctxLine.lineTo(p2.x, p2.y);
ctxLine.closePath();
ctxLine.globalAlpha = opacity;
ctxLine.stroke();
}
}
//Specify a circle to be drawn here
ctxArc.moveTo(p.x, p.y);
ctxArc.arc(p.x, p.y, p.size, 0, 2 * Math.PI);
}
//Draw all circles
ctxArc.closePath();
ctxArc.fill();
}
//Random number between min and max
function rand(min, max) {
return Math.random() * (max - min + 1) + min;
}
//Distance (in units) between two points, where both objects have an x and y property
function distance(p1, p2) {
return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2));
}
//Applies new settings, or resets the canvas (for resize)
function refresh(newSettings) {
cancelAnimationFrame(req);
var o = element.getBoundingClientRect();
particles = [];
//Create a "buddy" (for lack of a better term) to render circles on
if (!element.buddy) {
var elementClone = element.cloneNode();
elementClone.removeAttribute("id");
elementClone.style.position = "absolute";
elementClone.style.pointerEvents = "none";
elementClone.style.top = element.offsetTop + "px";
elementClone.style.left = element.offsetLeft + "px";
element.buddy = elementClone;
element.parentNode.appendChild(elementClone);
}
//Resize both elements to the new size
element.width = o.width;
element.height = o.height;
element.buddy.width = o.width;
element.buddy.height = o.height;
//Create two separate contexts
//One draws lines, the other draws circles
ctxLine = element.getContext("2d");
ctxArc = element.buddy.getContext("2d");
//Overwrite any existing settings with new settings
localSettings = (newSettings || initSettings || {}).extend(defaultSettings);
//Create particles
for (var m = 0; m < localSettings.particles; ++m) {
particles.push(
new Particle({
id: m,
x: rand(0, element.width),
y: rand(0, element.height),
size: rand(1, 3)
}));
}
//Apply setting colours
ctxArc.fillStyle = localSettings.fill;
ctxLine.strokeStyle = localSettings.stroke;
//Do not animate if this particle web is destined to be frozen
if (localSettings.frozen) {
drawFrame();
} else {
//The first particle will always follow the mouse, and be slightly bigger
window.addEventListener("mousemove", function(e) {
particles[0].x = e.pageX - element.offsetLeft;
particles[0].y = e.pageY - element.offsetTop;
});
particles[0].size = 4;
animate();
}
}
//Exports
return {
refresh: refresh,
}
}
var renderer = new ParticleRenderer(document.getElementById("canvas"), {
particles: 100,
connectDistance: 50,
fill: "#00ff00",
stroke: "#00aa00"
});
renderer.refresh();
html,
body {
margin: 0;
padding: 0;
background: #111;
overflow: hidden;
}
#canvas {
width: 100vw;
height: 100vh;
}
<canvas id="canvas"></canvas>
Is there anything I can do to make this more performant? Can I make the code better in any other way? Any and all criticism is much appreciated!