Cairo GTK рисует линию с прозрачностью (например, ручкой маркера)

Я пытаюсь создать приложение простого рисования с использованием Python, GTK3 и cairo. Инструмент должен иметь различные кисти и некоторую маркер highlighter. Я решил, что могу использовать альфа-свойство инсульта для его создания. Однако, точки соединения создаются перекрывающимися, что создает странный эффект.

введите описание изображения здесь

Вот код, отвечающий за эту красную кисть и режим подсветки:

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()

Код, который нажимает на щелчок мыши:

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

Может ли кто-нибудь указать способ предотвращения перекрытия точек на этой кривой?

Update

Решение Uli, кажется, предлагает частичное средство, но удар все еще не выглядит красивым, кажется, что он перерисовывается снова и снова:

введите описание изображения здесь

Обновление с частично работающим кодом

Мне еще не удалось создать маркер с каиром. Самое близкое, что я могу получить, находится в следующем gist. Затвор приложения имеет аналогичную функциональность, но он написан на Perl поверх libgoocanvas, который больше не поддерживается. Надеюсь, щедрость здесь изменит ситуацию...

Обновление

доступные операторы (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']

Ответ 1

Во-первых, извините за причинение всей этой путаницы в комментариях к вашему вопросу. Оказывается, я затруднял проблему (частично) без причины! Вот мой (сильно модифицированный) код:

#!/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()

Вот список изменений, которые я сделал:

  • Замените наследование в коде с помощью на основе композиции. То есть вместо наследования от Gtk.Window или Gtk.DrawingArea я создал объекты Brush, Canvas и DrawingApp, которые содержат эти элементы Gtk. Идея этого заключается в том, чтобы обеспечить большую гибкость в создании соответствующих классов для нашего приложения и максимально скрывать все неприятные внутренние элементы Gtk в настройках. Надеюсь, это сделает код более понятным. Я не знаю, почему все учебники для Gtk настаивают на использовании наследования.
  • Говоря о классе Brush, теперь существует класс Brush! Его цель проста: она просто содержит информацию об обращении координат для данного хода, ширине его линии и ее цвете. Список мазков кисти, составляющих чертеж, сохраняется как свойство DrawingApp. Это удобно, потому что...
  • ... все рендеринг содержится в функции draw класса Canvas! Все это делает чернильный экран, а затем визуализирует мазки кисти один за другим в виде отдельных путей к экрану. Это решает проблему с кодом, предоставленным @UliSchlachter. Хотя идея одного связанного пути была правильной (и я использовал это здесь), все итерации этого пути накапливались и рисовались друг над другом. Это объясняет ваше изображение обновления, где начало каждого удара было более непрозрачным из-за накопления наиболее неполных штрихов.
  • Ради разнообразия цветов я заставил приложение генерировать случайные цвета маркера каждый раз, когда вы нажимаете левой кнопкой мыши!

Обратите внимание, что последний пункт иллюстрирует проблему с смешиванием. Попробуйте выполнить несколько перекрывающихся штрихов и посмотреть, что произойдет! Вы обнаружите, что чем больше перекрытий есть, тем более непрозрачным оно становится. Вы можете использовать параметр cairo.OPERATOR_SOURCE, чтобы противодействовать этому, но я не думаю, что это идеальное решение, поскольку я считаю, что он переписывает содержимое под ним. Дайте мне знать, хорошо ли это решение, или если это также необходимо исправить. Вот изображение окончательного результата, для вашей справки:

Изображение рабочего приложения подсветки - обратите внимание на цвета с несколькими штрихами!

Надеюсь, это поможет!

Ответ 2

Каждый move_to() создает новый дополнительный путь, который рисуется отдельно. То, что вы хотите, это один, связанный с ним путь.

Насколько я знаю, cairo превращает a line_to() -call в move_to(), если нет текущей точки, поэтому следующее должно работать:

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:
        cr.new_path()
        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()
            else:
                cr.line_to(point['x'], point['y'])                
        cr.stroke()

    cr.save() # What this for?

Обратите внимание, что после cr.fill() я удалил cr.stroke(), потому что он ничего не делает. Заполнение просто очистило путь, поэтому нечего делать.