Draggable Analogue Clock in JavaScript

18 October 2008

Permalink: http://douggreenall.co.uk/site/?p=455

Hello all - well I must admit that it's been quite a while since my last post - I've been snowed under with Outerin work over the past month, what with trying to get the new version of Optimii (our expense and time management system) out of the door. We've also been chosen as finalists in the BlackBerry Partners Fund Developers Challenge so our CEO Jonathan Evans and our BlackBerry guru Jonathan Fisher are off to Santa Clara next week and we've been doing everything we can to get stuff ready before they go.

Now I was going to post my window manager this time, but I'm not yet entirely satisfied with it, so instead I'm going to post a little widget that I wrote a couple of weeks ago for Optimii ? a rather splendid analogue clock in JavaScript. You can either drag the clock hands to set the time or type the value in the input box below. Finally you can click the meridiem to set ante or post. Let's have a look at a working example (it's worth noting by the way that getting the hour hand to go to 12 with the mouse when we have a whole hour can be a little fiddly - it will go but it could certainly be refined - I'll implement a fix over the next couple of days):


Okay, so how does it work? Well it's not actually that complex believe it or not:all we're doing is displaying an image for the clock-face and overlaying a couple of dynamic lines and draggable layers for the hands. When clicked and dragged, we calculate the angular difference between the respective hand vector and a vector from the centre of the clock to the new mouse position. Finally we convert this angular difference to its value in hours or minutes and add it to the clock time and redraw the hands and digital time. For the input formatting, each time a key is pressed we get the position of the caret within the input box and check the value of the key - if it's valid (numeric and between a particular range) we update that digit of the time and again redraw the hands, otherwise we force the caret to stay in position.

So to achieve all of this, we first need an image of a clock-face. I created one in Paint Shop Pro by drawing a circle, overlaying a cross-hair through its centre and then rotating copies of the cross-hair by 30 and 60 degrees. Remove the excess lines in the middle of the circle and you'll end up with the desired effect (or you can just use mine). A couple of things worth mentioning before you start:

1) You don't want to draw the image too big ? drawing lines in JavaScript is quite expensive and the larger the radius of the clock, the more work our line-drawing algorithm is going to have to do. I'd recommend keeping it around 100 pixels or so (the one above is 103x103).

2) The diameter of the clock should be an odd value ? remember we're going to draw the hands from the exact centre so as to not have a bias, we need an equal number of pixels on each side of the circle origin - above we have 51 pixels either side and 1 pixel in the middle.

Once we have our clock-face we then need a method of drawing lines. This can be done by taking an appropriate line-drawing algorithm and then placing an absolutely positioned 1x1 pixel image on the screen each time the algorithm plots a point. If we use an 1x1 transparent gif, we can also apply a background colour to the image with CSS and draw any colour line we like.

So that appropriate line-drawing algorithm then - well to be honest, for straight lines there's only really one worth mentioning:Bresenham's. Now I'm not going to explain how it works here but I am going to explain the modifications I've made to optimise it for the web. Simply, a natural Bresenham implementation would render a single pixel to the screen for each point of the line but as we don't have low-level graphics access, we have to compromise by placing images instead. Unfortunately, placing many individual images in quick succession is quite expensive in the browser (especially IE) but we do have something of a get-out:as we can stretch the image we're placing, we can instead render a separate image for each line segment instead of each pixel. For example, imagine we have a horizontal 40x1 pixel line - instead of rendering 40 individual images for each point on the line, we can stretch a single 1x1 pixel image to 40x1 and display that instead - by doing this we've reduced the rendering work the browser has to do by a factor of 40 - extending this concept out across the algorithm means we can get a pretty substantial reduction in the number of images the browser actually draws. In fact, the only time we have to draw the same amount of images as the natural implementation is when we have a 45 degree line, otherwise we'll always have an improvement. Let's have a look at the code:

// preload our transparent image - this code will execute when the browser first loads this script

var imagePlot = new Image();
imagePlot.src = 'images/trans.gif';

