DoneJS StealJS jQuery++ FuncUnit DocumentJS
4.3.0
5.0.0 3.13.1 2.3.35
  • About
  • Guides
  • API Docs
  • Community
  • Contributing
  • Bitovi
    • Bitovi.com
    • Blog
    • Design
    • Development
    • Training
    • Open Source
    • About
    • Contact Us
  • About
  • Guides
    • experiment
      • Chat Guide
      • TodoMVC Guide
      • ATM Guide
    • getting started
      • Setting Up CanJS
      • Technology Overview
      • Reading the Docs (API Guide)
      • Experimental ES Module Usage
    • recipes
      • Canvas Clock (Simple)
      • Credit Card Guide (Advanced)
      • Credit Card Guide (Simple)
      • CTA Bus Map (Medium)
      • File Navigator Guide (Advanced)
      • File Navigator Guide (Simple)
      • Playlist Editor (Advanced)
      • Signup and Login (Simple)
      • Text Editor (Medium)
      • Tinder Carousel (Medium)
      • TodoMVC with StealJS
      • Video Player (Simple)
    • topics
      • Debugging
      • Forms
    • upgrade
      • Migrating to CanJS 3
      • Migrating to CanJS 4
      • Using Codemods
  • API Docs
  • Community
  • Contributing
  • GitHub
  • Twitter
  • Chat
  • Forum
  • News
Bitovi

Canvas Clock (Simple)

  • Edit on GitHub

This 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:

CanJS Canvas Clock on jsbin.com

The following sections are broken down 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 CLONING THE FOLLOWING JS BIN:

Click the JS Bin button. The JS Bin will open in a new window. In that new window, under File, click Clone.

CanJS Canvas Clock on jsbin.com

This JS Bin 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 JS Bin 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:

can.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"/>
  `
});

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:

can.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");
    }
  }
});

can.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 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; it will be passed the element like:
    can.Component.extend({
      tag: "my-element",
      view: "<h1>first child</h1>",
      ViewModel: {
        connectedCallback(element) {
          element.firstChild //-> <h1>
        }
      }
    });
    
  • 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:
    canvas.lineWidth = 4.0
    canvas.strokeStyle = "#567"
    
    Then you start path with:
    canvas.beginPath()
    
    Then make arcs and lines for your path like:
    canvas.arc(125, 125, 125, 0, Math.PI * 2, true)
    
    Then close the path like:
    canvas.closePath()
    
    Finally, use stroke to actually draw the line:
    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:

can.Component.extend({
  tag: "analog-clock",
  view: '<canvas id="analog" width="255" height="255"></canvas>',
  ViewModel: {
    connectedCallback(element) {
      const canvas = element.firstChild.getContext("2d");
      const diameter = 255;
      const radius = diameter/2 - 5;
      const center = diameter/2;
      // draw circle
      canvas.lineWidth = 4.0;
      canvas.strokeStyle = "#567";
      canvas.beginPath();
      canvas.arc(center, center, radius, 0, Math.PI * 2, true);
      canvas.closePath();
      canvas.stroke();
    }
  }
});

can.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");
    }
  }
});

can.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:

    can.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:

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

can.Component.extend({
  tag: "analog-clock",
  view: '<canvas id="analog" width="255" height="255"></canvas>',
  ViewModel: {
    connectedCallback(element) {
      const canvas = element.firstChild.getContext("2d");
      const diameter = 255;
      const radius = diameter/2 - 5;
      const center = diameter/2;
      // draw circle
      canvas.lineWidth = 4.0;
      canvas.strokeStyle = "#567";
      canvas.beginPath();
      canvas.arc(center, center, radius, 0, Math.PI * 2, true);
      canvas.closePath();
      canvas.stroke();

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

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

        Object.assign(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));
        canvas.beginPath();
        canvas.moveTo(center, center);
        canvas.lineTo(x, y);
        canvas.closePath();
        canvas.stroke();
      });
    }
  }
});

can.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");
    }
  }
});

can.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 function

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: {
      connectedCallback() {
        const canvas = element.firstChild.getContext("2d");
        const diameter = 255;
        const radius = diameter/2 - 5;
        const center = diameter/2;
    
        const drawNeedle = (length, base60Distance, styles) => {
          canvas // -> the canvas element
          // ...
        };
      }
    }
    

The solution

Update the JavaScript tab to:

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

can.Component.extend({
  tag: "analog-clock",
  view: '<canvas id="analog" width="255" height="255"></canvas>',
  ViewModel: {
    connectedCallback(element) {
      const canvas = element.firstChild.getContext("2d");
      const diameter = 255;
      const radius = diameter/2 - 5;
      const center = diameter/2;

      const drawNeedle = (length, base60Distance, styles) => {
        Object.assign(canvas, styles);
        const x = center + length * Math.sin(base60ToRadians(base60Distance));
        const y = center + length * -1 * Math.cos(base60ToRadians(base60Distance));
        canvas.beginPath();
        canvas.moveTo(center, center);
        canvas.lineTo(x, y);
        canvas.closePath();
        canvas.stroke();
      };

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

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

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

can.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");
    }
  }
});

can.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:

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

can.Component.extend({
  tag: "analog-clock",
  view: '<canvas id="analog" width="255" height="255"></canvas>',
  ViewModel: {
    connectedCallback(element) {
      const canvas = element.firstChild.getContext("2d");
      const diameter = 255;
      const radius = diameter/2 - 5;
      const center = diameter/2;

      const drawNeedle = (length, base60Distance, styles) => {
        Object.assign(canvas, styles);
        const x = center + length * Math.sin(base60ToRadians(base60Distance));
        const y = center + length * -1 * Math.cos(base60ToRadians(base60Distance));
        canvas.beginPath();
        canvas.moveTo(center, center);
        canvas.lineTo(x, y);
        canvas.closePath();
        canvas.stroke();
      };

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

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

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

can.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");
    }
  }
});

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

Result

When finished, you should see something like the following JS Bin:

CanJS Canvas Clock on jsbin.com

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

On this page

Get help

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