# Canvas animation

Just started learning how to use the canvas today as I wanted to copy an animated GIF.

The GIF I tried to copy is below.

The animation does work but I've hacked it left and right. There's obviously alot of refactoring to do but am I going about it the right way?

<canvas id="canvas" width="300" height="300"></canvas>

html, body {
background-color: lightgrey;
}

var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
var segments = {};
segments['0'] = {
last: 0,
height: 15,
distanceToOuter: 30,
broke: false,
outerAnimStarted: false,
outerAnimThickness: 3,
outerAnimDistance: 5,
incValue: 0.5,
colour: 'green',
outerColour: 'green'
};
var percentage = 0;
var lastPercentage = 0;
var maxPercentage = 100;
(function loop() {
setTimeout(function () {
//set segment start size
lastPercentage = percentage;
//set percentage
percentage += randomNumber(1, 20);
//if percentage goes over 100, reduce back to 100
if (percentage > 100) {
percentage = 100;
}
//push segment into object stack
segments[percentage] = {
last: lastPercentage,
height: 15,
distanceToOuter: 30,
broke: false,
outerAnimStarted: false,
outerAnimThickness: 3,
outerAnimDistance: 5,
incValue: 0.5,
colour: 'green',
outerColour: 'green'
};
//if percentage is under 100, continue to loop
if (percentage === 100) {
segments[100].broke = true;
segments[segments[100].last].broke = true;
} else if (percentage === 0) {
segments[0].broke = true;
} else {
segments[lastPercentage].broke = true;
}
if (percentage < maxPercentage) {
loop();
draw(percentage);
requestAnimationFrame(increaseDistanceToOuter);
}
}, randomNumber(1, 10) * 100);
}());

function draw(percentage) {
//clear canvas
ctx.clearRect(0, 0, 300, 300);
//begin outer white circle
ctx.beginPath();
ctx.lineWidth = 15;
ctx.strokeStyle = 'white';
ctx.stroke();
//end outer white circle
ctx.beginPath();
ctx.fillStyle = grd;
ctx.fill();
//begin inner white circle
ctx.beginPath();
ctx.lineWidth = 1;
ctx.strokeStyle = 'lightgrey';
ctx.fillStyle = 'white';
ctx.fill();
ctx.stroke();
//end inner white circle
//start text
ctx.fillStyle = 'green';
ctx.font = "16pt Arial";
ctx.textAlign = "center";
ctx.fillText(percentage, 150, 158);
//end text
//start segments
for (var key in segments) {
circleSegment(
Math.floor(segments[key].last * 3.6),
Math.ceil(key * 3.6),
segments[key].height,
segments[key].distanceToOuter,
segments[key].colour
);
if(segments[key].outerAnimStarted) {
outerAnim(
Math.floor(segments[key].last * 3.6),
Math.ceil(key * 3.6),
segments[key].outerAnimThickness,
segments[key].outerAnimDistance,
segments[key].outerColour
);
}
}
//end segments
}

function increaseDistanceToOuter() {
for (var key in segments) {
if (!segments[key].broke) {
segments[key].height += segments[key].incValue;
segments[key].incValue = segments[key].incValue / 2;
segments[key].distanceToOuter += 0.05;
} else {
if (segments[key].distanceToOuter < 120) {
segments[key].distanceToOuter++;
} else if (segments[key].distanceToOuter >= 120) {
segments[key].height = 15;
segments[key].distanceToOuter = 120;
if (!segments[key].outerAnimStarted) {
segments[key].outerAnimStarted = true;
} else {
if(segments[key].outerAnimThickness >= 0){
segments[key].outerAnimThickness = segments[key].outerAnimThickness - 0.01;
} else {
segments[key].outerColour = 'rgba(255,255,255,0)';
}
segments[key].outerAnimDistance += 0.1;
}
}
}
}
draw(percentage);
requestAnimationFrame(increaseDistanceToOuter);
}

return deg * Math.PI / 180;
}

function circleSegment(degreesFrom, degreesTo, thickness, distanceToOuter, colour) {
ctx.beginPath();
ctx.lineWidth = thickness;
ctx.strokeStyle = colour;
ctx.stroke();
}

function outerAnim(degreesFrom, degreesTo, thickness, distance, colour) {
ctx.beginPath();
ctx.lineWidth = thickness;
ctx.strokeStyle = colour;
ctx.stroke();
}

function randomNumber(min, max) {
return Math.floor(Math.random() * (max - min + 1) + min);
}


jsfiddle

• What is the expected behaviour if maxPercentage is not 100? – Stuart Aug 2 '14 at 13:22
• maxPercentage will always be 100, I just saved it as a variable for easier reference. – Ben Fortune Aug 2 '14 at 13:56

The programme is rather mixed up at the moment although I can't see any obvious 'hacks' as such. You are calling draw and requestAnimationFrame from multiple places, sometimes unnecessarily. I would rewrite it to separate the actual drawing from the underlying logic of the changing characteristics of the segments. It would include the following components:

1. a view module containing the canvas and context variables, the functions that do all the actual drawing, and a method render which renders the whole canvas. render will be run upon requestAnimationFrame. Within view there should also be some generic drawing functions like circle and arc, and a specific function renderSegment which takes a segment object as its argument.

2. a Segment object constructor which stores all the information about each segment. The Segment prototype would have a method update which does the work currently done by increaseDistanceToOuter. These methods would be run on a separate setTimeout timer.

3. a newSegment function that is run on a setTimeout similar to the function you have called loop, and which creates the new segments

4. an overall controller function which sets off newSegment and starts the view rendering the objects on the screen.

• a matter of taste, but when you have a complicated set of if ... else statements I always find it less confusing to start with the positive condition, e.g. if (broke) ... else ... rather than if (!broke)...
• Thanks for the pointers. The loop I have at the moment is just to simulate a download/upload request. When finished I'd want it to be usable by calling a function which would update the percentage, so similar to what you have in point number 3 but without the setTimeout. – Ben Fortune Aug 2 '14 at 14:35
• I don't think newSegment can be a method of Segment. I would store the segment objects in an array and do segments.push(new Segment(percent, lastPercent) to make a new one. Otherwise fine. – Stuart Aug 2 '14 at 15:01