Skip to content

Using Lua scripts (Part 12): Clock and circular things

Brenden Matthews edited this page May 1, 2024 · 5 revisions

xii: Clock and circular things

Part 1

In this section I'm going to go through the code to get an analogue-style clock in Conky.

And as always to start with you need to think about what you want it to look like ...

Let's keep it simple and have a thin white circle on the outside, marks for the positions of the hours around the face just on the inside of the circle. Marks will be short white lines. 3 hands, hours and minutes in white, keeping with convention in terms of hand sizes. Second hand in red, also keeping with convention in the order in which you see the hands.

Let's thing about some of the specifics.

We want to be able to configure clock total diameter and set the line width for the outline. We want to be able to configure the shape and size of the hands, set length for all hands. We will be using lines to draw the hands, so we want to be able to set the end cap type see here: Cairo Manual Line Caps. We want to be able to configure the size of the hour marks and their caps.

We can draw a circle easily enough. We will start that in the main function, and we can start to separate our settings from our drawing code.

function conky_main
	-- Main function setup lines.
	-- CLOCK SETTINGS
	clock_radius = 60
	clock_centerx = 100
	clock_centery = 100
	-- Set border options.
	clock_border_width = 2
	-- Set color and alpha for clock border.
	cbr, cbg, cbb, cba = 1, 1, 1, 1 -- Full opaque white.

	-- DRAWING CODE
	cairo_set_source_rgba (cr, cbr, cbg, cbb, cba)
	cairo_set_line_width (cr, clock_border_width)
	cairo_arc (clock_centerx, clock_centery, clock_radius, 0, 2 * math, pi)
	cairo_stroke (cr)
	-- Main function close out lines.
end

OOPS, try the above code and you get this error (usually best to killall conky before you go hunting the error down):

conky: llua_do_call: function conky_main execution failed: /home/benjamin/lua/codetest.lua:24: attempt to perform arithmetic on global 'math' (a table value)

look at line 24 which in my script is this cairo_arc (clock_centerx, clock_centery, clock_radius, 0, 2 * math, pi).

And the error tells me it has something to do with the "math" part. I put in a comma math**,** pi when it should have been a period math**.**pi.

Ok ... fixed, fire it up again! ...

conky: llua_do_call: function conky_main execution failed: /home/benjamin/lua/codetest.lua:24: error in function 'cairo_arc'.
     argument #1 is 'number'; '_cairo' expected.

Doh! KFC (killall conky and restart). Look at line 24 again ... cairo_arc (clock_centerx, clock_centery, clock_radius, 0, 2 * math.pi).

Yes, as the error said I had not put in the "cr" as the first entry in the curved brackets. It should be like this cairo_arc (cr, clock_centerx, clock_centery, clock_radius, 0, 2 * math.pi).

Fixed ... fire it up! Success! I have a white circle.

The amended code ...

function conky_main
	-- Main function setup lines.
	-- CLOCK SETTINGS
	clock_radius = 60
	clock_centerx = 100
	clock_centery = 100
	-- Set border options.
	clock_border_width = 2
	-- Set color and alpha for clock border.
	cbr, cbg, cbb, cba = 1, 1, 1, 1 -- Full opaque white.

	-- DRAWING CODE
	cairo_set_source_rgba (cr, cbr, cbg, cbb, cba)
	cairo_set_line_width (cr, clock_border_width)
	cairo_arc (cr, clock_centerx, clock_centery, clock_radius, 0, 2 * math.pi)
	cairo_stroke (cr)
	-- Main function close out lines.
end

NEXT, let's get our hour marks in there. I think its easiest to get the static things done first.

We want our marks to look like the kind of marks you would see on a real analogue clock, as if they were radiating out from the middle of the circle to the edges (like this clock template).

THIS is how we are going to do it. I use this bit of code A LOT. What it does is allow you to specify a center point and a radius and then calculate for you the coordinates of any point, defined by degrees, around that circle.

I like to use the the code like this:

point = (math.pi / 180) * degree
x = 0 + radius * (math.sin (point))
y = 0 - radius * (math.cos (point))

