8
\$\begingroup\$

I was sitting in my bedroom, looking at my Powers of 2 clock sitting next to my radio, under my analog clock on the wall, and wondered to myself: "Why have all these different clocks when I can have just one?"

So I decided to make my own. This program contains four different display modes: true binary, BCD, analog, and digital. If showing BCD or digital the user can select 12- or 24-hour time format.

This was written using Processing, but each of the classes I used for the different clocks are bona fide Java. The JavaDoc for the Processing Library can provide information on the PApplet class used for the basic functions.

If you are interested in running this program, here is a link to my sketch folder on Google Drive with an executable, or simply cut and paste everything below into a new Processing sketch and hit run.

To run the source, you will of course need Processing, and the G4P library, which can be added via the tool manager.


multi_clock.pde

import g4p_controls.*;

int mode = 0;

GButton[] buttons;
GCheckbox toggle;

AnalogClock analog;
BinaryClock binary;
DigitalClock digital;

void setup() {
  size(340, 300);
  surface.setResizable(true);
  background(50);
  noStroke();

  ellipseMode(CENTER);

  initButtons();

  analog = new AnalogClock(this);
  binary = new BinaryClock(this);
  digital = new DigitalClock(this);
}

/**
 * Initialize GButton objects and 24-hour mode toggle.
 * Each GButton is assigned and event handler and moved
 * to their location for the default view (BCD)
 */
void initButtons() {
  buttons = new GButton[4];

  for (int i = 0; i < buttons.length; i++) {
    buttons[i] = new GButton(this, 0, 0, 60, 30);
    buttons[i].setLocalColorScheme(5);
    buttons[i].setTextAlign(GAlign.CENTER, GAlign.MIDDLE);
    buttons[i].addEventHandler(this, "handleButtonEvents");
  }

  buttons[0].moveTo(20, 20);
  buttons[0].setText("Show BCD");
  buttons[0].setEnabled(false);

  buttons[1].moveTo(100, 20);
  buttons[1].setText("Show Binary");
  buttons[1].setEnabled(true);

  buttons[2].moveTo(180, 20);
  buttons[2].setText("Show Analog");
  buttons[2].setEnabled(true);

  buttons[3].moveTo(260, 20);
  buttons[3].setText("Show Digital");
  buttons[3].setEnabled(true);

  toggle = new GCheckbox(this, 35, 270, 100, 20, "24 Hour Time");
  toggle.setLocalColorScheme(0);
  toggle.setSelected(true);
}

/**
 * Event Handler for GButton objects, called when any button
 * is clicked.  Checks which button triggered the call, and
 * set the mode appropriately. Disable the button that was clicked
 * and enable all others.
 * This also moves the buttons to where they need to be based
 * on the mode.
 *
 * @param source GButton that triggered the event
 * @param event GEvent triggered. By default, only CLICKED, but could
 *              also be PRESSED or RELEASED
 */
void handleButtonEvents(GButton source, GEvent event) {
  for (int i = 0; i < buttons.length; i++) {
    if (buttons[i] == source) {
      buttons[i].setEnabled(false);
      mode = i;
    } else {
      buttons[i].setEnabled(true);
    }
  }
  // space the buttons according to width of window
  if (mode == 0 || mode == 1 || mode == 2) {
    buttons[0].moveTo(20, 20);
    buttons[1].moveTo(100, 20);
    buttons[2].moveTo(180, 20);
    buttons[3].moveTo(260, 20);
  } else {
    buttons[0].moveTo(32, 20);
    buttons[1].moveTo(124, 20);
    buttons[2].moveTo(216, 20);
    buttons[3].moveTo(308, 20);
  }
}

// Placeholder for event handler
void handleToggleControlEvents(GToggleControl source, GEvent event) {
}