///-function-//////////////////////////////////////////////////////////////////
// Plot
///////////////////////////////////////////////////////////////////////////////
function Plot(X, Y, Width, Height, Colour)
{
  // return our image markup as a string - this will allow us to collate the entire line markup
  // into a single string and render all the segments at once

  return '<img style="position:absolute; ' +
      'left:' + X + 'px; ' +
      'top:' + Y + 'px; ' +
      'background:' + Colour + '; ' +
      'border:none;" ' +
      'src="' + imagePlot.src + '" ' +
      'width="' + Width + '" ' +
      'height="' + Height + '" />\n';
}

///-function-//////////////////////////////////////////////////////////////////
// DrawLine
///////////////////////////////////////////////////////////////////////////////
function DrawLine(VectorStart, VectorEnd, Colour, Layer)
{
  if (Layer)
  {
    var vectorDelta = VectorEnd.Clone().SubVec(VectorStart).Abs();
    var swap = false;

    if (vectorDelta._x < vectorDelta._y)
    {
      swap = true;
      VectorStart.Swap();
      VectorEnd.Swap();
      vectorDelta.Swap();
    }

    if (VectorStart._x > VectorEnd._x)
    {
      var temp = VectorStart.Clone();
      VectorStart = VectorEnd.Clone();
      VectorEnd = temp.Clone();
    }

    var yInc = 1;
    if (VectorStart._y > VectorEnd._y)
      yInc = -1;

    var error = 0;
    var output = '';
    var position = VectorStart.Clone();
    var size = 0;

    for (VectorStart._x; VectorStart._x <= VectorEnd._x; ++VectorStart._x)
    {
      if (position._y != VectorStart._y)
      {
        if (swap)  output += Plot(position._y, position._x, 1, size, Colour);
        else    output += Plot(position._x, position._y, size, 1, Colour);

        position = VectorStart.Clone();
        size = 1;
      }
      else
        ++size;

      error += vectorDelta._y;

      if ((error << 1) >= vectorDelta._x)
      {
        VectorStart._y += yInc;
        error -= vectorDelta._x;
      }
    }

    if (swap)  output += Plot(position._y, position._x, 1, size, Colour);
    else    output += Plot(position._x, position._y, size, 1, Colour);

    Layer.innerHTML += output;
  }
}


I think the image preloading and Plot() function are pretty self-explanatory so I'll just explain the modification to the Bresenham's algorithm. Whereas in the original implementation a point would be plotted for each pixel of the line, we're instead keeping track of the distance we've travelled since we last drew a line segment to the screen - we do this with the following code:

function DrawLine(VectorStart, VectorEnd, Colour, Layer)
{
  .
  .
  .

    var position = VectorStart.Clone();
    var size = 0;

    for (VectorStart._x; VectorStart._x <= VectorEnd._x; ++VectorStart._x)
    {
      if (position._y != VectorStart._y)
      {
        if (swap)  output += Plot(position._y, position._x, 1, size, Colour);
        else    output += Plot(position._x, position._y, size, 1, Colour);

        position = VectorStart.Clone();
        size = 1;
      }
      else
        ++size;
      .
      .
      .
    }
  .
  .
  .
}


As we traverse the line, we record the length of the current segment and compare each new point with the last. If the new point is part of the same segment we simply increment the recorded length of the segment in the local variable "size" and move on. If it's different, we add the segment to the entire output and repeat the process for the next part of the line. I'm happy to explain this further if anyone has trouble understanding it - just drop me a line [sic] and I'll append an additional explanation to the end of the post. Also, as mentioned in the Plot() function comments, the output is stored as a string in the local variable "output" until we have completed the line. - only then is it actually drawn to the screen using the target layer's innerHTML property. This proves to be a bit quicker than drawing the image that constitutes each segment individually.

You may also have noticed that we've again revisited our old friend Vector2D - there are couple of new methods that we need to add, some for our Bresenham's implementation and some for the clock itself:

function Vector2D(X, Y)
{
  .
  .
  .

  this.Equals = function(Vector)
  {
    return Math.round(this._x) == Math.round(Vector._x) && Math.round(this._y) == Math.round(Vector._y);
  };

  this.Swap = function()
  {
    var temp = this._x;

    this._x = this._y;
    this._y = temp;

    return this;
  };

  this.Abs = function()
  {
    this._x = Math.abs(this._x);
    this._y = Math.abs(this._y);
    return this;
  };

  this.Round = function()
  {
    this._x = Math.round(this._x);
    this._y = Math.round(this._y);
    return this;
  };

  this.AngleSigned = function(Vector)
  {
    return Math.atan2(Vector._x, Vector._y) - Math.atan2(this._x, this._y);
  };
}


