第五章:状态栈与变换¶
本章目标:理解 Canvas 的状态管理机制(save/restore),掌握平移、缩放、旋转等坐标变换,以及矩形和路径裁剪。
5.1 Canvas 状态栈¶
Canvas 内部维护一个状态栈。每次调用 save() 会将当前状态(变换矩阵、裁剪区域)压入栈中;调用 restore() 会弹出并恢复之前的状态。
这是避免绘制操作相互干扰的核心机制。
canvas->save();
// 在保存的状态下做变换和绘制
canvas->translate(100, 100);
canvas->drawRect(wsc::RectF(0, 0, 50, 50), paint);
canvas->restore(); // 变换被撤销
// 此处坐标系恢复原样
canvas->drawRect(wsc::RectF(0, 0, 50, 50), paint); // 左上角
嵌套 save¶
canvas->save(); // saveCount = 1
canvas->translate(50, 0);
canvas->save(); // saveCount = 2
canvas->translate(0, 50);
// 此时偏移 (50, 50)
canvas->restore(); // 恢复到偏移 (50, 0)
canvas->restore(); // 恢复到无偏移
restoreToCount¶
可以一次恢复到指定的保存层级:
int count = canvas->save(); // 记录保存点
canvas->save();
canvas->save();
// ... 多层操作 ...
canvas->restoreToCount(count); // 一次回到最初
5.2 平移 (translate)¶
将坐标原点移动到新位置:
canvas->save();
canvas->translate(150, 100);
// 以 (150, 100) 为原点绘制
wsc::Paint paint;
paint.setColor(wsc::Color(66, 133, 244, 255));
canvas->drawRect(wsc::RectF(0, 0, 100, 80), paint); // 实际位置 (150, 100)
canvas->restore();
5.3 缩放 (scale)¶
以当前原点为中心进行缩放:
canvas->save();
canvas->translate(200, 200); // 先移动到画布中心
canvas->scale(2.0f, 2.0f); // 放大 2 倍
// 绘制一个"逻辑上"50x50 的矩形,实际渲染为 100x100
wsc::Paint paint;
paint.setColor(wsc::Color(76, 175, 80, 255));
canvas->drawRect(wsc::RectF(-25, -25, 50, 50), paint);
canvas->restore();
镜像翻转¶
5.4 旋转 (rotate)¶
以当前原点为中心旋转,角度单位为弧度:
float pi = 3.14159265f;
canvas->save();
canvas->translate(200, 200); // 移到旋转中心
canvas->rotate(pi / 4.0f); // 旋转 45°
wsc::Paint paint;
paint.setColor(wsc::Color(255, 152, 0, 255));
paint.setAntiAlias(true);
canvas->drawRect(wsc::RectF(-60, -60, 120, 120), paint);
canvas->restore();
关键技巧:旋转总是绕当前原点。如果要绕某个点旋转: 1.
translate(cx, cy)— 移到旋转中心 2.rotate(angle)— 旋转 3.translate(-cx, -cy)— 移回(或直接以中心为原点绘制)
5.5 变换组合¶
变换是按调用顺序累积的。顺序不同,结果不同:
// 先平移再旋转
canvas->save();
canvas->translate(200, 200);
canvas->rotate(pi / 6);
canvas->drawRect(wsc::RectF(-40, -40, 80, 80), paint);
canvas->restore();
// 先旋转再平移(结果不同!)
canvas->save();
canvas->rotate(pi / 6);
canvas->translate(200, 200);
canvas->drawRect(wsc::RectF(-40, -40, 80, 80), paint);
canvas->restore();
矩阵操作¶
对于更复杂的变换,可以直接操作矩阵:
// 获取当前矩阵
wsc::Matrix4 mat = canvas->getMatrix();
// 设置自定义矩阵
canvas->setMatrix(customMatrix);
// 重置为单位矩阵
canvas->resetMatrix();
// 连接(右乘)矩阵
canvas->concat(additionalMatrix);
5.6 坐标映射¶
将点从局部坐标映射到设备坐标(或反向):
canvas->save();
canvas->translate(100, 50);
canvas->scale(2.0f, 2.0f);
// 正向映射:局部坐标 → 设备坐标
wsc::PointF devicePt = canvas->mapPoint(wsc::PointF(10, 20));
// devicePt = (100 + 10*2, 50 + 20*2) = (120, 90)
// 反向映射:设备坐标 → 局部坐标(用于处理点击事件)
wsc::PointF localPt;
canvas->inverseMapPoint(wsc::PointF(120, 90), localPt);
// localPt = (10, 20)
canvas->restore();
5.7 裁剪 (Clip)¶
裁剪限制了后续绘制的可见区域。超出裁剪区域的内容不会被渲染。
矩形裁剪¶
canvas->save();
// 设置裁剪区域
canvas->clipRect(wsc::RectF(50, 50, 200, 200));
// 这个圆会被裁剪为矩形内的部分
wsc::Paint paint;
paint.setColor(wsc::Color(244, 67, 54, 255));
paint.setAntiAlias(true);
canvas->drawCircle(150, 150, 120, paint);
canvas->restore(); // 裁剪区域也被恢复
路径裁剪¶
使用任意路径作为裁剪区域:
canvas->save();
// 圆形裁剪区域
wsc::Path clipCircle;
clipCircle.addCircle(200, 200, 100);
canvas->clipPath(clipCircle);
// 在圆形区域内绘制图片或复杂内容
canvas->drawRect(wsc::RectF(0, 0, 400, 400), gradientPaint);
canvas->restore();
裁剪的累积特性¶
多次裁剪会取交集(区域越来越小):
canvas->save();
canvas->clipRect(wsc::RectF(50, 50, 200, 200));
canvas->clipRect(wsc::RectF(100, 100, 200, 200));
// 最终裁剪区域 = 两个矩形的交集 = (100, 100, 150, 150)
canvas->restore();
5.8 Quick Reject¶
在大量绘制操作中,quickReject 可以快速判断某个区域是否完全在裁剪区域之外,从而跳过不必要的绘制:
canvas->clipRect(wsc::RectF(0, 0, 200, 200));
// 快速判断:如果完全在裁剪区外就跳过
if (!canvas->quickReject(wsc::RectF(300, 300, 100, 100))) {
canvas->drawRect(wsc::RectF(300, 300, 100, 100), paint);
}
5.9 设备像素比(DPR)¶
DPR 不是普通的内容缩放。Canvas 仍以物理像素创建,DPR 作为根变换把逻辑坐标映射到 framebuffer:
int framebufferWidth = 720;
int framebufferHeight = 820;
float dpr = 2.0f;
auto canvas = wsc::Canvas::create(
wsc::Canvas::Backend::Software,
framebufferWidth,
framebufferHeight);
canvas->setDevicePixelRatio(dpr);
float logicalWidth = framebufferWidth / dpr; // 360
float logicalHeight = framebufferHeight / dpr; // 410
此后绘制 RectF(0, 0, 100, 40) 会占用 100 × 40 个逻辑单位,并输出为 200 × 80 个物理像素。文字也按当前 DPR 直接在目标分辨率栅格化,不需要先画一张低分辨率位图再放大。
需要注意以下行为:
Canvas::create、setSize、getWidth和getHeight使用物理像素。- 绘制坐标、描边宽度和
Paint::setTextSize使用逻辑单位。 setDevicePixelRatio会把当前矩阵重置为新的 DPR 根变换,应在业务translate、scale、rotate之前调用。resetMatrix()只清除业务变换,不会丢失 DPR。- 不要再执行
canvas->scale(dpr, dpr),否则几何和文字都会重复缩放。
窗口尺寸或显示器缩放发生变化时,应一起更新物理尺寸、DPR 和逻辑布局尺寸。点击或触摸坐标也要转换到相同的逻辑坐标系后再做 hit-testing。
5.10 综合示例:旋转的花朵¶

