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!