In which case you then have to add on the x and y values it calculates to your existing center coordinates.

We are going to use the line drawing command for our marks so we need a start point and an end point and to get those points we need to think about a few things ...

We have an outer radius for our clock of 60. How close to the outside circle do we want out marks to come? How long do we want our marks to be in total? How thick do we want our marks to be? How do we want the ends of our marks to look?

We need to add to our settings ...

-- Gap from clock border to hour marks.
b_to_m = 5

-- Set mark length.
m_length = 10

-- Set mark line width.
m_width = 3

-- Set mark line cap type.
m_cap = CAIRO_LINE_CAP_ROUND

-- Set mark color and alpha, red blue green alpha.
mr, mg, mb, ma = 1, 1, 1, 1 -- Opaque white.

Then lets think about how to draw these marks.

Since we want our marks to end 5 pixels before the border we can imagine a circle that encompasses the outer points of all our marks.

In our drawing code section we can start doing some calculations

-- Calculate mark outer radius.
m_end_rad = clock_radius - b_to_m

m_end_rad = 60 - 5 = 55 and that is the radius of where all our marks will end.

To calculate the radius of the circle from which the lines will start we can do this:

-- Calculate mark start radius.
m_start_rad = m_end_rad - m_length

m_start_rad = 55 - 10 = 45

SO lets just think about the 12'o clock mark.

It will start 45 pixels straight up from the center point of our circle, be drawn vertically for 10 pixels and end at a point 55 pixels straight up from the clock center.

It would be pretty straightforward to just punch in these numbers to cairo_move_to and cairo_line_to but try punching in the numbers for the 1 or 2 o'clock mark and things get a little more tricky :).

We are going to use our point-finding code to do the hard math for us!

Again, just for the 12 o'clock mark, which occurs at 360 or 0 degrees, whichever you want to use.

-- Calculate end and start radius for marks.
m_end_rad = clock_radius - b_to_m
m_start_rad = m_end_rad - m_length

-- Calculate start point for 12 oclock mark.
radius = m_start_rad
point = (math.pi / 180) * 0
x = 0 + radius * (math.sin (point))
y = 0 - radius * (math.cos (point))

-- Set start point for line.
cairo_move_to (cr, clock_centerx + x, clock_centery + y)

-- Calculate end point for 12 oclock mark.
radius = m_end_rad
point = (math.pi / 180) * 0
x = 0 + radius * (math.sin (point))
y = 0 - radius * (math.cos (point))

-- Set path for line.
cairo_line_to (cr, clock_centerx + x, clock_centery + y)

-- Set line cap type.
cairo_set_line_cap  (cr, m_cap)

-- Set line width.
cairo_set_line_width (cr, m_width)

-- Set color and alpha for marks.
cairo_set_source_rgba (cr, mr, mg, mb, ma)

-- Draw the line.
cairo_stroke (cr)

Seems like a lot of code to draw a little line :) and we still need 11 more marks!

We could just copy and paste the code 11 more times, editing the degrees values to get each mark ...

BUT whenever you have a situation where you're thinking about repeating code a number of times think FOR LOOP.

We need 12 marks, and that tells us that we will start our for loop like this: for i = 1, 12 do.

I'm a stickler for making my code as efficient as I can, so before I go ahead and stick everything inside my for loop, I'm going to look through the code and see what I can put outside of the loop ...

In this case I can put all the setup lines (width, cap, color) before the loop begins. No point in calling each one of those commands 12 times. Also we only need to calculate our end and start radius once.

The next thing is to think about how the changing value of "i" within the loop will be used. In this case it will be used in a calculation to get the degrees for each mark.

We have these lines dealing with degrees: point = (math.pi / 180) * 0.

We know that there are 360 degrees in a circle. Since we want 12 equally-spaced marks, each mark will be 360 / 12 = 30 -- 30 degrees around the circle from the previous mark.

Inside our for loop we will apply "i" like this: point = (math.pi / 180) * ((i - 1) * 30).