void draw() {
  // draw appropriate clock
  switch (mode) {
  case 0:
    resize(340, 300);
    toggle.setVisible(true);
    if (toggle.isSelected()) {
      binary.update(hour(),minute(),second(),2);
    } else {
      binary.update(hour(),minute(),second(),1);
    }
    break;
  case 1:
    resize(340, 300);
    toggle.setVisible(false);
    binary.update(hour(),minute(),second(),0);
    break;
  case 2:
    resize(340, 400);
    toggle.setVisible(false);
    analog.update(hour(), minute(), second());
    break;
  case 3:
    resize(400, 300);
    toggle.setVisible(true);
    if (toggle.isSelected()) {
      digital.update(hour(), minute(), second(), 0);
    } else {
      digital.update(hour(), minute(), second(), 1);
    }
    break;
  }
}

/**
 * Resize the canvas, only if new size is
 * different than the current size. This means
 * this function can be called every loop
 * without recalculating the canvas each time.
 *
 * @param w the new width of the window
 * @param h the new height of the window
 */
void resize(int w, int h) {
  if (width != w || height != h)
    surface.setSize(w, h);
}

AnalogClock.java

import processing.core.*;

/**
 * Class containing functions to draw an analog clock.
 * 
 * @author Eric Roch
 * @date 12-13-15
 */
public class AnalogClock extends PApplet {
  private final int CENTER_X = 170;
  private final int CENTER_Y = 230;
  private final int FACE_DIA = 300;

  private final int R_HOUR      =   FACE_DIA * 4 / 15;    /* fractions are not reduced to  */
  private final int R_MIN       =   FACE_DIA * 5 / 15;    /* show the relative lengths     */
  private final int R_SEC_MAJOR =   FACE_DIA * 6 / 15;
  private final int R_SEC_MINOR = - FACE_DIA * 2 / 15;    // short "tail" on second hand
  private final int R_MARK      =   FACE_DIA * 7 / 15;    // hour marks on outside of face

  private final float PI = PConstants.PI;
  private final float TWO_PI = PConstants.TWO_PI;

  private float[] angles;
  private float[][]coords;

  private PApplet parent;

  /**
   * Constructor. Initializes arrays to hold
   * angle and coordinate values so they don't
   * have to be re-initialized every time the
   * clock is drawn.
   */
  AnalogClock(PApplet p) {
    parent = p;
    angles = new float[3];
    coords = new float[3][4];
  }

  /**
   * Render the clock to the canvas.
   *
   * @param h integer hours in 12- or 24-hour format
   * @param m integer minutes
   * @param s integer seconds
   */
  void update(int h, int m, int s) {
    if (!Time.isValid(h, m, s)) {
      print(h + ":" + m + ":" + s);
      println(" is not a valid time. Aborting.");
      return;
    }
    parent.background(50);
    parent.stroke(0);
    parent.strokeWeight(5);

    angles = getAngles(h, m, s);
    coords = getXY(angles);

    // clock face
    parent.fill(255);
    parent.ellipse(CENTER_X, CENTER_Y, FACE_DIA, FACE_DIA);
    parent.fill(0);
    parent.ellipse(CENTER_X, CENTER_Y, 12, 12);

    // dots for numbers
    for (float i = 0; i < TWO_PI; i += PI/6) {
      parent.point(CENTER_X + cos(i)*R_MARK, CENTER_Y + sin(i)*R_MARK);
    }

    // clock hands
    parent.line(coords[0][0], coords[0][1], coords[0][2], coords[0][3]);
    parent.line(coords[1][0], coords[1][1], coords[1][2], coords[1][3]);
    parent.stroke(220, 0, 0);
    parent.ellipse(CENTER_X, CENTER_Y, 8, 8);
    parent.line(coords[2][0], coords[2][1], coords[2][2], coords[2][3]);
  }

