Recently, I am working on a project which requires a timer/counter component. The tricky part is the outer rim of the timer which has the countdown animation and I can’t think of an easy CSS trick to create this effect. Besides, I also want to manage the duration and goal in js. So my best chance is canvas.

Thankfully, I used to take a Game design class back in the days. So I have basic concept of animation frame rendering. After digging for a while, I comes up this simple timer example which inspired by Android Material design.

First of all, create a Class for the timer.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Timer(el) {
// params
this.WIDTH = 200;
this.HEIGHT = 200;
this.TIMER_BORDER = 3;
this.TIMER_COLOR1 = "#ececec";
this.TIMER_COLOR2 = "#3366CC";
this.TIMER_DURATION = 60000;
this.TIME_ELAPSED = 0;
this.DOT_RADIUS = 6;
this.MAXFPS = 60;
this.el = el;
this.el.width = this.WIDTH;
this.el.height = this.HEIGHT;
this.ctx = el.getContext('2d');
}

The most important thing is drawing the frame for the timer which includes 4 parts, outer rim foreground, outer rim background, dot indicator and time string.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
Timer.prototype.drawTimer = function () {
var self = this;
var ctx = this.ctx;
var center = {
x: this.el.width / 2,
y: this.el.height / 2
};
var r = (this.el.width - this.TIMER_BORDER) / 2 - this.DOT_RADIUS;
var eAngle = (1.5 - 2.0 * this.TIME_ELAPSED / this.TIMER_DURATION) * Math.PI;
var dot = {
x: center.x + r * Math.cos(eAngle),
y: center.y + r * Math.sin(eAngle)
};
dot.r = this.DOT_RADIUS;
// draw background arc
ctx.lineWidth = this.TIMER_BORDER;
ctx.strokeStyle = this.TIMER_COLOR1;
ctx.beginPath();
ctx.arc(center.x, center.y, r, -0.5 * Math.PI, eAngle); // -0.5pi ~ 1.5pi
ctx.stroke();
// draw foreground arc
ctx.beginPath();
ctx.arc(center.x, center.y, r, -0.5 * Math.PI, eAngle, 1); // counterclockwise
ctx.strokeStyle = this.TIMER_COLOR2;
ctx.stroke();
// draw dot
ctx.beginPath();
ctx.arc(dot.x, dot.y, dot.r, 0, 2 * Math.PI);
ctx.fillStyle = this.TIMER_COLOR2;
ctx.fill();
// draw time string
ctx.textAlign = "center";
ctx.textBaseline = 'middle';
ctx.font = "300 32pt Roboto";
ctx.fillText(self.timerString(), center.x, center.y);
}

In order to draw the rim and dot. I use context.arc. You can find the definition from w3schools.

There are two methods to generate time string and clear the canvas.

1
2
3
4
5
6
7
8
9
10
11
Timer.prototype.timerString = function () {
var ts = Math.ceil((this.TIMER_DURATION - this.TIME_ELAPSED) / 1000);
var h = parseInt(ts / 3600) % 24;
var m = parseInt(ts / 60) % 60;
var s = ts % 60;
return (m < 10 ? "0" + m : m) + ":" + (s < 10 ? "0" + s : s);
}

Timer.prototype.clearFrame = function () {
this.ctx.clearRect(0, 0, this.el.width, this.el.height);
}

For the animation logic, I use requestAnimationFrame instead of setTimeout. The benefit is browser will call your render function adaptively which gives you the best fps. Although I set the MAXFPS to throttle the frame rate to 60fps. Drawing is expansive and I don’t really need to update view that often.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Timer.prototype.render = function () {
this.clearFrame();
this.drawTimer();
return Date.now();
}

Timer.prototype.run = function () {
var self = this;
if (self.TIME_ELAPSED >= self.TIMER_DURATION) return false; // exit
if (!self.lastRender) self.lastRender = Date.now();
var delta = Date.now() - self.lastRender;
// Trick to throttle FPS
if (delta > (1000 / self.MAXFPS)) {
self.TIME_ELAPSED += delta;
self.lastRender = self.render();
}
requestAnimationFrame(self.timerRun.bind(self));
}

To use this, simply instantiate the timer object and then call run method.

1
2
var t = new Timer(document.querySelector('canvas'));
t.run();

Tips for animated UI with Canvas

  • Use requestAnimationFrame for best FPS
  • You can set the FPS CAP in the render function if lower frame rate accepted