Windows 10 Progress widget with the HTML5 Canvas

I recently decided to take the plunged and updated from Windows 8 to Windows 10.  Wow! it seemed to take what felt like hours.  I was granted the privilege to see Microsoft’s new please wait progress tracker for quite sometime.  If you have not seen it; progress is measured through movement by using the perimeter of a circle.  As the progress moves from from zero, through to one-hundred percent the perimeter of the circle is filled, forming a simple and clean user interface.  This is the progress widget used during the upgrade:

Windows 10 Progress Widget

The mission of the posts is to explain how to replicate the above using HTML5’s canvas.

Let us start with the finished product and then dive into how this was achieved:

html5-windows1-style-progress

The above image shows three versions of the progress bar.  Each is produced using the canvas tag and a small JavaScript module.  The slider control beneath each can be used to interact with the control.  The page with the functional progress bars can be seen here.  To explain how this is achieved we first need to take a look at the HTML to draw the page.  I’ve extracted the HTML to draw one of the three controls for brevity.

<canvas id="progress1" width="380" height="380"></pre>
<div>
<div>Use the slider to modify the progress</div>
<input id="progressSlider1" max="100" min="0" type="range" value="76" /></div>
<pre>


The mark-up above renders the first of the three progress bars.  The canvas tag is used to draw the bar and its label.  The other elements make up the slider that can be used to control the progress.  This HTML is really simple; but, it does not give away much towards explaining how the canvas is turned into a progress bar.  This magic takes place within a collection of JavaScript functions.

Before we dig into the JavaScript it would be prudent to explain how the JavaScript is invoked.   When any web page is loaded the page itself can include external JavaScript files.  This is exactly how the JavaScript code that is used to draw the progress control is included.  The page above includes these JavaScript files:

<script src='./victor.min.js'>

The victor.min.js file brings is a mathematical library which provides a set of Vector based functions.  This is used to calculate the x and y positions of the various points on the perimeter of the circle.  The second script provides the main logic for drawing the progress control onto the canvas.

The whole drawing processes is started when the first bit of JavaScript is executed. This can be found within ‘onload’ event of the page:

<body onload='init()'>

The above reveals that a function called init is executed when the body element is loaded. This init function creates the JavaScript object that draws the progress controls. Let us take a look:

</pre>

function init() {
 progress1 = new Progress();

 progress1
 .init('progress1')
 .setBackgroundColour('transparent')
 .setArcBackgroundColour('transparent')
 .setForegroundColour('#7CFC00')
 .setProgress(76);

 progress2 = new Progress();

 progress2
 .init('progress2')
 .setProgress(55);

 progress3 = new Progress();

 progress3
 .init('progress3')
 .setArcBackgroundColour('#11052F')
 .setForegroundColour('#FF054F')
 .setBackgroundImage('bg.jpg')
 .setProgress(22);
 }

As stated above, this function creates all three progress controls. This is clear from the repeated blocks of code. Each block instantiates a Progress object and then uses to the chaining pattern to configure the look and feel of the control, as well as setting the default values such as the starting place of the control. The look and feel parameters includes the hexadecimal colour codes for the foreground and background colours.

The Progress object is created within the script called “progress.js” that we mentioned earlier. The code for the follows:

var Progress = (function() {

  var radius = 0;
  var scaleWidth = 360;
  var scaleHeight = 360;

  var centerVector;
  var zeroDegreesVector;
  var lengthVector;

  var foregroundColour = '#289ACA';
  var arcBackgroundColour = '#efefef';
  var backgroundColour = '#202020';
  var backgroundImage = null;
  var progress = 0;

  var MAX = 0;
  var ctx;

  function clearCanvas() {
    ctx.clearRect(0, 0, scaleWidth, scaleHeight);
  }

  function colourBackground() {
    ctx.rect(0, 0, scaleWidth, scaleHeight);
    ctx.fillStyle = backgroundColour;
    ctx.fill();
  }

  function drawLabel(text, position) {

    ctx.save();
    ctx.textBaseline = 'middle';
    ctx.strokeStyle = foregroundColour;
    ctx.fillStyle = foregroundColour;
    ctx.font = radius / 2 + 'px Arial';
    ctx.textAlign = 'center';
    ctx.translate(position.x, position.y);
    ctx.fillText(text, 0, 0);
    ctx.restore();

  }

  function drawProgressArc() {

    var counter = 0;
    var currentPoint = null;
    var max = (360 / 100) * progress;

    ctx.beginPath();
    ctx.strokeStyle = foregroundColour;
    ctx.lineWidth = 3;

    for (counter = 0; counter <= max; counter += 1) { currentPoint = centerVector .clone() .add(lengthVector .clone() .norm() .rotateDeg(counter) .multiply(new Victor(radius, radius)) ); ctx.lineTo(currentPoint.x, currentPoint.y); } ctx.stroke(); } function drawProgressArcBackground() { ctx.beginPath(); ctx.strokeStyle = arcBackgroundColour; ctx.arc(centerVector.x, centerVector.y, radius, 0, 2 * Math.PI); ctx.stroke(); } function draw() { clearCanvas(); if (backgroundImage !== null) { ctx.drawImage(backgroundImage, 0, 0, scaleWidth, scaleHeight); } else { colourBackground(backgroundColour); } drawProgressArcBackground(); drawProgressArc(); drawLabel(progress + '%', centerVector); window.requestAnimationFrame(draw); } return { init: function(canvasId) { var canvas = document.getElementById(canvasId); if (canvas.getContext('2d')) { ctx = canvas.getContext('2d'); var scaleX = scaleWidth / canvas.width; var scaleY = scaleHeight / canvas.height; var center = { x: scaleWidth / 2, y: scaleHeight / 2 }; MAX = scaleWidth > scaleHeight ? scaleWidth : scaleHeight;
        radius = (MAX * 0.8) / 2;

        ctx.scale(scaleX, scaleY);
        ctx.translate(10, 10);

        centerVector = new Victor(center.x, center.y);
        zeroDegreesVector = new Victor(center.x, center.y - radius);
        lengthVector = zeroDegreesVector.subtract(centerVector);

        window.requestAnimationFrame(draw);

      } else {
        alert('Canvas not supported!');
      }

      return this;
    },

    setProgress: function(progression) {
      progress = progression;

      if (progress < 0) { progress = 0; } else if (progress > 100) {
        progress = 100;
      }

      window.requestAnimationFrame(draw);

      return this;
    },

    setBackgroundColour: function(colour) {
      backgroundColour = colour;
      window.requestAnimationFrame(draw);
      return this;
    },

    setArcBackgroundColour: function(colour) {
      arcBackgroundColour = colour;
      window.requestAnimationFrame(draw);
      return this;
    },

    setBackgroundImage: function(image) {

      backgroundImage = new Image();
      backgroundImage.src = image;

      backgroundImage.onload = function() {
        window.requestAnimationFrame(draw);
      };

      return this;
    },

    setForegroundColour: function(colour) {
      foregroundColour = colour;
      window.requestAnimationFrame(draw);
      return this;
    }
  };
});

