Clipping & masking in PyCairo
last modified July 17, 2023
In this part of the PyCairo tutorial we talk about clipping and masking operations.
Clipping
Clipping is restricting of drawing to a certain area.
This is done for efficiency reasons and to create interesting effects.
PyCairo has a clip
method to set the clipping.
#!/usr/bin/python ''' ZetCode PyCairo tutorial This program shows how to perform clipping in PyCairo. author: Jan Bodnar website: zetcode.com ''' from gi.repository import Gtk, GLib import cairo import math import random class Example(Gtk.Window): def __init__(self): super(Example, self).__init__() self.init_ui() self.load_image() self.init_vars() def init_ui(self): self.darea = Gtk.DrawingArea() self.darea.connect("draw", self.on_draw) self.add(self.darea) GLib.timeout_add(100, self.on_timer) self.set_title("Clipping") self.resize(300, 200) self.set_position(Gtk.WindowPosition.CENTER) self.connect("delete-event", Gtk.main_quit) self.show_all() def load_image(self): self.image = cairo.ImageSurface.create_from_png("beckov.png") def init_vars(self): self.pos_x = 128 self.pos_y = 128 self.radius = 40 self.delta = [3, 3] def on_timer(self): self.pos_x += self.delta[0] self.pos_y += self.delta[1] self.darea.queue_draw() return True def on_draw(self, wid, cr): w, h = self.get_size() if (self.pos_x < 0 + self.radius): self.delta[0] = random.randint(5, 9) elif (self.pos_x > w - self.radius): self.delta[0] = -random.randint(5, 9) if (self.pos_y < 0 + self.radius): self.delta[1] = random.randint(5, 9) elif (self.pos_y > h - self.radius): self.delta[1] = -random.randint(5, 9) cr.set_source_surface(self.image, 1, 1) cr.arc(self.pos_x, self.pos_y, self.radius, 0, 2*math.pi) cr.clip() cr.paint() def main(): app = Example() Gtk.main() if __name__ == "__main__": main()
In this example we clip an image. A circle is moving on the window area and showing a part of the underlying image. This is as if we looked through a hole.
def load_image(self): self.image = cairo.ImageSurface.create_from_png("beckov.png")
This is the underlying image. Each timer cycle we see a portion of this image.
if (self.pos_x < 0 + self.radius): self.delta[0] = random.randint(5, 9) elif (self.pos_x > w - self.radius): self.delta[0]= -random.randint(5, 9)
If the circle hits the left or the right side of the window, the direction of the circle movement changes randomly. Same applies for the top and bottom sides.
cr.arc(self.pos_x, self.pos_y, self.radius, 0, 2*math.pi)
This line adds a circular path to the Cairo context.
cr.clip()
The clip
sets a clipping region. The clipping region is the
current path used. The current path was created by the arc
method call.
cr.paint()
The paint
paints the current source everywhere within the
current clip region.
Masking
Before the source is applied to the surface, it is filtered first. The mask is used as a filter. The mask determines where the source is applied and where not. Opaque parts of the mask allow to copy the source. Transparent parts do not let to copy the source to the surface.
#!/usr/bin/python ''' ZetCode PyCairo tutorial This program demonstrates masking. author: Jan Bodnar website: zetcode.com ''' from gi.repository import Gtk import cairo class Example(Gtk.Window): def __init__(self): super(Example, self).__init__() self.init_ui() self.load_image() def init_ui(self): darea = Gtk.DrawingArea() darea.connect("draw", self.on_draw) self.add(darea) self.set_title("Masking") self.resize(310, 100) self.set_position(Gtk.WindowPosition.CENTER) self.connect("delete-event", Gtk.main_quit) self.show_all() def load_image(self): self.ims = cairo.ImageSurface.create_from_png("omen.png") def on_draw(self, wid, cr): cr.mask_surface(self.ims, 0, 0); cr.fill() def main(): app = Example() Gtk.main() if __name__ == "__main__": main()
In the example, the mask determines where to paint and where not to paint.
cr.mask_surface(self.ims, 0, 0); cr.fill()
We use an image as a mask, thus displaying it on the window.
Blind down effect
In this code example we blind down our image. This is similar to what we do with a roller-blind.
#!/usr/bin/python ''' ZetCode PyCairo tutorial This program creates a blind down effect using masking operation. author: Jan Bodnar website: zetcode.com ''' from gi.repository import Gtk, GLib import cairo import math class Example(Gtk.Window): def __init__(self): super(Example, self).__init__() self.init_ui() self.load_image() self.init_vars() def init_ui(self): self.darea = Gtk.DrawingArea() self.darea.connect("draw", self.on_draw) self.add(self.darea) GLib.timeout_add(35, self.on_timer) self.set_title("Blind down") self.resize(325, 250) self.set_position(Gtk.WindowPosition.CENTER) self.connect("delete-event", Gtk.main_quit) self.show_all() def load_image(self): self.image = cairo.ImageSurface.create_from_png("beckov.png") def init_vars(self): self.timer = True self.h = 0 self.iw = self.image.get_width() self.ih = self.image.get_height() self.ims = cairo.ImageSurface(cairo.FORMAT_ARGB32, self.iw, self.ih) def on_timer(self): if (not self.timer): return False self.darea.queue_draw() return True def on_draw(self, wid, cr): ic = cairo.Context(self.ims) ic.rectangle(0, 0, self.iw, self.h) ic.fill() self.h += 1 if (self.h == self.ih): self.timer = False cr.set_source_surface(self.image, 10, 10) cr.mask_surface(self.ims, 10, 10) def main(): app = Example() Gtk.main() if __name__ == "__main__": main()
The idea behind the blind down effect is quite simple. The image is h pixels high. We draw 0, 1, 2 ... lines of 1px height. Each cycle the portion of the image is 1px higher, until the whole image is visible.
def load_image(self): self.image = cairo.ImageSurface.create_from_png("beckov.png")
In the load_image
method, we create an image surface
from a PNG image.
def init_vars(self): self.timer = True self.h = 0 self.iw = self.image.get_width() self.ih = self.image.get_height() self.ims = cairo.ImageSurface(cairo.FORMAT_ARGB32, self.iw, self.ih)
In the init_vars() method, we initiate some variables. We initiate the self.timer and the self.h variables. We get the width and height of the loaded image. And we create an empty image surface. It is going to be filled with lines of pixels from the image surface that we have created earlier.
ic = cairo.Context(self.ims)
We create a cairo context from the empty image source.
ic.rectangle(0, 0, self.iw, self.h) ic.fill()
We draw a rectangle into the initially empty image. The rectangle will be 1px higher each cycle. The image created this way will serve as a mask later.
self.h += 1
The height of the image to show is increased by one unit.
if (self.h == self.ih): self.timer = False
We stop the timer method when we draw the whole image on the GTK window.
cr.set_source_surface(self.image, 10, 10) cr.mask_surface(self.ims, 10, 10)
The image of a castle is set as a source for painting. The
mask_surface
paints the current
source using the alpha channel of surface as a mask.
This chapter covered clipping and masking in PyCairo.