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.
Gif
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;
//simulate download
(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.arc(150, 150, 120, toRadians(0), toRadians(360));
ctx.stroke();
//end outer white circle
//begin inner gradient
var grd = ctx.createRadialGradient(150, 150, 60, 150, 150, 1);
grd.addColorStop(0, "rgba(255,255,255,0)");
grd.addColorStop(1, "white");
ctx.beginPath();
ctx.arc(150, 150, 60, toRadians(0), toRadians(360));
ctx.fillStyle = grd;
ctx.fill();
//end inner gradient
//begin inner white circle
ctx.beginPath();
ctx.lineWidth = 1;
ctx.strokeStyle = 'lightgrey';
ctx.arc(150, 150, 20, toRadians(0), toRadians(360));
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);
}
function toRadians(deg) {
return deg * Math.PI / 180;
}
function circleSegment(degreesFrom, degreesTo, thickness, distanceToOuter, colour) {
ctx.beginPath();
ctx.lineWidth = thickness;
ctx.strokeStyle = colour;
ctx.arc(150, 150, distanceToOuter, toRadians(degreesFrom), toRadians(degreesTo));
ctx.stroke();
}
function outerAnim(degreesFrom, degreesTo, thickness, distance, colour) {
ctx.beginPath();
ctx.lineWidth = thickness;
ctx.strokeStyle = colour;
ctx.arc(150, 150, 120 + distance, toRadians(degreesFrom), toRadians(degreesTo));
ctx.stroke();
}
function randomNumber(min, max) {
return Math.floor(Math.random() * (max - min + 1) + min);
}
-
\$\begingroup\$ What is the expected behaviour if maxPercentage is not 100? \$\endgroup\$Stuart– Stuart2014年08月02日 13:22:35 +00:00Commented Aug 2, 2014 at 13:22
-
\$\begingroup\$ maxPercentage will always be 100, I just saved it as a variable for easier reference. \$\endgroup\$Ben Fortune– Ben Fortune2014年08月02日 13:56:48 +00:00Commented Aug 2, 2014 at 13:56
1 Answer 1
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:
a
view
module containing the canvas and context variables, the functions that do all the actual drawing, and a methodrender
which renders the whole canvas.render
will be run uponrequestAnimationFrame
. Within view there should also be some generic drawing functions likecircle
andarc
, and a specific functionrenderSegment
which takes asegment
object as its argument.a
Segment
object constructor which stores all the information about each segment. TheSegment
prototype would have a methodupdate
which does the work currently done byincreaseDistanceToOuter
. These methods would be run on a separatesetTimeout
timer.a
newSegment
function that is run on asetTimeout
similar to the function you have calledloop
, and which creates the new segmentsan overall controller function which sets off
newSegment
and starts the view rendering the objects on the screen.
A few other comments:
it would help to have a comment line at the start of the long functions explaining what they do
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 thanif (!broke)...
EDIT - additional point on updating the segments:
- As you can't always rely on setTimeout firing at the right times, it will be better to keep track of the time elapsed since each segment has been created, and use that to determine its position.
-
\$\begingroup\$ 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
. \$\endgroup\$Ben Fortune– Ben Fortune2014年08月02日 14:35:31 +00:00Commented Aug 2, 2014 at 14:35 -
\$\begingroup\$ I've created a quick structure here, how does that look? \$\endgroup\$Ben Fortune– Ben Fortune2014年08月02日 14:43:45 +00:00Commented Aug 2, 2014 at 14:43
-
\$\begingroup\$ I don't think
newSegment
can be a method ofSegment
. I would store the segment objects in an array and dosegments.push(new Segment(percent, lastPercent)
to make a new one. Otherwise fine. \$\endgroup\$Stuart– Stuart2014年08月02日 15:01:05 +00:00Commented Aug 2, 2014 at 15:01