I think it's reasonably obvious what these methods do:Equals() returns true or false depending on whether the passed Vector2D instance is equal to the current vector, Swap() swaps the internal vector values (I'd like to use an XOR swap but it doesn't work with floats and we don't have pointers in JavaScript - anyone know of some other cool trick that I'm unaware of?) and Abs() sets the vector to its absolute values. Round() rounds the vector values and AngleSigned() gives the angle between two vectors - this is called AngleSigned() because there is also another method of calculating the angle between two vectors using their dot product, but it only gives an unsigned value. For us to manipulate the hands with the mouse, we need to know whether the mouse vector is ahead or behind the hand vector.

Okay, now we've got the foundations sorted, let's look at the really interesting stuff - the clock itself. It's comprised of a single class and as usual, we'll start by looking at a complete code listing:

///-class-/////////////////////////////////////////////////////////////////////
// Clock
///////////////////////////////////////////////////////////////////////////////
function Clock(ID, LayerID, MouseTracker)
{
  ///-statics-///////////////////////////////////////////////////////////////
  ///////////////////////////////////////////////////////////////////////////

  Clock.Radians = { Hours:Math.PI / 6, Minutes:Math.PI / 30 };

  Clock.GetHoursFromAngle = function(Value) { return (Value / Clock.Radians.Hours); };
  Clock.GetMinutesFromAngle = function(Value) { return (Value / Clock.Radians.Minutes); };
  Clock.GetAngleFromHours = function(Value) { return (Value * Clock.Radians.Hours); };
  Clock.GetAngleFromMinutes = function(Value) { return (Value * Clock.Radians.Minutes); };
  Clock.GetHandPosition = function(Radius, Angle)
  {
    return new Vector2D(Radius * Math.sin(Angle), Radius * (-Math.cos(Angle))).Round();
  };

  ///-data-//////////////////////////////////////////////////////////////////
  ///////////////////////////////////////////////////////////////////////////

  this._ID = ID;
  this._layerID = LayerID;
  this._mouseTracker = MouseTracker;

  this._time = new Array(12, 0);
  this._pm = false;

  this._input = null;
  this._meridiemLayer = null;
  this._handsLayer = null;
  this._hourHotspot = null;
  this._minuteHotspot = null;

  this._centre = new Vector2D(51, 51);

  this._hourRadius = 30;
  this._minuteRadius = 40;

  this._hourCurrentPos = new Vector2D(0, 0);
  this._minuteCurrentPos = new Vector2D(0, 0);

  this._hourMousePos = new Vector2D(0, 0);
  this._minuteMousePos = new Vector2D(0, 0);

  ///-toString-//////////////////////////////////////////////////////////////
  ///////////////////////////////////////////////////////////////////////////

  this.toShortString = function()
  {
    var hours = this._time[0];
    var minutes = this._time[1];

    if (hours < 10) hours = '0' + hours.toString();
    if (minutes < 10) minutes = '0' + minutes.toString();

    return hours.toString() + ':' + minutes.toString();
  };

  this.toMeridiemString = function()
  {
    if (this._pm)
      return 'pm';

    return 'am';
  };

  this.toString = function()
  {
    return this.toShortString() + this.toMeridiemString();
  };

  ///-elements-//////////////////////////////////////////////////////////////
  ///////////////////////////////////////////////////////////////////////////

  this.GetInput = function()
  {
    if (!this._input)
      this._input = wGet(this._layerID + 'Input');

    return this._input;
  };

  this.GetMeridiemLayer = function()
  {
    if (!this._meridiemLayer)
      this._meridiemLayer = wGet(this._layerID + 'Meridiem');

    return this._meridiemLayer;
  };

  this.GetHandsLayer = function()
  {
    if (!this._handsLayer)
      this._handsLayer = wGet(this._layerID + 'Hands');

    return this._handsLayer;
  };

  this.GetHourHotspot = function()
  {
    if (!this._hourHotspot)
      this._hourHotspot = wGet(this._layerID + 'HourHotspot');

    return this._hourHotspot;
  };

  this.GetMinuteHotspot = function()
  {
    if (!this._minuteHotspot)
      this._minuteHotspot = wGet(this._layerID + 'MinuteHotspot');

    return this._minuteHotspot;
  };

  ///-display-///////////////////////////////////////////////////////////////
  ///////////////////////////////////////////////////////////////////////////

  this.UpdateHandsDisplay = function()
  {
    if (this.GetHandsLayer())
    {
      this._handsLayer.innerHTML = '';

      this._hourCurrentPos = Clock.GetHandPosition(
          this._hourRadius,
          Clock.GetAngleFromHours(this._time[0]) + (Clock.GetAngleFromMinutes(this._time[1]) / 12)
        );

      this._minuteCurrentPos = Clock.GetHandPosition(
          this._minuteRadius,
          Clock.GetAngleFromMinutes(this._time[1])
        );

      DrawLine(
          this._centre.Clone(),
          this._minuteCurrentPos.Clone().AddVec(this._centre),
          '#c0c0c0',
          this._handsLayer
        );

      DrawLine(
          this._centre.Clone(),
          this._hourCurrentPos.Clone().AddVec(this._centre),
          '#c0c0c0',
          this._handsLayer
        );
    }

    if (this.GetHourHotspot())
    {
      this._hourHotspot.style.left =
          (this._hourCurrentPos.GetXInt() + this._centre.GetXInt() - 10) + 'px';
      this._hourHotspot.style.top =
          (this._hourCurrentPos.GetYInt() + this._centre.GetYInt() - 10) + 'px';
    }

    if (this.GetMinuteHotspot())
    {
      this._minuteHotspot.style.left =
          (this._minuteCurrentPos.GetXInt() + this._centre.GetXInt() - 10) + 'px';
      this._minuteHotspot.style.top =
          (this._minuteCurrentPos.GetYInt() + this._centre.GetYInt() - 10) + 'px';
    }
  };

  this.UpdateInputDisplay = function()
  {
    if (this.GetInput())
      this._input.value = this.toShortString();
  };

  this.UpdateMeridiemDisplay = function()
  {
    if (this.GetMeridiemLayer())
      this._meridiemLayer.innerHTML = this.toMeridiemString();
  };

  this.UpdateDisplay = function()
  {
    this.UpdateHandsDisplay();
    this.UpdateInputDisplay();
    this.UpdateMeridiemDisplay();
  };

  ///-set time-//////////////////////////////////////////////////////////////
  ///////////////////////////////////////////////////////////////////////////

  this.SetHours = function(Value)
  {
    try { Value = parseInt(Value, 10); }
    catch (e) { Value = 12; }

    if (Value < 1 || Value > 12)
      Value = 12;

    this._time[0] = Value;
  };

  this.SetMinutes = function(Value)
  {
    try { Value = parseInt(Value, 10); }
    catch (e) { Value = 0; }

    if (Value < 0 || Value > 59)
      Value = 0;

    this._time[1] = Value;
  };

  this.SetAM = function() { this._pm = false; };
  this.SetPM = function() { this._pm = true; };

  this.SwitchMeridiem = function()
  {
    this._pm = !this._pm;
    this.UpdateMeridiemDisplay();
  };

  ///-drag hands-////////////////////////////////////////////////////////////
  ///////////////////////////////////////////////////////////////////////////

  this.DragHour = function(Event, Run)
  {
    if (!Run)
    {
      this._hourMousePos = this._hourCurrentPos.Clone();
      this._mouseTracker.Reset();

      setTimeout(this._ID + '.DragHour(null, true)', 10);
      return;
    }

    if (this._mouseTracker._buttonDown)
    {
      if (!this._mouseTracker._offset.Equals(new Vector2D(0, 0)))
      {
        this._hourMousePos.AddVec(this._mouseTracker._offset);

        var angle = this._hourMousePos.AngleSigned(this._hourCurrentPos);
        var mouseHours = Clock.GetHoursFromAngle(angle);

        if (mouseHours >= 1 || mouseHours <= -1)
        {
          mouseHours = parseInt(mouseHours, 10) + this._time[0];

          if (mouseHours > 12)
            mouseHours -= 12;
          else if (mouseHours < 1)
            mouseHours += 12;

          this.SetHours(mouseHours);
          this.UpdateDisplay();
        }

        this._mouseTracker.Reset();
      }


      setTimeout(this._ID + '.DragHour(null, true)', 10);
    }
  };

  this.DragMinute = function(Event, Run)
  {
    if (!Run)
    {
      this._minuteMousePos = this._minuteCurrentPos.Clone();
      this._mouseTracker.Reset();

      setTimeout(this._ID + '.DragMinute(null, true)', 10);
      return;
    }

    if (this._mouseTracker._buttonDown)
    {
      if (!this._mouseTracker._offset.Equals(new Vector2D(0, 0)))
      {
        this._minuteMousePos.AddVec(this._mouseTracker._offset);

        var angle = this._minuteMousePos.AngleSigned(this._minuteCurrentPos);
        var mouseMinutes = Clock.GetMinutesFromAngle(angle);

        if (mouseMinutes >= 1 || mouseMinutes <= -1)
        {
          mouseMinutes = parseInt(mouseMinutes, 10) + this._time[1];

          if (mouseMinutes > 59)
            mouseMinutes -= 60;
          else if (mouseMinutes < 0)
            mouseMinutes += 60;

          this.SetMinutes(mouseMinutes);
          this.UpdateDisplay();
        }

        this._mouseTracker.Reset();
      }

      setTimeout(this._ID + '.DragMinute(null, true)', 10);
    }
  };

  ///-format input-//////////////////////////////////////////////////////////
  ///////////////////////////////////////////////////////////////////////////

  this.Format = function(Event)
  {
    if (this.GetInput())
    {
      var digits = this.toShortString();

      digits = new Array(parseInt(digits.substr(0, 1), 10),
                parseInt(digits.substr(1, 1), 10),
                parseInt(digits.substr(3, 1), 10),
                parseInt(digits.substr(4, 1), 10));

      var caretPos = GetCaretPosition(this._input);
      var keyCode = -1;

      if (window.event)
        keyCode = Event.keyCode;
      else if (Event.which)
        keyCode = Event.which;

      if (keyCode >= 96 && keyCode <= 105)
        keyCode -= 48;

      if (keyCode >= 48 && keyCode <= 57)
      {
        keyCode -= 48;

        if (caretPos == 2)
          ++caretPos;

        if (caretPos == 0)
        {
          if (keyCode == 0 || keyCode == 1)
          {
            digits[caretPos] = keyCode;

            if (digits[0] == 1 && digits[1] > 2)
              digits[1] = 0;
          }
          else
            –caretPos;
        }
        else if (caretPos == 1)
        {
          if (digits[0] == 0 && keyCode >= 1 && keyCode <= 9 ||
            digits[0] == 1 && keyCode >= 0 && keyCode <= 2)
            digits[caretPos] = keyCode;
          else
            –caretPos;
        }
        else if (caretPos == 3)
        {
          if (keyCode >= 0 && keyCode <= 5)
            digits[caretPos - 1] = keyCode;
          else
            –caretPos;
        }
        else if (caretPos == 4)
        {
          if (keyCode >= 0 && keyCode <= 9)
            digits[caretPos - 1] = keyCode;
          else
            –caretPos;
        }

        ++caretPos;
      }

      this.SetHours(digits[0].toString() + digits[1].toString());
      this.SetMinutes(digits[2].toString() + digits[3].toString());

      this._input.value = this.toShortString();
      this.UpdateHandsDisplay();

      SetCaretPosition(this._input, caretPos);
    }
  };
}


