Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Cairo GTK draw a line with transparency (like a highlighter pen)

I am trying to create a simple drawing application using Python, GTK3 and cairo. The tool should have different brushes and some kind of a highlighter pen. I figured I can use the alpha property of the stroke to create it. However, the connecting points are created overlapping and that creates a weird effect.

enter image description here

Here is the code responsible for this red brush and the highlighter mode:

def draw_brush(widget, x, y, odata, width=2.5, r=1, g=0, b=0, alpha=1):

    cr = cairo.Context(widget.surface)
    cr.set_source_rgba(r, g, b, alpha)
    cr.set_line_width(width)
    cr.set_line_cap(1)
    cr.set_line_join(0)   

    for stroke in odata:
        for i, point in enumerate(stroke):
            if len(stroke) == 1:
                radius = 2
                cr.arc(point['x'], point['y'], radius, 0, 2.0 * math.pi)
                cr.fill()
                cr.stroke()
            elif i != 0:
                cr.move_to(stroke[i - 1]['x'], stroke[i - 1]['y'])
                cr.line_to(point['x'], point['y'])                
                cr.stroke() 

    cr.save()

The code that draws on mouse click:

def motion_notify_event_cb(self, widget, event):

    point = {'x': event.x, 'y': event.y, 'time': time.time()}

    if self.odata:
        self.odata[-1].append(point)

    if widget.surface is None:
        return False

    if event.state & Gdk.EventMask.BUTTON_PRESS_MASK:
        if self.buttons['current'] == 'freehand':
            draw_brush(widget, event.x, event.y, self.odata)
        if self.buttons['current'] == 'highlight':
            draw_brush(widget, event.x, event.y, self.odata, width=12.5,
                       r=220/255, g=240/255, b=90/255, alpha=0.10)

    widget.queue_draw()

    return True

Can someone point out a way to prevent the overlapping points in this curve?

Update

Uli's solution seems to offer a partial remedy, but the stroke is still not good looking, it seems that it's redrawn over and over:

enter image description here

Update with partially working code

I still have not succeeded in creating a highlighter pen with cairo. The closest I can get is in the following gist. The application shutter, has a similar functionality but it's written in Perl on top of the libgoocanvas which is not maintained anymore. I hope a bounty here will change the situation ...

update

available operators (Linux, GTK+3):

In [3]: [item for item in dir(cairo) if item.startswith("OPERATOR")]
Out[3]: 
['OPERATOR_ADD',
 'OPERATOR_ATOP',
 'OPERATOR_CLEAR',
 'OPERATOR_DEST',
 'OPERATOR_DEST_ATOP',
 'OPERATOR_DEST_IN',
 'OPERATOR_DEST_OUT',
 'OPERATOR_DEST_OVER',
 'OPERATOR_IN',
 'OPERATOR_OUT',
 'OPERATOR_OVER',
 'OPERATOR_SATURATE',
 'OPERATOR_SOURCE',
 'OPERATOR_XOR']
like image 212
oz123 Avatar asked Jul 01 '16 10:07

oz123


1 Answers

First, sorry for causing all of that confusion in the comments to your question. It turns out that I was complicating the problem for (partially) no reason! Here is my (heavily-modified) code:

#!/usr/bin/python

from __future__ import division
import math
import time
import cairo
import gi; gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk
from gi.repository.GdkPixbuf import Pixbuf
import random

class Brush(object):
    def __init__(self, width, rgba_color):
        self.width = width
        self.rgba_color = rgba_color
        self.stroke = []

    def add_point(self, point):
        self.stroke.append(point)

