A few weeks ago, I shared a simple digital clock, a bash script that made a figlet-like display that animated in the terminal.
Then @freebird54 reacted with a emoji and I got an idea…
I quickly dismissed this idea as too complicated, and decided not to bother with it. But two nights ago I had a dream in which I figured out how to do it, so yesterday and last night, I did it.
So, I present to you bashclock2
, an animated scalable analogue clock, completely written in bash:
#!/usr/bin/env bash
# An analogue version of bashclock
function cleanup_() {
printf '\033[?25h' # unhide cursor
stty echo 2>/dev/null
}
trap cleanup_ EXIT
REFRESH_INTERVAL=0.2 # display the clock every 0.2 seconds, adjust if necessary
CLOCK_RADIUS=23 # default size of the clock, adjust if necessary
# parsing the only option
# bashclock N, where N is an integer, for a clock of a different size.
# minumum radius is 7, maximum radius is 37, adjust if necessary
if (( $# >= 1 )) && (( $1 >= 7 && $1 <= 37 )); then
CLOCK_RADIUS=$1
fi
# Trigonometric & other mathematical functions -------------------------------
# Table of sines, first quadrant, 1° increment
declare -r sin_table=(
0 17452406 34899497 52335956 69756474 87155743 104528463
121869343 139173101 156434465 173648178 190808995 207911691 224951054
241921896 258819045 275637356 292371705 309016994 325568154 342020143
358367950 374606593 390731128 406736643 422618262 438371147 453990500
469471563 484809620 500000000 515038075 529919264 544639035 559192903
573576436 587785252 601815023 615661475 629320391 642787610 656059029
669130606 681998360 694658370 707106781 719339800 731353702 743144825
754709580 766044443 777145961 788010754 798635510 809016994 819152044
829037573 838670568 848048096 857167301 866025404 874619707 882947593
891006524 898794046 906307787 913545458 920504853 927183855 933580426
939692621 945518576 951056516 956304756 961261696 965925826 970295726
974370065 978147601 981627183 984807753 987688341 990268069 992546152
994521895 996194698 997564050 998629535 999390827 999847695 1000000000
)
function cosE9() { # returns 1000000000*cos($1), $1=0..360°
if (($1 >= 0 && $1 <= 90)); then printf ${sin_table[90-$1]}
elif (($1 > 90 && $1 <= 180)); then printf -- -${sin_table[$1-90]}
elif (($1 > 180 && $1 <= 270)); then printf -- -${sin_table[270-$1]}
elif (($1 > 270 && $1 <= 360)); then printf ${sin_table[$1-270]}
fi
}
function sinE9() { # returns 1000000000*sin($1), $1=0..360°
if (($1 >= 0 && $1 <= 90)); then printf ${sin_table[$1]}
elif (($1 > 90 && $1 <= 180)); then printf ${sin_table[180-$1]}
elif (($1 > 180 && $1 <= 270)); then printf -- -${sin_table[$1-180]}
elif (($1 > 270 && $1 <= 360)); then printf -- -${sin_table[360-$1]}
fi
}
function abs() {
if (($1 >= 0)); then printf $1
else printf -- -$1
fi
}
function signum() {
if (($1 > 0)); then printf 1
elif (($1 == 0)); then printf 0
else printf -- -1
fi
}
#-----------------------------------------------------------------------------
W=$((2*CLOCK_RADIUS+1)) # clock width
H=$((CLOCK_RADIUS+1)) # clock height
declare RASTER_MATRIX # array of pixels
declare CLOCK_MATRIX # the clock face without hands
# runs once on startup, draws the clock and saves it as the CLOCK_MATRIX
function generate_CLOCK_MATRIX() {
# Midpoint circle rasterising algorithm
local E=-$CLOCK_RADIUS
local x=$CLOCK_RADIUS
local y=0
while ((y <= x)); do
CLOCK_MATRIX[$(((CLOCK_RADIUS+x)+(CLOCK_RADIUS+y)*W))]=1
CLOCK_MATRIX[$(((CLOCK_RADIUS+x)+(CLOCK_RADIUS-y)*W))]=1
CLOCK_MATRIX[$(((CLOCK_RADIUS-x)+(CLOCK_RADIUS+y)*W))]=1
CLOCK_MATRIX[$(((CLOCK_RADIUS-x)+(CLOCK_RADIUS-y)*W))]=1
CLOCK_MATRIX[$(((CLOCK_RADIUS+y)+(CLOCK_RADIUS+x)*W))]=1
CLOCK_MATRIX[$(((CLOCK_RADIUS+y)+(CLOCK_RADIUS-x)*W))]=1
CLOCK_MATRIX[$(((CLOCK_RADIUS-y)+(CLOCK_RADIUS+x)*W))]=1
CLOCK_MATRIX[$(((CLOCK_RADIUS-y)+(CLOCK_RADIUS-x)*W))]=1
E=$((E+2*y+1))
y=$((y+1))
if ((E >= 0)); then
E=$((E-(2*x-1)))
x=$((x-1))
fi
done
# hour ticks
for ((phi = 0; phi < 360; phi+=30)) {
x=$((CLOCK_RADIUS+((CLOCK_RADIUS*$(cosE9 $phi))/1050000000)))
y=$((CLOCK_RADIUS-((CLOCK_RADIUS*$(sinE9 $phi))/1050000000)))
CLOCK_MATRIX[x+y*W]=1
}
CLOCK_MATRIX[CLOCK_RADIUS+CLOCK_RADIUS*W]=1
}
function reset_RASTER_MATRIX() {
local l=$((W*(W+1)))
for ((i = 0; i < l; i++)); do
RASTER_MATRIX[i]=${CLOCK_MATRIX[i]}
done
}
function plot_line() {
# Bresenham's line rasterising algorithm
local x0=$1
local y0=$2
local x1=$3
local y1=$4
local dx=$((x1-x0))
local sx=$(signum $dx)
local dx=$(abs $dx)
local dy=$((y1-y0))
local sy=$(signum $dy)
local dy=-$(abs $dy)
local E=$((dx+dy))
while true; do
local x=$((CLOCK_RADIUS+x0))
local y=$((CLOCK_RADIUS-y0))
RASTER_MATRIX[x+y*W]=1
local e2=$((2*E))
if ((e2 >= dy)); then
((x0 == x1)) && return
E=$((E+dy))
x0=$((x0+sx))
fi
if ((e2 <= dx)); then
((y0 == y1)) && return
E=$((E+dx));
y0=$((y0+sy))
fi
done
}
#-----------------------------------------------------------------------------
function draw_HOURS_hand() {
local phi=$(((($HOURS*30+$MINUTES*30/60)+270)%360))
local x=$(((CLOCK_RADIUS*$(cosE9 $phi))/1700000000))
local y=-$(((CLOCK_RADIUS*$(sinE9 $phi))/1700000000))
plot_line 0 0 $x $y
}
function draw_MINUTES_hand() {
local phi=$(((($MINUTES*6+$SECONDS*6/60)+270)%360))
local x=$(((CLOCK_RADIUS*$(cosE9 $phi))/1200000000))
local y=-$(((CLOCK_RADIUS*$(sinE9 $phi))/1200000000))
plot_line 0 0 $x $y
}
function draw_SECONDS_hand() {
local phi=$(((($SECONDS*6)+270)%360))
local x=$(((CLOCK_RADIUS*$(cosE9 $phi))/1060000000))
local y=-$(((CLOCK_RADIUS*$(sinE9 $phi))/1060000000))
plot_line 0 0 $x $y
}
# converts the pixel array into characters █, ▀, and ▄, prints to stdout
function print_RASTER_MATRIX() {
for ((r = 0; r < H; r++)); do
printf ' ' # indentation
for ((c = 0; c < W; c++)); do
if ((RASTER_MATRIX[c+2*r*W] == 1 && RASTER_MATRIX[c+2*r*W+W] == 1))
then printf '█'
elif ((RASTER_MATRIX[c+2*r*W] == 1 && RASTER_MATRIX[c+2*r*W+W] == 0))
then printf '▀'
elif ((RASTER_MATRIX[c+2*r*W] == 0 && RASTER_MATRIX[c+2*r*W+W] == 1))
then printf '▄'
else
printf ' '
fi
done
printf '\n'
done
}
#-----------------------------------------------------------------------------
generate_CLOCK_MATRIX
stty -echo 2>/dev/null
printf '\033[?25l\n' # hide cursor
unset EXIT_
while true; do
HOURS=$((10#$(printf "%(%H)T" $((EPOCHSECONDS)))))
MINUTES=$((10#$(printf "%(%M)T" $((EPOCHSECONDS)))))
SECONDS=$((10#$(printf "%(%S)T" $((EPOCHSECONDS)))))
reset_RASTER_MATRIX
draw_HOURS_hand
draw_MINUTES_hand
draw_SECONDS_hand
print_RASTER_MATRIX
[[ -z $EXIT_ ]] || exit 0
read -t $REFRESH_INTERVAL -n 1 && EXIT_=1;
printf "\033[${H}A\r" # move cursor to beginning
done
You can change the size of the clock with a command line argument. The default radius is 20, but it can be anything between 7 and 37. The bigger clock looks nicer. The big sizes require a fast terminal emulator. It works very nice on Konsole (with the Hack font). It also looks great in the TTY.
This is a bit more complex than the previous script. It uses the same trick of writing the escape sequence \033[F
to move the cursor to the beginning of the last line, so as to overwrite what was written previously. However, this drawing is slightly more complex than just printing out the “figletised” digits. It involves rasterising vector graphics composed of two primitive types: a line and a circle. The algorithms for that are well-known, and used all the time: Midpoint Circle Algorithm and the Bresenham’s Line Algorithm. You can find a very nice description of both (which I used as a guide when writing this), here:
https://members.chello.at/~easyfilter/Bresenham.pdf
After figuring that out, drawing lines and circles using the characters █, ▀, and ▄ was quite easy, however the bigger problem with the script was performance. In order for the animation to be fairly smooth and not skip seconds, the clock has to be updated several times a second. The problem is that drawing the hands of the clock requires trigonometry, and bash has no support for floating point computations. Using integers, it is fairly straightforward to implement fixed point computation in bash, but computing sine and cosine functions in fixed point, at runtime, was out of the question (due to precision and time). The obvious way to use floating point computations in bash is to outsource it and do the calculation in another program like bc
or awk
, but running a separate process for every frame of the animation was out of the question. The solution, which works rather well, is to hardcode the trigonometric functions. BTW, this was done in Doom (1993), in order to maximise performance.
Oh, and just for @freebird54, I’m using read
instead of sleep
, so that one can exit with the press of the Any key (that, and the clock looks ugly when you exit in the middle of the frame).
Three additional tricks: using escape sequences to hide the cursor, turning off stty echo, and using trap
to cleanup the mess on exit.
PS. After spending hours working on this, when I close my eyes, all I see is clocks.
Edit: updated the script to get rid of calling the external date
process. Bash has builtin printf
which provides the facilities for outputting the time without calling an external process. This should improve performance significantly.