DoneJS StealJS jQuery++ FuncUnit DocumentJS
5.33.3
6.0.0 4.3.0 3.14.1 2.3.35
  • About
  • Guides
  • API Docs
  • Community
  • Contributing
  • Bitovi
    • Bitovi.com
    • Blog
    • Design
    • Development
    • Training
    • Open Source
    • About
    • Contact Us
  • About
  • Guides
    • getting started
      • CRUD Guide
      • Setting Up CanJS
      • Technology Overview
    • topics
      • HTML
      • Routing
      • Service Layer
      • Debugging
      • Forms
      • Testing
      • Logic
      • Server-Side Rendering
    • app guides
      • Chat Guide
      • TodoMVC Guide
      • TodoMVC with StealJS
    • beginner recipes
      • Canvas Clock
      • Credit Card
      • File Navigator
      • Signup and Login
      • Video Player
    • intermediate recipes
      • CTA Bus Map
      • Multiple Modals
      • Text Editor
      • Tinder Carousel
    • advanced recipes
      • Credit Card
      • File Navigator
      • Playlist Editor
      • Search, List, Details
    • upgrade
      • Migrating to CanJS 3
      • Migrating to CanJS 4
      • Migrating to CanJS 5
      • Using Codemods
    • other
      • Reading the API Docs
  • API Docs
  • Community
  • Contributing
  • GitHub
  • Twitter
  • Chat
  • Forum
  • News
Bitovi

Canvas Clock

  • Edit on GitHub

This beginner guide walks you through building a clock with the Canvas API.

In this guide you will learn how to:

  • Create custom elements for digital and analog clocks
  • Use the canvas API to draw the hands of the analog clock

The final widget looks like:

See the Pen Canvas Clock (Simple) [Finished] by Bitovi (@bitovi) on CodePen.

The following sections are broken down into the following parts:

  • The problem — A description of what the section is trying to accomplish.
  • What you need to know — Information about CanJS that is useful for solving the problem.
  • How to verify it works - How to make sure the solution works (if it’s not obvious).
  • The solution — The solution to the problem.

Setup

START THIS TUTORIAL BY CLICKING THE “EDIT ON CODEPEN” BUTTON IN THE TOP RIGHT CORNER OF THE FOLLOWING EMBED:

See the Pen Canvas Clock (Simple) [Starter] by Bitovi (@bitovi) on CodePen.

This CodePen has initial prototype HTML, CSS, and JS to bootstrap a basic CanJS application.

What you need to know

There’s nothing to do in this step. The CodePen is already setup with:

  • A basic CanJS setup.
  • A <clock-controls> custom element that:
    • updates a time property every second
    • passes the time value to <digital-clock> and <analog-clock> components that will be defined in future sections.

Please read on to understand the setup.

A Basic CanJS Setup

A basic CanJS setup is usually a custom element. In the HTML tab, you’ll find a <clock-controls> element. The following code in the JS tab defines the behavior of the <clock-controls> element:

import { Component } from "//unpkg.com/can@5/core.mjs";

Component.extend({
  tag: "clock-controls",
  ViewModel: {
    time: {Default: Date, Type: Date},
    connectedCallback() {
      setInterval(() => {
        this.time = new Date();
      }, 1000);
    }
  },
  view: `
    <p>{{ this.time }}</p>
    <digital-clock time:from="this.time"/>
    <analog-clock time:from="this.time"/>
  `
});

can-component is used to define the behavior of the <clock-controls> element. Components are configured with three main properties that define the behavior of the element:

  • tag is used to specify the name of the custom element (e.g. "clock-controls").
  • view is used as the HTML content within the custom element; by default, it is a can-stache template.
  • ViewModel provides methods and values to the view; by default, the ViewModel is a can-define/map/map.

Here, a time property is defined using the value behavior. This uses resolve to set the value of time to be an instance of a Date and then update the value every second to be a new Date.

Create a digital clock component

The problem

In this section, we will:

  • Create a <digital-clock> custom element.
  • Pass the time from the <clock-controls> element to the <digital-clock> element.
  • Write out the time in the format: hh:mm:ss.