  /**
   * Calculate the angles of the clock hands.
   *
   * @param h hour, in 12 or 24 hour format
   * @param m minute
   * @param s second
   * @return array of floats containing the angle of the hours,
   *         minutes, and seconds hands.
   */
  private float[] getAngles(int h, int m, int s) {
    float[] a = new float[3];
    // make the hour hand move a little bit extra each minute
    // PI/6 = (2PI / 12 hrs / 60 min)
    a[0] = map(h % 12, 0, 12, 0, TWO_PI) + map(m, 0, 60, 0, PI/6) - HALF_PI;
    // make the minute hand move a little bit extra every couple of seconds
    // (s/12)*(PI/150) --> s/12 will output {0,1,2,3,4}, dividing the minute into 5 sections
    //                 --> PI/150 = (2PI / 60 min / 5 movements)
    a[1] = map(m, 0, 60, 0, TWO_PI) + (s/12)*(PI/150) - HALF_PI;
    a[2] = map(s, 0, 60, 0, TWO_PI) - HALF_PI;

    return a; 
  }

  /**
   * Create a 2-Dimentional array to hold the coordinates
   * for the clock hands.  Each row in the matrix is a hand,
   * while each column is a coordinate, in the same format
   * as the parameters for @code{Line()}, shown below.
   *   {{x1, y1, x2, y2},  // hours
   *    {x1, y1, x2, y2},  // minutes
   *    {x1, y1, x2, y2}}  // seconds
   *
   * @param a 1-Dimensional array containing 3 values for the
   *          angle (in radians) of the hour, minute, and second
   *          hands, respectively.
   * @return array containing xy coordinates of the hands
   */
  private float[][] getXY(float[] a) {
    float[][] xy = new float[3][4];
    xy[0][0] = CENTER_X;
    xy[0][1] = CENTER_Y;
    xy[0][2] = CENTER_X + cos(a[0])*R_HOUR;
    xy[0][3] = CENTER_Y + sin(a[0])*R_HOUR;

    xy[1][0] = CENTER_X;
    xy[1][1] = CENTER_Y;
    xy[1][2] = CENTER_X + cos(a[1])*R_MIN;
    xy[1][3] = CENTER_Y + sin(a[1])*R_MIN;

    xy[2][0] = CENTER_X + cos(a[2])*R_SEC_MINOR;
    xy[2][1] = CENTER_Y + sin(a[2])*R_SEC_MINOR;
    xy[2][2] = CENTER_X + cos(a[2])*R_SEC_MAJOR;
    xy[2][3] = CENTER_Y + sin(a[2])*R_SEC_MAJOR;

    return xy;
  }
}

BinaryClock.java

import processing.core.*;

/**
 * Class containing functions to draw binaray clocks.
 * Capable of drawing traditional binary, 12-hour binary
 * coded decimal, and 24-hour binary coded decimal.
 *
 * @author Eric Roch
 * @date 12-13-15
 */
public class BinaryClock extends PApplet {
  private final int BOX_SIZE = 40;
  private final int GAP = 10;

  private final int BIN_X = 315;
  private final int BIN_1_Y = 70;
  private final int BIN_2_Y = 150;
  private final int BIN_3_Y = 230;

  private final int BCD_1_X = 15;
  private final int BCD_2_X = 65;
  private final int BCD_3_X = 125;
  private final int BCD_4_X = 175;
  private final int BCD_5_X = 235;
  private final int BCD_6_X = 285;
  private final int BCD_Y = 260;

  private PApplet parent;

  BinaryClock(PApplet p) {
    parent = p;
  }

