Prototype.js Polar Clock

I've recently discovered a type of clock, called a polar clock. It has arcs which extend from 0 degrees to 360 degrees as that interval (eg: hour, min, sec) extend. The arcs roll back once they reach the end of the interval. A windows and MacOSX implementation in Flash compiled into a screensaver can be found at PixelBreaker As i've never used Canvas before now, i'm going to be showing you how to use Canvas with Prototype 1.6 and throw in some more advanced techniques.

Internet Explorer doesn't support the Canvas tag, but all the other major browsers do. To get around this problem, we'll be using exCanvas to provide support via it's native VML (similar to SVG) drawing markup.

What we're building

The finished product

Lets get started...

Start with a blank HTML document which includes prototype 1.6.0.2 and exCanvas

For those of you new to cross browser scripting, don't forget the doctype is extremely important to make sure all browsers are adherring to the same specification. a list apart

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
    <script language="javascript" type="text/javascript" src="prototype-1.6.0.2.js"></script>
    <!--[if IE]><script type="text/javascript" src="excanvas.js"></script><![endif]-->
    <script language="javascript" type="text/javascript" src="polar.js"></script>
    <link rel="stylesheet" type="text/css" href="Polar.css"></link>
</head>
<body>
    <canvas id="cv" width="550" height="550"></canvas>
</body>
</html>

Note that i'm including prototype locally and i've already included a blank canvas tag in my document. The excanvas.js file is conditionally included for IE. I've also included a link to a blank javascript file which will be where most of the action takes place.

Starting to build

The first thing we are going to do is provide an event which will fire when the document is ready. This means that the Canvas tag in the document is available for us to begin work, even though the window load event hasn't fired. Back in the day, you had a number of ways to achieve this (such as a script tag at the bottom of your document) but with Prototype 1.6 we can just observe a custom event on the document and everything else is done for us. We are using Event.observe (extended to the document object) and an anonymous function.

document.observe("dom:loaded", function() { 
    alert("Document Ready");
});

Right, now we're going to obtain a reference to the canvas tag, and begin by drawing a simple box. With exCanvas included, the canvas tag is fairly well implemented in explorer, and there's no need to write custom code for different browsers. The canvas examples show this to get a context for drawing onto the canvas

var ctx = document.getElementById('c').getContext('2d');

But with prototype, we can shorten the document.getElementById portion down to just $. I'm also going to cache the canvas object (not the context) for later use.

var c = $('cv');
var ctx = c.getContext('2d');

Now we have a context, lets go ahead and draw that simple box.

ctx.fillStyle = 'blue';
ctx.fillRect(10, 10, 100, 100);

This gives us a lovely blue box on our canvas. (the arguments to fillRect are x,y,width,height)

A box isn't a polar clock!

In order to render our polar clock, we'll need a lot more than simple boxes. When you're drawing more than simple rectangles, canvas uses paths. To use a path, you must beginPath, issue the information command, in this case arc, and then stroke (which draws the object to the canvas) The arc drawing function in canvas gives us what we need. I'm going to add this in following the rectangle code.

The function signature for arc is: arc(x, y, radius, startAngle, endAngle, anticlockwise)

ctx.beginPath();
ctx.arc(25, 25, 20, 0, Math.PI, false);
ctx.stroke();

It's important to realise that angles in the arc function are measured in radians, not degrees. To convert degrees to radians, the following formula works radians = (Math.PI/180)*degrees. We're going to be drawing a lot of arcs, so it'll be useful to have this as a method on the Number prototype. To add this method into number, we use the prototype Object.extend method to extend the Number prototype.

Object.extend(Number.prototype, {
    toRadians: function(){ return (Math.PI/180)*this; }
});

Now, we can go

alert((65).toRadians());

(note you only need the brackets when using literals)

Polar.js currently looks like this:

document.observe("dom:loaded", function() { 
    var c = $('cv');
    var ctx = cv.getContext('2d');
    ctx.fillStyle = 'blue';
    ctx.fillRect(10, 10, 100, 100);

    ctx.beginPath();
    ctx.arc(25, 25, 20, 0, Math.PI, false);
    ctx.stroke();

    alert((65).toRadians());
});

Object.extend(Number.prototype, {
    toRadians: function(){ return (Math.PI/180)*this; }
});

