Wednesday, September 10, 2014

Adding Blur in Cairo

I've made several widgets now that light up. I've worked around it mostly through gradients. Radial gradients are perfect for a circular LED. A linear gradient filling a carefully shaped path got me by for a square LED. But now I'm making an analog oscilloscope type widget, and a 7 segment LED and gradients aren't cutting it. If only you could do blur in Cairo.

Oh, but you can...


Cairo doesn't NATIVELY support blur, but you can totally blur in Cairo. In fact there is an "official" blur example in the cairo cookbook on their site. I'm no Cairo master, so it took me a bit to figure out how to really apply it to my svg2cairo converted code, dispite having several other examples. But if you've been following along on my ffffltk development you'll also find this post useful. If you haven't... then this is just another example, (though the only one I know of using the "official" blur function).

The goal of ffffltk is to get from inkscape drawing to GUI binary with as little coding as possible, and its not terrible. You do have to do some coding, but its much better than my last attempt was using Qt (more because I've learned a lot than any shortcoming of Qt). Blur unfortunately departs slightly from that goal, but its useful enough that it's worth it. Overall the process is to make your drawing with blur, remove the blur before you convert to cairo (else svg2cario generates unbuildable code), convert to cairo, add the blur in the code, proceed making your gui in ntk-fluid. We'll break down what "add the blur in the code" constitutes as we go.

To make the cairo code generated by svg2cairo work as an ffffltk widget you already have to get your hands dirty editing the code, and adding blur is just a little bit extra work. So lets play with the 16 segment LED display I'm currently working with. Here's the drawing with and without blur:



You see even a 3 pixel blur makes a nice difference to make it look like its glowing. Its also perfect for shadows. The code generated by the non-blurred version (blur doesn't work with svg2cairo) (and run through the script  I gave you in this previous post) looks roughly like this:
#ifndef DRAW_BLUE16SEG_H
#define DRAW_BLUE16SEG_H
#include"blur.h"
inline int cairo_code_draw_16seg_get_width() { return 30; }
inline int cairo_code_draw_16seg_get_height() { return 45; }
inline short char2seg(char c);
inline void cairo_code_draw_16seg_render(cairo_t *cr, char num, unsigned char color) {
unsigned short val = char2seg(num);

cairo_surface_t *temp_surface;
cairo_t *old_cr;
cairo_pattern_t *pattern;
cairo_matrix_t matrix;

cairo_set_operator(cr, CAIRO_OPERATOR_OVER);
pattern = cairo_pattern_create_rgba(0.4,0.4,0.4,1);
cairo_set_source(cr, pattern);
cairo_pattern_destroy(pattern);
cairo_new_path(cr);
cairo_move_to(cr, 3.753906, 0.761719);
cairo_line_to(cr, 26.492188, 0.761719);
cairo_curve_to(cr, 28.148438, 0.761719, 29.492188, 2.101562, 29.492188, 3.757812);
cairo_line_to(cr, 29.492188, 41.550781);
cairo_curve_to(cr, 29.492188, 43.207031, 28.148438, 44.550781, 26.492188, 44.550781);
cairo_line_to(cr, 3.753906, 44.550781);
cairo_curve_to(cr, 2.097656, 44.550781, 0.757812, 43.207031, 0.757812, 41.550781);
cairo_line_to(cr, 0.757812, 3.757812);
cairo_curve_to(cr, 0.757812, 2.101562, 2.097656, 0.761719, 3.753906, 0.761719); cairo_close_path(cr);
cairo_set_tolerance(cr, 0.1);
cairo_set_antialias(cr, CAIRO_ANTIALIAS_DEFAULT);
cairo_set_fill_rule(cr, CAIRO_FILL_RULE_WINDING);
cairo_fill_preserve(cr);
 /********************/
cairo_set_operator(cr, CAIRO_OPERATOR_OVER);
cairo_set_line_width(cr, 1.5);
cairo_set_miter_limit(cr, 4);
cairo_set_line_cap(cr, CAIRO_LINE_CAP_SQUARE);
cairo_set_line_join(cr, CAIRO_LINE_JOIN_MITER);
pattern = cairo_pattern_create_rgba(0,0,0,1);
cairo_set_source(cr, pattern);
cairo_pattern_destroy(pattern);
cairo_pattern_set_matrix(pattern, &matrix);
/********dot************/
cairo_set_operator(cr, CAIRO_OPERATOR_OVER);
pattern = cairo_pattern_create_rgba(0,0,.3333,1);
cairo_set_source(cr, pattern);
cairo_pattern_destroy(pattern);
cairo_new_path(cr);
cairo_move_to(cr, 27.773438, 39.507812);
cairo_curve_to(cr, 27.773438, 40.652344, 26.847656, 41.578125, 25.707031, 41.578125);
cairo_curve_to(cr, 24.5625, 41.578125, 23.636719, 40.652344, 23.636719, 39.507812);
cairo_curve_to(cr, 23.636719, 38.367188, 24.5625, 37.441406, 25.707031, 37.441406);
cairo_curve_to(cr, 26.847656, 37.441406, 27.773438, 38.367188, 27.773438, 39.507812);
cairo_close_path(cr);
cairo_set_tolerance(cr, 0.1);
cairo_set_antialias(cr, CAIRO_ANTIALIAS_DEFAULT);
cairo_set_fill_rule(cr, CAIRO_FILL_RULE_WINDING);
cairo_fill_preserve(cr);
 /********************/
cairo_set_operator(cr, CAIRO_OPERATOR_OVER);
pattern = cairo_pattern_create_rgba(0,0,.3333,1);
cairo_set_source(cr, pattern);
cairo_pattern_destroy(pattern);
cairo_new_path(cr);
cairo_move_to(cr, 18.671875, 3.734375);
cairo_line_to(cr, 18.234375, 6.359375);
cairo_line_to(cr, 19.601562, 8.484375);
cairo_line_to(cr, 23.050781, 8.484375);
cairo_line_to(cr, 26.503906, 4.953125);
cairo_line_to(cr, 25.71875, 3.734375);
cairo_close_path(cr);
cairo_set_tolerance(cr, 0.1);
cairo_set_antialias(cr, CAIRO_ANTIALIAS_DEFAULT);
cairo_set_fill_rule(cr, CAIRO_FILL_RULE_WINDING);
cairo_fill_preserve(cr);
 /********************/

...

I added the #include blur.h which is really the blur.c in the cookbook edited just  a bit. The full code has 16 dark segments, and 16 identically shaped light ones on top. To make this work as a widget I edited the render function to accept a character and either draw or don't draw light segments based on that character. The full implementation of course is available in my infamous repository. I have a function that encodes the character to a bit sequence where each bit corresponds to a segment (char2seg()). Therefore I use an if to decide if the segment should be drawn like so:
... if(val&0x4000){
cairo_set_operator(cr, CAIRO_OPERATOR_OVER);
pattern = cairo_pattern_create_rgba(.3,.6,1,1);
cairo_set_source(cr, pattern);
cairo_pattern_destroy(pattern);
cairo_new_path(cr);
cairo_move_to(cr, 18.671875, 3.734375);
cairo_line_to(cr, 18.234375, 6.359375);
cairo_line_to(cr, 19.601562, 8.484375);
cairo_line_to(cr, 23.050781, 8.484375);
cairo_line_to(cr, 26.503906, 4.953125);
cairo_line_to(cr, 25.71875, 3.734375);
cairo_close_path(cr);
cairo_set_tolerance(cr, 0.1);
cairo_set_antialias(cr, CAIRO_ANTIALIAS_DEFAULT);
cairo_set_fill_rule(cr, CAIRO_FILL_RULE_WINDING);
cairo_fill_preserve(cr);
}
 /********************/
if(val&0x8000){
cairo_set_operator(cr, CAIRO_OPERATOR_OVER);
pattern = cairo_pattern_create_rgba(.3,.6,1,1);
cairo_set_source(cr, pattern);
cairo_pattern_destroy(pattern);
cairo_new_path(cr);
cairo_move_to(cr, 10.835938, 3.734375);
cairo_line_to(cr, 9.613281, 4.984375);
cairo_line_to(cr, 11.871094, 8.484375);
cairo_line_to(cr, 15.34375, 8.484375);
cairo_line_to(cr, 17.421875, 6.359375);
cairo_line_to(cr, 17.859375, 3.734375);
cairo_close_path(cr);
cairo_set_tolerance(cr, 0.1);
cairo_set_antialias(cr, CAIRO_ANTIALIAS_DEFAULT);
cairo_set_fill_rule(cr, CAIRO_FILL_RULE_WINDING);
cairo_fill_preserve(cr);
}
 /********************/
...

Now we have a draw function that we can use with the ffffltk_display widget. But it's not blurred. We want the light segments to be blurred and the dark segments and the frame/background to remain solid. The function provided by the cairo cookbook blurs the whole surface that we're drawing to, so we need to create a new, separate surface, draw the things to be blurred on it, blur the new surface, then embed the temporary surface onto the target surface which is really our window. It looks a little like:
...
 /********************/
cairo_set_operator(cr, CAIRO_OPERATOR_OVER);
pattern = cairo_pattern_create_rgba(0,0,.3333,1);
cairo_set_source(cr, pattern);
cairo_pattern_destroy(pattern);
cairo_new_path(cr);
cairo_move_to(cr, 7.144531, 36.796875);
cairo_line_to(cr, 3.71875, 40.296875);
cairo_line_to(cr, 4.527344, 41.546875);
cairo_line_to(cr, 11.542969, 41.578125);
cairo_line_to(cr, 11.976562, 38.984375);
cairo_line_to(cr, 10.566406, 36.796875);
cairo_close_path(cr);
cairo_set_tolerance(cr, 0.1);
cairo_set_antialias(cr, CAIRO_ANTIALIAS_DEFAULT);
cairo_set_fill_rule(cr, CAIRO_FILL_RULE_WINDING);
cairo_fill_preserve(cr);
/********************/

//blur
temp_surface = 
   cairo_image_surface_create( CAIRO_FORMAT_ARGB32,
                               30,45);
old_cr = cr;
cr = cairo_create(temp_surface);

/********dot************/
if(num < 0){
cairo_set_operator(cr, CAIRO_OPERATOR_OVER);
pattern = cairo_pattern_create_rgba(.3,.6,1,1);
cairo_set_source(cr, pattern);
cairo_pattern_destroy(pattern);
cairo_new_path(cr);
cairo_move_to(cr, 27.773438, 39.507812);
cairo_curve_to(cr, 27.773438, 40.652344, 26.847656, 41.578125, 25.707031, 41.578125);
cairo_curve_to(cr, 24.5625, 41.578125, 23.636719, 40.652344, 23.636719, 39.507812);
cairo_curve_to(cr, 23.636719, 38.367188, 24.5625, 37.441406, 25.707031, 37.441406);
cairo_curve_to(cr, 26.847656, 37.441406, 27.773438, 38.367188, 27.773438, 39.507812);
cairo_close_path(cr);
cairo_set_tolerance(cr, 0.1);
cairo_set_antialias(cr, CAIRO_ANTIALIAS_DEFAULT);
cairo_set_fill_rule(cr, CAIRO_FILL_RULE_WINDING);
cairo_fill_preserve(cr);
}
 /********************/
...
 
So you see the last dark segment being drawn, the new surface is created (in temp surface), an associated cairo context is made and then all the light segments are drawn into it. Then as we get to the last light segment:
...
 /********************/
if(val&0x0002){
cairo_set_operator(cr, CAIRO_OPERATOR_OVER);
pattern = cairo_pattern_create_rgba(.3,.6,1,1);
cairo_set_source(cr, pattern);
cairo_pattern_destroy(pattern);
cairo_new_path(cr);
cairo_move_to(cr, 7.144531, 36.796875);
cairo_line_to(cr, 3.71875, 40.296875);
cairo_line_to(cr, 4.527344, 41.546875);
cairo_line_to(cr, 11.542969, 41.578125);
cairo_line_to(cr, 11.976562, 38.984375);
cairo_line_to(cr, 10.566406, 36.796875);
cairo_close_path(cr);
cairo_set_tolerance(cr, 0.1);
cairo_set_antialias(cr, CAIRO_ANTIALIAS_DEFAULT);
cairo_set_fill_rule(cr, CAIRO_FILL_RULE_WINDING);
cairo_fill_preserve(cr);
}
/********************/

blur_image_surface(temp_surface,3);
cairo_set_source_surface(old_cr,temp_surface,0,0);
cairo_paint(old_cr);
cairo_surface_destroy(temp_surface);
cairo_destroy(cr);
}


We blurred the new surface (with blur radius 3 pixels) and the "embeded" the surfaces together using cairo_set_source_surface, then painted the original source surface. After that its just a bit of cleanup. The blur_image_surface()function is where the magic happens and its provided in blur.c in the cairo cookbook. All I did was rename the file to .h and change the functions to be inline. I guess its not really an official cairo function since it doesn't start with cairo. A native implementation is on their todo list, but for now, here's a workaround.

That's all. Pretty simple really. Hopefully that helps someone out there. I can't make any guarantees about performance or anything, it seems like this is doing it fairly manually, so there might be a cheaper way to do it. All of ffffltk could probably use some optimization techniques. If you have tips or tutorials on how to make light cairo programs please comment!

No comments: