6
\$\begingroup\$

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);
}

jsfiddle

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

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 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 than if (!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.
\$\endgroup\$
  • \$\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 Aug 2 '14 at 14:35
  • \$\begingroup\$ I've created a quick structure here, how does that look? \$\endgroup\$ – Ben Fortune Aug 2 '14 at 14:43
  • \$\begingroup\$ 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. \$\endgroup\$ – Stuart Aug 2 '14 at 15:01

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.