  /**
   * Render one of the clocks to the canvas.
   *
   * @param h integer hours in 12- or 24-hour format
   * @param m integer minutes
   * @param s integer seconds
   * @param mode select which clock to display:
   *             0-Binary, 1-BCD 12 hour, 2-BCD 24 hour
   */
  void update(int hour, int minute, int second, int mode) {
    if (!Time.isValid(hour, minute, second)) {
      print(hour + ":" + minute + ":" + second);
      println(" is not a valid time. Aborting.");
      return;
    }
    if (mode < 0 || mode > 2) {
      println(mode + " is not a valid mode. Aborting");
      return;
    }

    parent.background(50);
    parent.noStroke();

    if (mode == 0) {    // Straight binary mode
      drawLights(hour, 5, BIN_X, BIN_1_Y, 'x');
      drawLights(minute, 6, BIN_X, BIN_2_Y, 'x');
      drawLights(second, 6, BIN_X, BIN_3_Y, 'x');
    }
    else {
      if (mode == 1) {    // BCD, 12-hour mode
        hour %= 12;
        if (hour == 0) {
          drawLights(1, 2, BCD_1_X, BCD_Y, 'y');
          drawLights(2, 4, BCD_2_X, BCD_Y, 'y');
        } else {
          drawLights(hour / 10, 2, BCD_1_X, BCD_Y, 'y');
          drawLights(hour % 10, 4, BCD_2_X, BCD_Y, 'y');
        }
      }
      else if (mode == 2) {    // BCD, 24-hour mode
        drawLights(hour / 10, 2, BCD_1_X, BCD_Y, 'y');
        drawLights(hour % 10, 4, BCD_2_X, BCD_Y, 'y');
      }
      // Mode 1 & 2 both do this part
      drawLights(minute / 10, 3, BCD_3_X, BCD_Y, 'y');
      drawLights(minute % 10, 4, BCD_4_X, BCD_Y, 'y');
      drawLights(second / 10, 3, BCD_5_X, BCD_Y, 'y');
      drawLights(second % 10, 4, BCD_6_X, BCD_Y, 'y');
    }
  }

  /**
   * Handles the repetitive action of drawing the lights
   * either on or off.  Determines state by extracting each
   * bit from passed integer.  Boxes can be drawn vertically
   * or horizontally, starting with the LSB at the bottom/right.
   *
   * @param var integer variable to display
   * @param n number of bits to extract/boxes to draw
   * @param x coordinate of the bottom/right box
   * @param y coordinate of the bottom/right box
   * @param axis whether to draw the boxes horizontally ('x')
   *             or vertically ('y')
   */
  private void drawLights(int var, int n, int x, int y, char axis) {
    if (axis == 'x') x -= BOX_SIZE;  // convert right edge to left corner
    if (axis == 'y') y -= BOX_SIZE;  // convert bottom edge to top corner

    for (int i = 0; i < n; i ++) {
      int bit = (var >> i) & 0x1;
      int c = (bit == 1) ? color(50, 50, 255) : color(155);
      parent.fill(c);

      parent.rect(x, y, BOX_SIZE, BOX_SIZE);
      if (axis == 'x') x -= (BOX_SIZE + GAP);
      if (axis == 'y') y -= (BOX_SIZE + GAP);
    }
  }
}

DigitalClock.java

import processing.core.*;

/**
 * Class containing functions to draw a digital clock.
 * Uses PShape objects to create seven segment displays.
 * 
 * @author Eric Roch
 * @date 12-13-15
 */
class DigitalClock extends PApplet {
  private final int HOUR_1_X = 20;    // hour tens
  private final int HOUR_2_X = 110;   // hour units
  private final int MIN_1_X = 224;    // minute tens
  private final int MIN_2_X = 314;    // minute units
  private final int DIGITAL_Y = 100;  // y-value for digital segments 

  private PApplet parent;

  private PShape hSegment = new PShape(PShape.GEOMETRY);
  private PShape vSegment = new PShape(PShape.GEOMETRY);
  private final int[] digits = {
    unbinary("1111110"), // 0
    unbinary("0110000"), // 1
    unbinary("1101101"), // 2
    unbinary("1111001"), // 3
    unbinary("0110011"), // 4
    unbinary("1011011"), // 5
    unbinary("1011111"), // 6
    unbinary("1110000"), // 7
    unbinary("1111111"), // 8
    unbinary("1111011"), // 9
    unbinary("0000000")  // off
  };

  /**
   * Constructor. Initializes PShape objects
   * for drawing seven segment displays.
   */
  DigitalClock(PApplet p) {
    parent = p;
    createShapes();
  }

