Clock design

A basic example of how to draw an analog clock with Processing.

 

Why clock design

Trying to design a clock face can be a good basic exercise for information design and data visualization.

There are a number of design ideas for clocks and watches.
But naturally enough, almost all of them are intended to represent the same kind of information: the hour, minute and (sometimes) second.

The thing is, while the data is extremely simple, there are so many options of how to represent this data.
It may be even non-visual:

The earliest mechanical clocks in the 13th century didn’t have a visual indicator and signalled the time audibly by striking bells.
— https://en.wikipedia.org/wiki/Clock

So why not seek possibilities?

 

Creating a sketch

Before writing codes, let's see what kind of parts a clock consists of.
Let's say,

  • An analog clock consists of a clock face and clock hands. (Ignore the internal mechanism)
  • Each clock hand indicates the time (hour, minute or second) by its angle.
  • Clock face includes the tick marks and hour numbers. It provides a metric how to read the indicators.

And here is an example of a class diagram for an analog clock:

Clock-class-diagram
 

Implementation example

For now, a basic general one. Will make further examples another time.

Built with Processing and Processing.js

Source code:
/* @pjs font="Lato-Bold.ttf"; */
/* "Lato-bold.ttf" is a web font designed by Łukasz Dziedzic, available in Google Fonts. */
/* Font file path need to be customized when running on the web */

// Processing 3.2.1

AnalogClock currentClock;
PFont currentFont;

void setup() {
  size(320, 240);
  
  String fontFilePath = "Lato-Bold.ttf";    // Need to be customized when running on the web
  float fontLoadSize = 24f;
  currentFont = createFont(fontFilePath, fontLoadSize, true);
  textFont(currentFont, fontLoadSize);

  float clockFaceRadius = 100f;
  currentClock = new AnalogClock(width / 2, height / 2, clockFaceRadius);
}

void draw() {
  background(255f);
  currentClock.draw();
}

final class AnalogClock {
  final ClockFace face;
  final ArrayList<ClockHand> hands = new ArrayList<ClockHand>();
  float positionX;
  float positionY;
  
  AnalogClock(float posX, float posY, float radius) {
    positionX = posX;
    positionY = posY;
    
    face = new ClockFace(radius);

    hands.add(new HourHand(radius * 0.5f, 7f));
    hands.add(new MinuteHand(radius * 0.85f, 3f));
    hands.add(new SecondHand(radius * 0.85f, 1f));
  }
  
  void draw() {
    translate(positionX, positionY);
    face.draw();
    for(ClockHand currentHand : hands) {
      currentHand.draw();
    }
  }
}

final class ClockFace {
  float radius;
  
  ClockFace(float rad) {
    radius = rad;
  }
  
  void draw() {
    // Background
    fill(255f);
    stroke(32f);
    strokeWeight(6f);
    ellipseMode(RADIUS);
    ellipse(0f, 0f, radius, radius);
  
    // Ticks
    fill(32f);
    strokeCap(SQUARE);
    textAlign(CENTER, BASELINE);
    int tickCount = 60;
    for (int i = 0; i < tickCount; i++) {
      float angle = i * TWO_PI / tickCount - HALF_PI;

      if (i % 5 == 0) {
        // Hour ticks
        drawTickMark(angle, 0.08f, 2f);
        
        // Hour numbers
        int currentHour = i / 5;
        if (currentHour == 0) {
          currentHour = 12;
        }
        drawHourNumber(angle, 0.25, currentHour);
      } else {
        // Minute ticks
        drawTickMark(angle, 0.05f, 1f);
      }
    }
  }
  
  void drawTickMark(float tickAngle, float tickSizeFactor, float tickWeight) {
      float x = cos(tickAngle) * radius * 0.95f;
      float y = sin(tickAngle) * radius * 0.95f;

      stroke(32f);
      strokeWeight(tickWeight);
      pushMatrix();
      translate(x, y);
      rotate(tickAngle);
      line(- radius * tickSizeFactor, 0f, 0f, 0f);
      popMatrix();
  }
  
  void drawHourNumber(float tickAngle, float numberSizeFactor, int hour) {
      float x = cos(tickAngle) * radius * 0.72f;
      float y = sin(tickAngle) * radius * 0.72f;

      textSize(radius * numberSizeFactor);
      noStroke();
      pushMatrix();
      translate(x, y);
      float valignFactor = 1.2f;  // Different for each font
      text(hour, 0f, (textAscent() - textDescent()) * valignFactor / 2);
      popMatrix();
  }
}

abstract class ClockHand {
  float handLength;
  float handWidth;
  
  ClockHand(float len, float wid) {
    handLength = len;
    handWidth = wid;
  }
  
  abstract float getAngle();
  
  void draw() {
    stroke(32f);
    strokeWeight(handWidth);
    strokeCap(PROJECT);
    line(0f, 0f, cos(getAngle()) * handLength, sin(getAngle()) * handLength);
  }
}

final class HourHand extends ClockHand {
  HourHand(float len, float wid) {
    super(len, wid);
  }
  
  float getAngle() {
    float currentHour = float(hour()) + norm(float(minute()), 0f, 60f);
    return map(currentHour, 0f, 24f, 0f, TWO_PI * 2) - HALF_PI;
  }
}

final class MinuteHand extends ClockHand {
  MinuteHand(float len, float wid) {
    super(len, wid);
  }
  
  float getAngle() {
    float currentMinute = float(minute()) + norm(float(second()), 0f, 60f);
    return map(currentMinute, 0f, 60f, 0f, TWO_PI) - HALF_PI;
  }
}

final class SecondHand extends ClockHand {
  SecondHand(float len, float wid) {
    super(len, wid);
  }
  
  float getAngle() {
    float currentSecond = float(second());
    return map(currentSecond, 0f, 60f, 0f, TWO_PI) - HALF_PI;
  } 
}