We start off by declaring some static data and methods - this stuff will never change and is not specific to a class instance so we won't define it as member data, that is, instead of using the "this" keyword, we use the name of the class as the prefix:

  Clock.Radians = { Hours:Math.PI / 6, Minutes:Math.PI / 30 };

  Clock.GetHoursFromAngle = function(Value) { return (Value / Clock.Radians.Hours); };
  Clock.GetMinutesFromAngle = function(Value) { return (Value / Clock.Radians.Minutes); };
  Clock.GetAngleFromHours = function(Value) { return (Value * Clock.Radians.Hours); };
  Clock.GetAngleFromMinutes = function(Value) { return (Value * Clock.Radians.Minutes); };
  Clock.GetHandPosition = function(Radius, Angle)
  {
    return new Vector2D(Radius * Math.sin(Angle), Radius * (-Math.cos(Angle))).Round();
  };


"Clock.Radians" contains the two values that equate to hours and minutes on a clock - if we consider that a circle is made of 2*PI radians, then it stands to order that a single hour is equivalent to (2*PI) / 12 radians and a single minute is (2*PI) / 60 radians - refactoring these calculations gives us PI / 6 and PI / 30.

GetHoursFromAngle(), GetMinutesFromAngle(), GetAngleFromHours() and GetAngleFromMinutes() simply translate hours and minutes into angles and vice versa.

