Chrome Experiments are always a great source of inspiration for visual design. Yesterday, I ran into Helvetica Clock and I thougth that it would make an interesting comparison point with Tao3D, from a programmer’s perspective.
The original Helvetica Clock
The Helvetica Clock Chrome Experiment is an interesting way to show time using columns of digits:
The source code for this experiment is using Three.js, and is quite short as far as Chrome experiments are concerned: 603 lines of code, 26594 characters without comments.
So… what would it look like using Tao3D?
The end result is 63 lines and 1373 characters with comments and ample spacing. That’s already a rather impressive 9.5 times less lines and 19 times less characters.
Let’s see how this code is built. I did not try to replicate the original logic, only the result.
Setting the drawing up
The original experiment is drawn in white on a black background. It also rotates with the mouse. It uses the Arial font (or maybe Helvetica) with a relatively large size. Characters centers are aligned with one another horizontally and vertically. I will also need the Animate module for simple animations.
The following code sets up these conditions:
// Draw in white on a black background, rotate globally with the mouse
rotate_x -180 * mouse_y / window_height
rotate_y 180 * mouse_x / window_width
// Font and text alignment
font "Arial", 300
Setting up the grid
The characters are setup on a grid, based on the size of the characters. In Helvetica, all th digits have practically the same size, so we are going to use the size of the character 0.
We store the character width in the W variable, and the character height in the H variable. This computation needs only be done once in our example, so we bracket it in a once block.
// The size of the boxes used to show each digit
H := 1.1 * text_height "0"
W := 1.1 * text_width "0"
// The variables we use
H -> 15.0
W -> 15.0
Where the variable is defined does not really matter, so in the final programs, I’ve put them at the end of the file.
The primary draw loop
To draw the clock, we need to draw hours, minutes and seconds, each of them with two digits. We are going to use the notation function with argument-list, which invokes a function repeatedly with a number of arguments. Here, we call a function called two_digits in turn with hours, minutes and then seconds. We will se shortly how this function is defined.
// The primary draw loop
Index := 0
two_digits with hours, minutes, seconds
Before drawing characters, we move to the left 2.5 times the width of a character. The reason is that the total width will be 6 characters. So the left of the drawing area is at 3 times the width of a character, and the center of the leftmost character is one half the width on the right of that. That’s the translate_x -2.5*W
Highlighting the current time
One thing that is missing in the original is that the current time is a bit hard to read, because it’s lost in all the digits. That being said, it may be part of the interest of this clock, so if you prefer, you can very well remove the code and see the clock in its original all-white glory.
// Overlay to highlight the current time
blend_function "DST_COLOR", "ONE_MINUS_SRC_ALPHA"
color_hsv 2 * time, 0.3, 0.5, 0.7
rectangle 0, 0, 6.5*W, H
Drawing the columns
Each number (hours, minutes or seconds) is drawn in two columns. We use the function with argument-list notation again, but this time, we take the high and low digit of the two_digit function’s input (hours, minutes or seconds) and pass them as input to the digit_column function. N/10 is the first digit, N mod 10 (rest of the division modulo 10) is the second digit.
// Drawing two digits
two_digits N:integer -> digit_column with N/10, N mod 10
Drawing each column
The digit_column function draws a full column of digits.
// Drawing a single digit column
digit_column N:integer ->
adjust -N*H, currentY[Index]
digit with 0..9
// Move to the next one
Index := Index+1
It does so by adjusting the position of the column vertically, something that is implemented in the adjust function, and then iterating over all digits. We once again use function with argument-list, but this time, the argument list is a range, 0..9, which is the same thing as 0,1,2,3,4,5,6,7,8,9, but is shorter to write and may be implemented more efficiently.
We use locally around the code that adjusts the position vertically, to make sure that the vertical translation stays inside. In other words, the vertical translation is “undone” once you exit the locally block.
Once we are done with the drawing of the digits, we translate horizontally by an amount corresponding to the width of a character, using tranlsate_x. This ensures that the next column will be placed immediately to the right of the current one.
We also increment the Index variable. We will see in a moment how this Index variable is used to index the vertical position of the columns in the currentY array.
Drawing a single digit
To draw a single digit, we use a text_box. The center of the text box is 0, 0, but remember that we have translated horizontally (for the columns) and vertically (to bring the correct digit in the middle of the screen), so that 0, 0 is the center of the digit.
The width of the text box is 2*W and its height is 2*H. That’s, strictly speaking, more than absolutely necessary. But it does not matter since we centered the display in the text box using align 0.5 and align_vertically 0.5. Using a larger size is a safety in case you want to use a font where some digits are slightly larger than 0.
// Drawing a single digit in a text box
digit N:integer ->
Once we have drawn this digit, we move up by H using translate_y, so that the next digit will be drawn immediately above it.
We now get to the most interesting part of this short program, the animation for the vertical displacement of the columns. This happens in the following code from digit_column:
adjust -N*H, currentY[Index]
The -N*H is the target value for the vertical translation. When we want to display 7 in the middle of the screen, we translate 5*H down, and then we draw digits starting at 0, and move up by H with each digit, so that by the time we reach 7, we are back at the middle of the screen.
In order to smooth the displacement of the columns, however, we do not directly translate by the target amount, -N*H. Instead, we translate by a “current” Y value, stored in currentY[Index]. This is why we pass currentY[Index] to the adjust function when we call it from digit_column.
// Vertical adjustment - Common case
adjust TY:real, Y:real ->
interpolate 0.1, TY, Y
This code uses the interpolate function defined in the Animate module to interpolate between a target value, TY (target Y), and the current value, Y. Of interest, the interpolate function updates the value of Y. This works because Tao3D passes arguments by reference. In other words, the value Y that interpolate updates is also the value of currentY[Index] passed by digit_column.
However, an interesting case happens the first time we run throught the code. Initially, currentY[Index] is not initialized. Therefore, it is not a real number, and the argument currentY[Index] does not match the Y:real parameter definition.
In that case, we fall back to the other possible definition for adjust, which initializes currentY[Index], before calling the normal definition with the initialized value.
// Vertical adjustment - Initialization case
adjust TY:real, Other ->
currentY[Index] := TY
adjust TY, currentY[Index}
You can do a number of variations on the theme. For example, this code adds a small shader program to enhance the feeling of depth, and putting the digits on a ring instead of putting them in a column.
Here is what it looks like:
- The notation function with argument-list is a very handy shorcut for calling a function repeatedly, either with unrelated values such as hours,minutes,seconds, or with a sequence such as 0..9.
- Translations (or rotations) to move shapes or text around in 3D space.
- Storing and accessing indexed values with the array-like notation currentY[Index]. Note that you could index with integer values like here, but that you could also use discontinuous elements, text indexes or real numbers.
- Pattern matching and using type differences to deal with different kinds, like initialization for adjust.