使用Cairo GTK绘制带透明度的直线(就像荧光笔一样)

17

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

输入图像描述

这是造成红色画笔和荧光笔模式的代码:

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。 应用程序shutter具有类似的功能,但它是用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']

我的直觉是这个问题是混合的问题,荧光笔线条的RGBA颜色被相互叠加,导致越来越不透明、更亮的笔画。你可能需要查看这个。然而,在我安装了python + gtk3 + cairo的Windows系统上,混合选项是有限的。在你的系统上有哪些cairo.OPERATOR_*选项可用?你可以使用print dir(cairo)列出它们。 - CodeSurgeon
请查看我的最新更新以获取可用的运算符。 - oz123
在这种情况下,你和我一样有相同的限制!我的下一个问题是,当你使用荧光笔绘制时出现自交点时,你希望发生什么?也就是说,当你绘制一个循环并且荧光笔两次穿过同一点时,它应该看起来更不透明还是只像你只在交叉点上绘制了一次? - CodeSurgeon
在这种情况下,部分解决方案是使用OPERATOR_SOURCE。但是,您的代码还需要进行一些其他结构性更改。基本上,我们应该在draw_brush中绘制到一个空纹理,并手动将这些像素与真实背景像素相乘。 - CodeSurgeon
你能 fork 我的 gist 并在那里发布建议的更改吗?请将其作为答案添加到这里。我已经对设置非常低的 alpha 和运算符 "ATOP" 感到满意了。我真的会非常感激,而且还有赏金... - oz123
显示剩余3条评论
2个回答

10

首先,很抱歉在您的问题评论中造成了那么多的混乱。结果是,我有些没有必要地让问题变得更加复杂!这里是我(经过大量修改)的代码:

#!/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.WindowGtk.DrawingArea继承,而是创建包含这些 Gtk 元素的BrushCanvasDrawingApp对象。这样可以更灵活地创建与应用程序相关的类,并尽可能地在设置函数中隐藏所有令人讨厌的 Gtk 内部细节。希望这会使代码更加清晰。我不知道为什么所有关于 Gtk 的教程都坚持使用继承。
  2. 说到Brush类,现在有了一个Brush类!它的目的很简单:只包含给定笔画所画坐标、线宽和颜色的信息。由刷子笔画构成的列表存储在DrawingApp的属性中。这很方便,因为...
  3. ... 所有渲染都包含在Canvas类的draw函数中!这个函数只需要绘制黑屏幕,然后逐个路径将笔画渲染到屏幕上。这解决了由@UliSchlachter提供的代码中的问题。虽然使用一个单一的连接路径的想法是正确的(我在这里使用了它),但是该路径的所有迭代都被累积并绘制在彼此之上。这就解释了你的更新图像,因为每个笔画的起点由于累积最不完整的笔画而更加不透明。
  4. 为了增加颜色的多样性,我让应用程序在每次单击鼠标左键时生成随机荧光笔颜色!

请注意,最后一个要点说明了混合的问题。尝试绘制多个重叠的笔画,看看会发生什么!您会发现,重叠越多,不透明度就越高。你可以使用cairo.OPERATOR_SOURCE设置来抵消这一点,但我认为这并不是一个理想的解决方案,因为我相信它会覆盖底下的内容。如果这个解决方案可以接受,请让我知道,或者是否还需要进行更正。以下是最终结果的图片,供您参考:

工作高亮应用程序的图像 - 请注意多种笔画颜色!

希望这会有所帮助!


1
嗨,这很有帮助!你会愿意通过电子邮件进一步沟通吗? - oz123
我认为升级的方法是使用建议的操作cairo.OPERATOR_SOURCE来绘制每个“刷子”,但使用不同的组(通过cairo_push_group / cairo_pop_group_to_source / cairo_paint)来避免不同的刷子相互擦除。 - Cimbali

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.fill()之后删除了cr.stroke(),因为它没有任何作用。填充操作已经清除了路径,因此没有需要描边的内容。

这已经好多了。谢谢你。但我认为在事件处理程序中记录OData仍然存在问题。请看上面的图片。 - oz123
至于cr.save(),我是从某个示例中获取的代码,我不确定它是否真的需要。 - oz123
嗯...不知道?也许在绘制之前清除所有内容。在设置源颜色之前添加 cr.paint()。这应该会用黑色填充整个画布。如果这有帮助,那么你可能是在旧图形的基础上进行绘制,从而多次涂抹早期线条,使它们变得不透明。 - Uli Schlachter
你的最后一个建议会导致每次笔画都填充整个画布为黑色。这样我就无法创建多个笔画或使用预先存在的画布。 - oz123

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接