Use RxJS to Build a Simple Stream-Driven Stopwatch

web reactive-programming rxjs html-canvas

You must have heard of the thing called Reactive Programming, a way of programming with asynchronous data streams. And you want to learn what ReactiveX is. Let’s build a simple stream-driven stopwatch today using Reactive Extension for JavaScript (RxJS).

ReactiveX is a library for composing asynchronous and event-based programs using observable sequences. What we are going to learn today is its JavaScript implementation, RxJS. We will build a simple stream-driven stopwatch web app using ReactiveX (RxJS). The source code for this Reactive Programming tutorial can be found at chen-yumin/rxjs-canvas-stopwatch.


Run on a new window.

What Is a Stream?

Streams are a reactive programming abstraction for modelling asynchronous, infinitely-sized data. It might sound a bit complicated for now, but imagine – a simple list of data similar to an array, but you just don’t know the size of the array, and when you will have the actual data. It is actually just that simple. Streams offer many similar operations to arrays but also include many more to deal specifically with their time-ordered semantics.

Let’s go ahead and create a stream that emits an element every 100 milliseconds.

const source = Rx.Observable
  .interval(100 /* ms */ )
  .timeInterval();

Now that we have a time-ordered stream, we can subscribe it to a function, so every time an element gets emitted, which in this case only takes 100 milliseconds, this function will be called.

const digital = document.getElementById('digital');
let started = false;
let time = 0; // 1/10 seconds
const subscription = source.subscribe(
  x => {
    if(!started) return;
    time++;
    draw(time);
    digital.innerHTML = Math.floor(time / 600) + ":" + Math.floor((time / 10) % 60) + ":" + (time % 10) + "0";
  });

We could just use the integer it emits to count the time passed, but to better control when to start and stop counting, and thus fully implement the start and stop functions of the stopwatch, here we use started and time variables to track the time.

HTML5 Canvas

The stopwatch is drawn using HTML5 Canvas, an element for drawing graphics on a web page.

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

The <canvas> element is just a container for graphics, but it doesn’t allow you to define the actual shapes or paths. The actual graphics are drawn using its JavaScript API.

const canvas = document.getElementById('canvas');
if (canvas.getContext) {
  const ctx = canvas.getContext('2d');
}

Once you have the context from your <canvas> element, you can start drawing stuff on it.

ctx.clearRect(0, 0, canvas.width, canvas.height);

const watchSize = 96;
const contentSize = 0.92;

// Center doc
ctx.fillStyle = "#13414E";
ctx.beginPath();
ctx.arc(watchSize, watchSize, 2, 0, 2 * Math.PI, true);
ctx.fill();

ctx.strokeStyle = "DimGray";
ctx.beginPath();
// Outer circle
ctx.arc(watchSize, watchSize, watchSize, 0, Math.PI * 2, true);
ctx.arc(watchSize, watchSize, watchSize - 2, 0, Math.PI * 2, true);

// 12 longer lines
for (let i = 0; i < 12; i++) {
  let angle = i * (Math.PI * 2 / 12);
  const armLength = watchSize * 0.15;
  ctx.moveTo(watchSize + watchSize * Math.cos(angle) * contentSize, watchSize + watchSize * Math.sin(angle) * contentSize);
  ctx.lineTo(watchSize + (watchSize - armLength) * Math.cos(angle) * contentSize, watchSize + (watchSize - armLength) * Math.sin(angle) * contentSize);
}

// 60 shorter lines
for (let i = 0; i < 60; i++) {
  let angle = i * (Math.PI * 2 / 60);
  const armLength = watchSize * 0.05;
  ctx.moveTo(watchSize + watchSize * Math.cos(angle) * contentSize, watchSize + watchSize * Math.sin(angle) * contentSize);
  ctx.lineTo(watchSize + (watchSize - armLength) * Math.cos(angle) * contentSize, watchSize + (watchSize - armLength) * Math.sin(angle) * contentSize);

}

// Longer hand (minute), each minute goes one step
let angle = (time / 600 / 60 - 0.25) * (Math.PI * 2);
let armLength = watchSize * 0.5;
ctx.moveTo(watchSize, watchSize);
ctx.lineTo(watchSize + armLength * Math.cos(angle), watchSize + armLength * Math.sin(angle));

// Shorter hand (second), each second goes one step
angle = (time / 10 / 60 - 0.25) * (Math.PI * 2);
armLength = watchSize * 0.8;
ctx.moveTo(watchSize, watchSize);
ctx.lineTo(watchSize + armLength * Math.cos(angle), watchSize + armLength * Math.sin(angle));

ctx.stroke();

We put the above code snippet in a function called draw(). And each time the time changes, this function is called with the updated time so the angle is recalculated and the stopwatch refreshes with hands pointing to the correct position.

Now that you see the full stopwatch being drawn with correct time being displayed. There are some details you might be able to improve:

Have you noticed the ctx.clearRect() method at the beginning of the function that is being called every time the stopwatch redraws? This method clears the canvas and then it has to draw everything over again. However, the face of the clock never changes, the only things that are different are the angles of the hands. Would it be possible to optimize this function to only redraw the hands, but leave the face of the clock untouched? Leave your comments down below to share your ideas!

Chen Yumin
Chen Yumin

Hi, my name is Chen Yumin.
I am the author of the stuff you're reading right now.

Latest Project
CoderBox.io
CoderBox.io

Connect With Me

Enjoy what you see? Follow me on:

Subscribe

Subscribe via RSS