5
\$\begingroup\$

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
var radMin = 5,
 radMax = 125,
 filledCircle = 60, //percentage of filled circles
 concentricCircle = 30, //percentage of concentric circles
 radThreshold = 25; //IFF special, over this radius concentric, otherwise filled
//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;
//min distance for links
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.radius = background ? hyperRange(radMin, radMax) * backgroundMlt : hyperRange(radMin, radMax);
 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) {
 //circle.radius *= circleExp;
 var radius = circle.background ? circle.radius *= circleExp : circle.radius /= circleExp;
 ctx.beginPath();
 ctx.arc(circle.x, circle.y, radius * circleExp, 0, 2 * Math.PI, false);
 ctx.lineWidth = Math.max(1, circleBorder * (radMin - circle.radius) / (radMin - radMax));
 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.lineWidth = Math.max(1, circleBorder * (radMin - circle.radius) / (radMin - radMax));
 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
 if (dist <= arr[i].radius + arr[j].radius) continue;
 //otherwise we connect them only if the dist is < linkDist
 if (dist < 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.lineWidth = (arr[i].background ? lineBorder * backgroundMlt : lineBorder) * ((linkDist - dist) / linkDist); //*((linkDist-dist)/linkDist);
 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?

Sᴀᴍ Onᴇᴌᴀ
29.5k16 gold badges45 silver badges201 bronze badges
asked Sep 13, 2015 at 19:57
\$\endgroup\$
2
  • 1
    \$\begingroup\$ StackOverflow is better for specific problems, but do your own research before posting to make sure that your question is well received. \$\endgroup\$ Commented Sep 13, 2015 at 20:08
  • 3
    \$\begingroup\$ The demo seems to work well enough, in my opinion, for this question to be on-topic for Code Review. \$\endgroup\$ Commented Sep 14, 2015 at 5:23

1 Answer 1

2
\$\begingroup\$

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...

answered Oct 29, 2019 at 17:40
\$\endgroup\$
1
  • 1
    \$\begingroup\$ 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 \$\endgroup\$ Commented Oct 30, 2019 at 10:08

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.