Finally, GetHandPosition() calculates the position of a vector on a circle given the vector length and an angle. This uses the circle equation discussed in my last post.

Now we'll look at the member data:

  this._ID = ID;
  this._layerID = LayerID;
  this._mouseTracker = MouseTracker;

  this._time = new Array(12, 0);
  this._pm = false;

  this._input = null;
  this._meridiemLayer = null;
  this._handsLayer = null;
  this._hourHotspot = null;
  this._minuteHotspot = null;

  this._centre = new Vector2D(51, 51);

  this._hourRadius = 30;
  this._minuteRadius = 40;

  this._hourCurrentPos = new Vector2D(0, 0);
  this._minuteCurrentPos = new Vector2D(0, 0);

  this._hourMousePos = new Vector2D(0, 0);
  this._minuteMousePos = new Vector2D(0, 0);


"_ID" is the name of the variable used to hold our instance of Clock - we store this so the JavaScript engine can refer to the correct instance via a string (as in "setTimeout()")

"_layerID" is the prefix of the ID used for the clock div elements

"_mouseTracker" is an instance of our MouseTracker class so we can have the clock react to the mouse (we use the same MouseTracker used for the carousel)

"_time" is a 2D array to hold the hours and minutes of the clock time - we could use the inbuilt JavaScript Date() class but this adds a number of complications in the code (12-24 hour conversions etc.). I think it's better to store this separately and then have a translator if we want to apply the clock time to a Date instance

