DoneJS StealJS jQuery++ FuncUnit DocumentJS
6.0.1
5.33.2 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
      • Migrating to CanJS 6
      • 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 { StacheElement } from "//unpkg.com/can@6/core.mjs";

class ClockControls extends StacheElement {
  static view = `
    <p>{{ this.time }}</p>
    <digital-clock time:from="this.time"/>
    <analog-clock time:from="this.time"/>
  `;

  static props = {
    time: {
      get default() {
        return new Date();
      }
    }
  };

  connected() {
    setInterval(() => {
      this.time = new Date();
    }, 1000);
  }
}

customElements.define("clock-controls", ClockControls);

can-stache-element is used to define the behavior of the <clock-controls> element. Elements are configured with two main properties that define their behavior:

  • view is used as the HTML content within the custom element; by default, it is a can-stache template.
  • props provides values to the view.

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.

Finally, the name of the custom element (e.g. clock-controls) is added to the custom element registry so the browser can render the <clock-controls> element properly.

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-stache-element to create a custom element.
    • view is used as the HTML content within the custom element; by default, it is a can-stache template (hint: "Your {{content}}").
    • props provides values to the view like:
      class MyElement extends StacheElemet {
        static view = `...`;
        static props = {
          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 element.
  • 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").
  • Use customElements.define to specify the name of the custom element (hint: "digital-clock").

The solution

Update the JavaScript tab to:

import { StacheElement } from "//unpkg.com/can@6/core.mjs";

class DigitalClock extends StacheElement {
  static view = `{{ hh() }}:{{ mm() }}:{{ ss() }}`;

  static props = {
    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");
  }
}

customElements.define("digital-clock", DigitalClock);

class ClockControls extends StacheElement {
  static view = `
    <p>{{ this.time }}</p>
    <digital-clock time:from="this.time"/>
    <analog-clock time:from="this.time"/>
  `;

  static props = {
    time: {
      value({ resolve }) {
        const intervalID = setInterval(() => {
          resolve(new Date());
        }, 1000);

        resolve(new Date());

        return () => clearInterval(intervalID);
      }
    }
  };
}

customElements.define("clock-controls", ClockControls);

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 custom element 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>).
  • An element connected hook 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 { StacheElement } from "//unpkg.com/can@6/core.mjs";

class AnalogClock extends StacheElement {
  static view = `
    <canvas this:to="canvasElement" id="analog" width="255" height="255"></canvas>
  `;

  static props = {
    // the canvas element
    canvasElement: HTMLCanvasElement,

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

  connected() {
    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();
  }
}

customElements.define("analog-clock", AnalogClock);

class DigitalClock extends StacheElement {
  static view = "{{ hh() }}:{{ mm() }}:{{ ss() }}";

  static props = {
    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");
  }
}

customElements.define("digital-clock", DigitalClock);

class ClockControls extends StacheElement {
  static view = `
    <p>{{ time }}</p>
    <digital-clock time:from="time"/>
    <analog-clock time:from="time"/>
  `;

  static props = {
    time: {
      value({ resolve }) {
        const intervalID = setInterval(() => {
          resolve(new Date());
        }, 1000);

        resolve(new Date());

        return () => clearInterval(intervalID);
      }
    }
  };
}

customElements.define("clock-controls", ClockControls);

Draw the second hand

The problem

In this section, we will:

  • Draw the second hand needle when the time value changes on the element.
  • 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 connected hook to listen to changes in the element props like:

    import { StacheElement } from "//unpkg.com/can@6/core.mjs";
    
    class AnalogClock extends StacheElement {
      static props = {
        time: Date
      },
      connected() {
        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 { StacheElement } from "//unpkg.com/can@6/core.mjs";

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

class AnalogClock extends StacheElement {
  static view = `
    <canvas this:to="canvasElement" id="analog" width="255" height="255"></canvas>
  `;

  static props = {
    // the canvas element
    canvasElement: HTMLCanvasElement,

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

  connected() {
    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();
    });
  }
}

customElements.define("analog-clock", AnalogClock);

class DigitalClock extends StacheElement {
  static view = "{{ hh() }}:{{ mm() }}:{{ ss() }}";

  static props = {
    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");
  }
}

customElements.define("digital-clock", DigitalClock);

class ClockControls extends StacheElement {
  static view = `
    <p>{{ time }}</p>
    <digital-clock time:from="time"/>
    <analog-clock time:from="time"/>
  `;

  static props = {
    time: {
      value({ resolve }) {
        const intervalID = setInterval(() => {
          resolve(new Date());
        }, 1000);

        resolve(new Date());

        return () => clearInterval(intervalID);
      }
    }
  };
}

customElements.define("clock-controls", ClockControls);

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 [can-stache-element/lifecycle-methods.connected connected hook] that will have access to all the variables created above it like:
    class AnalogClock extends StacheElement {
      static view = "...";
      static props = {};
      drawNeedle(length, base60Distance, styles, center) {
        // ...
      }
    }
    

The solution

Update the JavaScript tab to:

import { StacheElement } from "//unpkg.com/can@6/core.mjs";

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

class AnalogClock extends StacheElement {
  static view = `
    <canvas this:to="canvasElement" id="analog" width="255" height="255"></canvas>
  `;

  static props = {
    // 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();
  }

  connected() {
    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
      );
    });
  }
}

customElements.define("analog-clock", AnalogClock);

class DigitalClock extends StacheElement {
  static view = "{{ hh() }}:{{ mm() }}:{{ ss() }}";

  static props = {
    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");
  }
}

customElements.define("digital-clock", DigitalClock);

class ClockControls extends StacheElement {
  static view = `
    <p>{{ time }}</p>
    <digital-clock time:from="time"/>
    <analog-clock time:from="time"/>
  `;

  static props = {
    time: {
      value({ resolve }) {
        const intervalID = setInterval(() => {
          resolve(new Date());
        }, 1000);

        resolve(new Date());

        return () => clearInterval(intervalID);
      }
    }
  };
}

customElements.define("clock-controls", ClockControls);

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 { StacheElement } from "//unpkg.com/can@5/core.mjs";

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

class AnalogClock extends StacheElement {
  static view = `
    <canvas this:to="canvasElement" id="analog" width="255" height="255"></canvas>
  `;

  static props = {
    // 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();
  }

  connected() {
    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
      );
    });
  }
}

customElements.define("analog-clock", AnalogClock);

class DigitalClock extends StacheElement {
  static view = "{{ hh() }}:{{ mm() }}:{{ ss() }}";

  static props = {
    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");
  }
}

customElements.define("digital-clock", DigitalClock);

class ClockControls extends StacheElement {
  static view = `
    <p>{{ time }}</p>
    <digital-clock time:from="time"/>
    <analog-clock time:from="time"/>
  `;

  static props = {
    time: {
      value({ resolve }) {
        const intervalID = setInterval(() => {
          resolve(new Date());
        }, 1000);

        resolve(new Date());

        return () => clearInterval(intervalID);
      }
    }
  };
}

customElements.define("clock-controls", ClockControls);

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 6.0.1.

On this page

Get help

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