1. 引言为什么变换是 Cairo 绘图的核心能力在二维图形编程中变换Transform几乎贯穿每一个绘图环节。无论你是在绘制一个简单的矩形、一段文字还是构建复杂的图表、地图、游戏界面你都需要决定图形应该出现在哪里、以什么角度呈现、放大或缩小到什么程度以及哪些区域允许绘制、哪些区域需要裁剪掉。Cairo 是一个广泛使用的 2D 矢量图形库它把「变换」设计成了绘图上下文层面的核心状态。理解 Cairo 的变换、旋转、缩放与剪切机制不仅能帮助你写出更简洁的绘图代码还能避免很多看似奇怪、实则由矩阵运算引起的坐标错位、线宽异常、文字变形等问题。很多初学 Cairo 的开发者会遇到这样一个场景明明代码里写的是在坐标 (100, 100) 画矩形图形却跑到了别的地方又或者调用缩放函数之后原来 2 像素宽的线条突然变得粗细不均。这些问题本质上都源于对 Cairo 变换模型的误解。本文将从数学基础、API 用法、组合顺序、裁剪路径到实战案例系统讲解 Cairo 中的变换体系。本文会重点覆盖以下内容用户空间与设备空间的区别以及为什么变换发生在两者之间平移translate、缩放scale、旋转rotate三类最常用变换的完整用法变换矩阵的数学原理以及如何直接读写 Cairo 的变换矩阵变换组合的顺序问题理解为什么先旋转再平移与先平移再旋转结果不同通过cairo_save和cairo_restore管理图形状态的正确姿势剪切路径clipping的原理、API 与高级用法包括圆角裁剪、挖空效果和图片遮罩变换如何影响文本、线宽、渐变和图案动态时钟、径向布局、缩略图生成、头像圆角裁剪等完整实战案例常见问题、调试技巧和性能注意事项。需要特别说明的是在图形学中文语境里「剪切」一词有两个常见含义一个是通过矩阵实现的斜切或倾斜变形skew / shear另一个是用路径限制绘制区域的裁剪clip。Cairo 本身没有提供单独的cairo_shear函数但可以通过设置自定义矩阵实现斜切同时 Cairo 提供了完善的cairo_clip系列接口用于裁剪。本文将同时覆盖这两层含义既不遗漏矩阵斜切也完整讲解裁剪路径。本文代码示例以 Cairo 官方 C API 为主。如果你使用 Python 的 Pycairo、Rust 的 cairo-rs 或其他语言绑定API 名称和参数几乎一一对应掌握 C API 后很容易迁移。2. Cairo 绘图基础回顾在深入变换之前有必要先回顾 Cairo 的基本绘图模型。如果不理解表面、绘图上下文、路径这三者的关系后面的变换内容会变得非常抽象。2.1 绘图表面与绘图上下文Cairo 的绘图目标称为表面surface。表面可以是内存中的像素缓冲区、PNG 或 SVG 文件、窗口系统的绘图区域也可以是 PDF 等矢量输出。表面上并不直接执行绘制命令而是通过一个独立的绘图上下文drawing context进行绘图。这个上下文通常用cairo_t类型表示。创建绘图上下文之后Cairo 内部会维护一整套图形状态包括当前路径current path当前点current point画笔颜色与填充颜色线宽、线帽和连接样式虚线样式字体与字号抗锯齿模式复合操作符compositing operator当前的变换矩阵CTM当前剪切区域clip region。变换矩阵和剪切区域正是本文讨论的重点。它们都会影响后续所有绘图命令的执行结果。2.2 路径、描边与填充Cairo 采用「路径 描边/填充」模型。你可以使用cairo_rectangle、cairo_arc、cairo_line_to、cairo_curve_to等函数在上下文中构建路径但这些函数本身不会产生任何可见输出。只有调用cairo_stroke、cairo_fill或cairo_clip时Cairo 才真正根据当前路径执行绘制或裁剪。路径上的每一个坐标都属于所谓的「用户空间」。Cairo 在执行描边或填充时会把这些用户空间坐标通过当前变换矩阵转换到设备空间然后再光栅化到表面上。理解这一点是理解后续所有变换行为的关键。2.3 用户空间与设备空间用户空间user space是你写代码时使用的逻辑坐标系。它没有固定的单位你可以把它想象成一块无限大的画布坐标原点默认在左上角还是右下角并不重要重要的是它独立于输出设备。设备空间device space则是实际输出设备使用的像素坐标系。对于一张 800 乘以 600 的 PNG 图片设备空间的坐标范围就是水平 0 到 799、垂直 0 到 599 的像素网格。对于打印输出或高分辨率屏幕设备空间单位可能是物理点或物理像素。在默认情况下用户空间和设备空间是重合的用户空间中的 (50, 50) 直接对应设备空间中的第 50 行、第 50 列像素。一旦你修改了变换矩阵这个对应关系就会改变。例如经过一次平移后用户空间原点可能移动到设备空间的 (100, 80)。Cairo 的变换本质上就是用户空间到设备空间的坐标映射。你可以把它理解为你在用户空间坐标系里构建图形Cairo 最终把图形投影到设备坐标系中。所谓平移、旋转、缩放都是对这个投影方式的调整。2.4 一个最小可运行示例先看一个不含任何显式变换的最小示例以便建立直观感受#include cairo.h int main(void) { /* 创建一个 400x400 的 ARGB32 图像表面 */ cairo_surface_t *surface cairo_image_surface_create(CAIRO_FORMAT_ARGB32, 400, 400); /* 基于该表面创建绘图上下文 */ cairo_t *cr cairo_create(surface); /* 白色背景 */ cairo_set_source_rgb(cr, 1.0, 1.0, 1.0); cairo_paint(cr); /* 黑色边框矩形 */ cairo_set_source_rgb(cr, 0.0, 0.0, 0.0); cairo_set_line_width(cr, 2.0); cairo_rectangle(cr, 50, 50, 200, 150); cairo_stroke(cr); /* 输出 PNG */ cairo_surface_write_to_png(surface, minimal.png); /* 释放资源 */ cairo_destroy(cr); cairo_surface_destroy(surface); return 0; }这段代码在 (50, 50) 处绘制了一个宽度 200、高度 150 的矩形边框。此时没有显式调用任何变换函数因此用户空间和默认设备空间完全重合。接下来我们就从这里出发逐步加入平移、旋转、缩放和裁剪。编译上述代码时通常使用 pkg-config 获取头文件路径和链接库gcc minimal.c -o minimal $(pkg-config --cflags --libs cairo)如果环境中没有安装 Cairo可以查看 Cairo 官方文档或使用系统包管理器安装开发包。Ubuntu 和 Debian 下通常是libcairo2-devmacOS 下可以通过 Homebrew 安装cairoWindows 下常用 MSYS2 的mingw-w64-cairo包。3. 变换的本质仿射变换与矩阵Cairo 所支持的所有二维变换都属于仿射变换affine transformation。仿射变换有一个非常好的性质直线经过变换后仍然是直线平行线经过变换后仍然平行。因此一个矩形经过旋转后仍然是矩形只是可能改变了位置、大小和朝向但矩形不会变成曲线围成的图形。这个性质保证了 Cairo 的矢量图形可以在变换后仍然被精确、高效地光栅化。3.1 四种基本变换二维仿射变换可以由以下四类基本变换组合而成平移变换translation把图形沿 x 轴和 y 轴移动一段距离不改变图形大小和方向。缩放变换scaling把图形沿 x 轴和 y 轴分别放大或缩小。如果两个方向缩放因子不同会发生非等比缩放图形产生拉伸。旋转变换rotation把图形围绕某个中心点旋转一定角度不改变图形大小和形状。斜切变换shearing / skewing把图形沿着一个坐标轴方向按比例偏移例如让矩形变成平行四边形。这是中文语境下「剪切」的一种含义。Cairo 为前三种常用变换提供了专门的 API分别是cairo_translate、cairo_scale和cairo_rotate。对于斜切Cairo 没有单独函数但可以通过cairo_matrix_init或直接修改cairo_matrix_t结构体来实现。3.2 变换的矩阵表示在数学上Cairo 使用一个 3 乘以 3 的仿射矩阵来表示二维变换。但由于第三行恒为 (0, 0, 1)实际只存储 6 个有效数字。Cairo 把这 6 个数字封装在cairo_matrix_t结构体中typedef struct _cairo_matrix { double xx; double yx; double xy; double yy; double x0; double y0; } cairo_matrix_t;将这个 6 元组展开为完整的仿射矩阵形式如下| x | | xx xy x0 | | x | | y | | yx yy y0 | * | y | | 1 | | 0 0 1 | | 1 |也就是说一个用户空间坐标点 (x, y) 经过变换后得到设备空间坐标 (x, y)计算方法为x xx * x xy * y x0 y yx * x yy * y y0其中xx和yy分别控制 x 轴和 y 轴方向上的缩放xy和yx控制旋转以及斜切中的坐标混合x0和y0控制平移。注意cairo_matrix_t结构体中字段的排列顺序是xx, yx, xy, yy, x0, y0而矩阵展开后第二列是xy, yy。这两者的顺序容易混淆阅读代码时要特别留意。矩阵中xx、xy、x0组成第一行yx、yy、y0组成第二行。3.3 单位矩阵不执行任何变换时Cairo 的当前变换矩阵是单位矩阵x 1 * x 0 * y 0 y 0 * x 1 * y 0对应结构体为cairo_matrix_init_identity(matrix);单位矩阵意味着用户空间和设备空间完全重合所有图形严格按原始坐标绘制。3.4 平移、旋转、缩放对应的矩阵平移 (tx, ty)x x tx y y ty 矩阵xx1, xy0, x0tx, yx0, yy1, y0ty缩放 (sx, sy)x sx * x y sy * y 矩阵xxsx, xy0, x00, yx0, yysy, y00绕原点旋转弧度 αx cos(α) * x - sin(α) * y y sin(α) * x cos(α) * y 矩阵xxcos(α), xy-sin(α), x00, yxsin(α), yycos(α), y00斜切沿 x 轴方向的斜切系数为 k 时x x k * y沿 y 轴方向的斜切系数为 m 时y y m * x。通过这一公式可以轻松把矩形变成平行四边形。后面的章节会分别演示这些矩阵在 Cairo API 中如何落地。4. 当前变换矩阵 CTM4.1 CTM 是什么Cairo 绘图上下文中保存的当前变换矩阵称为 CTMCurrent Transformation Matrix。每一条路径构建命令、每一次描边和填充都会受到 CTM 的影响。CTM 决定了用户空间坐标如何映射到设备空间。把 CTM 想象成是「相机」或「画布摆放方式」会更好理解你的图形是在用户空间里绘制的而 CTM 决定了这块画布在输出设备上的位置、方向和缩放程度。4.2 变换具有累积性CTM 的一个重要特性是累积性。每次调用cairo_translate、cairo_scale或cairo_rotate并不是把 CTM 重置为一个新的变换而是在现有 CTM 的基础上继续叠加新变换。可以理解成新CTM 旧CTM × 新变换注意这里是旧矩阵在前新变换在后。由于矩阵乘法不满足交换律调用变换函数的顺序会直接影响最终结果。第 8 章会详细讨论这个问题。正因为变换会累积如果在一个很长的绘图流程中不断叠加变换后期很可能会忘记当前画布已经被改到哪里了。此时最好的办法是用cairo_save和cairo_restore快照、恢复图形状态而不是试图反向撤销已经累加进去的变换。4.3 重置为单位矩阵如果需要把 CTM 完全重置为初始状态可以使用cairo_identity_matrix(cr);调用之后用户空间和设备空间重新重合之前累加的所有平移、旋转、缩放都会失效。这个函数等效于cairo_matrix_t identity; cairo_matrix_init_identity(identity); cairo_set_matrix(cr, identity);在复杂绘图场景中显式重置矩阵有时比反复 save/restore 更清晰但需要注意它只重置变换矩阵不会恢复其他图形状态例如颜色、线宽和剪切区域。5. 平移变换cairo_translate5.1 函数签名与语义void cairo_translate(cairo_t *cr, double tx, double ty);cairo_translate把当前用户空间原点沿 x 轴移动tx个单位、沿 y 轴移动ty个单位。这里移动的单位是用户空间单位移动方向和直觉一致正的tx向右移动正的ty向下移动。更准确地说平移之后原来位于用户空间 (0, 0) 的点会被映射到设备空间中原原点正右方tx、正下方ty的位置。因此你在用户空间 (0, 0) 开始画的图形最终会显示在设备空间 (tx, ty) 附近。5.2 基本示例移动原点到画布中央假设我们有一张 400 乘以 400 的图像表面希望以画布中心 (200, 200) 为原点绘制图形#include cairo.h int main(void) { cairo_surface_t *surface cairo_image_surface_create(CAIRO_FORMAT_ARGB32, 400, 400); cairo_t *cr cairo_create(surface); cairo_set_source_rgb(cr, 1.0, 1.0, 1.0); cairo_paint(cr); /* 把用户空间原点平移到画布中心 */ cairo_translate(cr, 200.0, 200.0); /* 现在 (0,0) 位于画布中心在中心画一个圆 */ cairo_set_source_rgb(cr, 0.2, 0.4, 0.8); cairo_arc(cr, 0.0, 0.0, 80.0, 0.0, 2.0 * 3.14159265358979323846); cairo_fill(cr); cairo_surface_write_to_png(surface, translate.png); cairo_destroy(cr); cairo_surface_destroy(surface); return 0; }这里的cairo_arc(cr, 0, 0, 80, ...)看起来像是以用户空间原点为圆心绘制圆。由于我们先把原点平移到了设备空间的 (200, 200)这个圆最终出现在画布正中心。这个技巧非常常用先把原点移到某个重要位置再用相对坐标绘制图形代码会更加清晰。5.3 平移不会改变图形大小和方向平移只影响位置不影响尺寸、角度和方向。所以在平移之后原来边长为 100 的正方形仍然边长 100原来水平的线段仍然水平。不要担心平移会造成拉伸或旋转平移矩阵里只改变x0和y0其余部分等同于单位矩阵。5.4 平移的典型使用场景把原点移动到窗口或画布中央方便绘制中心对称图形。在循环绘制多个相同图形时通过平移迅速改变绘制位置。配合旋转实现「绕任意点旋转」先平移到目标旋转中心再旋转最后再把坐标移回。处理文本基线的偏移。6. 缩放变换cairo_scale6.1 函数签名与语义void cairo_scale(cairo_t *cr, double sx, double sy);cairo_scale把用户空间坐标沿 x 轴缩放sx倍、沿 y 轴缩放sy倍。缩放因子为 1 表示不变大于 1 表示放大介于 0 和 1 之间表示缩小。例如调用cairo_scale(cr, 2.0, 2.0)之后用户空间中一个 50 乘以 50 的正方形会显示为 100 乘以 100 像素并且它的位置也会相应远离原点。6.2 基本示例整体放大图形cairo_set_source_rgb(cr, 0.1, 0.1, 0.1); /* 原始正方形 */ cairo_rectangle(cr, 20, 20, 60, 60); cairo_stroke(cr); /* 整体放大 2 倍后再画同样的正方形 */ cairo_set_source_rgb(cr, 0.8, 0.2, 0.2); cairo_scale(cr, 2.0, 2.0); cairo_rectangle(cr, 20, 20, 60, 60); cairo_stroke(cr);第二段代码虽然仍然写的是(20, 20, 60, 60)但因为整个用户空间被放大了 2 倍最终矩形位于设备空间的 (40, 40)宽高均为 120 像素。这说明缩放不仅改变了尺寸也改变了距原点的位置。这一点非常关键缩放围绕的是用户空间原点。原点到图形上每个点的向量都被乘以了缩放因子因此离原点越远位移越大。6.3 非等比缩放与负值镜像sx和sy可以不同。如果sx 2而sy 1图形只在水平方向上拉伸垂直方向保持不变圆会变成横向的椭圆。如果传入负值就会产生镜像效果/* 水平镜像 */ cairo_scale(cr, -1.0, 1.0); /* 垂直镜像 */ cairo_scale(cr, 1.0, -1.0); /* 同时水平和垂直镜像相当于旋转 180 度 */ cairo_scale(cr, -1.0, -1.0);负缩放经常用于修正坐标系方向。例如某些设备坐标的 y 轴向上而 Cairo 默认 y 轴向下通过cairo_scale(cr, 1.0, -1.0)再平移可以把坐标系调整为常见的数学坐标系。6.4 缩放对线宽的影响这是 Cairo 开发者最容易遇到的陷阱之一。线宽是在用户空间单位中定义的而描边最终发生在设备空间。因此当你缩放用户空间后线宽也会被同样缩放。下面这段代码会清晰地展示问题cairo_set_source_rgb(cr, 0.0, 0.0, 0.0); cairo_set_line_width(cr, 1.0); /* 第一个正方形线宽 1 像素 */ cairo_rectangle(cr, 10, 10, 100, 100); cairo_stroke(cr); /* 放大 5 倍后再画同样的正方形 */ cairo_scale(cr, 5.0, 5.0); cairo_rectangle(cr, 10, 10, 100, 100); cairo_stroke(cr);第二段代码中虽然用户空间线宽仍然是 1.0但由于 CTM 被放大了 5 倍最终在设备空间中的实际线宽变成了大约 5 像素。结果就是第二个矩形虽然尺寸被放大了 5 倍但边框也意外地变粗了。如果你希望无论缩放如何变化边框始终保持固定的设备像素宽度需要使用坐标距离转换函数cairo_set_line_width(cr, 1.0); cairo_rectangle(cr, 10, 10, 100, 100); cairo_stroke(cr); cairo_scale(cr, 5.0, 5.0); /* 计算用户在输出设备上保持 1 像素宽需要的用户空间线宽 */ double dx 1.0; double dy 1.0; cairo_device_to_user_distance(cr, dx, dy); /* 使用换算后的线宽 */ cairo_set_line_width(cr, dx); cairo_rectangle(cr, 10, 10, 100, 100); cairo_stroke(cr);这里先将设备空间中的 1 像素距离转换为用户空间距离再把这个距离作为线宽。这样无论缩放因子是多少最终渲染出来的线条都接近 1 个设备像素宽。这个方法在绘制缩略图、可缩放图表和地图时非常实用。6.5 缩放后的文字与位图文字同样受到 CTM 的影响。如果先缩放再调用cairo_show_text文字会相应放大或缩小。非等比缩放还会让文字产生挤压变形。位图图像表面作为图案绘制时也会受到 CTM 缩放的影响在第 14 章会详细讨论图案矩阵。7. 旋转变换cairo_rotate7.1 函数签名与语义void cairo_rotate(cairo_t *cr, double angle);cairo_rotate把用户空间围绕其原点顺时针旋转angle弧度。注意单位是弧度而不是角度并且旋转方向与设备坐标系的 y 轴方向有关。Cairo 默认设备坐标系 y 轴向下正角度表现为顺时针旋转。如果你更习惯使用角度可以写一个简单的转换宏#define PI 3.14159265358979323846 #define DEG2RAD(deg) ((deg) * PI / 180.0) cairo_rotate(cr, DEG2RAD(45.0));7.2 基本示例旋转一个正方形cairo_translate(cr, 200.0, 200.0); cairo_set_source_rgb(cr, 0.9, 0.3, 0.3); cairo_rectangle(cr, 0.0, -30.0, 120.0, 60.0); cairo_stroke(cr); cairo_rotate(cr, 3.14159265358979323846 / 4.0); cairo_set_source_rgb(cr, 0.2, 0.5, 0.9); cairo_rectangle(cr, 0.0, -30.0, 120.0, 60.0); cairo_stroke(cr);这段代码先平移原点到画布中心然后画一个水平矩形接着让用户空间绕原点旋转 45 度再次画同样的矩形。第二个矩形会以原点为中心旋转 45 度。由于矩形使用y -30作为上边界矩形中心大致位于原点旋转效果看起来像是绕矩形中心旋转。7.3 旋转始终围绕用户空间原点需要反复强调的是cairo_rotate永远围绕当前用户空间的 (0, 0) 点进行。如果你想绕任意一个点旋转不能直接调用cairo_rotate而需要先把原点移动到那个点。7.4 绕任意点旋转的标准做法要在用户空间中的点 (cx, cy) 处旋转图形典型步骤如下/* 1. 平移原点至旋转中心 */ cairo_translate(cr, cx, cy); /* 2. 旋转用户空间 */ cairo_rotate(cr, angle); /* 3. 把原点移回使得图形坐标相对旋转中心表达 */ cairo_translate(cr, -cx, -cy); /* 4. 正常绘制几何图形 */之所以第三步需要反向平移是因为我们希望保留图形在原始坐标系中的坐标表达同时让旋转轴落在 (cx, cy)。如果省略第三步图形会绕错误的位置旋转产生明显的偏移如果只做前两步也能绕点旋转但后续绘制的坐标都需要相对旋转中心重新计算代码会变得不直观。因此推荐始终使用「平移到旋转中心、旋转、反向平移」这个标准三段式。8. 变换组合顺序先旋转再平移与先平移再旋转前面反复提到Cairo 的cairo_translate、cairo_scale和cairo_rotate具有累积性它们会依次右乘到当前变换矩阵 CTM 上。由于矩阵乘法不满足交换律调用顺序会显著改变最终结果。这一章通过实例说明「先旋转再平移」和「先平移再旋转」为什么会产生完全不同的效果。8.1 右乘规则与「变换靠近图形」的直觉Cairo 的每个变换函数都相当于在旧 CTM 后面乘一个新的变换矩阵新CTM 旧CTM × 新变换这个公式可以理解为后调用的变换更靠近「图形绘制」这一端先调用的变换更靠近「设备映射」这一端。因此当你调用cairo_translate后再调用cairo_rotate相当于先把整个坐标系平移到某处再让这个坐标系绕原点旋转而反过来先旋转再平移则相当于先旋转整个坐标系再沿着旋转后的坐标轴方向平移。8.2 先平移再旋转绕画布中心旋转整个坐标系下面的代码先把原点平移到画布中心再旋转 30 度这样坐标系中心保持不动整个画布围绕中心旋转cairo_translate(cr, 200.0, 200.0); cairo_rotate(cr, DEG2RAD(30.0)); /* 此时 (0,0) 位于画布中心x 轴和 y 轴均旋转了 30 度 */ cairo_rectangle(cr, 0.0, 0.0, 120.0, 80.0); cairo_stroke(cr);此时矩形以原点为顶点沿旋转后的坐标轴方向展开。这是制作旋转 UI 组件、雷达图、圆形菜单最常用的组合。8.3 先旋转再平移平移方向被旋转如果顺序反过来先旋转再平移那么平移的方向会沿着旋转后的坐标轴移动而不是沿屏幕的水平或垂直方向cairo_rotate(cr, DEG2RAD(30.0)); cairo_translate(cr, 100.0, 0.0); /* 这个矩形会出现在距原点 100 个用户单位、方向为 30 度的位置 */ cairo_rectangle(cr, 0.0, -40.0, 120.0, 80.0); cairo_stroke(cr);这段代码中cairo_translate(cr, 100.0, 0.0)的 x 方向是旋转后的 x 轴因此矩形会落在原点的 30 度方向上而不是水平向右。这种组合可以用于让对象沿着某个倾斜方向移动例如模拟物体沿斜面滑动。8.4 实用结论从「变换对象」的角度思考当你需要实现绕点旋转时正确顺序是「平移到中心、旋转、反向平移」当你需要让对象沿旋转后的方向移动时应该「先旋转、再平移」当你需要把图形整体放大并平移到某个位置时通常「先缩放、再平移」这样图形尺寸和位置才符合直觉。顺序错误不会报错但会让图形出现在意想不到的位置调试时可以先检查变换调用顺序。9. 图形状态快照cairo_save 与 cairo_restoreCairo 上下文维护的是一整套图形状态如果在一个长流程中反复叠加变换很容易陷入「当前坐标系到底在哪里」的混乱。Cairo 提供的cairo_save与cairo_restore正是为了解决这个问题。9.1 save/restore 保存和恢复哪些状态cairo_save会把当前图形状态压入内部栈cairo_restore则弹出最近一次保存的状态。它保存的范围非常完整包括当前路径与当前点当前变换矩阵 CTM当前剪切区域线宽、线帽、连接样式颜色、渐变、图案字体、字号与文字样式复合操作符、抗锯齿模式等。因此cairo_save与cairo_restore才是恢复「整体绘图环境」的正确方式它比单独重置矩阵安全得多。9.2 典型配对用法下面是一个将局部变换与全局绘制隔离开来的典型结构/* 保存初始状态 */ cairo_save(cr); /* 局部修改平移、旋转、缩放、设置颜色等 */ cairo_translate(cr, 150.0, 150.0); cairo_rotate(cr, DEG2RAD(45.0)); cairo_set_source_rgb(cr, 0.8, 0.2, 0.2); cairo_rectangle(cr, -50.0, -50.0, 100.0, 100.0); cairo_fill(cr); /* 恢复到保存时的状态清除局部变换、颜色和路径 */ cairo_restore(cr);调用cairo_restore之后CTM、颜色、路径等全部回到保存时的状态完全不会影响后面的绘制。这种写法在绘制多个独立图形时非常推荐。9.3 save/restore 与 identity_matrix 的区别cairo_identity_matrix只重置 CTM不清除剪切区域、颜色、线宽和其他状态。而cairo_save与cairo_restore是成对出现的完整状态快照。如果你只需要把矩阵恢复成单位矩阵继续沿用当前颜色和线宽可以使用cairo_identity_matrix如果希望彻底撤出一段局部的绘图配置应该优先使用cairo_save和cairo_restore。10. 直接读写变换矩阵矩阵 API 与用户自定义变换Cairo 不仅提供了cairo_translate、cairo_scale、cairo_rotate三个便捷函数还允许直接设置和读取 CTM。这让我们可以实现任意仿射变换包括斜切、镜像、复合变换等。10.1 cairo_set_matrix 设置完整矩阵void cairo_set_matrix(cairo_t *cr, const cairo_matrix_t *matrix);cairo_set_matrix会把传入的矩阵整体替换为新的 CTM。与便捷函数不同它不是右乘而是直接覆盖。使用前需要先构造一个cairo_matrix_t结构体。10.2 cairo_get_matrix 获取当前矩阵void cairo_get_matrix(cairo_t *cr, cairo_matrix_t *matrix);cairo_get_matrix会把当前 CTM 复制到matrix中。结合cairo_set_matrix你可以实现矩阵的保存、修改和恢复。例如在某个局部逻辑中保存旧矩阵设置新矩阵绘制完成后再恢复旧矩阵。10.3 使用 cairo_matrix_init 族函数构造矩阵Cairo 提供多个初始化函数cairo_matrix_init_identity(cairo_matrix_t *matrix); cairo_matrix_init_translate(cairo_matrix_t *matrix, double tx, double ty); cairo_matrix_init_scale(cairo_matrix_t *matrix, double sx, double sy); cairo_matrix_init_rotate(cairo_matrix_t *matrix, double radians);这些函数分别生成单位矩阵、平移矩阵、缩放矩阵和旋转矩阵。设置完结构体后再调用cairo_set_matrix即可应用。10.4 用户自定义矩阵的典型场景当需要实现不支持便捷函数的变换时可以手工填充cairo_matrix_t的六个字段。例如沿 x 轴斜切 0.3 的矩阵为cairo_matrix_t shear_x; cairo_matrix_init(shear_x, 1.0, 0.0, 0.3, 1.0, 0.0, 0.0); cairo_set_matrix(cr, shear_x);这种直接构造方式非常灵活几乎所有 2D 仿射变换都可以表示。11. 斜切变换用矩阵实现 skew 与 shear在中文资料中「剪切」一词经常被用来描述斜切变形。Cairo 虽然没有独立的cairo_shear函数但借助矩阵 API 可以轻松实现水平斜切和垂直斜切。11.1 水平斜切与垂直斜切沿 x 轴方向斜切的变换公式为x x k * y y y对应的矩阵字段是xy k其余保持单位矩阵。斜切系数k决定了斜切角度k越大图形越倾斜。沿 y 轴方向斜切的公式为x x y y m * x对应的矩阵字段是yx m。11.2 cairo_matrix_init 构造斜切矩阵下面示例将一个正方形向右倾斜 0.4cairo_matrix_t shear; cairo_matrix_init(shear, 1.0, 0.0, 0.4, 1.0, 0.0, 0.0); cairo_set_matrix(cr, shear); cairo_rectangle(cr, 50, 50, 100, 100); cairo_fill(cr);绘制出来的图形会是一个平行四边形底边向右倾斜。如果希望围绕图形中心斜切需要配合平移把中心移动到原点应用斜切后再平移回去。11.3 斜切在文字阴影与伪 3D 效果中的应用斜切变换经常被用来制作伪 3D 效果例如让文字沿 x 轴倾斜模拟斜体或透视效果。做法是先绘制原始文字再设置斜切矩阵绘制一层半透明文字并让两次绘制错开一点位置就能产生类似阴影或立体的视觉效果。12. 裁剪路径基础cairo_clip 原理与 API变换决定图形「画在哪里、怎么变形」而裁剪路径决定图形「哪些区域可以显示出来」。Cairo 使用当前路径作为裁剪区域通过cairo_clip系列函数限制后续绘制的作用范围。12.1 裁剪的基本流程使用裁剪路径通常需要以下步骤构建一个或多个几何形状路径调用cairo_clip把当前路径设置为裁剪区域之后所有绘制只会出现在裁剪区域内部如需恢复可以使用cairo_save与cairo_restore包裹局部裁剪。12.2 裁剪与描边、填充的区别cairo_clip不会直接产生可见输出它只是把当前路径转成裁剪区域。而cairo_stroke和cairo_fill会立即绘制。裁剪区域一旦设置就持续影响后续所有绘制直到被 save/restore 恢复或显式重置。12.3 cairo_clip_preserve 保留路径cairo_clip会清空当前路径而cairo_clip_preserve在设置裁剪区域后保留路径方便你继续描边或填充。例如想把一个圆作为裁剪区域同时画出圆的轮廓cairo_arc(cr, 100, 100, 60, 0, 2 * 3.14159265358979323846); cairo_clip_preserve(cr); /* 设置裁剪并保留路径 */ cairo_set_source_rgb(cr, 0.2, 0.2, 0.2); cairo_stroke(cr); /* 描边外轮廓 */ cairo_set_source_rgb(cr, 0.8, 0.2, 0.2); cairo_rectangle(cr, 0, 0, 200, 200); cairo_fill(cr); /* 只填充圆形内部 */12.4 裁剪区域是持久状态裁剪区域也是图形状态的一部分会和变换一样持续影响后续绘制。也就是说如果你先设置了一个矩形裁剪再绘制任何内容这些内容都会被矩形裁剪。因此建议总是用cairo_save和cairo_restore把裁剪范围局部化。13. 剪切路径进阶圆角裁剪、挖空效果与图片遮罩掌握cairo_clip后可以组合出非常多实用的裁剪效果。这一章聚焦于复杂裁剪任务。13.1 圆角矩形裁剪CAIRO 没有直接的圆角矩形函数但可以通过圆弧和直线手动画出圆角矩形路径然后cairo_clipvoid rounded_rect_path(cairo_t *cr, double x, double y, double w, double h, double r) { cairo_move_to(cr, x r, y); cairo_line_to(cr, x w - r, y); cairo_arc(cr, x w - r, y r, r, -3.14159265358979323846 / 2, 0); cairo_line_to(cr, x w, y h - r); cairo_arc(cr, x w - r, y h - r, r, 0, 3.14159265358979323846 / 2); cairo_line_to(cr, x r, y h); cairo_arc(cr, x r, y h - r, r, 3.14159265358979323846 / 2, 3.14159265358979323846); cairo_line_to(cr, x, y r); cairo_arc(cr, x r, y r, r, 3.14159265358979323846, 3.14159265358979323846 * 1.5); cairo_close_path(cr); } /* 使用此路径进行裁剪即可得到圆角裁剪效果 */13.2 利用 even-odd 规则挖空内容Cairo 支持设置填充规则为 even-odd通过cairo_set_fill_rule(cr, CAIRO_FILL_RULE_EVEN_ODD)可以让嵌套路径产生挖空效果。例如先画一个大圆再画一个小圆使用 even-odd 规则设置裁剪后小圆内部会被挖空。13.3 把裁剪区域限制在多个几何形状中可以把多个几何形状添加到当前路径然后一次性cairo_clip。最终裁剪区域是这些形状的并集。如果希望交集效果需要配合 even-odd 规则或多次嵌套裁剪。13.4 图片遮罩与 PDF 导出裁剪路径最常见的用途之一是把图片限制在某个形状内例如把用户头像裁成圆形。实现方式是创建圆形路径cairo_clip然后使用cairo_set_source_surface叠加图片这样图片只会显示在圆形内部。当输出到 PDF 或 SVG 时裁剪路径依然生效非常适合生成头像、图标等资源。14. 变换对线宽、文字、渐变和图案的影响CTM 会影响后续所有绘制不仅限于几何图形的顶点坐标还影响线宽、文字字形、渐变方向和图案平铺。理解这些影响有助于避免视觉错误和性能问题。14.1 线宽用户空间单位与设备像素之前已经展示过缩放下线宽会成倍变化。这里补充一点非等比缩放下水平方向和垂直方向的线宽会不同。例如cairo_scale(cr, 4.0, 1.0)会让线条在水平方向变粗 4 倍在垂直方向保持原样导致描边看起来像「拉长」的椭圆边缘。要获得均匀线宽需要使用cairo_device_to_user_distance计算两个方向的距离并取合适的值。14.2 文字字形变换与可读性文字也是矢量路径因此经过非等比缩放后会变形。在旋转或缩放后文字仍然保持清晰因为 Cairo 会重新光栅化字形。但要注意如果先缩放再显示文字字号的视觉效果会改变如果希望文字始终保持固定设备大小可以使用cairo_device_to_user把设备尺寸转换成用户空间尺寸再设置字号。14.3 渐变与图案变换矩阵的叠加Cairo 的渐变色线性渐变、径向渐变和图案pattern也有自己的变换矩阵。当设置渐变或图案时也可以调用cairo_pattern_set_matrix单独设置图案矩阵它和 CTM 相乘后共同决定最终渲染。因此即使 CTM 发生变化也可以通过图案矩阵控制渐变或纹理的局部表现。在第 15 章的实战案例中可以看到图案矩阵的用法。15. 五个完整实战案例这一章把前面学到的平移、旋转、缩放、矩阵读写和裁剪路径结合起来给出五个完整可运行的案例。每个案例都包含核心代码和简要说明。15.1 案例 1动态时钟表盘目标是绘制一个带刻度和时针、分针的时钟。核心思路是平移到表盘中心然后根据角度旋转绘制刻度线再旋转绘制指针。cairo_translate(cr, 200, 200); for (int i 0; i 12; i) { cairo_save(cr); cairo_rotate(cr, i * 2.0 * 3.14159265358979323846 / 12.0); cairo_move_to(cr, 0, -90); cairo_line_to(cr, 0, -100); cairo_stroke(cr); cairo_restore(cr); } /* 时针旋转到对应小时角度 */ cairo_save(cr); cairo_rotate(cr, 3.0 * 2.0 * 3.14159265358979323846 / 12.0); cairo_set_source_rgb(cr, 0.1, 0.1, 0.1); cairo_set_line_width(cr, 8); cairo_move_to(cr, 0, 0); cairo_line_to(cr, 0, -50); cairo_stroke(cr); cairo_restore(cr);15.2 案例 2径向排列的菜单按钮圆形菜单中的按钮沿圆周排列。核心是平移到中心然后旋转不同角度再沿旋转后的 y 轴负方向平移绘制按钮。cairo_translate(cr, 200, 200); int count 8; double radius 120; for (int i 0; i count; i) { cairo_save(cr); double angle i * 2.0 * 3.14159265358979323846 / count; cairo_rotate(cr, angle); cairo_translate(cr, 0, -radius); cairo_arc(cr, 0, 0, 20, 0, 2 * 3.14159265358979323846); cairo_fill(cr); cairo_restore(cr); }15.3 案例 3批量生成缩略图把一个大尺寸绘图缩放到 1/4 大小居中摆放。这里使用cairo_scale配合居中平移double scale 0.25; cairo_translate(cr, 200, 200); cairo_scale(cr, scale, scale); cairo_translate(cr, -w / 2.0, -h / 2.0); draw_scene(cr); /* 原始尺寸绘图函数 */这种方式可以保证内容按比例缩小且居中显示适合生成缩略图。15.4 案例 4头像圆角裁剪把头加载为 surface用圆形路径裁剪后绘制到画布上cairo_save(cr); cairo_arc(cr, 100, 100, 50, 0, 2 * 3.14159265358979323846); cairo_clip(cr); cairo_set_source_surface(cr, avatar_surface, 50, 50); cairo_paint(cr); cairo_restore(cr);如果需要描边可以调用cairo_clip_preserve再描边。15.5 案例 5可缩放坐标轴与网格绘制带有自适应缩放的坐标轴和网格。根据视口大小计算缩放比例再使用cairo_scale和cairo_translate把数据坐标映射到画布坐标。核心是使用cairo_device_to_user或直接设置矩阵double data_min 0.0, data_max 100.0; double view_width 800.0, view_height 600.0; double margin 40.0; double sx (view_width - 2 * margin) / (data_max - data_min); double sy (view_height - 2 * margin) / (data_max - data_min); cairo_translate(cr, margin, margin); cairo_scale(cr, sx, -sy); /* y 轴翻转使数据向上增加 */ cairo_translate(cr, -data_min, -data_min); draw_axes_and_grid(cr);这个案例把用户空间改成常见的数据坐标系便于后续按数据值直接绘制点、线和柱状图。16. 常见问题、调试与性能优化实际开发中变换相关的问题占 Cairo 调试时间的很大比例。下面列出几个高频问题和排查思路。16.1 坐标原点总在左上角怎么办如果你希望使用数学坐标系y 轴向上可以在设置完表面后执行cairo_translate(cr, 0, height); cairo_scale(cr, 1.0, -1.0);这样原点会移动到左下角y 轴方向向上。之后所有绘制都符合常见数学坐标。16.2 图形位置错乱的排查思路如果图形跑到了意料之外的地方优先检查是否在错误的位置调用了变换函数变换顺序是否符合预期是否忘记 save/restore导致变换累积到后续绘制是否使用了未初始化的矩阵是否在设置裁剪后没有恢复裁剪区域。16.3 线宽不一致的处理如果缩放下线宽忽粗忽细使用cairo_device_to_user_distance把需要的设备像素宽度转换回用户空间再设置线宽。或者尽量在缩放之前设置线宽并避免非等比缩放。16.4 裁剪区域无法恢复如果发现裁剪区域一直生效可能是因为没有使用 save/restore。Cairo 的裁剪区域是持久状态只会越来越小不会自动扩大。解决办法是在每次局部裁剪前cairo_save使用后cairo_restore。16.5 性能方面的注意事项虽然变换和裁剪在 Cairo 中非常高效但在大量重复绘制时仍应注意避免在循环内反复创建和设置矩阵把不变的几何路径保存为 path 对象减少不必要的 save/restore只在必要时使用对于复杂裁剪优先使用简单几何形状组合避免过密的曲线路径适当使用cairo_push_group和cairo_pop_group隔离绘制层。17. 总结与学习资源本文从设备空间与用户空间的关系出发系统讲解了 Cairo 的平移、旋转、缩放、斜切、矩阵读写和剪切路径。核心要点可以概括为变换是用户空间到设备空间的映射当前变换矩阵 CTM 决定坐标如何映射cairo_translate、cairo_scale、cairo_rotate具有累积性调用顺序影响结果使用cairo_save与cairo_restore管理局部状态是最佳实践斜切变换需要通过cairo_matrix_t和cairo_set_matrix实现剪切路径通过cairo_clip系列函数设置配合裁剪规则可以实现复杂遮罩效果。如果想要继续深入学习推荐查阅 Cairo 官方文档、示例代码以及《Cairo Graphics》相关书籍。重点动手复现本文的五个实战案例并根据自己的业务场景调整变换顺序和裁剪策略。