Lets go ahead and get rid of that ugly rectangle and test arc. In order to draw a polar clock bar, we'll have to make a few different calls. First, we need to begin a path. A path in canvas is a set of drawing instructions which result in a shape. You can see a beginPath call above. Once a path is started, the draw-list is cleared and all the calls until the path is complete (via fill or stroke) will stack. Because we're effectively drawing partial circles, we want to move our axis point to the center of the canvas. This will make our math easier. To do this, we must first save the context (so that we can restore it later) and then issue a translate function. Because we want to use the center as the origin, we can use the prototype getDimensions() method against the canvas tag (using the c reference from above) You can also see us setting a colour to draw in as well as clearing the canvas by drawing a black rectangle that covers everything.

var canvasDimensions = c.getDimensions();
ctx.fillStyle = '#000000';
ctx.fillRect(0, 0, canvasDimensions.width, canvasDimensions.height);
ctx.save();
ctx.strokeStyle = "#9CFF00";
ctx.translate(canvasDimensions.width / 2,canvasDimensions.height / 2);

Now we want to draw an arc. Lets start with the seconds.

There's 60 seconds in a min, and 360 degrees in a circle. In javascript, you can get the current date with new Date(); and seconds is available as a method call to getSeconds();

var d = new Date();
var s = d.getSeconds();
var secondDegrees = (s/60)*360;
alert(secondDegrees);

We're going to shorten this using prototype shortly.

0 degrees is at 3 o'clock in a circle, so we need to subtract 90 to use 12 o'clock as our arc origin. We could always do this subtraction, but it's going to be quicker to rotate the canvas by 90 degrees anti-clockwise.

ctx.rotate(-(90).toRadians())

Now, lets draw our first second-based arc.

ctx.arc(0, 0, 100, 0, secondDegrees.toRadians(), false);