  /**
   * Render the clock to the canvas.
   *
   * @param h integer hours in 24-hour format
   * @param m integer minutes
   * @param s integer seconds
   * @param mode select whether to display 12- or 24-hour time
   */
  void update(int hour, int minute, int second, int mode) {
    if (!Time.isValid(hour, minute, second)) {
      print(hour + ":" + minute + ":" + second);
      println(" is not a valid time. Aborting.");
      return;
    }
    if (mode != 0 && mode != 1) {
      println(mode + " is not a valid mode. Aborting.");
      return;
    }
    parent.background(50);
    parent.noStroke();

    // 24 hour mode, draw time as reported
    if (mode == 0) {
      drawDigit(hour / 10, HOUR_1_X, DIGITAL_Y);
      drawDigit(hour % 10, HOUR_2_X, DIGITAL_Y);
      drawAM(false);
      drawPM(false);
    }
    // 12 hour mode, display
    else {
      // show AM/PM indicator
      if (hour < 12) {
        drawAM(true);
        drawPM(false);
      } else {
        drawAM(false);
        drawPM(true);
      }

      hour = hour % 12;
      if (hour == 0) {
        drawDigit(1, HOUR_1_X, DIGITAL_Y);
        drawDigit(2, HOUR_2_X, DIGITAL_Y);
      } else {
        // prevent leading zero in hours
        if (hour < 10) {
          drawDigit(10, HOUR_1_X, DIGITAL_Y);
        } else {
          drawDigit(1, HOUR_1_X, DIGITAL_Y);
        }
        drawDigit(hour % 10, HOUR_2_X, DIGITAL_Y);
      }
    }
    drawDigit(minute / 10, MIN_1_X, DIGITAL_Y);
    drawDigit(minute % 10, MIN_2_X, DIGITAL_Y);

    // seconds are not displayed on the clock,
    // but we can use them to flash the separator dots
    if (second % 2 == 0) {
      parent.fill(240, 0, 0);
    } else {
      parent.fill(70);
    }
    parent.ellipse(200, 145, 10, 10);
    parent.ellipse(200, 175, 10, 10);
  }

  /**
   * Initialize segment shapes
   */
  private void createShapes() {
    hSegment.beginShape();
    hSegment.fill(color(0));
    hSegment.vertex(0, 5);
    hSegment.vertex(5, 0);
    hSegment.vertex(45, 0);
    hSegment.vertex(50, 5);
    hSegment.vertex(45, 10);
    hSegment.vertex(5, 10);
    hSegment.endShape(CLOSE);

    vSegment.beginShape();
    vSegment.fill(color(0));
    vSegment.vertex(5, 0);
    vSegment.vertex(10, 5);
    vSegment.vertex(10, 45);
    vSegment.vertex(5, 50);
    vSegment.vertex(0, 45);
    vSegment.vertex(0, 5);
    vSegment.endShape(CLOSE);
  }

  /**
   * Draw the AM indicator.
   *
   * @param enabled whether to draw the indicator "on"=true,
   *                or "off"=false
   */
  private void drawAM(boolean enabled) {
    if (enabled) {
      parent.fill(255, 0, 0);
    } else {
      parent.fill(70);
    }
    parent.rect(40, 230, 10, 10);
    parent.text("AM", 55, 240);
  }

  /**
   * Draw the PM indicator.
   *
   * @param enabled whether to draw the indicator "on"=true,
   *                or "off"=false
   */
  private void drawPM(boolean enabled) {
    if (enabled) {
      parent.fill(255, 0, 0);
    } else {
      parent.fill(70);
    }
    parent.rect(40, 245, 10, 10);
    parent.text("PM", 55, 255);  
  }