"_pm" is a boolean flag to denote whether the clock is ante or post meridiem

"_input", "_meridiemLayer", "_handsLayer", "_hourHotspot", "_minuteHotspot" are all locally stored page elements, retrieved using window.document.getElementById() - we store them to reduce DOM traversals during the animation loop (as seen in Carousel)

"_centre" is a Vector2D instance holding the centre of the clock, relative to its containing layer

"_hourRadius" and "_minuteRadius" are lengths of the hour and minute hands respectively

"_hourCurrentPos" and "_minuteCurrentPos" are the positions of the ends of the hands in relation to the centre of the clock

"_hourMousePos" and "_minuteMousePos" are the position of the mouse when dragging either of the hands

Okay, we'll look at some of the methods. There are a collection of toString() methods that simply output various clock values as strings. These are followed by GetInput() to GetMinuteHotspot(), which recover the page elements for the components of the clock. Next we have UpdateHandDisplay() - let's have a closer look at this one:

  this.UpdateHandsDisplay = function()
  {
    if (this.GetHandsLayer())
    {
      this._handsLayer.innerHTML = '';

      this._hourCurrentPos = Clock.GetHandPosition(
          this._hourRadius,
          Clock.GetAngleFromHours(this._time[0]) + (Clock.GetAngleFromMinutes(this._time[1]) / 12)
        );

      this._minuteCurrentPos = Clock.GetHandPosition(
          this._minuteRadius,
          Clock.GetAngleFromMinutes(this._time[1])
        );

      DrawLine(
          this._centre.Clone(),
          this._minuteCurrentPos.Clone().AddVec(this._centre),
          '#c0c0c0',
          this._handsLayer
        );

      DrawLine(
          this._centre.Clone(),
          this._hourCurrentPos.Clone().AddVec(this._centre),
          '#c0c0c0',
          this._handsLayer
        );
    }

    if (this.GetHourHotspot())
    {
      this._hourHotspot.style.left =
          (this._hourCurrentPos.GetXInt() + this._centre.GetXInt() - 10) + 'px';
      this._hourHotspot.style.top =
          (this._hourCurrentPos.GetYInt() + this._centre.GetYInt() - 10) + 'px';
    }

    if (this.GetMinuteHotspot())
    {
      this._minuteHotspot.style.left =
          (this._minuteCurrentPos.GetXInt() + this._centre.GetXInt() - 10) + 'px';
      this._minuteHotspot.style.top =
          (this._minuteCurrentPos.GetYInt() + this._centre.GetYInt() - 10) + 'px';
    }
  };