What you need to know

  • Use can-component to create a custom element.
    • tag is used to specify the name of the custom element (hint: "digital-clock").
    • view is used as the HTML content within the custom element; by default, it is a can-stache template (hint: "Your {{content}}").
    • ViewModel provides methods and values to the view; by default, the ViewModel is a can-define/map/map that defines properties and functions like:
      ViewModel: {
        property: Type, // hint -> time: Date
        method() {
          return // ...
        }
      }
      
  • can-stache can insert the return value from function calls into the page like:
    {{method()}}
    
    These methods are often functions on the ViewModel.
  • Date has methods that give you details about that date and time:
    • date.getSeconds()
    • date.getMinutes()
    • date.getHours()
  • Use padStart to convert a string like "1" into "01" like .padStart(2, "00").

The solution

Update the JavaScript tab to:

import { Component } from "//unpkg.com/can@5/core.mjs";

Component.extend({
  tag: "digital-clock",
  view: `{{ hh() }}:{{ mm() }}:{{ ss() }}`,
  ViewModel: {
    time: Date,
    hh() {
      const hr = this.time.getHours() % 12;
      return hr === 0 ? 12 : hr;
    },
    mm() {
      return this.time.getMinutes().toString().padStart(2, "00");
    },
    ss() {
      return this.time.getSeconds().toString().padStart(2, "00");
    }
  }
});

Component.extend({
  tag: "clock-controls",
  ViewModel: {
    time: {
      value({ resolve }) {
        const intervalID = setInterval(() => {
          resolve( new Date() );
        }, 1000);

        resolve( new Date() );

        return () => clearInterval(intervalID);
      }
    }
  },
  view: `
    <p>{{ this.time }}</p>
    <digital-clock time:from="this.time"/>
    <analog-clock time:from="this.time"/>
  `
});

Draw a circle in the analog clock component

The problem

In this section, we will:

  • Create a <analog-clock> custom element.
  • Draw a circle inside the <canvas/> element within the <analog-clock>.

What you need to know

  • Use another can-component to define a <analog-clock> component.
  • Define the component’s view to write out a <canvas> element (hint: <canvas id="analog" width="255" height="255"></canvas>).
  • A component’s ViewModel can be defined as an object which will be passed to DefineMap.extend (hint:ViewModel: {}).
  • A viewModel’s connectedCallback will be called when the component is inserted into the page.
  • Pass an element reference to the scope, like the following:
<div this:to="key">...</div>
  • To get the canvas rendering context from a <canvas> element, use canvas = canvasElement.getContext("2d").
  • To draw a line (or curve), you generally set different style properties of the rendering context like:
    this.canvas.lineWidth = 4.0
    this.canvas.strokeStyle = "#567"
    
    Then you start path with:
    this.canvas.beginPath()
    
    Then make arcs and lines for your path like:
    this.canvas.arc(125, 125, 125, 0, Math.PI * 2, true)
    
    Then close the path like:
    this.canvas.closePath()
    
    Finally, use stroke to actually draw the line:
    this.canvas.stroke();
    
  • The following variables will be useful for coordinates:
    this.diameter = 255;
    this.radius = this.diameter/2 - 5;
    this.center = this.diameter/2;
    

The solution

Update the JavaScript tab to:

import { Component } from "//unpkg.com/can@5/core.mjs";

Component.extend({
  tag: "analog-clock",
  view: `<canvas this:to="canvasElement" id="analog" width="255" height="255"></canvas>`,
  ViewModel: {
    // the canvas element
    canvasElement: HTMLCanvasElement,

    // the canvas 2d context
    get canvas() {
      return this.canvasElement.getContext("2d");
    },

    connectedCallback(element) {
      const diameter = 255;
      const radius = diameter/2 - 5;
      const center = diameter/2;
      // draw circle
      this.canvas.lineWidth = 4.0;
      this.canvas.strokeStyle = "#567";
      this.canvas.beginPath();
      this.canvas.arc(center, center, radius, 0, Math.PI * 2, true);
      this.canvas.closePath();
      this.canvas.stroke();
    }
  }
});