  /**
   * Draw a single digit seven-segment display to the screen.
   * Uses an array of integers as a lookup for the binary values
   * for each number. Digits are 67px wide by 123px tall.
   * Segments are illuminated red when on and grey when off.
   *
   * @param val number to be displayed, must be 0-9 or 10 for all segments off
   * @param x x-coordinate of top left corner
   * @param y y-coordinate of top left corner
   */
  private void drawDigit(int val, int x, int y) {
    val = digits[val];    // get the binary value that represents which segments are on

    hSegment.setFill(color(255,0,0));
    vSegment.setFill(color(255,0,0));
    if (((val >> 6) & 0x1) == 1) parent.shape(hSegment, x + 8, y);         // seg a
    if (((val >> 5) & 0x1) == 1) parent.shape(vSegment, x + 56, y + 8);    // seg b
    if (((val >> 4) & 0x1) == 1) parent.shape(vSegment, x + 56, y + 64);   // seg c
    if (((val >> 3) & 0x1) == 1) parent.shape(hSegment, x + 8, y + 112);   // seg d
    if (((val >> 2) & 0x1) == 1) parent.shape(vSegment, x, y + 64);        // seg e
    if (((val >> 1) & 0x1) == 1) parent.shape(vSegment, x, y + 8);         // seg f
    if ((val & 0x1) == 1)        parent.shape(hSegment, x + 8, y + 56);    // seg g

    hSegment.setFill(color(70));
    vSegment.setFill(color(70));
    if (((val >> 6) & 0x1) == 0) parent.shape(hSegment, x + 8, y);        // seg a
    if (((val >> 5) & 0x1) == 0) parent.shape(vSegment, x + 56, y + 8);   // seg b
    if (((val >> 4) & 0x1) == 0) parent.shape(vSegment, x + 56, y + 64);  // seg c
    if (((val >> 3) & 0x1) == 0) parent.shape(hSegment, x + 8, y + 112);  // seg d
    if (((val >> 2) & 0x1) == 0) parent.shape(vSegment, x, y + 64);       // seg e
    if (((val >> 1) & 0x1) == 0) parent.shape(vSegment, x, y + 8);        // seg f
    if ((val & 0x1) == 0)        parent.shape(hSegment, x + 8, y + 56);   // seg g
  }
}

Time.java

/**
 * Static helper class to hold validation and conversion
 * methods relating to time.
 *
 * @author Eric Roch
 * @date 12-13-15
 */
final class Time {
  /**
   * Private constructor to prevent client code from
   * creating an object of this static class.
   */
  private Time() {
  }

  /**
   * Validate time. If hours, minutes, or seconds
   * are outside of the normal range for time, the
   * validation fails.
   *
   * @param h hours in 12- or 24-hour format
   * @param m minutes
   * @param s seconds
   * @return boolean indicating if the time is in
   *                 the standard range
   */
  static boolean isValid(int h, int m, int s) {
    if (h < 0 || h > 23) {
      return false;
    }
    else if (m < 0 || m > 59) {
      return false;
    }
    else if (s < 0 || s > 59) {
      return false;
    }
    return true;
  }

  /**
   * Take time in 24-hour format and return time in 12-hour
   * format with AM/PM indicator.
   *
   * @param h hours (0-23)
   * @param m minutes
   * @param s seconds
   * @return integer array containing hours, minutes, seconds,
   *         and AM=0/PM=1
   */
  static int[] convertTo12(int h, int m, int s) {
    int[] time12 = new int[4];
    int hour = h % 12;
    if (hour != 0) {
      time12[0] = hour;
    } else {
      time12[0] = 12;
    }

    time12[1] = m;
    time12[2] = s;
    time12[3] = (h >= 12) ? 1 : 0;

    return time12;
  }

  /**
   * Take time in 12-hour format with AM/PM indicator and return
   * time in 24-hour format.
   *
   * @param h hours (1-12)
   * @param m minutes
   * @param s seconds
   * @param isPM integer acting as boolean. AM=0, PM=1
   * @return integer array containing hours, minutes, seconds
   */
  static int[] convertTo24(int h, int m, int s, boolean isPM) {
    if (isPM) {
      h += 12;
      if (h == 24) {
        h = 0;
      }
    }
    else if (h == 12) {
      h = 0;    // 12:00 AM = 0000 hours
    }  

    return new int[]{h, m, s};
  }
}
\$\endgroup\$
1
\$\begingroup\$

Looks good to me. There's a little bit of copy-n-paste but nothing serious. Good commenting. Thanks for sharing!

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.