As its name suggests, this method updates the position of the clock hands onscreen. It first checks that the layer that holds the markup for the hands exists and that we have a valid reference to it. It then clears the contents of the layer and updates the stored positions of the hour and minute hands - note how the hour hand position is made up of both the hour value and the minute value - this is so the hour hand will gradually move towards the next hour as the minutes increase. Following this, the method draws a line for each hand and updates the positions of the draggable hour hotspots.

We also have UpdateInputDisplay() and UpdateMeridiemDisplay() which update the values of the time input box and the meridiem switch. UpdateDisplay() simply calls all the update functions one after the other.

These methods are followed by a collection of set functions:SetHours(), SetMinutes(), SetAM(), SetPM() and SwitchMeridiem(). SetHours() and SetMinutes() ensure that the value we're passing is valid for hours and minutes respectively and if so, update the time value. SetAM() and SetPM() simply set the meridiem flag to true or false while SwitchMeridiem() inverts the flag.

Right, we're onto the final three methods now - let's have a look at DragHour():

  this.DragHour = function(Event, Run)
  {
    if (!Run)
    {
      this._hourMousePos = this._hourCurrentPos.Clone();
      this._mouseTracker.Reset();

      setTimeout(this._ID + '.DragHour(null, true)', 10);
      return;
    }

    if (this._mouseTracker._buttonDown)
    {
      if (!this._mouseTracker._offset.Equals(new Vector2D(0, 0)))
      {
        this._hourMousePos.AddVec(this._mouseTracker._offset);

        var angle = this._hourMousePos.AngleSigned(this._hourCurrentPos);
        var mouseHours = Clock.GetHoursFromAngle(angle);

        if (mouseHours >= 1 || mouseHours <= -1)
        {
          mouseHours = parseInt(mouseHours, 10) + this._time[0];

          if (mouseHours > 12)
            mouseHours -= 12;
          else if (mouseHours < 1)
            mouseHours += 12;

          this.SetHours(mouseHours);
          this.UpdateDisplay();
        }

        this._mouseTracker.Reset();
      }

      setTimeout(this._ID + '.DragHour(null, true)', 10);
    }
  };


So the method starts by setting the member mouse position variable to the same as the hour position. It then resets the mouse tracker and sets a timeout to call itself again in 10 milliseconds. It does this because the method depends on the mouse button being depressed and there's a good chance that this won't have registered in the mouse tracker yet - by introducing a short delay, we ensure that the "_buttonDown" variable in MouseTracker has been updated.

When we reenter the method a short while later, we check the mouse button is still depressed and if so, make sure the mouse has moved by comparing its offset with a zeroed vector. If the mouse has moved, we add the movement to the member mouse position and then work out how much that movement is in terms of hours. If it's greater or less than 1, we add the value to our current time and update our display accordingly. We then recall the method, again with a short delay and the process repeats itself until the mouse button is released. DragMinute() is the equivalent of this method for the minute hand - it's almost identical in its implementation so you shouldn't have any trouble understanding how it works.