I will not go into detail for each of the functions above. But, instead try to cover the basics of how the Progress object is put together.

The first important part to understand is the construction of the object. I have used the revealing module pattern. This is most common in JavaScript. It allows a programmer to keep parts of a object private – meaning these parts cannot be modified outside of the object. Further reading on this pattern can be found here.

Taking the first point into consideration we can then list the functions that are exposed. Or, in object terms, the functions that are public. These includes functions such as setProgress and setBackGroundColour.  These setter functions each accept one parameter.  This parameter is stored as a private variable. After storing the value the function then makes a call to the draw function. The draw action is invoked using the request animation frame function provided by the window object.

window.requestAnimationFrame(draw);

An excellent explanation on why you should be using the request animation frame call can be found here.

As you can see from the call to requestAnimationFrame, we pass in a callback function whose purpose is to draw the progress control. Let’s drill into this function:


  function draw() {

    clearCanvas();

    if (backgroundImage !== null) {
      ctx.drawImage(backgroundImage, 0, 0, scaleWidth, scaleHeight);
    } else {
      colourBackground(backgroundColour);
    }

    drawProgressArcBackground();
    drawProgressArc();

    drawLabel(progress + '%', centerVector);

    window.requestAnimationFrame(draw);
  }

The function above is responsible for drawing the progress control. It is less than a dozen lines of code. This is really no more than a wrapper function. A wrapper function is one that calls a number of smaller functions in a given order. The order in which I wanted achieve related directly to the actions I wished to perform on the canvas. These are:

  1. Clearing the CanvasEvery-time the progress control is redrawn it would make sense to clear the canvas.  If not, we would have to work out which pixels have changed and update each accordingly.  In most cases it is easier to call the clear canvas function and redraw everything.  The canvas is cleared by calling the clearCanvas function which simply calls the clearRect function provided by the canvas API.
  2. Render the background.The setter functions referenced earlier allow a progress control to either have a background image rendered, or, simply filled with a solid colour.  Again these action are performed using API calls provided by the canvas API.  Specifically the rect, fillStyle and fill calls as demonstrated within the colourBackgound function:
    function colourBackground() {
    ctx.rect(0, 0, scaleWidth, scaleHeight);
    ctx.fillStyle = backgroundColour;
    ctx.fill();
    }
    

    And, the drawImage function as demonstrated within the draw function:

    ctx.drawImage(backgroundImage, 0, 0, scaleWidth, scaleHeight);
    
  3. Draw the arcsThis is done in two steps.  The first step is to draw the background arc.  The background arc covers the full three-hundred and sixty degrees of the circle in a given colour.  The forms a base and the second arc overlays the base.  The second arc is own drawn from zero degree up to the point that represents the current percentage value.  This means a value of twenty-five will result in one-quarter of the arc being filled:
    progress-bar-quarterIt is worth noting that the position of the arc is determined using vector math.  This post does not cover the math involved here – if needed have a read of the BBC bite-size Vector math page for more information.
  4. The last step is to print the label in the middle of the circle.  The main API called here is the fillText canvas API call.

Once the steps above are achieve we then simply call the draw function following any updates to the progress position.

That concludes the Post :-). I hope this helps someone out there. A working version of the above can be viewed here. If you have any feedback please comment below.  This is also available at my git repository HTML5 goodies.

Advertisements

One thought on “Windows 10 Progress widget with the HTML5 Canvas

  1. Hey, great tutorial. How I can turn the code into a reusable component, with setter and getter method for background color or radius?
    Regards, Dulcinea

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s