class Canvas(object):
    def __init__(self):
        self.draw_area = self.init_draw_area()
        self.brushes = []

    def draw(self, widget, cr):
        da = widget
        cr.set_source_rgba(0, 0, 0, 1)
        cr.paint()
        #cr.set_operator(cairo.OPERATOR_SOURCE)#gets rid over overlap, but problematic with multiple colors
        for brush in self.brushes:
            cr.set_source_rgba(*brush.rgba_color)
            cr.set_line_width(brush.width)
            cr.set_line_cap(1)
            cr.set_line_join(cairo.LINE_JOIN_ROUND)
            cr.new_path()
            for x, y in brush.stroke:
                cr.line_to(x, y)
            cr.stroke()

    def init_draw_area(self):
        draw_area = Gtk.DrawingArea()
        draw_area.connect('draw', self.draw)
        draw_area.connect('motion-notify-event', self.mouse_move)
        draw_area.connect('button-press-event', self.mouse_press)
        draw_area.connect('button-release-event', self.mouse_release)
        draw_area.set_events(draw_area.get_events() |
            Gdk.EventMask.BUTTON_PRESS_MASK |
            Gdk.EventMask.POINTER_MOTION_MASK |
            Gdk.EventMask.BUTTON_RELEASE_MASK)
        return draw_area

    def mouse_move(self, widget, event):
        if event.state & Gdk.EventMask.BUTTON_PRESS_MASK:
            curr_brush = self.brushes[-1]
            curr_brush.add_point((event.x, event.y))
            widget.queue_draw()

    def mouse_press(self, widget, event):
        if event.button == Gdk.BUTTON_PRIMARY:
            rgba_color = (random.random(), random.random(), random.random(), 0.5)
            brush = Brush(12, rgba_color)
            brush.add_point((event.x, event.y))
            self.brushes.append(brush)
            widget.queue_draw()
        elif event.button == Gdk.BUTTON_SECONDARY:
            self.brushes = []

    def mouse_release(self, widget, event):
        widget.queue_draw()

class DrawingApp(object):
    def __init__(self, width, height):
        self.width = width
        self.height = height
        self.window = Gtk.Window()
        self.window.set_border_width(8)
        self.window.set_default_size(self.width, self.height)
        self.window.connect('destroy', self.close)
        self.box = Gtk.Box(spacing=6)
        self.window.add(self.box)
        self.canvas = Canvas()
        self.box.pack_start(self.canvas.draw_area, True, True, 0)
        self.window.show_all()

    def close(self, window):
        Gtk.main_quit()

if __name__ == "__main__":
    DrawingApp(400, 400)
    Gtk.main()

Here are the list of changes I made:

  1. Replaced the inheritance in your code with a composition-based approach. That is, instead of inheriting from Gtk.Window or Gtk.DrawingArea, I created Brush, Canvas, and DrawingApp objects that contain these Gtk elements. The idea of this is to allow more flexibility in creating relevant classes to our application and hides all of the nasty Gtk internals as much as possible in setup functions. Hopefully this makes the code a bit clearer. I have no idea why all the tutorials for Gtk insist on using inheritance.
  2. Speaking of the Brush class, there is now a Brush class! Its purpose is simple: it just contains information about the coordinates draw for a given stroke, its line width, and its color. A list of brush strokes making the drawing is stored as a property of DrawingApp. This is convenient because...
  3. ... all of the rendering is contained within the draw function of the Canvas class! All this does is draw the black screen, followed by rendering the brush strokes one by one as individual paths to the screen. This solves the problem with the code provided by @UliSchlachter. While the idea of a single connected path was right (and I used that here), all of the iterations of that path were being accumulated and drawn on top of each other. This explains your update image, where the start of each stroke was more opaque due to accumulating the most incomplete strokes.
  4. For the sake of color variety, I made the app generate random highlighter colors every time you click with the left mouse button!

Note that the last point illustrates an issue with the blending. Try drawing multiple overlapping strokes and see what happens! You will find that the more overlaps there are, the more opaque it gets. You can use the cairo.OPERATOR_SOURCE setting to counteract this, but I don't think this is an ideal solution as I believe it overwrites the content underneath. Let me know if this solution is fine or if this also needs to be corrected. Here is a picture of the final result, for your reference:

Image of working highlighting application - note the multiple stroke colors!

Hope this helps!

like image 182
CodeSurgeon Avatar answered Oct 06 '22 08:10

CodeSurgeon