Finally we have Format(), which is called whenever a key is pressed in the time input box. Let's have a look:

  this.Format = function(Event)
  {
    if (this.GetInput())
    {
      var digits = this.toShortString();

      digits = new Array(parseInt(digits.substr(0, 1), 10),
                parseInt(digits.substr(1, 1), 10),
                parseInt(digits.substr(3, 1), 10),
                parseInt(digits.substr(4, 1), 10));

      var caretPos = GetCaretPosition(this._input);
      var keyCode = -1;

      if (window.event)
        keyCode = Event.keyCode;
      else if (Event.which)
        keyCode = Event.which;

      if (keyCode >= 96 && keyCode <= 105)
        keyCode -= 48;

      if (keyCode >= 48 && keyCode <= 57)
      {
        keyCode -= 48;

        if (caretPos == 2)
          ++caretPos;

        if (caretPos == 0)
        {
          if (keyCode == 0 || keyCode == 1)
          {
            digits[caretPos] = keyCode;

            if (digits[0] == 1 && digits[1] > 2)
              digits[1] = 0;
          }
          else
            –caretPos;
        }
        else if (caretPos == 1)
        {
          if (digits[0] == 0 && keyCode >= 1 && keyCode <= 9 ||
            digits[0] == 1 && keyCode >= 0 && keyCode <= 2)
            digits[caretPos] = keyCode;
          else
            –caretPos;
        }
        else if (caretPos == 3)
        {
          if (keyCode >= 0 && keyCode <= 5)
            digits[caretPos - 1] = keyCode;
          else
            –caretPos;
        }
        else if (caretPos == 4)
        {
          if (keyCode >= 0 && keyCode <= 9)
            digits[caretPos - 1] = keyCode;
          else
            –caretPos;
        }

        ++caretPos;
      }

      this.SetHours(digits[0].toString() + digits[1].toString());
      this.SetMinutes(digits[2].toString() + digits[3].toString());

      this._input.value = this.toShortString();
      this.UpdateHandsDisplay();

      SetCaretPosition(this._input, caretPos);
    }
  };
}


Format() simply converts the time to an array of digits and then updates the correct digit depending on the location of the caret. It ensures that a numeric key has been pressed by checking the keycode of the passed event and then normalises that value between 0 and 9. Following this it checks the position of the caret and then depending on its location, updates the correct digit of the time if the new value is valid or moves the caret to its previous position if not. Finally it updates the clock time and calls UpdateHandsDisplay() to redraw the clock hands.

Right, we'll finish off with the page markup you need for the clock - if anyone has any questions, please drop me a line. Cheers and have fun:

<div id="clock" style="position:relative; left:0px; top:0px; width:103px; height:140px;">
  <div id="clockFace" style="position:absolute; left:0px; top:0px; width:103px; height:103px; background-image:url(images/5/clockface.gif); background-repeat:no-repeat;">
    <div id="clockHands" style="position:absolute; left:0px; top:0px; width:100%; height:100%;"></div>
    <div style="position:absolute; left:0px; top:0px; width:100%; height:100%;">
      <div id="clockHourHotspot" style="position:absolute; left:0px; top:0px; width:20px; height:20px; background:transparent; background-image:url(images/5/trans.gif); cursor:move;" onmousedown="javascript:clock.DragHour(event);"></div>
      <div id="clockMinuteHotspot" style="position:absolute; left:0px; top:0px; width:20px; height:20px; background:transparent; background-image:url(images/5/trans.gif); cursor:move;" onmousedown="javascript:clock.DragMinute(event);"></div>
    </div>
  </div>
  <input id="clockInput" name="clockInput" style="position:absolute; left:5px; bottom:0px; width:60px; height:26px; border:none; margin:0px 0px 0px 0px; padding:0px 0px 0px 0px; background:transparent; text-align:center; font-family:Lucida Sans, Tahoma, Verdana, Arial; font-size:20px; color:#ffffff;" maxlength="5" tabindex="1" type="text" value="" onkeyup="javascript:clock.Format(event);" />
  <div id="clockMeridiem" style="position:absolute; right:5px; bottom:0px; width:30px; height:22px; text-align:left; font-family:Lucida Sans, Tahoma, Verdana, Arial; font-size:20px; color:#ffffff; cursor:pointer;" onclick="javascript:clock.SwitchMeridiem();"></div>
</div>


Download Support.js
Download Clock.js
Download clockface.gif
Download trans.gif (transparent pixel)

Draggable Analogue Clock in JavaScript
Status: Ready...
Close Window Maximise Window Minimise Window Minimise Window
Contact