The Clock Face
Recently
on the Revolution
mailing list, there was a discussion on coding Transcript. Someone
complained that, "a simple CLOCK FACE with a SECOND HAND ticking
by....takes 100 lines of CODE to ACHIEVE....and DAYS of
work...." Several people took up the challenge, and the result was some
beautiful Transcript.
1. Geoff Canyon -- 22 lines of
code in 20 minutes
I spent about twenty minutes to create an example. It used polygon
graphics to draw the hands, and set the points of them to change the
time. Here is the code. Including everything but white lines, it's 22
lines of code. It took about twenty minutes to code.
on openCard
setTime
end openCard
on setTime
put the loc of group "clock" into tOrig
put min(the width of group "clock",the height of group "clock") div 2 into tLength
put word 1 of the long time into tTime
set the itemDelimiter to ":"
set the points of grc "hour" to getRect(tOrig,(tLength div 2),hoursToRadians(item 1 of tTime))
set the points of grc "minute" to getRect(tOrig,(tLength * .7),minutesToRadians(item 2 of tTime))
set the points of grc "second" to getRect(tOrig,(tLength * .8),minutesToRadians(item 3 of tTime))
send "setTime" to me in 55 ticks
end setTime
function minutesToRadians pTime
return (12 * ((pTime + 45) mod 60) * pi / 360)
end minutesToRadians
function hoursToRadians pTime
return minutesToRadians(5*pTime)
end hoursToRadians
function getRect pOrigin,pLength,pAngle
return pOrigin,round((item 1 of pOrigin) + cos(pAngle) * pLength),round((item 2 of pOrigin) + sin(pAngle) * pLength)
end getRect
2. Malte Brill -- 17 lines of
code in 3 minutes
Malte Brill, meanwhile, had also posted an example to the Use
Revolution list. I missed his example before posting my own. Malte's
example features a very clever use of oval graphics for the clock
hands. He sets their arcAngle to 0, which turns an oval into a single
line from the center of the graphic to the edge. He then sets their
startAngle to the angle he needs for the time.
Using oval graphics this way has two advantages. First, the startAngle
is specified in degrees, making the math simpler when converting from
hours, minutes, and seconds. My example, using polygon graphics, relied
on the trigonometric functions sin and cos, which take radians in their
arguments. It's a small thing, but it complicates the solution. Second,
again because I used polygon graphics, I had to be concerned with the
length of the graphics each time I set them. Malte's oval graphics
(circles, actually) have the length (radius) set in the design process.
All he has to do is set the angle, and the length is automatic.
Here is Malte's orginal code. It's 17 lines, and he says it took all of
three minutes. That sounds reasonable, as most of the time I spent
coding my example was spent trying to remember (and get right) the
conversion to/from radians, which Malte doesn't have
to worry about.
on mouseUp
if the flag of me is empty then set the flag of me to false
set the flag of me to not the flag of me
if the flag of me then startClock
end mouseUp
on startclock
put the long time into myTime
put myTime into fld 1
set the itemdel to ":"
put char 1 to 2 of item 3 of myTime into daSecs
set the startAngle of grc "seconds" to 90-dasecs*6
put item 2 of myTime into daMinutes
set the startangle of grc minutes to 90-daminutes*6
put item 1 of myTime into daHours
set the startAngle of grc "hours" to 90-dahours*30-daminutes*30/60
if the flag of me then send startClock to me in 500 milliseconds
end startclock
3. Better Second Hand Timing
-- 18 lines
There is a problem with both Malte's and my original solutions. Both
use send...in to repeatedly trigger the code and set the time. I used a
delay of 55 ticks, Monte used 500 milliseconds.
No matter what the value (apart from very small values that would be
wasteful of the CPU) this results in the second hand being jumpy. The
reason is simple.
Consider Malte's code. It sends the message in 500 milliseconds, or
half a second. Suppose the code gets executed at 12:01:15.99. It will
set the second hand to 15 seconds. Send...In is not guaranteed to
execute on time. If something else is going on it gets delayed. Suppose
the code executes again roughly 510 milliseconds later, at 12:01:16.5.
At that point the second hand will be set to 16 seconds, but it may
have been at 15 seconds for as much as 1.5 seconds. The code gets
executed again 500 milliseconds later, at 12:01:17, and the second hand
moves to 17 seconds, having only been at 16 seconds for about half a
second.
I switched to Monte's method of using oval graphics, which is obviously
better suited to this problem than the polygon graphics I used.
To solve the issue of the jumpy second hand, I added code to make the
send...in routine exit if it wasn't time to move the second hand, and
call itself more frequently until the time came.
I retained my method of dealing with the AM/PM at the end of the long
time. The long time returns a value in the form "6:05:40 PM." The PM
(or AM) part is unnecessary for this task. Both Monte and I set the
itemDelimiter to a ":" to get the components of the time. Monte gets
rid of the AM/PM by getting char 1 to 2 of item 3 of the long time. I
started off by getting word 1 of the long time, which automatically
gets only the characters up to but not including the first space.
In addition, since the values for the angle of the hour, minute, and
second hand are only used once, I changed this:
put item 2 of myTime into daMinutes
set the startangle of grc minutes to 90-daminutes*6
into this:
set the startangle of grc "minute" to 90 - (6 * item 2 of sTime) - (item 3 of sTime) / 10
It's a longer line of code, but it reduces two lines to one and
eliminates a variable, so it's a net win.
Here's the whole routine. Note how whenever it successfully updates the
clockface, it calls itself in 50 ticks, which is less than a second.
When that call hits, it's likely the seconds haven't changed. The
previous value for the seconds is stored in a script local and
compared. If it's the same, the routine exits and calls itself in just
5 ticks. This repeats until the seconds tick over and the clock face is
updated. This works to stop the jumpy second hand, but a much better
solution is possible.
on openCard
setTime
end openCard
local sTime
on setTime
put word 1 of the long time into tTime
if tTime is sTime then
send "setTime" to me in 5 ticks
exit setTime
end if
put tTime into sTime
put sTime && the long seconds into fld "time"
set the itemDelimiter to ":"
set the startangle of grc "hour" to 90 - (30 * item 1 of sTime) - (item 2 of sTime) / 2
set the startangle of grc "minute" to 90 - (6 * item 2 of sTime) - (item 3 of sTime) / 10
set the startangle of grc "second" to 90 - (6 * item 3 of sTime)
send "setTime" to me in 50 ticks
end setTime
4. Dennis Brown -- Send...In <the right amount of time> -- 12
lines
Dennis brown removed my kludgey repeated send, and replaced it by
calculating how long it would be to the next time the second hand needs
to be moved. This is a great modification. My code assumes
(incorrectly) that we don't know when the routine actually needs to run
until it is that time. Dennis calculates when it will be time, and uses
that as the argument to the send command.
on openCard
setTime
end openCard
on setTime
set the itemDelimiter to ":"
get word 1 of the long time --8:13:15
put it & char 2 to 5 of (the long seconds mod 1) into fld "Time"
set the startangle of grc "hour" to 90 - (30 * item 1 of it) - (item 2 of it) / 2
set the startangle of grc "minute" to 90 - (6 * item 2 of it) - (item 3 of it) / 10
set the startangle of grc "second" to 90 - (6 * item 3 of it)
send "setTime" to me in 60-(the long seconds mod 1)*60 ticks
end setTime
5. Sending in Fractions of a Second -- 12 lines
I had come to the same realization as Dennis about calculating when the
routine needs to run. My initial replacement was inferior to his. I
used two lines to avoid getting the long seconds twice:
put the long seconds into t
send "setTime" to me in (1 - trunc(t) + t)
Where Dennis used only one, taking advantage of the fact that the mod
command can be used to return the fractional portion of a number:
send "setTime" to me in 60-(the long seconds mod 1)*60 ticks
Dennis's one-line implementation is much better, but I saw there was a
minor improvement possible. Dennis goes to the trouble of converting to
ticks, which is the default unit for the send...in command. But seconds
are also perfectly acceptible, as are fractions of a second, which is
what we're after here. So I replaced his line with this:
send "setTime" to me in (1 - (the long seconds mod 1)) seconds
Now the routine looks like this. It's still 12 lines. It generally
calls itself within a thousandth of a second of the correct time, so
the second hand looks great. Dar Scott and I both thought that there
might be a need for a fudge factor on the send...in command, but it
turns out not to be the case. It works like a charm.
on openCard
setTime
end openCard
on setTime
put word 1 of the long time into T
put T && the long seconds into fld "time"
set the itemDelimiter to ":"
set the startangle of grc "hour" to 90 - (30 * item 1 of T) - (item 2 of T) / 2
set the startangle of grc "minute" to 90 - (6 * item 2 of T) - (item 3 of T) / 10
set the startangle of grc "second" to 90 - (6 * item 3 of T)
send "setTime" to me in (1 - (the long seconds mod 1)) seconds
end setTime
6. Avoiding Needless Work -- 14 lines
At this point Dennis Brown suggested that we might be at the end of the
process; that the routine was down to its essence. I disagreed,
suggesting that the routine still might need code to only move the hour
and minute hands when they need it, instead of setting their angle
every second. Dennis tested, and this turned out to be the case.
Needlessly setting the startAngle of the hour and minute hands is
wasteful of CPU time. So he wrote code to only move them when
necessary.
on openCard
setTime
end openCard
on setTime
set the itemDelimiter to ":"
put word 1 of the long time into T --8:13:15
put T & char 2 to 5 of (the long seconds mod 1) into fld "Time"
get 360+90-(30 * item 1 of T) - trunc((item 2 of T) / 2)
if (the angle of grc "Hour") <> it then set the angle of grc "Hour" to it
get 360+90-(6 * item 2 of T) - trunc((item 3 of T) / 10)
if (the angle of grc "Minute") <> it then set the angle of grc "Minute" to it
set the angle of grc "Second" to 360+90-(6 * item 3 of T)
send "setTime" to me in 1-(the long seconds mod 1) seconds
end setTime
7. Faster Still -- The Runner-Up -- 22 lines
I wrote in the same sort of checks, but used two script local variables
to store the values for comparison later to see if they had changed.
This is the result. It's not as simple as Dennis's example, but using
script locals is faster than comparing to the oval graphic's
startAngle. The overall difference is slight, though.
I also changed out the itemDelimiter code. All previous examples had
first set the itemDelimiter to ":" and then referenced item 1, item 2,
and item 3 of the time. Instead, I use the split command to turn the
time into an array, based on the ":" character. The end result is the
same, but it's always good to avoid setting the itemDelimiter in case
you need it later, and the array syntax is perhaps a bit cleaner than
the item syntax.
If speed is critical, this is likely the example to go with.
on openCard
setTime
end openCard
local sHourAngle -- the angle for the hour hand
local sMinuteAngle -- the angle for the minute hand
on setTime
put word 1 of the long time into T
put T && the long seconds into fld "time"
split T using ":"
get 90 - (30 * T[1]) - trunc(T[2] / 2)
if it is not sHourAngle then
put it into sHourAngle
set the startangle of grc "hour" to sHourAngle
end if
get 90 - (6 * T[2]) - trunc(T[3] / 10)
if it is not sMinuteAngle then
put it into sMinuteAngle
set the startangle of grc "minute" to sMinuteAngle
end if
set the startangle of grc "second" to 90 - (6 * T[3])
send "setTime" to me in (1 - (the long seconds mod 1)) seconds
end setTime
8. The Winner -- 14 lines
Unless speed is a critical concern, there is little need to add eight
lines to use script locals over simply checking the angle of the oval
graphic. As I wrote this, I noticed a bug in the
orginal solution: setting the angle to a value outside the range 0-359
won't return that same value when the angle is retrieved. Therefore
it's important when comparing
the calculated value to the angle of the graphic to include "mod 360"
in the calculation. Otherwise the location of the minute and hour hands
will be set every second for some portion of the hour/day. This code
includes the necessary mod command.
In addition, someone (don't be shy, step forward and claim credit)
pointed out that trunc(T[2] / 2) can be replaced by T[2] div 2, which
is both cleaner and faster.
Finally, I replaced 360 + 90 with 450.
This is about as beautiful as this code is likely to become. Every
statement has a clear purpose. The code runs very quickly and
efficiently. There are no needless lines of code to confuse the issue.
About the only thing this code needs is some comments to explain the
overall process and the math.
on openCard
setTime
end openCard
on setTime
put word 1 of the long time into T --8:13:15
put T & char 2 to 5 of (the long seconds mod 1) into fld "Time"
split T using ":"
get (450 - (30 * T[1]) - (T[2] div 2)) mod 360
if (the angle of grc "Hour") <> it then set the angle of grc "Hour" to it
get (450 - (6 * T[2]) - (T[3] div 10)) mod 360
if (the angle of grc "Minute") <> it then set the angle of grc "Minute" to it
set the angle of grc "Second" to 450 - (6 * T[3])
send "setTime" to me in 1 - (the long seconds mod 1) seconds
end setTime
If you have any improvements to offer, contact gcanyon@inspiredlogic.com
This article is one of the Beautiful Transcript series. If you have any
suggestions or comments feel free to email gcanyon@inspiredlogic.com