Cairo GTK 画一条透明线(像荧光笔)
Cairo GTK draw a line with transparency (like a highlighter pen)
我正在尝试使用 Python、GTK3 和 cairo 创建一个简单的绘图应用程序。该工具应该有不同的画笔和某种 highlighter pen。
我想我可以使用笔划的 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的解决方案貌似可以解决部分问题,但是笔画还是不好看,好像重画了一遍又一遍:
更新部分工作代码
我还是没能用cairo做出荧光笔。
我能得到的最接近的是下面的gist。
应用程序快门具有类似的功能,但它是在不再维护的 libgoocanvas 之上用 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']
每个move_to()
创建一个单独绘制的新子路径。你想要的是一个单一的、连接的路径。
据我所知,如果还没有当前点,cairo 会将 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's this for?
请注意,我删除了 cr.fill()
之后的 cr.stroke()
,因为它没有任何作用。填充只是清除了路径,所以没有什么可以描边的。
首先,对于在您的问题的评论中造成的所有混乱,我们深表歉意。事实证明,我(部分)无缘无故地使问题复杂化!这是我的(经过大量修改的)代码:
#!/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
继承,而是创建了包含这些 Gtk 元素的 Brush
、Canvas
和 DrawingApp
对象。这样做的想法是允许更灵活地创建与我们的应用程序相关的 classes,并尽可能地在设置函数中隐藏所有讨厌的 Gtk 内部结构。希望这能让代码更清晰一些。我不知道为什么所有的 Gtk 教程都坚持使用继承。
- 说到
Brush
class,现在有一个Brush
class!它的目的很简单:它只包含有关给定笔画的坐标绘制、线条宽度和颜色的信息。绘图的笔触列表存储为 DrawingApp
的 属性。这很方便,因为...
- ...所有渲染都包含在
Canvas
class的draw
函数中!所有这一切都是绘制黑屏,然后将画笔笔划一个接一个地渲染为屏幕的单独路径。这解决了@UliSchlachter 提供的代码的问题。虽然单一连接路径的想法是正确的(我在这里使用了它),但该路径的所有迭代都被累积并绘制在彼此之上。这解释了您的更新图像,由于累积了最不完整的笔画,每个笔画的开始更加不透明。
- 为了颜色的多样性,我让应用程序在您每次单击鼠标左键时生成随机的荧光笔颜色!
请注意,最后一点说明了混合问题。尝试绘制多个重叠笔划,看看会发生什么!你会发现重叠的越多,它就越不透明。您可以使用 cairo.OPERATOR_SOURCE
设置来抵消这种情况,但我认为这不是理想的解决方案,因为我认为它会覆盖下面的内容。让我知道这个解决方案是否合适,或者是否还需要更正。附上最终效果图,供大家参考:
希望对您有所帮助!
我正在尝试使用 Python、GTK3 和 cairo 创建一个简单的绘图应用程序。该工具应该有不同的画笔和某种 highlighter pen。 我想我可以使用笔划的 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的解决方案貌似可以解决部分问题,但是笔画还是不好看,好像重画了一遍又一遍:
更新部分工作代码
我还是没能用cairo做出荧光笔。 我能得到的最接近的是下面的gist。 应用程序快门具有类似的功能,但它是在不再维护的 libgoocanvas 之上用 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']
每个move_to()
创建一个单独绘制的新子路径。你想要的是一个单一的、连接的路径。
据我所知,如果还没有当前点,cairo 会将 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's this for?
请注意,我删除了 cr.fill()
之后的 cr.stroke()
,因为它没有任何作用。填充只是清除了路径,所以没有什么可以描边的。
首先,对于在您的问题的评论中造成的所有混乱,我们深表歉意。事实证明,我(部分)无缘无故地使问题复杂化!这是我的(经过大量修改的)代码:
#!/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
继承,而是创建了包含这些 Gtk 元素的Brush
、Canvas
和DrawingApp
对象。这样做的想法是允许更灵活地创建与我们的应用程序相关的 classes,并尽可能地在设置函数中隐藏所有讨厌的 Gtk 内部结构。希望这能让代码更清晰一些。我不知道为什么所有的 Gtk 教程都坚持使用继承。 - 说到
Brush
class,现在有一个Brush
class!它的目的很简单:它只包含有关给定笔画的坐标绘制、线条宽度和颜色的信息。绘图的笔触列表存储为DrawingApp
的 属性。这很方便,因为... - ...所有渲染都包含在
Canvas
class的draw
函数中!所有这一切都是绘制黑屏,然后将画笔笔划一个接一个地渲染为屏幕的单独路径。这解决了@UliSchlachter 提供的代码的问题。虽然单一连接路径的想法是正确的(我在这里使用了它),但该路径的所有迭代都被累积并绘制在彼此之上。这解释了您的更新图像,由于累积了最不完整的笔画,每个笔画的开始更加不透明。 - 为了颜色的多样性,我让应用程序在您每次单击鼠标左键时生成随机的荧光笔颜色!
请注意,最后一点说明了混合问题。尝试绘制多个重叠笔划,看看会发生什么!你会发现重叠的越多,它就越不透明。您可以使用 cairo.OPERATOR_SOURCE
设置来抵消这种情况,但我认为这不是理想的解决方案,因为我认为它会覆盖下面的内容。让我知道这个解决方案是否合适,或者是否还需要更正。附上最终效果图,供大家参考:
希望对您有所帮助!