PiBox: PiClock themes using Cairo


The original clock was primitive, mostly because I didn't understand how to use images and transformations correctly, even though I'd used them in the pibox launcher!

Having just completed work on a wifi scanner for PiBox Network Config, I decided to expand on the lessons learned with Cairo to add themes to PiClock.  The original clock was a simple drawing of the clock face boundary with three hands.  The biggest task there was to compute the end points of the lines drawn for each hand.  Nothing complex there.

But there is no reason custom clock face images and hands can't be used.  The trick is learning to work with Cairo layers (which happens without extra coding) and discovering how to properly rotate and position the hands.  Turns out none of this is hard, but it was hard finding out how to use the Cairo API.

So the first thing to do for a theme is define what I wanted to display.  There are five images you define for PiClock themes:  the clock face, the hour, minute and second hands and an overlay image.  The overlay allows you to add special effects like a glass cover.  The theme needs a configuration file to specify these images.  I chose to use a JSON format.  Previously, PiBox apps used XML for configuration files but in the last year I've become a big fan of JSON due to its easy-to-read name/value pair format.  Also, I've found a very simple to use and lightweight JSON library called Parson.  I've integrated this into libpibox so all apps can use it. 

The JSON format for PiClock themes is extremely simple.

{
     "theme": {





         "center":{
             "hour": { "x":n, "y":m },
             "min": { "x":n, "y":m },
             "sec": { "x":n, "y":m }
         },
     }
}

The clock face has a dimension limit of 512×512 which the code enforces.  After experimentation on the PiBox hardware I found it was best to make faces at that size and not smaller, but I don't enforce that.  The center object defines the rotation point within each of the hands images as it cannot be assumed the hand rotates around its real center.  Other than that, the theme is self explanatory.

Before I could parse the file I needed it to have a home.  I use the -T option when testing PiBox apps which points the code at files within the source tree.  In production the data files are stored under /etc/piclock/themes.  Since there can be more than one theme there needs to be a top level file stating the currently selected theme. 

Parsing the file requires a single line of Parson code, plus extracting a reference to access fields.

JSON_Value data = json_parse_file_with_comments(buf);
obj = json_value_get_object(data);

The data object is abstract.  Fields can be extracted rather easily using the dot functions:

face = json_object_dotget_string(obj, "theme.face");
hour = json_object_dotget_string(obj, "theme.hour");
min = json_object_dotget_string(obj, "theme.min");
sec = json_object_dotget_string(obj, "theme.sec");
overlay = json_object_dotget_string(obj, "theme.overlay");

This extracts the filenames, which must be prefixed with the installation directory.  Following this is an in depth evaluation of the configuration to make sure files exist, can be read into Cairo surfaces and have reasonable sizes with centers properly defined.

A clock theme is drawn by first drawing a black background (for any transparent regions in the images).  The clock face is drawn over this.  The surfaces for the face and hands are generated when the theme is read so that they can be reused every time the clock is updated.  The hands are drawn over the face and the overlay over the hands.  Those are the layers.  As long as they are drawn in this order the layers are composited correctly.

Now for the important trick:  translation and rotation.  What was difficult to discover the use of patterns and matrices for the drawing operation.  The pattern is effectively a copy of the original image surface.  The pattern is first translated so its defined center is at the origin (0,0) at which point the rotation is applied.  Then the pattern is moved to the center of the clock window.  Obviously the face and overlay don't need rotations, just the hands.  Here is the generic handler for hands.

void draw_hand(GtkWidget *widget, cairo_t *cr, gint hand)
{
    cairo_status_t  rc;
    cairo_surface_t *image = NULL;
    cairo_pattern_t *pattern;
    gint            width, height;
    gdouble         offset_x, offset_y;
    GtkPiclock      *piclock;
    GtkRequisition  req;
    cairo_matrix_t  matrix;

    piclock = GTK_PICLOCK(widget);
    req.width = gdk_window_get_width(widget->window);
    req.height = gdk_window_get_height(widget->window);

    /*
     * Setup based on the hand requested.
     */
    switch( hand )
    {
        case HAND_HOUR:
            image = piclock->theme->hour_sf;
            degrees = -1*(double)piclock->hoursHand/60.0*360.0;
            offset_x = (double)piclock->theme->hour_pt.x;
            offset_y = (double)piclock->theme->hour_pt.y;
            break;
        case HAND_MIN:
            image = piclock->theme->min_sf;
            degrees = -1*(double)piclock->minutesHand/60.0*360.0;
            offset_x = (double)piclock->theme->min_pt.x;
            offset_y = (double)piclock->theme->min_pt.y;
            break;
        case HAND_SEC:
            image = piclock->theme->sec_sf;
            degrees = -1*(double)piclock->secondsHand/60.0*360.0;
            offset_x = (double)piclock->theme->sec_pt.x;
            offset_y = (double)piclock->theme->sec_pt.y;
            break;
    }
    if ( image == NULL )
        return;

    width = cairo_image_surface_get_width(image);
    height = cairo_image_surface_get_height(image);
    pattern = cairo_pattern_create_for_surface (image);

    /* translate the rotation point of the hand to the origin. */
    cairo_matrix_init_identity (&matrix);
    cairo_matrix_translate (&matrix, (double)offset_x, (double)offset_y);

    /* Rotate to the correct clock position. */
    cairo_matrix_rotate (&matrix, deg2rad(degrees));

    /* Move to the center of the clock window */
    cairo_matrix_translate (&matrix,
            -req.width/2,
            -req.height/2);
    cairo_pattern_set_matrix (pattern, &matrix);

    cairo_set_source (cr, pattern);
    cairo_pattern_destroy (pattern);
    cairo_paint(cr);
}

The test theme has a reflective overlay appliedover the face and hands.

After getting the window dimensions and accessing the previously created image surface, a pattern is created and a matrix initialized.  Then the translation is done to move the configured center point to the origin.  A rotation is applied and the pattern is moved to the center of the window.  Once the pattern is set as a source for the main Cairo surface it can be destroyed and the main surface updated.

There are now two themes for PiClock.  The first is a test theme that includes the overlay.  This isn't as clean as I'd like – the overlay doesn't quite look like a reflective surface to me.  The overlay is made from a couple of white to transparent radial gradients made with GIMP.  Cairo composites transparency quite nicely, as can be seen with the clock hands. 

Once the test theme was working I had to make one more theme, to prove that the design worked in general.  So I created the roman theme, which is simply an old clock face with roman numerals.  The hands were run through gimp a few times to clean them up and remove colored areas around the hands that were an artifact of the poor original image. 

The roman theme doesn't use an overlay, but the overlay image is required because I didn't go to the length of allowing it to be optional. The the image is simply the size of the clock face image but is completely transparent.

The theme design worked perfectly on the second theme.  I created it to spec and updated the default theme name, then ran PiClock.  It displayed as shown.  No code changes were required.   I've got some videos of the clocks running, if anyone is interested in watching a clock ticking.

It's not perfected quite yet, however.  I need to add an option to cycle through themes.  But its the holidays and I've already spent far too much time on PiBox so it's time to let it go a bit and get back to the things that matter.  At least for awhile.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.