# Canvas animation of network nodes and edges

This code generates an animation that gives the idea of an ever changing network of nodes (each with different impact and possibly more than one color) connecting each other to create something.

I wanted to give it depth perception, so I ended up using two canvases around the title: one in the foreground, even over the words, and the other in background, with slightly larger and blurred elements.

Demo here, full JavaScript code at the moment:

// min and max radius, radius threshold and percentage of filled circles
filledCircle = 60, //percentage of filled circles
concentricCircle = 30, //percentage of concentric circles

//min and max speed to move
var speedMin = 0.3,
speedMax = 2.5;

//max reachable opacity for every circle and blur effect
var maxOpacity = 0.6;

//default palette choice
var colors = ['52,168,83', '117,95,147', '199,108,23', '194,62,55', '0,172,212', '120,120,120'],
bgColors = ['52,168,83', '117,95,147', '199,108,23', '194,62,55', '0,172,212', '120,120,120'],
circleBorder = 10,
backgroundLine = bgColors[0];
var backgroundMlt = 0.85;

var linkDist = Math.min(canvas.width, canvas.height) / 2.4,
lineBorder = 2.5;

//most importantly: number of overall circles and arrays containing them
var maxCircles = 12,
points = [],
pointsBack = [];

//populating the screen
for (var i = 0; i < maxCircles * 2; i++) points.push(new Circle());
for (var i = 0; i < maxCircles; i++) pointsBack.push(new Circle(true));

//experimental vars
var circleExp = 1,
circleExpMax = 1.003,
circleExpMin = 0.997,
circleExpSp = 0.00004,
circlePulse = false;

//circle class
function Circle(background) {
//if background, it has different rules
this.background = (background || false);
this.x = randRange(-canvas.width / 2, canvas.width / 2);
this.y = randRange(-canvas.height / 2, canvas.height / 2);
this.filled = this.radius < radThreshold ? (randint(0, 100) > filledCircle ? false : 'full') : (randint(0, 100) > concentricCircle ? false : 'concentric');
this.color = background ? bgColors[randint(0, bgColors.length - 1)] : colors[randint(0, colors.length - 1)];
this.borderColor = background ? bgColors[randint(0, bgColors.length - 1)] : colors[randint(0, colors.length - 1)];
this.opacity = 0.05;
this.speed = (background ? randRange(speedMin, speedMax) / backgroundMlt : randRange(speedMin, speedMax)); // * (radMin / this.radius);
this.speedAngle = Math.random() * 2 * Math.PI;
this.speedx = Math.cos(this.speedAngle) * this.speed;
this.speedy = Math.sin(this.speedAngle) * this.speed;
var spacex = Math.abs((this.x - (this.speedx < 0 ? -1 : 1) * (canvas.width / 2 + this.radius)) / this.speedx),
spacey = Math.abs((this.y - (this.speedy < 0 ? -1 : 1) * (canvas.height / 2 + this.radius)) / this.speedy);
this.ttl = Math.min(spacex, spacey);
};

Circle.prototype.init = function() {
Circle.call(this, this.background);
}

//support functions
//generate random int a<=x<=b
function randint(a, b) {
return Math.floor(Math.random() * (b - a + 1) + a);
}
//generate random float
function randRange(a, b) {
return Math.random() * (b - a) + a;
}
//generate random float more likely to be close to a
function hyperRange(a, b) {
return Math.random() * Math.random() * Math.random() * (b - a) + a;
}

//rendering function
function drawCircle(ctx, circle) {
ctx.beginPath();
ctx.arc(circle.x, circle.y, radius * circleExp, 0, 2 * Math.PI, false);
ctx.strokeStyle = ['rgba(', circle.borderColor, ',', circle.opacity, ')'].join('');
if (circle.filled == 'full') {
ctx.fillStyle = ['rgba(', circle.borderColor, ',', circle.background ? circle.opacity * 0.8 : circle.opacity, ')'].join('');
ctx.fill();
ctx.lineWidth=0;
ctx.strokeStyle = ['rgba(', circle.borderColor, ',', 0, ')'].join('');
}
ctx.stroke();
if (circle.filled == 'concentric') {
ctx.beginPath();
ctx.arc(circle.x, circle.y, radius / 2, 0, 2 * Math.PI, false);
ctx.strokeStyle = ['rgba(', circle.color, ',', circle.opacity, ')'].join('');
ctx.stroke();
}
circle.x += circle.speedx;
circle.y += circle.speedy;
if (circle.opacity < (circle.background ? maxOpacity : 1)) circle.opacity += 0.01;
circle.ttl--;
}

//initializing function
function init() {
window.requestAnimationFrame(draw);
}