This starts at 0,0 (center of the canvas now) for a 100 pixel radius, starting at 0 degrees (now 12 o'clock) and rotates for however many degrees the current seconds into the minute we are. To see it, issue a ctx.stroke();

A plain line isn't much fun, so lets expand on this.

We want something we can fill, so we want to draw the arc, draw an endcap, and then rotate back to the origin and then close the arc. Once we have a shape (a closed path) we can fill it.

This took a lot of tweaking, it's more difficult than you might imagine.

I've replaced the above secondDegrees code with the same but a radian conversion.

var d = new Date(); 
var s = d.getSeconds();
var secondRadians = ((s/60)*360).toRadians();

var w = 20; //define a width for our 'arc'
var r = 260; //this is the radius for the arc
ctx.beginPath(); //start a path
ctx.arc(0, 0, r, 0, secondRadians, false); //draw the first rotation

now, we need to draw the endcap.

Because we're using arcs, we don't actually know where our current drawing position is. I know that I need the endcap to go up to where the other arc should stop, so we can use some simple trig math to determine where our current x and y is and draw an endcap.

var x = (r-w)*Math.cos(secondRadians); 
var y = (r-w)*Math.sin(secondRadians); 
ctx.lineTo(x,y);

Then we arc back the other way.

r-w gives us the inner radius of our arc, and we rotate from the current angle, back to 0. The true means anti-clockwise.

ctx.arc(0,0,(r-w),secondRadians,0,true);

Now we close and fill the path.

ctx.closePath();
ctx.fill();

Voila, we have a solid arc, 20px wide.

The newly written code in one block is:

var w = 20;
var r = 260;
var d = new Date();
var s = d.getSeconds();

var secondRadians = ((s/60)*360).toRadians();
ctx.beginPath();
ctx.arc(0, 0, r, 0, secondRadians, false);
var x = (r-w)*Math.cos(secondRadians); 
var y = (r-w)*Math.sin(secondRadians);
ctx.lineTo(x,y);
ctx.arc(0,0,(r-w),secondRadians,0,true);
ctx.closePath();
ctx.fill();

We can clean this up a little.

Lets create a class called PolarClock and refactor our current code

var PolarClock = Class.create({
  initialize: function(bgColor) {
    this.cc = new ColorConverter(); 
    this.c = $('cv');
    this.ctx = $('cv').getContext('2d');
    this.canvasDimensions = this.c.getDimensions();
    this.ctx.save();
    this.ctx.translate(this.canvasDimensions.width / 2, this.canvasDimensions.height / 2);
    this.ctx.rotate(-(90).toRadians())
    this.clearCanvas(bgColor);
    this.ctx.save();
  },

  clearCanvas: function(bgColor) {
    if (bgColor) this.bgColor = bgColor;
    else bgColor = this.bgColor;
    this.setColor(bgColor);
    var dim = this.canvasDimensions
    this.ctx.fillRect(-(dim.width/2), -(dim.height/2), dim.width, dim.height);
  },

  drawSolidArc: function(color, radius, width, radians) {
    this.setColor(color);
    var innerRadius = radius-width;
    this.ctx.beginPath();
    this.ctx.arc(0, 0, radius, 0, radians, false);
    var endPoint = this.getArcEndpointCoords(innerRadius, radians);
    this.ctx.lineTo(endPoint.x, endPoint.y);
    this.ctx.arc(0, 0, innerRadius, radians, 0, true);
    this.ctx.closePath();
    this.ctx.fill();
  },

  getArcEndpointCoords: function(radius, radians) {
    return  {
      x: radius * Math.cos(radians), 
      y: radius * Math.sin(radians)
    };
  },

  setColor: function(color) {
    this.ctx.strokeStyle = color;
    this.ctx.fillStyle = color;
  }
});

document.observe("dom:loaded", function() { 
    var c = $('cv');
    var ctx = $('cv').getContext('2d');

    var w = 20;
    var r = 260;
    var d = new Date();
    var s = d.getSeconds();
    var secondRadians = ((s/60)*360).toRadians();

    var pc = new PolarClock("#000000");
    pc.drawSolidArc("#9CFF00", r, w, secondRadians);
});

Object.extend(Number.prototype, {
  toRadians: function() {
    return (Math.PI / 180) * this;
  }
});

document.observe("dom:loaded", function() { 
    var c = $('cv');
    var ctx = $('cv').getContext('2d');

    var w = 20;
    var r = 260;
    var d = new Date();
    var s = d.getSeconds();
    var secondRadians = ((s/60)*360).toRadians();

    var pc = new PolarClock("#000000");
    pc.drawSolidArc("#9CFF00", r, w, secondRadians);
});

The code, explained

This may look a little intimidating but it's really quite straight forward. First we create an empty class with a constructor, using prototype's Class.Create method. We define a javascript object with the curly braces, as the only arguments to Class.create which defines the methods to be added to the class. Then, we just refactored the code into standard functions, using the prefix 'this.' to refer to the PolarClock class and it's methods. We then modified the dom:loaded anonymous method so that it calls the class. Note that in clearCanvas we use coordinates that reference the newly translated system and that we call clearCanvas -after- the translation method.

Basing our javascript polar clock on the PixelBreaker one means we have something to work towards.

Changing the colours

The next feature i'm going to implement is the changing of the arc colour, depending on the number of degrees into the circle. I investigated how the pixelbreaker one colour cycles (by taking a few screenshots and checking pixel colours) and found that the hue is being altered while the saturation and lightness remains the same. The hue starts at approximately 140 and cycles up to 255, then wraps around to 0 again and back up to 140 as the arc rotates around the circle. Prototype offers Number.toColorPart which converts a 0-255 number into its representative hex value. We can pass RGB values to canvas, but Passing html strings allows us to more easily check the methods.

Color conversion

Because we'll be altering the HSL values and need RGB comparisons, I went to the trusty google and looked up a javascript algorithm for this. I read the wikipedia page and reailsed that it'd be quicker to find someone elses implementation. This one came from: Color Conversions in Javascript

I've refactored their code into a Prototype class, ColorConverter so that all of the colour manipulation methods are namespaced. This script is actually included in polar.js but i've put a seperate copy of it in colorconverter so that you can view it standalone.

I had to make a couple of simple modifications to alter the ranges. Their methods took 0..1 and I wanted to use 0..255.

in HtoC; return parseInt(c*255);

and in hslToRgb; h = (h/255), s = (s/255), l = (l/255);

I've also added a couple of methods (rgbToHex that uses Number.toColorPart and Array.map() to convert a RGB array to a Hex Color code) and rgbToCss which returns a string css(r,g,b);.

rgbToHex: function(rgbArray)
{
    return '#' + rgbArray.map(function(c) { return c.toColorPart(); }).join('').strip();
},
rgbToCss: function(rgbArray)
{
    return 'rgb(' + rgbArray.join(',').strip() + ');';
},

I added a new method to Number which allows me to wrap a value,

Object.extend(Number.prototype, {
    toRadians: function(){ return (Math.PI/180)*this; },
    wrap: function(min,max) { 
        if(this < min) return this + max;
        if(this > max) return this - max;
        return this;
    }
});

so that I can add 145 to my colour interval and get it wrapping on 0-255.

The new anonymous dom:loaded function looks like this:

var w = 20;
var r = 260;
var d = new Date();
var s = (d.getSeconds() * 1000) + d.getMilliseconds();
var secondRadians = ((s/60000)*360).toRadians();

var c = new ColorConverter();   
var color = this.cc.rgbToCss(this.cc.hslToRgb(((si*255)+145).wrap(0,255),205,127));

var pc = new PolarClock("#000000");
pc.drawSolidArc(color, r,w,secondRadians);

You can see that i'm now changing the colour depending on how far through the min we are, and i've also increased the resolution to take into account milliseconds.

Now, we need to animate this...

lets create a new method in polarClock called draw. For now, it'll only be drawing seconds.

draw:function(){
    var w = 20;
    var r = 260;
    var d = new Date();
    var s = (d.getSeconds() * 1000) + d.getMilliseconds();
    var si = (s/60000);
    var secondRadians = (si*360).toRadians();
    var color = this.cc.rgbToCss(this.cc.hslToRgb(((si*255)+145).wrap(0,255),205,127));
    this.clearCanvas();
    this.drawSolidArc(color, r,w, secondRadians);
}

Our new dom:loaded function:

var pc = new PolarClock("#000000");
pc.draw();

To animate, we can use setInterval. Prototype wraps this with PeriodicalExecutor(seconds).

Lets add two new methods to our PolarClock class: start and stop.

start:function()
{
    if(!this.animator)
        this.animator = new PeriodicalExecuter(function(pe) {   
            this.draw();
        }.bind(this), .25); 
},
stop:function()
{
    if(this.animator)
        this.animator.stop();
}

and modify our dom:loaded; So as to not tie up resources, we're going to stop the clock after 50 seconds.

var pc = new PolarClock("#000000");
pc.start();
pc.stop.bind(pc).delay(50)

Now that drawing works, we need to add some more rings.

To do this, we'll be modifying draw, so lets disable the animation and return to drawing a single frame.

/*
pc.start();
pc.stop.bind(pc).delay(50)
*/
pc.draw();

We add a method to the date object

Object.extend(Date.prototype, {
    daysInCurrentMonth: function() {
        var dd = new Date(this.getYear(), this.getMonth(), 0);
        return dd.getDate();
    }
});

And add a new method to our PolarClock class:

intervalToDegrees: {
    second: function() { return ((((this.getSeconds() * 1000) + this.getMilliseconds()) / 60000) * 360); },
    min: function() { return (((this.getMinutes()+1) / 60) * 360); },
    hour: function() { return (((this.getHours()+1) / 24) * 360); },
    weekday: function() { return (((this.getDay()+1) / 7) * 360); },
    day: function() { return (((this.getDate()+1) / this.daysInCurrentMonth()) * 360); },
    month: function() { return (((this.getMonth()+1) / 12) * 360); }
},

This method provides the calculations in order to convert the portion of a date (this, inside the functions) into the number of degrees it should be represented by.

Modifying draw;

this.clearCanvas();
var w = 20;
var r = 260;
this.date = new Date(); 
var cr = r;
$w("month day weekday space hour min second").reverse().each(function(interval){
    cr = cr - w - w/2;
    if(interval != 'space') 
    {
        var ir = this.intervalToDegrees[interval].bind(this.date)();
        alert(ir);
    }
}.bind(this));

We intialize our globals (well, class-globals), copy the max radius to cr (current radius). The $w function creates an array from words, then we reverse this (just for readability) and iterate over the array. For each item in the array, (eg; interval: month) we call the anoynmous method passing it interval. This updates the current radius by subtracting the width of one arc, and half the width of an arc (spacer).

It then calls into the intervalToDegrees collection; referencing it with [interval] to get the appropriate function in the object. Once the object is retreived, it is bound using .bind() to the current date. By caching new Date() to this.date before this loop, it ensures that all arcs are drawn using the same date. Once the function is retreived and bound, it is called with () and the result stored into ir. For now, ir is just alerted.

Once this is working, we replace the alert with the following code.

var i = ((ir / 360) * 255) + 147;
var color = this.cc.rgbToCss(this.cc.hslToRgb(i.wrap(0,255),205,127));
this.drawSolidArc(color, cr, w, ir.toRadians());

This takes ir (in degrees) and calculates it as a fraction of 255, and adds 147. This means that our hue rotation starts in the middle and goes around. Once the hue value (stored in i) is calculated, it is wrapped using Number.wrap, and then converted to RGB then to a hex code. Then we call into the drawSolidArc code from earlier, passing it the required values.

To finish off, we need to put some labels into the arcs. Lets add another function-mapping-object to return the labels. We'll need 3 globals too, which could be in the PolarClock class but as we are binding the interval methods to this.date, we wont have access to them when in the context of interval methods, which is when they're needed. The daySuffix method is courtesy of frequency-decoder date suffix

var dayNames = function() { 
    return $w('Sunday Monday Tuesday Wednesday Thursday Friday Saturday'); 
};
var monthNames = function() { 
    return $w('January Febuary March April May June July August September October November December'); 
};
function daySuffix(d) {
    d = String(d);
    return d.substr(-(Math.min(d.length, 2))) > 3 && 
        d.substr(-(Math.min(d.length, 2))) < 21 ? "th" : ["th", "st", "nd", "rd", "th"][Math.min(Number(d)%10, 4)];
}

You can see me using $w to create two array literals.

Add some Css to polar.css

body {
    background-color:#000000;
    margin:50px;
    text-align:center;
}

canvas {
    z-index:1;
    border:solid 2px #ffffff;
    padding:2px 2px 2px 2px;
}

#labels
{
    color:#ffffff;
    font-family:verdana;
    font-size:11px;
    z-index:2;
    position:absolute;
    left:250px;
    bottom:10px;
    text-align:left;
}
#description
{
    color:#ffffff;
    font-family:verdana;
    font-size:11px;
    z-index:2;
    position:absolute;
    right:250px;
    bottom:10px;
    text-align:left;
}

Replace the canvas tag with

<body>
    <div style="position:relative;">
    <canvas id="cv" width="550" height="550"></canvas>
    <div id="labels">
    </div>
    <div id="description">
        PrototypeJS Polar Clock. Gareth Evans, 2008.
    </div>
    </div>
    <div id="debug"></div>
</body>

And then modify the draw method

draw:function(){
    this.clearCanvas();
    var w = 20;
    var r = 260;
    this.date = new Date();
    var cr = r;
    $('labels').update()
    $w("month day weekday space hour min second").reverse().each(function(interval){
        cr = cr - w - w/2;
        if(interval != 'space') 
        {
            var ir = this.intervalToDegrees[interval].bind(this.date)();
            var i = ((ir / 360) * 255) + 147;
            var color = this.cc.rgbToCss(this.cc.hslToRgb(i.wrap(0,255),205,127));
            this.drawSolidArc(color, cr, w, ir.toRadians());            
            $('labels').insert(this.getIntervalLabel[interval].bind(this.date)() + '<br/>');
        }
    }.bind(this));
},

Finally, modify our dom:loaded function so that the animation works again.

document.observe("dom:loaded", function() { 
    var pc = new PolarClock("#000000");
    pc.start();
});

A final note;

This is just one example of utilizing prototype and i've tried to make things as clear as possible. While I haven't used all of the possible features that prototype offers, I've tried to make use of a few of the common ones. Object.extend, Class.create, bind, enumerables, $w, $.

A working example can be found here

You can download the finished and required files here:

Cross Browser?

Sorry guys, To make this truly cross browser would take a lot of time, as i'm not completely familiar with all the quirks. If you can help, let me know.

In Safari2, the clock appears in the top left corner and draws off the edges of the canvas. I think this is due to the canvas translation causing the coordinate system to change incorrectly. It draws in the correct place in Safari3. I can't test in Safari 2 but I welcome any user-submitted fix.

In Opera, the clock doesn't even render. I don't know why this would be, unless the coordiate system places it outside of the canvas. Again, I welcome any user-submitted fix.

Finally

Thanks to Tobie Langel for proofreading this example and recommending a few syntax changes.

You can contact me at agrath@gmail.com for any changes, suggestions, compliments, rage, requests or other correspondence.

Design of this page was based on kangax's blog, perfection kills