7
\$\begingroup\$

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!

\$\endgroup\$
1
\$\begingroup\$

Of course arc and stroke will get a high share of CPU since it's being used for drawing. Nothing weird there.

return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2));

Math operations tend to be slow, depending on the browser. You might want to check for bitwise alternatives and see which works best for most of the browsers.

//Create a "buddy" (for lack of a better term) to render circles on
if (!element.buddy) {

I lolled on this one. But yeah, better naming.

p.id == p2.id

It's usually recommended to use === unless there's a special case where == is really needed.

Lastly, browser support. Don't forget to fill up that space with something for older browsers. You could fill it with a static image of a frame of that animation. If you want a little fun, you could port this over to use one of the cross-browser drawing libraries like Raphael (svg + vml).

\$\endgroup\$
  • \$\begingroup\$ Great suggestions, thank you very much! I'll look into bitwise operations for the math-heavy functions. \$\endgroup\$ – Scott Aug 11 '15 at 18:08

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.