Component.extend({
  tag: "digital-clock",
  view: "{{ hh() }}:{{ mm() }}:{{ ss() }}",
  ViewModel: {
    time: Date,
    hh() {
      const hr = this.time.getHours() % 12;
      return hr === 0 ? 12 : hr;
    },
    mm() {
      return this.time.getMinutes().toString().padStart(2, "00");
    },
    ss() {
      return this.time.getSeconds().toString().padStart(2, "00");
    }
  }
});

Component.extend({
  tag: "clock-controls",
  ViewModel: {
    time: {
      value({ resolve }) {
        const intervalID = setInterval(() => {
          resolve( new Date() );
        }, 1000);

        resolve( new Date() );

        return () => clearInterval(intervalID);
      }
    }
  },
  view: `
    <p>{{ time }}</p>
    <digital-clock time:from="time"/>
    <analog-clock time:from="time"/>
  `
});

Draw the second hand

The problem

In this section, we will:

  • Draw the second hand needle when the time value changes on the viewModel.
  • The needle should be 2 pixels wide, red (#FF0000), and 85% of the clock’s radius.

What you need to know

  • this.listenTo can be used in a component’s connectedCallback to listen to changes in the ViewModel like:

    import { Component } from "//unpkg.com/can@5/core.mjs";
    
    Component.extend({
      tag: "analog-clock",
      // ...
      ViewModel: {
        connectedCallback() {
          this.listenTo("time", (event, time) => {
            // ...
          });
        }
      }
    });
    
  • Use canvas.moveTo(x1,y1) and canvas.lineTo(x2,y2) to draw a line from one position to another.

  • To get the needle point to move around a “unit” circle, you’d want to make the following calls given the number of seconds:

    0s  -> .lineTo(.5,0)
    15s -> .lineTo(1,.5)
    30s -> .lineTo(.5,1)
    45s -> .lineTo(0,.5)
    
  • Our friends Math.sin and Math.cos can help here… but they take radians.

    Sine and Cosine Graph

    Use the following base60ToRadians method to convert a number from 0–60 to one between 0 and 2π:

    // 60 = 2π
    const base60ToRadians = (base60Number) =>
      2 * Math.PI * base60Number / 60;
    

The solution

Update the JavaScript tab to:

import { Component } from "//unpkg.com/can@5/core.mjs";

// 60 = 2π
const base60ToRadians = (base60Number) =>
  2 * Math.PI * base60Number / 60;

Component.extend({
  tag: "analog-clock",
  view: `<canvas this:to="canvasElement" id="analog" width="255" height="255"></canvas>`,
  ViewModel: {
    // the canvas element
    canvasElement: HTMLCanvasElement,

    // the canvas 2d context
    get canvas() {
      return this.canvasElement.getContext("2d");
    },

    connectedCallback(element) {
      const diameter = 255;
      const radius = diameter/2 - 5;
      const center = diameter/2;
      
      // draw circle
      this.canvas.lineWidth = 4.0;
      this.canvas.strokeStyle = "#567";
      this.canvas.beginPath();
      this.canvas.arc(center, center, radius, 0, Math.PI * 2, true);
      this.canvas.closePath();
      this.canvas.stroke();
      
      this.listenTo("time", (ev, time) => {
        this.canvas.clearRect(0, 0, diameter, diameter);

        // draw circle
        this.canvas.lineWidth = 4.0;
        this.canvas.strokeStyle = "#567";
        this.canvas.beginPath();
        this.canvas.arc(center, center, radius, 0, Math.PI * 2, true);
        this.canvas.closePath();
        this.canvas.stroke();

        Object.assign(this.canvas, {
          lineWidth:  2.0,
          strokeStyle: "#FF0000",
          lineCap: "round"
        });
        // draw second hand
        const seconds = time.getSeconds() + this.time.getMilliseconds() / 1000;
        const size = radius * 0.85;
        const x = center + size * Math.sin(base60ToRadians(seconds));
        const y = center + size * -1 * Math.cos(base60ToRadians(seconds));
        this.canvas.beginPath();
        this.canvas.moveTo(center, center);
        this.canvas.lineTo(x, y);
        this.canvas.closePath();
        this.canvas.stroke();
      });
    }
  }
});

Component.extend({
  tag: "digital-clock",
  view: "{{ hh() }}:{{ mm() }}:{{ ss() }}",
  ViewModel: {
    time: Date,
    hh() {
      const hr = this.time.getHours() % 12;
      return hr === 0 ? 12 : hr;
    },
    mm() {
      return this.time.getMinutes().toString().padStart(2, "00");
    },
    ss() {
      return this.time.getSeconds().toString().padStart(2, "00");
    }
  }
});

Component.extend({
  tag: "clock-controls",
  ViewModel: {
    time: {
      value({ resolve }) {
        const intervalID = setInterval(() => {
          resolve( new Date() );
        }, 1000);

        resolve( new Date() );

        return () => clearInterval(intervalID);
      }
    }
  },
  view: `
    <p>{{ time }}</p>
    <digital-clock time:from="time"/>
    <analog-clock time:from="time"/>
  `
});

Clear the canvas and create a drawNeedle method

The problem

In this section, we will:

  • Clear the canvas before drawing the circle and needle.
  • Refactor the needle drawing code into a drawNeedle(length, base60Distance, styles) method where:
    • length is the length in pixels of the needle.
    • base60Distance is a number between 0–60 representing how far around the clock the needle should be drawn.
    • styles is an object of canvas context style properties and values like:
      {
        lineWidth: 2.0,
        strokeStyle: "#FF0000",
        lineCap: "round"
      }
      

What you need to know

  • Move the draw circle into the this.listenTo("time", /* ... */) event handler so it is redrawn when the time changes.
  • Use clearRect(x, y, width, height) to clear the canvas.
  • Add a function inside the connectedCallback that will have access to all the variables created above it like:
    ViewModel: {
      // ...
      drawNeedle(length, base60Distance, styles, center) {
          // ...
      }
      // ...
    }
    

The solution

Update the JavaScript tab to:

import { Component } from "//unpkg.com/can@5/core.mjs";

// 60 = 2π
const base60ToRadians = (base60Number) =>
  2 * Math.PI * base60Number / 60;

Component.extend({
  tag: "analog-clock",
  view: `<canvas this:to="canvasElement" id="analog" width="255" height="255"></canvas>`,
  ViewModel: {
    // the canvas element
    canvasElement: HTMLCanvasElement,

    // the canvas 2d context
    get canvas() {
      return this.canvasElement.getContext("2d");
    },

    drawNeedle(length, base60Distance, styles, center) {
      Object.assign(this.canvas, styles);
      const x = center + length * Math.sin(base60ToRadians(base60Distance));
      const y = center + length * -1 * Math.cos(base60ToRadians(base60Distance));
      this.canvas.beginPath();
      this.canvas.moveTo(center, center);
      this.canvas.lineTo(x, y);
      this.canvas.closePath();
      this.canvas.stroke();
    },
    
    connectedCallback(element) {
      const diameter = 255;
      const radius = diameter/2 - 5;
      const center = diameter/2;

      this.listenTo("time", (ev, time) => {
        this.canvas.clearRect(0, 0, diameter, diameter);

        // draw circle
        this.canvas.lineWidth = 4.0;
        this.canvas.strokeStyle = "#567";
        this.canvas.beginPath();
        this.canvas.arc(center, center, radius, 0, Math.PI * 2, true);
        this.canvas.closePath();
        this.canvas.stroke();

        // draw second hand
        const seconds = time.getSeconds() + this.time.getMilliseconds() / 1000;
        this.drawNeedle(
          radius * 0.85,
          seconds, {
            lineWidth: 2.0,
            strokeStyle: "#FF0000",
            lineCap: "round"
          },
          center
        );
      });
    }
  }
});

Component.extend({
  tag: "digital-clock",
  view: "{{ hh() }}:{{ mm() }}:{{ ss() }}",
  ViewModel: {
    time: Date,
    hh() {
      const hr = this.time.getHours() % 12;
      return hr === 0 ? 12 : hr;
    },
    mm() {
      return this.time.getMinutes().toString().padStart(2, "00");
    },
    ss() {
      return this.time.getSeconds().toString().padStart(2, "00");
    }
  }
});

Component.extend({
  tag: "clock-controls",
  ViewModel: {
    time: {
      value({ resolve }) {
        const intervalID = setInterval(() => {
          resolve( new Date() );
        }, 1000);

        resolve( new Date() );

        return () => clearInterval(intervalID);
      }
    }
  },
  view: `
    <p>{{ time }}</p>
    <digital-clock time:from="time"/>
    <analog-clock time:from="time"/>
  `
});

Draw the minute and hour hand

The problem

In this section, we will:

  • Draw the minute hand 3 pixels wide, dark gray (#423), and 65% of the clock’s radius.
  • Draw the minute hand 4 pixels wide, dark blue (#42F), and 45% of the clock’s radius.

What you need to know

You know everything at this point. You got this!

The solution

Update the JavaScript tab to:

import { Component } from "//unpkg.com/can@5/core.mjs";

// 60 = 2π
const base60ToRadians = (base60Number) =>
  2 * Math.PI * base60Number / 60;

Component.extend({
  tag: "analog-clock",
  view: `<canvas this:to="canvasElement" id="analog" width="255" height="255"></canvas>`,
  ViewModel: {
    // the canvas element
    canvasElement: HTMLCanvasElement,

    // the canvas 2d context
    get canvas() {
      return this.canvasElement.getContext("2d");
    },

    drawNeedle(length, base60Distance, styles, center) {
      Object.assign(this.canvas, styles);
      const x = center + length * Math.sin(base60ToRadians(base60Distance));
      const y = center + length * -1 * Math.cos(base60ToRadians(base60Distance));
      this.canvas.beginPath();
      this.canvas.moveTo(center, center);
      this.canvas.lineTo(x, y);
      this.canvas.closePath();
      this.canvas.stroke();
    },
    
    connectedCallback(element) {
      const diameter = 255;
      const radius = diameter/2 - 5;
      const center = diameter/2;

      this.listenTo("time", (ev, time) => {
        this.canvas.clearRect(0, 0, diameter, diameter);

        // draw circle
        this.canvas.lineWidth = 4.0;
        this.canvas.strokeStyle = "#567";
        this.canvas.beginPath();
        this.canvas.arc(center, center, radius, 0, Math.PI * 2, true);
        this.canvas.closePath();
        this.canvas.stroke();

        // draw second hand
        const seconds = time.getSeconds() + this.time.getMilliseconds() / 1000;
        this.drawNeedle(
          radius * 0.85,
          seconds, {
            lineWidth: 2.0,
            strokeStyle: "#FF0000",
            lineCap: "round"
          },
          center
        );
        
        // draw minute hand
        const minutes = time.getMinutes() + seconds / 60;
        this.drawNeedle(
          radius * 0.65,
          minutes, {
            lineWidth: 3.0,
            strokeStyle: "#423",
            lineCap: "round"
          },
          center
        );
        // draw hour hand
        const hoursInBase60 = time.getHours() * 60 / 12 + minutes / 60;
        this.drawNeedle(
          radius * 0.45,
          hoursInBase60, {
            lineWidth: 4.0,
            strokeStyle: "#42F",
            lineCap: "round"
          },
          center
        );
      });
    }
  }
});

Component.extend({
  tag: "digital-clock",
  view: "{{ hh() }}:{{ mm() }}:{{ ss() }}",
  ViewModel: {
    time: Date,
    hh() {
      const hr = this.time.getHours() % 12;
      return hr === 0 ? 12 : hr;
    },
    mm() {
      return this.time.getMinutes().toString().padStart(2, "00");
    },
    ss() {
      return this.time.getSeconds().toString().padStart(2, "00");
    }
  }
});

Component.extend({
  tag: "clock-controls",
  ViewModel: {
    time: {
      value({ resolve }) {
        const intervalID = setInterval(() => {
          resolve( new Date() );
        }, 1000);

        resolve( new Date() );

        return () => clearInterval(intervalID);
      }
    }
  },
  view: `
    <p>{{ time }}</p>
    <digital-clock time:from="time"/>
    <analog-clock time:from="time"/>
  `
});

Result

When finished, you should see something like the following CodePen:

See the Pen Canvas Clock (Simple) [Finished] by Bitovi (@bitovi) on CodePen.

CanJS is part of DoneJS. Created and maintained by the core DoneJS team and Bitovi. Currently 5.33.3.

On this page

Get help

  • Chat with us
  • File an issue
  • Ask questions
  • Read latest news