An Analog Clock in DragonRuby

I’ve been a fan of DragonRuby since its release. I’ve been programming for a while, but I’m not familiar with game programming. I’ve been creating small projects for myself to explore and learn. This time I decided to make a simple analog clock with labels for the ordinals and lines for the second, minute, and hour hand.

The entry point to a DragonRuby project is a tick method in a main.rb file. I decided to take a object-oriented this time with a Clock class. I’ve seen examples where there are .new calls the tick method, but I think it would be better to create one instance outside of tick and pass it arguments. Each tick I rendered all of the parts.

My basic structure was this:

structure
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Clock
  def tick(args)
    render_ordinals(args)
    render_second_hand(args)
    render_minute_hand(args)
    render_hour_hand(args)
  end

  def render_ordinals(args)
  end

  def render_second_hand(args)
  end

  def render_minute_hand(args)
  end

  def render_hour_hand(args)
  end
end

@clock = Clock.new

def tick(args)
  @clock.tick(args)
end

I decided to work on the second hand first because I thought it would be tricky, but, once I figured it out, the rest of the hands would be easy. I would need some constants for doing the math. I wanted to put the clock in the middle. It would have been easier if I changed the coordinate system to be in the middle (DragonRuby lets you do that), but I purposely wanted to learn the default coordinate system that goes 0-1280 in the x direction and 0-720 in the y direction. I knew that I would need to convert degrees to radians, so I put that constant in. Lastly, I wanted a radius to go off of. This was the result:

constants
1
2
3
4
5
6
7
class Clock
  MID_X = 1280 / 2
  MID_Y = 720 / 2
  RADIAN = Math::PI / 180
  RADIUS = 180
  ...

For my render_second_hand method I decided I would need two parameters: the args so that I could output a line and seconds which would be the current seconds. I would need to convert the seconds to degrees around the clock’s circle (seconds * 360/60), those degrees to radians (seconds * 360/60 * RADIAN), and trigonometric functions (sin for y and cos for x) to x and y coordinates. I would also need to multiply by a fraction of my radius to get the line length (RADIUS * 0.8). Lastly, the trigonometric functions naturally go counterclockwise (I would need to reverse that) and start in the wrong position for my clock (I would need to add an offset). This was my result:

render_second_hand
1
2
3
4
5
6
def render_second_hand(args, seconds)
  seconds_x =  RADIUS * 0.8 * Math.cos((seconds + 45) * 360 / 60 * RADIAN) + MID_X
  seconds_y = -RADIUS * 0.8 * Math.sin((seconds + 45) * 360 / 60 * RADIAN) + MID_Y
  args.outputs.lines << [MID_X, MID_Y, seconds_x, seconds_y]
end

With this worked out, the render_minute_hand method was nearly identical:

render_minute_hand
1
2
3
4
5
6
def render_minute_hand(args, minutes)
  minutes_x =  RADIUS * 0.6 * Math.cos((minutes + 45) * 360 / 60 * RADIAN) + MID_X
  minutes_y = -RADIUS * 0.6 * Math.sin((minutes + 45) * 360 / 60 * RADIAN) + MID_Y
  args.outputs.lines << [MID_X, MID_Y, minutes_x, minutes_y]
end

And the render_hour_hand:

render_hour_hand
1
2
3
4
5
6
def render_hour_hand(args, hours)
  hours_x =  RADIUS * 0.4 * Math.cos((hours + 45) * 360 / 12 * RADIAN) + MID_X
  hours_y = -RADIUS * 0.4 * Math.sin((hours + 45) * 360 / 12 * RADIAN) + MID_Y
  args.outputs.lines << [MID_X, MID_Y, hours_x, hours_y]
end

Lastly, I needed to render the numbers (ordinals) around the outside:

render_ordinals
1
2
3
4
5
6
7
8
def render_ordinals(args)
  (1..12).each do |ordinal|
    x =  RADIUS * Math.cos((ordinal * 30 - 90) * RADIAN) + MID_X
    y = -RADIUS * Math.sin((ordinal * 30 - 90) * RADIAN) + MID_Y
    args.outputs.labels << { x: x, y: y, text: ordinal, size: 5, alignment: 1 }
  end
end

With all of this, my code was shaping up nicely:

main.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class Clock
  MID_X = 1280 / 2
  MID_Y = 720 / 2
  RADIAN = Math::PI / 180
  RADIUS = 180

  def tick(args)
    time = Time.now
    render_ordinals(args)
    render_second_hand(args, time.sec)
    render_minute_hand(args, time.min)
    render_hour_hand(args, time.hour)
  end

  def render_ordinals(args)
    (1..12).each do |ordinal|
      x =  RADIUS * Math.cos((ordinal*30 - 90) * RADIAN) + MID_X
      y = -RADIUS * Math.sin((ordinal*30 - 90) * RADIAN) + MID_Y
      args.outputs.labels << { x: x, y: y, text: ordinal, size: 5, alignment: 1 }
    end
  end

  def render_second_hand(args, seconds)
    seconds_x = RADIUS * 0.8 * Math.cos((seconds + 45) * 360 / 60 * RADIAN) + MID_X
    seconds_y = -RADIUS * 0.8 * Math.sin((seconds + 45) * 360 / 60 * RADIAN) + MID_Y
    args.outputs.lines << [MID_X, MID_Y, seconds_x, seconds_y]
  end

  def render_minute_hand(args, minutes)
    minutes_x =  RADIUS * 0.6 * Math.cos((minutes + 45) * 360 / 60 * RADIAN) + MID_X
    minutes_y = -RADIUS * 0.6 * Math.sin((minutes + 45) * 360 / 60 * RADIAN) + MID_Y
    args.outputs.lines << [MID_X, MID_Y, minutes_x, minutes_y]
  end

  def render_hour_hand(args, hours)
    hours_x =  RADIUS * 0.4 * Math.cos((hours + 45) * 360 / 12 * RADIAN) + MID_X
    hours_y = -RADIUS * 0.4 * Math.sin((hours + 45) * 360 / 12 * RADIAN) + MID_Y
    args.outputs.lines << [MID_X, MID_Y, hours_x, hours_y]
  end
end

@clock = Clock.new

def tick(args)
  @clock.tick(args)
end

There were two last things that bothered me. First was that my ordinals didn’t line up quite right, but labels are tricky and I didn’t care to worry about each one. Secondly, the motion was jerky and abrupt. This was because I was using integers for my time values. I decided to use fractional time in my final result and it worked better:

fractional time
1
2
3
4
5
6
7
8
def tick(args)
  time = Time.now
  render_ordinals(args)
  render_second_hand(args, time.sec + time.usec / 1000000)
  render_minute_hand(args, time.min + time.sec / 60)
  render_hour_hand(args, time.hour + time.min / 60)
end

Thanks for reading!