sábado, mayo 25, 2013

Nice cairo trick to draw transparent shapes with borders

I have learned a new cairo trick, and how I didn't find this explained in any place will share it here.

Imagine you want draw two figures with a border, is really easy:


The code is:


#!/usr/bin/python

from gi.repository import Gtk


class MinimalCairoTest(Gtk.Window):

    def __init__(self):
        super(MinimalCairoTest, self).__init__()
        self.set_size_request(300, 300)
        self.connect("destroy", Gtk.main_quit)
        darea = Gtk.DrawingArea()
        darea.connect("draw", self.__draw_cb)
        self.add(darea)
        self.show_all()

    def __draw_cb(self, widget, cr):

        cr.set_line_width(10)

        cr.set_source_rgb(1.0, 0.0, 0.0)
        cr.rectangle(140, 20, 120, 120)
        cr.fill_preserve()

        cr.set_source_rgb(0.0, 0.0, 1.0)
        cr.stroke()

        cr.set_source_rgb(1.0, 1.0, 0.0)
        cr.arc(150, 150, 70, 0, 2 * 3.14)
        cr.fill_preserve()
        cr.set_source_rgb(0.0, 1.0, 1.0)
        cr.set_line_width(10)
        cr.stroke()


MinimalCairoTest()
Gtk.main()

If we want draw the same figures using alpha, we can do:




#!/usr/bin/python

from gi.repository import Gtk


class MinimalCairoTest(Gtk.Window):

    def __init__(self):
        super(MinimalCairoTest, self).__init__()
        self.set_size_request(300, 300)
        self.connect("destroy", Gtk.main_quit)
        darea = Gtk.DrawingArea()
        darea.connect("draw", self.__draw_cb)
        self.add(darea)
        self.show_all()

    def __draw_cb(self, widget, cr):

        cr.set_line_width(10)

        # now the same but using alpha
        cr.set_source_rgba(1.0, 0.0, 0.0, 0.3)
        cr.rectangle(140, 20, 120, 120)
        cr.fill_preserve()

        cr.set_source_rgba(0.0, 0.0, 1.0, 0.3)
        cr.stroke()

        cr.set_source_rgba(1.0, 1.0, 0.0, 0.3)
        cr.arc(150, 150, 70, 0, 2 * 3.14)
        cr.fill_preserve()
        cr.set_source_rgba(0.0, 1.0, 1.0, 0.3)
        cr.set_line_width(10)
        cr.stroke()


MinimalCairoTest()
Gtk.main()





No very good. The problem is, the area filled and the border are superposed, because the path is defined by the middle of the stroke, and ignores the line width[1].



I tried use the stroke as a mask, to avoid filling the area defined by the width of the stroke [2]:


#!/usr/bin/python

from gi.repository import Gtk
import cairo


class MinimalCairoTest(Gtk.Window):

    def __init__(self):
        super(MinimalCairoTest, self).__init__()
        self.set_size_request(300, 300)
        self.connect("destroy", Gtk.main_quit)
        darea = Gtk.DrawingArea()
        darea.connect("draw", self.__draw_cb)
        self.add(darea)
        self.show_all()

    def __draw_cb(self, widget, cr):

        cr.set_line_width(10)

        cr.rectangle(140, 20, 120, 120)
        cr.set_source_rgba(1.0, 0.0, 0.0, 0.3)
        cr.fill_preserve()

        # use the border as a mask
        cr.set_operator(cairo.OPERATOR_SOURCE)
        cr.set_source_rgba(1.0, 1.0, 1.0, 1)
        cr.stroke_preserve()
        cr.set_operator(cairo.OPERATOR_OVER)

        cr.set_source_rgba(0.0, 0.0, 1.0, 0.3)
        cr.stroke()

        cr.arc(150, 150, 70, 0, 2 * 3.14)
        cr.set_source_rgba(1.0, 1.0, 0.0, 0.3)
        cr.fill_preserve()

        cr.set_operator(cairo.OPERATOR_SOURCE)
        cr.set_source_rgba(1.0, 1.0, 1.0, 1)
        cr.stroke_preserve()
        cr.set_operator(cairo.OPERATOR_OVER)

        cr.set_source_rgba(0.0, 1.0, 1.0, 0.3)
        cr.stroke()


MinimalCairoTest()
Gtk.main()





There are a problem: the border looks like if is not transparent. The problem really is the cairo operator source, with white, clear all what is in the surface.

I tried different alternatives, and finally asked in #cairo irc channel. The solution was provided by Søren Sandmann itself:


#!/usr/bin/python
 
from gi.repository import Gtk
import cairo
 
 
class MinimalCairoTest(Gtk.Window):
 
    def __init__(self):
        super(MinimalCairoTest, self).__init__()
        self.set_size_request(300, 300)
        self.connect("destroy", Gtk.main_quit)
        darea = Gtk.DrawingArea()
        darea.connect("draw", self.__draw_cb)
        self.add(darea)
        self.show_all()

    def __draw_cb(self, widget, cr):
 
        cr.set_line_width(10)

        cr.push_group()
        cr.rectangle(140, 20, 120, 120)
        cr.set_source_rgba(1.0, 0.0, 0.0, 1)
        cr.fill_preserve()
        cr.set_source_rgba(0.0, 0.0, 1.0, 1)
        cr.stroke()
        cr.pop_group_to_source()
        cr.paint_with_alpha(0.3)

        cr.push_group()
        cr.arc(150, 150, 70, 0, 2 * 3.14)
        cr.set_source_rgba(1.0, 1.0, 0.0, 1)
        cr.fill_preserve()
        cr.set_source_rgba(0.0, 1.0, 1.0, 1)
        cr.stroke()
        cr.pop_group_to_source()
        cr.paint_with_alpha(0.3)
 
MinimalCairoTest()
Gtk.main()


Excelent!
And the code is even cleaner. push_group creates a temporary surface, and can be painted, with alpha, using pop_group_to_source and paint_with_alpha.






[1] http://cairographics.org/tutorial/
[2] http://cairographics.org/operators/