So when i = 1, degrees will be 0 30 = 0, our 12 oclock. When i = 2, degrees will be 1 30 = 30, our 1 oclock. When i = 3, degrees will be 2 * 30 = 60 our 2 oclock mark. And so on ...

Remember that we need to apply this code in 2 positions, one to set degrees to get the start points, one to set degrees to set the end points.

SO:

-- Stuff that can be moved outside of the loop, needs only be set once.
-- Calculate end and start radius for marks.
m_end_rad = clock_radius - b_to_m
m_start_rad = m_end_rad - m_length

-- Set line cap type.
cairo_set_line_cap  (cr, m_cap)

-- Set line width.
cairo_set_line_width (cr, m_width)

-- Set color and alpha for marks.
cairo_set_source_rgba (cr, mr, mg, mb, ma)

-- Start for loop.
for i = 1, 12 do

	-- Drawing code uisng the value of i to calculate degrees.
	-- Calculate start point for 12 oclock mark.
	radius = m_start_rad
	point = (math.pi / 180) * ((i - 1) * 30)
	x = 0 + radius * (math.sin (point))
	y = 0 - radius * (math.cos (point))

	-- Set start point for line.
	cairo_move_to (cr, clock_centerx + x, clock_centery + y)

	-- Calculate end point for 12 oclock mark.
	radius = m_end_rad
	point = (math.pi / 180) * ((i - 1) * 30)
	x = 0 + radius * (math.sin (point))
	y = 0 - radius * (math.cos (point))

	-- Set path for line.
	cairo_line_to (cr, clock_centerx + x, clock_centery + y)

	-- Draw the line.
	cairo_stroke (cr)
end

And there we have our marks ... Now you can play around with the settings till you get them the way you want them.

I'll continue in part 2 with how to get the hands on the clock!

Part 2

We have to get our hands on the clock and get them moving but the first thing we need to know is how to get the time in Lua ...

We could use Conky parse to get the time via the Conky commands for time, but there is a more direct way I like to reference this page when working with time commands: PHP time manual.

seconds = tonumber (os.date ("%S"))
minutes = tonumber (os.date ("%M"))
hours = tonumber (os.date ("%I")) -- I is 12 hour clock, H is 24 hour.

The seconds hand. This is the most direct hand to get going, BUT it will also be the hand at the "front", ie it will be drawn in front of the minute and hour hands. So in terms of position in the code we will have to have our seconds hand drawn lowest down in the script.

-- Seconds hand setup. 
-- Set length of seconds hand.
sh_length = 50

-- Set hand width.
sh_width = 1

-- Set hand line cap.
sh_cap = CAIRO_LINE_CAP_ROUND

-- Set seconds hand color.
shr, shg, shb, sha = 1, 0, 0, 1 -- Fully opaque red.

The math ... 60 seconds to go all the way around, 360 degrees in a circle, so every second the hand will move 6 degrees.

We need to convert seconds to degrees because we are going to use the same circle point calculating code as before. We will draw the line of the seconds hand from the center point of the circle to the point corresponding to seconds which will be at a radius equal to the length we set the hand to be.

In our drawing section we can do the math:

-- Draw seconds hand.
seconds = tonumber (os.date ("%S")) 	-- I'm using tonumber to make sure that the
								     	--		output is read as a number.
sec_degs = seconds * 6

-- Set radius we will use to calculate hand points.
radius = sh_length

-- Set our starting line coordinates, the center of the circle.
cairo_move_to (cr, clock_centerx, clock_centery)

-- Calculate coordinates for end of seconds hand.
point = (math.pi / 180) * sec_degs
x = 0 + radius * (math.sin (point))
y = 0 - radius * (math.cos (point))

-- Describe the line we will draw.
cairo_line_to (cr, clock_centerx + x, clock_centery + y)

-- Set up line attributes.
cairo_set_line_width (cr, sh_width)
cairo_set_source_rgba (cr, shr, shg, shb, sha)
cairo_set_line_cap  (cr, sh_cap)
cairo_stroke (cr)

Drawing the other hands is essentially the same:

-- Minutes hand setup.
-- Set length of minutes hand.
mh_length = 50

-- Set hand width.
mh_width = 1

-- Set hand line cap.
mh_cap = CAIRO_LINE_CAP_ROUND

-- Set minute hand color.
mhr, mhg, mhb, mha = 1, 1, 1, 1 -- Fully opaque white.

There is a little bit of additional math involved, however.

We want the minute hand to move every second, so it moves smoothly around the clock instead of jumping from one minute to the next.

So instead of dividing the 360 degrees into 60 increments of 6 degrees, will will be dividing 360 by (60 * 60).

360 / 3600 = 0.1, so our minute hand will move 0.1 degrees every second.

We need to convert our minutes to seconds and then add on the current seconds to achieve our smooth minute hand movement.

minutes = tonumber (os.date ("%M"))

-- Convert minutes to seconds.
m_to_s = minutes * 60

-- Get current seconds.
seconds = tonumber (os.date ("%S"))

-- Add them together.
msecs = m_to_s + seconds

-- Calculate degrees for the hand each second.
msec_degs = msecs * 0.1

-- Set radius we will use to calculate hand points.
radius = mh_length

-- Set our starting line coordinates, the center of the circle.
cairo_move_to (cr, clock_centerx, clock_centery)

-- Calculate coordinates for end of minutes hand.
point = (math.pi / 180) * msec_degs
x = 0 + radius * (math.sin (point))
y = 0 - radius * (math.cos (point))

-- Describe the line we will draw.
cairo_line_to (cr, clock_centerx + x, clock_centery + y)

-- Set up line attributes and draw line.
cairo_set_line_width (cr, mh_width)
cairo_set_source_rgba (cr, mhr, mhg, mhb, mha)
cairo_set_line_cap  (cr, mh_cap)
cairo_stroke (cr)

REMEMBER, this chunk of drawing code must come BEFORE the code used to draw the seconds hand if you want the minutes hand to be drawn BEHIND the seconds hand.

Things further down in the script are drawn on top of things higher up.

So now we have 2 hands, and again you can go up to your settings and adjust till it looks how you want it.

FINALLY, the hours hand!

settings

-- Hour hand setup.
-- Set length of hour hand.
hh_length = 30

-- Set hand width.
hh_width = 5

-- Set hand line cap.
hh_cap = CAIRO_LINE_CAP_ROUND

-- Set hour hand color.
hhr, hhg, hhb, hha = 1, 1, 1, 1 -- Fully opaque white.

We want our hour hand to transition nice and smoothly, moving every second.

Math: 60 60 12 = number of seconds in 12 hours = 43 200.

360 / 43200 = 0.00833333333, and this is how many degrees our hour hand will move every second!

We will have to add onto that the value of minutes as seconds and the current seconds to get our smooth motion.

hours = tonumber (os.date ("%I"))-- 12 hour clock.

-- Convert hours to seconds.
h_to_s = hours * 60 * 60
minutes = tonumber (os.date ("%M"))

-- Convert minutes to seconds.
m_to_s = minutes * 60

-- Get current seconds.
seconds = tonumber (os.date ("%S"))

-- Add them all together.
hsecs = h_to_s + m_to_s + seconds

-- Calculate degrees for the hand each second.
hsec_degs = hsecs * (360 / 43200) 	-- I'm using an equation instead of typing the
									-- 		calculation straight in because the result
									-- 		of 360 / 43200 gave us decimal places.

-- set radius we will use to calculate hand points
radius = hh_length

-- Set our starting line coordinates, the center of the circle.
cairo_move_to (cr, clock_centerx, clock_centery)

-- Calculate coordinates for end of minutes hand.
point = (math.pi / 180) * hsec_degs
x = 0 + radius * (math.sin (point))
y = 0 - radius * (math.cos (point))

-- Describe the line we will draw.
cairo_line_to (cr, clock_centerx + x, clock_centery + y)

-- set up line attributes and draw line
cairo_set_line_width (cr, hh_width)
cairo_set_source_rgba (cr, hhr, hhg, hhb, hha)
cairo_set_line_cap  (cr, hh_cap)
cairo_stroke (cr)

Placement of this code will determine the order in which the hands are drawn, just as with the minutes hand.

Here is the code all together in a functional script:

--[[drawing a clock
in conky.conf

lua_load /path/clock.lua
lua_draw_hook_pre main
TEXT


]]

require("cairo")
require("cairo_xlib")

function conky_main()
	if conky_window == nil then
		return
	end

	local cs = cairo_xlib_surface_create(
		conky_window.display,
		conky_window.drawable,
		conky_window.visual,
		conky_window.width,
		conky_window.height
	)

	cr = cairo_create(cs)

	-- CLOCK SETTINGS
	clock_radius = 60
	clock_centerx = 100
	clock_centery = 100

	-- Set border options.
	clock_border_width = 2

	-- Set color and alpha for clock border.
	cbr, cbg, cbb, cba = 1, 1, 1, 1 -- Full opaque white.

	-- Gap from clock border to hour marks.
	b_to_m = 5

	-- Set mark length.
	m_length = 10

	-- Set mark line width.
	m_width = 3

	-- Set mark line cap type.
	m_cap = CAIRO_LINE_CAP_ROUND

	-- Set mark color and alpha, red blue green alpha.
	mr, mg, mb, ma = 1, 1, 1, 1 -- Opaque white.

	-- Seconds hand setup.
	-- Set length of seconds hand.
	sh_length = 50

	-- Set hand width.
	sh_width = 1

	-- Set hand line cap.
	sh_cap = CAIRO_LINE_CAP_ROUND

	-- Set seconds hand color.
	shr, shg, shb, sha = 1, 0, 0, 1 -- Fully opaque red.

	-- Minues hand setup.
	-- Set length of minutes hand.
	mh_length = 50

	-- Set hand width.
	mh_width = 3

	-- Set hand line cap.
	mh_cap = CAIRO_LINE_CAP_ROUND

	-- Set minute hand color.
	mhr, mhg, mhb, mha = 1, 1, 1, 1 -- Fully opaque white.

	-- Hour hand setup.
	-- Set length of hour hand.
	hh_length = 30

	-- Set hand width.
	hh_width = 5

	-- Set hand line cap.
	hh_cap = CAIRO_LINE_CAP_ROUND

	-- Set hour hand color.
	hhr, hhg, hhb, hha = 1, 1, 1, 1 -- Fully opaque white.

	-- DRAWING CODE
	-- Draw border.
	cairo_set_source_rgba(cr, cbr, cbg, cbb, cba)
	cairo_set_line_width(cr, clock_border_width)
	cairo_arc(cr, clock_centerx, clock_centery, clock_radius, 0, 2 * math.pi)
	cairo_stroke(cr)

	-- Draw marks.
	-- Stuff that can be moved outside of the loop, needs only be set once.
	-- Calculate end and start radius for marks.
	m_end_rad = clock_radius - b_to_m
	m_start_rad = m_end_rad - m_length

	-- Set line cap type.
	cairo_set_line_cap(cr, m_cap)

	-- Set line width.
	cairo_set_line_width(cr, m_width)

	-- Set color and alpha for marks.
	cairo_set_source_rgba(cr, mr, mg, mb, ma)

	-- Start for loop.
	for i = 1, 12 do
		-- Drawing code uisng the value of i to calculate degrees.
		-- Calculate start point for 12 oclock mark.
		radius = m_start_rad
		point = (math.pi / 180) * ((i - 1) * 30)
		x = 0 + radius * (math.sin(point))
		y = 0 - radius * (math.cos(point))

		-- Set start point for line.
		cairo_move_to(cr, clock_centerx + x, clock_centery + y)

		-- Calculate end point for 12 oclock mark.
		radius = m_end_rad
		point = (math.pi / 180) * ((i - 1) * 30)
		x = 0 + radius * (math.sin(point))
		y = 0 - radius * (math.cos(point))

		-- Set path for line.
		cairo_line_to(cr, clock_centerx + x, clock_centery + y)

		-- Draw the line.
		cairo_stroke(cr)
	end -- of for loop.

	-- time calculations
	hours = tonumber(os.date("%I")) -- 12 hour clock.

	-- Convert hours to seconds.
	h_to_s = hours * 60 * 60
	minutes = tonumber(os.date("%M"))

	-- Convert minutes to seconds.
	m_to_s = minutes * 60

	-- Get current seconds.
	seconds = tonumber(os.date("%S"))

	-- draw hours hand
	-- get hours minutes seconds as just seconds
	hsecs = h_to_s + m_to_s + seconds

	-- Calculate degrees for the hand each second.

	-- I'm using an equation instead of typing the calculation straight in
	-- 		because the result of 360 / 43200 gave us decimal places.
	hsec_degs = hsecs * (360 / (60 * 60 * 12))

	-- Set radius we will use to calculate hand points.
	radius = hh_length

	-- Set our starting line coordinates, the center of the circle.
	cairo_move_to(cr, clock_centerx, clock_centery)

	-- Calculate coordinates for end of minutes hand.
	point = (math.pi / 180) * hsec_degs
	x = 0 + radius * (math.sin(point))
	y = 0 - radius * (math.cos(point))

	-- Describe the line we will draw.
	cairo_line_to(cr, clock_centerx + x, clock_centery + y)

	-- Set up line attributes and draw line.
	cairo_set_line_width(cr, hh_width)
	cairo_set_source_rgba(cr, hhr, hhg, hhb, hha)
	cairo_set_line_cap(cr, hh_cap)
	cairo_stroke(cr)

	-- Draw minutes hand.
	-- Get minutes and seconds just as seconds.
	msecs = m_to_s + seconds

	-- Calculate degrees for the hand each second.
	msec_degs = msecs * 0.1

	-- Set radius we will use to calculate hand points.
	radius = mh_length

	-- Set our starting line coordinates, the center of the circle.
	cairo_move_to(cr, clock_centerx, clock_centery)

	-- Calculate coordinates for end of minutes hand.
	point = (math.pi / 180) * msec_degs
	x = 0 + radius * (math.sin(point))
	y = 0 - radius * (math.cos(point))

	-- Describe the line we will draw.
	cairo_line_to(cr, clock_centerx + x, clock_centery + y)

	-- Set up line attributes and draw line.
	cairo_set_line_width(cr, mh_width)
	cairo_set_source_rgba(cr, mhr, mhg, mhb, mha)
	cairo_set_line_cap(cr, mh_cap)
	cairo_stroke(cr)

	-- Draw seconds hand
	-- Calculate degrees for the hand each second.
	sec_degs = seconds * 6

	-- Set radius we will use to calculate hand points.
	radius = sh_length

	-- Set our starting line coordinates, the center of the circle.
	cairo_move_to(cr, clock_centerx, clock_centery)

	-- Calculate coordinates for end of seconds hand.
	point = (math.pi / 180) * sec_degs
	x = 0 + radius * (math.sin(point))
	y = 0 - radius * (math.cos(point))

	-- Describe the line we will draw.
	cairo_line_to(cr, clock_centerx + x, clock_centery + y)

	-- Set up line attributes.
	cairo_set_line_width(cr, sh_width)
	cairo_set_source_rgba(cr, shr, shg, shb, sha)
	cairo_set_line_cap(cr, sh_cap)
	cairo_stroke(cr)

	Cairo_destroy(cr)
	cairo_surface_destroy(cs)
	cr = nil
end

A couple of things to note.

I'm not using the if updates > 5 check, as I'm not anticipating using conky_parse ("${cpu}") in this script. So when you run the script the clock will pop up immediately as Conky starts.

Also, I've changed the code around a little bit. Instead of having multiple lines getting the data for minutes and seconds, I pulled those lines from the tops of each hand drawing code and put them together in one place ahead of the hand drawing code.

-- time calculations
hours = tonumber (os.date ("%I")) -- 12 hour clock.

-- Convert hours to seconds.
h_to_s = hours * 60 * 60
minutes = tonumber (os.date ("%M"))

-- Convert minutes to seconds.
m_to_s = minutes * 60

-- Get current seconds.
seconds = tonumber (os.date ("%S"))
Clone this wiki locally