PyCairo 中的变换
最后修改于 2023 年 7 月 17 日
在 PyCairo 图形编程教程的这一部分,我们将讨论变换。
仿射变换 由零个或多个线性变换(旋转、缩放或剪切)和翻译(平移)组成。多个线性变换可以组合成一个单一的矩阵。旋转 是将刚体围绕一个固定点移动的变换。缩放 是放大或缩小对象的变换。缩放因子在所有方向上都是相同的。平移 是将每个点沿指定方向移动恒定距离的变换。剪切 是将对象沿着给定轴的垂直方向移动的变换,在轴的一侧的值大于另一侧。
平移
以下示例描述了一个简单的平移。
def on_draw(self, wid, cr): cr.set_source_rgb(0.2, 0.3, 0.8) cr.rectangle(10, 10, 30, 30) cr.fill() cr.translate(20, 20) cr.set_source_rgb(0.8, 0.3, 0.2) cr.rectangle(0, 0, 30, 30) cr.fill() cr.translate(30, 30) cr.set_source_rgb(0.8, 0.8, 0.2) cr.rectangle(0, 0, 30, 30) cr.fill() cr.translate(40, 40) cr.set_source_rgb(0.3, 0.8, 0.8) cr.rectangle(0, 0, 30, 30) cr.fill()
该示例绘制了一个矩形。然后我们进行平移,并再次绘制几次相同的矩形。
cr.translate(20, 20)
translate
函数通过平移用户空间原点来修改当前变换矩阵。在我们的例子中,我们将原点在两个方向上都移动了 20 个单位。

剪切
在下面的示例中,我们执行了一个剪切操作。剪切是沿着特定轴的对象失真。此操作没有剪切方法。我们需要创建自己的变换矩阵。请注意,每个仿射变换都可以通过创建变换矩阵来执行。
def on_draw(self, wid, cr): cr.set_source_rgb(0.6, 0.6, 0.6) cr.rectangle(20, 30, 80, 50) cr.fill() mtx = cairo.Matrix(1.0, 0.5, 0.0, 1.0, 0.0, 0.0) cr.transform(mtx) cr.rectangle(130, 30, 80, 50) cr.fill()
在此代码示例中,我们执行了一个简单的剪切操作。
mtx = cairo.Matrix(1.0, 0.5, 0.0, 1.0, 0.0, 0.0)
此变换将 y 值按 x 值的 0.5 进行剪切。
cr.transform(mtx)
我们使用 transform
方法执行变换。

缩放
下一个示例演示了一个缩放操作。缩放是对象被放大或缩小的变换操作。
def on_draw(self, wid, cr): cr.set_source_rgb(0.2, 0.3, 0.8) cr.rectangle(10, 10, 90, 90) cr.fill() cr.scale(0.6, 0.6) cr.set_source_rgb(0.8, 0.3, 0.2) cr.rectangle(30, 30, 90, 90) cr.fill() cr.scale(0.8, 0.8) cr.set_source_rgb(0.8, 0.8, 0.2) cr.rectangle(50, 50, 90, 90) cr.fill()
我们绘制了三个 90x90 像素大小的矩形。其中两个我们进行了缩放操作。
cr.scale(0.6, 0.6) cr.set_source_rgb(0.8, 0.3, 0.2) cr.rectangle(30, 30, 90, 90) cr.fill()
我们将一个矩形按 0.6 的因子进行均匀缩放。
cr.scale(0.8, 0.8) cr.set_source_rgb(0.8, 0.8, 0.2) cr.rectangle(50, 50, 90, 90) cr.fill()
在这里,我们按 0.8 的因子执行了另一个缩放操作。如果我们看图,可以看到第三个黄色矩形是最小的。即使我们使用了较小的缩放因子。这是因为变换操作是累加的。实际上,第三个矩形被缩放了 0.528 (0.6x0.8) 的因子。

隔离变换
变换操作是累加的。要将一个操作与其他操作隔离,我们可以使用 save
和 restore
方法。save
方法会复制绘图上下文的当前状态,并将其保存在内部的保存状态堆栈中。restore
方法会将上下文恢复到保存的状态。
def on_draw(self, wid, cr): cr.set_source_rgb(0.2, 0.3, 0.8) cr.rectangle(10, 10, 90, 90) cr.fill() cr.save() cr.scale(0.6, 0.6) cr.set_source_rgb(0.8, 0.3, 0.2) cr.rectangle(30, 30, 90, 90) cr.fill() cr.restore() cr.save() cr.scale(0.8, 0.8) cr.set_source_rgb(0.8, 0.8, 0.2) cr.rectangle(50, 50, 90, 90) cr.fill() cr.restore()
在示例中,我们缩放了两个矩形。这次我们隔离了它们之间的缩放操作。
cr.save() cr.scale(0.6, 0.6) cr.set_source_rgb(0.8, 0.3, 0.2) cr.rectangle(30, 30, 90, 90) cr.fill() cr.restore()
我们将 scale
方法放在 save
和 restore
方法之间,从而隔离了缩放操作。

