开罗GTK绘制一条透明线(如荧光笔)

时间:2016-07-01 10:26:34

标签: python gtk3 cairo pycairo

我正在尝试使用Python,GTK3和cairo创建一个简单的绘图应用程序。该工具应该有不同的画笔和某种highlighter pen。 我想我可以使用笔划的alpha属性来创建它。然而, 连接点是重叠创建的,会产生奇怪的效果。

enter image description here

以下是负责此红色画笔和荧光笔模式的代码:

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

有人能指出防止此曲线中重叠点的方法吗?

更新

Uli的解决方案似乎提供了部分补救措施,但中风仍然不好看,似乎它反复重绘:

enter image description here

使用部分工作代码进行更新

我还没有成功用cairo制作荧光笔。 我能得到的最接近的是gist。 应用程序快门具有类似的功能,但它在liblocanvas的顶部用Perl编写,不再维护。 我希望这里的赏金会改变这种情况......

更新

可用的运算符(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']

2 个答案:

答案 0 :(得分:8)

首先,抱歉在评论中引起所有这些混乱。事实证明,我(部分)没有理由使问题复杂化!这是我的(经过大量修改的)代码:

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

以下是我所做的更改列表:

  1. 使用基于合成的方法替换代码中的继承。也就是说,我创建了包含这些Gtk元素的Gtk.WindowGtk.DrawingAreaBrush个对象,而不是继承自CanvasDrawingApp。这样做的想法是允许更灵活地为我们的应用程序创建相关类,并在设置函数中尽可能地隐藏所有讨厌的Gtk内部。希望这会使代码更清晰一些。我不知道为什么Gtk的所有教程都坚持使用继承。
  2. 说到Brush课程,现在有一个Brush课程!它的目的很简单:它只包含有关给定笔划的坐标绘制,线宽和颜色的信息。制作图形的画笔笔划列表存储为DrawingApp的属性。这很方便,因为......
  3. ...所有渲染都包含在draw类的Canvas函数中!所有这一切都是绘制黑色屏幕,然后逐个渲染画笔笔划作为到屏幕的单独路径。这解决了@UliSchlachter提供的代码的问题。虽然单个连接路径的想法是正确的(我在这里使用它),该路径的所有迭代都被累积并在彼此之上绘制。这解释了您的更新图像,其中每个笔划的开始由于累积最不完整的笔划而更不透明。
  4. 为了颜色的多样性,我每次点击鼠标左键都会让应用程序产生随机的荧光笔颜色!
  5. 请注意,最后一点说明了混合问题。尝试绘制多个重叠笔画,看看会发生什么!你会发现它们的重叠越多,它就越不透明。您可以使用cairo.OPERATOR_SOURCE设置来抵消这一点,但我不认为这是一个理想的解决方案,因为我认为它会覆盖下面的内容。如果这个解决方案没问题或是否需要纠正,请告诉我。以下是最终结果的图片,供您参考:

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

    希望这有帮助!

答案 1 :(得分:6)

每个move_to()创建一个单独绘制的新子路径。你想要的是一条连接的路径。

据我所知,如果还没有当前点,cairo会将line_to() - 调用转换为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's this for?

请注意,我在cr.stroke()之后删除了cr.fill(),因为它没有做任何事情。填充刚刚清除了路径,所以没有任何东西可以冲击。