效果重点:每片花瓣都在局部坐标系中绘制,通过 save、translate、rotate 和 restore 围绕同一中心排列。辅助圆保留在效果图中,用来提示旋转半径。下方代码与生成图片的可编译源码相同。
// Chapter 05 comprehensive example: a flower composed with save/restore transforms.
#include <wsc/wsc.h>
#include <cmath>
int main()
{
// 1. 创建离屏画布并定义花朵的共享几何数据。
auto canvas = wsc::Canvas::create(wsc::Canvas::Backend::Software, 720, 720);
if (!canvas || !canvas->initializeContext()) return 1;
constexpr float pi = 3.14159265f, cx = 360.0f, cy = 350.0f;
constexpr int petalCount = 10;
const wsc::Color colors[] = {
wsc::Color(255, 112, 132, 238), wsc::Color(255, 139, 113, 238),
wsc::Color(250, 169, 92, 238), wsc::Color(241, 114, 151, 238),
wsc::Color(216, 103, 169, 238), wsc::Color(180, 104, 190, 238),
wsc::Color(139, 111, 204, 238), wsc::Color(111, 130, 215, 238),
wsc::Color(91, 151, 217, 238), wsc::Color(91, 177, 197, 238),
};
// 2. 绘制背景和两条辅助圆,辅助圆用于说明旋转半径。
canvas->beginFrame();
wsc::Paint background;
background.setLinearGradient(0, 0, 720, 720,
wsc::Color(252, 248, 246, 255), wsc::Color(238, 243, 251, 255));
canvas->drawRect(wsc::RectF(0, 0, 720, 720), background);
wsc::Paint guide;
guide.setStyle(wsc::Paint::Style::STROKE);
guide.setStrokeWidth(2.0f);
guide.setColor(wsc::Color(142, 157, 184, 34));
guide.setAntiAlias(true);
canvas->drawCircle(cx, cy, 205, guide);
canvas->drawCircle(cx, cy, 92, guide);
// 3. 先绘制位于花瓣下方的茎和叶子。
wsc::Paint stem;
stem.setStyle(wsc::Paint::Style::STROKE);
stem.setStrokeWidth(18.0f);
stem.setStrokeCap(wsc::Paint::StrokeCap::ROUND);
stem.setColor(wsc::Color(72, 153, 116, 255));
stem.setAntiAlias(true);
canvas->drawLine(cx, cy + 48.0f, cx, 650.0f, stem);
wsc::Paint leaf;
leaf.setColor(wsc::Color(86, 177, 132, 235));
leaf.setAntiAlias(true);
canvas->save();
canvas->translate(cx - 8, 535);
canvas->rotate(-0.78f);
canvas->drawOval(wsc::RectF(-20, -12, 110, 48), leaf);
canvas->restore();
canvas->save();
canvas->translate(cx + 6, 585);
canvas->rotate(0.72f);
leaf.setColor(wsc::Color(58, 149, 107, 230));
canvas->drawOval(wsc::RectF(-85, -12, 110, 48), leaf);
canvas->restore();
// 4. 每片花瓣都在相同局部坐标中绘制,只改变画布旋转角度。
for (int i = 0; i < petalCount; ++i) {
canvas->save();
canvas->translate(cx, cy);
canvas->rotate(i * 2.0f * pi / petalCount);
wsc::Paint petal;
petal.setColor(colors[i]);
petal.setAntiAlias(true);
petal.setShadowLayer(12, 0, 8, wsc::Color(79, 58, 92, 34));
canvas->drawOval(wsc::RectF(-40, -226, 80, 172), petal);
canvas->restore();
}
// 5. 中心圆最后绘制,用来遮住花瓣交叠处。
wsc::Paint center;
center.setRadialGradient(cx - 20, cy - 20, 78,
wsc::Color(255, 232, 104, 255), wsc::Color(240, 176, 52, 255));
center.setAntiAlias(true);
center.setShadowLayer(14, 0, 6, wsc::Color(120, 78, 20, 50));
canvas->drawCircle(cx, cy, 66, center);
wsc::Paint seed;
seed.setColor(wsc::Color(196, 125, 42, 115));
for (int i = 0; i < 12; ++i) {
const float a = i * 2.0f * pi / 12.0f;
canvas->drawCircle(cx + std::cos(a) * 34, cy + std::sin(a) * 34, 4, seed);
}
// 6. 提交并保存结果图。
canvas->endFrame();
return canvas->savePixelsPPM("chapter05_flower.ppm") ? 0 : 2;
}
5.11 小结¶
本章学习了:
- [x] save/restore 状态栈机制
- [x] 平移 (translate)、缩放 (scale)、旋转 (rotate)
- [x] 变换的组合顺序与效果
- [x] 矩阵直接操作
- [x] 坐标映射(正向和反向)
- [x] 矩形裁剪和路径裁剪
- [x] Quick Reject 优化
- [x] 设备像素比
下一章:图片绘制 —— 学习加载、绘制和变换图片。