现在第三个黄色矩形比第二个红色矩形大。
甜甜圈
在下面的示例中,我们通过旋转一组椭圆来创建一个复杂的形状。
#!/usr/bin/python ''' ZetCode PyCairo tutorial This program creates a 'donut' shape in PyCairo. author: Jan Bodnar website: zetcode.com ''' from gi.repository import Gtk import cairo import math class Example(Gtk.Window): def __init__(self): super(Example, self).__init__() self.init_ui() def init_ui(self): darea = Gtk.DrawingArea() darea.connect("draw", self.on_draw) self.add(darea) self.set_title("Donut") self.resize(350, 250) self.set_position(Gtk.WindowPosition.CENTER) self.connect("delete-event", Gtk.main_quit) self.show_all() def on_draw(self, wid, cr): cr.set_line_width(0.5) w, h = self.get_size() cr.translate(w/2, h/2) cr.arc(0, 0, 120, 0, 2*math.pi) cr.stroke() for i in range(36): cr.save() cr.rotate(i*math.pi/36) cr.scale(0.3, 1) cr.arc(0, 0, 120, 0, 2*math.pi) cr.restore() cr.stroke() def main(): app = Example() Gtk.main() if __name__ == "__main__": main()
我们进行了旋转和缩放操作。我们还保存和恢复了 PyCairo 上下文。
cr.translate(w/2, h/2) cr.arc(0, 0, 120, 0, 2*math.pi) cr.stroke()
在 GTK 窗口的中间,我们创建了一个圆。这将是我们椭圆的边界圆。
for i in range(36): cr.save() cr.rotate(i*math.pi/36) cr.scale(0.3, 1) cr.arc(0, 0, 120, 0, 2*math.pi) cr.restore() cr.stroke()
我们在边界圆的路径上创建了 36 个椭圆。我们使用 save
和 restore
方法将每个旋转和缩放操作相互隔离开。

星形
下一个示例显示了一个旋转和缩放的星星。
#!/usr/bin/python ''' ZetCode PyCairo tutorial This is a star example which demonstrates scaling, translating and rotating operations in PyCairo. author: Jan Bodnar website: zetcode.com ''' from gi.repository import Gtk, GLib import cairo class cv(object): points = ( ( 0, 85 ), ( 75, 75 ), ( 100, 10 ), ( 125, 75 ), ( 200, 85 ), ( 150, 125 ), ( 160, 190 ), ( 100, 150 ), ( 40, 190 ), ( 50, 125 ), ( 0, 85 ) ) SPEED = 20 TIMER_ID = 1 class Example(Gtk.Window): def __init__(self): super(Example, self).__init__() self.init_ui() self.init_vars() def init_ui(self): self.darea = Gtk.DrawingArea() self.darea.connect("draw", self.on_draw) self.add(self.darea) self.set_title("Star") self.resize(400, 300) self.set_position(Gtk.WindowPosition.CENTER) self.connect("delete-event", Gtk.main_quit) self.show_all() def init_vars(self): self.angle = 0 self.scale = 1 self.delta = 0.01 GLib.timeout_add(cv.SPEED, self.on_timer) def on_timer(self): if self.scale < 0.01: self.delta = -self.delta elif self.scale > 0.99: self.delta = -self.delta self.scale += self.delta self.angle += 0.01 self.darea.queue_draw() return True def on_draw(self, wid, cr): w, h = self.get_size() cr.set_source_rgb(0, 0.44, 0.7) cr.set_line_width(1) cr.translate(w/2, h/2) cr.rotate(self.angle) cr.scale(self.scale, self.scale) for i in range(10): cr.line_to(cv.points[i][0], cv.points[i][1]) cr.fill() def main(): app = Example() Gtk.main() if __name__ == "__main__": main()
在此示例中,我们创建了一个星形对象。我们对其进行平移、旋转和缩放。
points = ( ( 0, 85 ), ( 75, 75 ), ( 100, 10 ), ( 125, 75 ), ( 200, 85 ), ...
星形对象将由这些点构成。
def init_vars(self): self.angle = 0 self.scale = 1 self.delta = 0.01 ...
在 init_vars
方法中,我们初始化了三个变量。self.angle
用于旋转,self.scale
用于缩放星形对象。self.delta
变量控制星形的增长和收缩。
glib.timeout_add(cv.SPEED, self.on_timer)
每 cv.SPEED
毫秒调用一次 on_timer
方法。
if self.scale < 0.01: self.delta = -self.delta elif self.scale > 0.99: self.delta = -self.delta
这些行控制星形是增长还是收缩。
cr.translate(w/2, h/2) cr.rotate(self.angle) cr.scale(self.scale, self.scale)
我们将星形移到窗口中间。对其进行旋转和缩放。
for i in range(10): cr.line_to(cv.points[i][0], cv.points[i][1]) cr.fill()
这里我们绘制星形对象。
在 PyCairo 教程的这一部分,我们讨论了变换。