//rendering function
function draw() {

if (circlePulse) {
if (circleExp < circleExpMin || circleExp > circleExpMax) circleExpSp *= -1;
circleExp += circleExpSp;
}
var ctxfr = document.getElementById('canvas').getContext('2d');
var ctxbg = document.getElementById('canvasbg').getContext('2d');

ctxfr.globalCompositeOperation = 'destination-over';
ctxfr.clearRect(0, 0, canvas.width, canvas.height); // clear canvas
ctxbg.globalCompositeOperation = 'destination-over';
ctxbg.clearRect(0, 0, canvas.width, canvas.height); // clear canvas

ctxfr.save();
ctxfr.translate(canvas.width / 2, canvas.height / 2);
ctxbg.save();
ctxbg.translate(canvas.width / 2, canvas.height / 2);

//function to render each single circle, its connections and to manage its out of boundaries replacement
function renderPoints(ctx, arr) {
for (var i = 0; i < arr.length; i++) {
var circle = arr[i];
//checking if out of boundaries
if (circle.ttl<0) {}
var xEscape = canvas.width / 2 + circle.radius,
yEscape = canvas.height / 2 + circle.radius;
if (circle.ttl < -20) arr[i].init(arr[i].background);
//if (Math.abs(circle.y) > yEscape || Math.abs(circle.x) > xEscape) arr[i].init(arr[i].background);
drawCircle(ctx, circle);
}
for (var i = 0; i < arr.length - 1; i++) {
for (var j = i + 1; j < arr.length; j++) {
var deltax = arr[i].x - arr[j].x;
var deltay = arr[i].y - arr[j].y;
var dist = Math.pow(Math.pow(deltax, 2) + Math.pow(deltay, 2), 0.5);
//if the circles are overlapping, no laser connecting them
//otherwise we connect them only if the dist is < linkDist
var xi = (arr[i].x < arr[j].x ? 1 : -1) * Math.abs(arr[i].radius * deltax / dist);
var yi = (arr[i].y < arr[j].y ? 1 : -1) * Math.abs(arr[i].radius * deltay / dist);
var xj = (arr[i].x < arr[j].x ? -1 : 1) * Math.abs(arr[j].radius * deltax / dist);
var yj = (arr[i].y < arr[j].y ? -1 : 1) * Math.abs(arr[j].radius * deltay / dist);
ctx.beginPath();
ctx.moveTo(arr[i].x + xi, arr[i].y + yi);
ctx.lineTo(arr[j].x + xj, arr[j].y + yj);
var samecolor = arr[i].color == arr[j].color;
ctx.strokeStyle = ["rgba(", arr[i].borderColor, ",", Math.min(arr[i].opacity, arr[j].opacity) * ((linkDist - dist) / linkDist), ")"].join("");
ctx.stroke();
}
}
}
}

var startTime = Date.now();
renderPoints(ctxfr, points);
renderPoints(ctxbg, pointsBack);
deltaT = Date.now() - startTime;

ctxfr.restore();
ctxbg.restore();

window.requestAnimationFrame(draw);
}

init();


I asked around and ctx.save() and ctx.restore() are in the top list of suspects, but I wouldn't know how to do this without them.

Notes: The first part with the general variables might not be the best practice, but it worked to let a non-technical staff member (UI designer) play on the variables to see different results.

This is my first animation with canvas, which AFAIK should have been the best option in terms of cross-browser support and (decent) performances, but any advice on this side is still welcome; also, seems to slow down significantly on FF, but just on some machines.

Should I use something other than canvas to do the animation?

• StackOverflow is better for specific problems, but do your own research before posting to make sure that your question is well received. – Caridorc Sep 13 '15 at 20:08
• The demo seems to work well enough, in my opinion, for this question to be on-topic for Code Review. – 200_success Sep 14 '15 at 5:23

Readability of this code is good because indentation is consistent and it has a good amount of comments to provide context on the variables and functions.

I see this block:

//support functions
//generate random int a<=x<=b
function randint(a, b) {
return Math.floor(Math.random() * (b - a + 1) + a);
}
//generate random float
function randRange(a, b) {
return Math.random() * (b - a) + a;
}


The comment above randRange explains that it creates a random float - which makes me wonder why the name isn't something like randFloat? And why are the closing brackets indented two characters? Maybe that was an issue with copying and pasting followed by formatting as code...

A common convention of c-based languages, as well as many JS style guides is to name constants in ALL_CAPS so things like maxOpacity would be converted to MAX_OPACTIY. This helps anyone reading the code distinguish constants from variables. One could also use const for any value that doesn't get re-assigned if features are supported by target browsers.

There doesn't appear to be any difference between colors and bgColors but maybe you intend for users to customize one or both of those...

• Correct on both counts; I have long moved forwards since then, but you clearly identified 2 of my earlier mistakes - thanks for your time and kind feedback. I believe the final version became this: codepen.io/GiacomoSorbi/pen/OyyzvO – Ajna Oct 30 at 10:08