Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / HTML

Alternative Analog SVG Clock

5.00/5 (11 votes)
25 Mar 2023CPOL7 min read 16.2K   443  
A pure Web browser application, an alternative to the article “An SVG Analog Clock”
The application has no server side and is based on HTML + SVG + CSS + JavaScript only

 

Application

 

Contents

Introduction
What's so Interesting?
Implementation
SVG
Clock
Comparison of Object Types
Arrow Rotation
Performance Optimization
Main Difference Between Analog and Digital Clocks
How to Add Decorations?
CSS: Docking Layout
CSS: Fitting SVG
What About Server Part?

Introduction

What’s so Interesting?

What’s so Interesting? Virtually nothing! This is the application alternative to the one shown in the article An SVG Analog Clock. I was surprised by the fact of how a clock could be over-engineered and decided to provide a lean and near-optimal solution which I think is way more adequate.

An on-screen clock has near-zero practical value, maybe even negative. This is just a traditional programming exercise, on par with editors, Tetris implementations, fractal image rendering, and the like.

However, this article explains several subtle points that could be useful for beginners.

Implementation

SVG

First, let’s see how SVG can be built dynamically. Everything is implemented as the class SVG:

JavaScript
class SVG {

    #implementation = {};

    constructor(viewBox, existingSvg) {
        const ns = "http://www.w3.org/2000/svg";
        this.#implementation.createNS = (name) =>
            document.createElementNS(ns, name);
        this.#implementation.attribute = (element, values) => {
            for (let attribute in values)
                element.setAttribute(attribute, values[attribute]);
            return element;
        }; //this.#implementation.attribute
        // defaults...
        this.#implementation.svg = existingSvg == null ?
            this.#implementation.createNS("svg") : existingSvg;
        if (viewBox != null) // it also covers undefined
            this.#implementation.svg.setAttribute("viewBox",
                `${viewBox.x.from} ${viewBox.y.from}
                 ${viewBox.x.to - viewBox.x.from}
                 ${viewBox.y.to - viewBox.y.from}`);
    } //constructor

    get element() { return this.#implementation.svg; }

    // ... get/set attributes, primitives: line, circle, ets.

    appendElement(element, group) {
        if (group)
            group.appendChild(element);
        else
            this.#implementation.svg.appendChild(element);
        return element;
    } //appendElement

    group() { return this.appendElement(this.#implementation.createNS("g")); }

} //class SVG

First, note that all the private code is hidden in the constructor and the private member this.#implementation.

An SVG element can be created using the document object in a bit different way than HTML elements because SVG uses a different namespace. This is implemented in a local method this.#implementation.createNS.

The class SVG can be used in two different ways. If existingSvg is specified, the class can be used to populate an existing <svg> element, and new content can be added to existing content. It can be used to add content to an <svg> element embedded in HTML.

Clock

The function createClock creates an instance of the clock. It can also be created in two different ways. If the parameter parent is of the class SVG (described above), the clock can be rendered on top of some existing <svg> element. Otherwise, it is created from scratch. As the instance of SVG.element is appended to some existing HTML element, the entire clock created from scratch will be appended to it. The function returns the function set used to set and render the current clock time.

For the rendering of the clock face and arrows, Z-order is important. If an SVG element is created later, it is placed in the foreground. For example, the red second arrow should be created at the very end, to make it well visible in all locations.

To choose the way to use, the code needs to compare objects by type. Let’s look at this.

In the application, the clock instance is initialized and used in “main.js”, in the handler of window.onload. The clock is set once at initialization and then periodically in the function passed to setInterval().

Comparison of Object Types

In the function createClock, we have:

JavaScript
if (parent.constructor != SVG)
    parent.appendChild(svg.element);

Naturally, at this point, we assume that the object parent is not undefined and not null. When needed, it can be checked up with if (parent != null), as this comparison also covers the case of undefined.

This is the way to compare types directly, not using magic strings. We can find many examples of JavaScript code where the types are compared using type names. This is dirty, less maintainable, and should be avoided.

Arrow Rotation

The central part of the clock is the method set. It rotates the arrows:

JavaScript
createClock = parent => {

    let currentTime = null;

    const set = time => {
        if (currentTime == time) return;
        currentTime = time;
        const date = new Date(time);
        let hour = date.getHours() % 12;
        let minute = date.getMinutes();
        let second = date.getSeconds();
        hour = (hour + minute / 60 + second / 3600) / 12;
        minute = (minute + second / 60) / 60;
        second = second / 60;
        arrowHour.style.transform = `rotate(${hour}turn)`;
        arrowMinute.style.transform = `rotate(${minute}turn)`;
        arrowSecond.style.transform = `rotate(${second}turn)`;
    }; //set

    return set;

}; //createClock

Here, arrowHour, arrowMinute, arrowSecond are SVG groups. Why? It is done to generalize the code and make it maintainable in case one needs to render some complicated arrow shapes composed of several SVG elements.

The other role of style.transform is to avoid trigonometric calculations and make the code more maintainable. Note that the transform assumes that the coordinate point (0, 0) is the center of rotation.

Performance Optimization

In our case, the clock state is updated every second. However, if we call the clock’s function set from the handler passed to setInterval every second, the system cloak events will be beating with this cycle, so part of the second clock updates will be lost. Therefore, to see all second updates, set should be called a few times per second. In the code, it is done approximately three times per second on average, and it looks good. But then some updates of the graphics will be redundant.

This problem can be resolved by storing currentTime locally and checking that the argument time passed to set has a new value, as it is shown in the set code:

JavaScript
if (currentTime == time) return;
    currentTime = time;

It works because the value for the argument time is obtained as Date.now(). The return value type is integer, and this operation is very fast because it is based on the kernel OS functionality, and the comparison is also very fast. On the other hand, the rest of the set function has expensive string operations and even more expensive graphical updates. Therefore, avoiding redundant updates of the clock states is important.

Main Difference Between Analog and Digital Clocks

The first idea of the calculation of the arrow angles would be using just hours, minutes, and seconds. This is what a digital clock does. These values are integers. And yes, it would be suitable for a digital clock.

An analog clock is different. It would be incorrect to say that an hour arrow shows just hours. A single hour arrow should show seconds, minutes, and hours. Likewise, a minute arrow should show both minutes and seconds. Only the accuracy is different.

This is wrong:

JavaScript
createClock = parent => {

    const set = time => {
        if (currentTime == time) return;
        currentTime = time;
        const date = new Date(time);
        let hour = date.getHours() % 12;
        let minute = date.getMinutes();
        let second = date.getSeconds();
        hour = hour  / 12; // wrong!
        minute = minute / 60; // wrong!
        second = second / 60;
        arrowHour.style.transform = `rotate(${hour}turn)`;
        arrowMinute.style.transform = `rotate(${minute}turn)`;
        arrowSecond.style.transform = `rotate(${second}turn)`;
    }; //set

    return set;

}; //createClock

If this calculation was the same as for a digital clock, the minute arrow would stay at some minute label and jump to a new position every minute. And the hour arrow would jump by an hour every hour. Instead, all the arrows should move by seconds. This is done by the correct calculation shown above.

How to Add Decorations?

The demo application is very simple. How to add all those Arabic or Roman numeric labels, fancy arrows, or fancy backgrounds?

The way to go would be using an existing <svg> element embedded in HTML. The SVG elements to be controlled by the script could be added on top of it. When I need to do such a thing, I draw appropriate vector graphics with some vector graphic editor and save it as an SVG file. I would recommend Inkscape. The file can be embedded into HTML. Usually, the file should be cleaned of comments, redundant metadata, and unused id attributes. Numeric colors should better be replaced with CSS color names.

Typically, adding some vector graphics to existing SVG requires some group. The reference to it can be obtained via its id attribute or some other way using Document.querySelector().

Here, most important thing is to match the coordinate system and a viewport.

CSS: Docking Layout

Note that the application behaves pretty much like a “desktop application”: when a browser window is resized, the <header> and <footer> elements always stay on top and at bottom of the browser client area. The central <main> area, where SVG is rendered, is scaled appropriately. At least, it happens if the brower window is not too small.

This is the behavior most adequate for a single-page application, but not only for this purpose.

This is achieved using the CSS Flexbox layout method.

The application of flex has many subtle points. In the application, the relevant part of CSS is commented on to show the right technique:

CSS
/* flex: */
html, body { height: 100%; }
body { display: flex; flex-flow: column; }
main { flex: auto; overflow: auto; }
/* end flex */

CSS: Fitting SVG

Another tricky part of the application CSS is fitting the SVG part in its container and its centering. It is used by using relative units:

CSS
section { text-align: center; }
svg { height: 80vmin; width: 80vmin; }

This way, the actual size of the vector graphics part is defined by either width or height of the client size of the browser window, depending on the current aspect ratio.

In this case, the image ratio is 1:1. If it is not so, one of the width or height attribute values can be auto. In some cases, the attribute max-content or min-content can be used.

What About Server Part?

The question is: why? Maybe this question could be addressed to the author of the original article who presented a much bigger 2-tier application. I think the Web application using some server part is one thing, and a clock component is a totally different thing and should be considered separately.

At best, the application can request the server part to change the time zone, or perhaps some reference point to count time from. It can be easily achieved with Ajax. Even in this case, separation of concerns should be used.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)