3、绘制基础:Canvas绘图API、颜色格式、抗锯齿原理
说实话,很多做TouchGFX的朋友一开始都绕过了Canvas。觉得它慢,觉得它复杂,不如直接用现成的控件。我以前也这么想,直到有一次客户要求做一个圆形的进度环,还得带渐变和阴影——标准控件根本搞不定。没办法,硬着头皮啃Canvas,结果发现这东西其实没那么玄乎。
这一章,我们就来把Canvas的底裤扒干净。你搞懂了这三个东西——绘图API、颜色格式、抗锯齿原理,后面做自定义控件就跟玩似的。
3.1 Canvas绘图API:你手里的“画笔”
TouchGFX的Canvas,说白了就是一块画布。你可以在上面画线、画圆、画弧、画路径。它的核心API都在touchgfx::Canvas这个类里。我习惯把它想象成一支带参数配置的笔——你告诉它画什么、怎么画,它就给你画出来。
3.1.1 基本绘图操作
先看几个最常用的API:
// 画一条直线
Canvas canvas(screenBuffer, CANVAS_BUFFER_SIZE);
canvas.moveTo(10, 10);
canvas.lineTo(100, 100);
canvas.stroke();
// 画一个矩形
canvas.moveTo(20, 20);
canvas.lineTo(80, 20);
canvas.lineTo(80, 80);
canvas.lineTo(20, 80);
canvas.closePath();
canvas.stroke();
// 画一个圆弧
canvas.arc(centerX, centerY, radius, startAngle, endAngle);
canvas.stroke();
这里有个坑——moveTo和lineTo是基础操作,但很多人忘了closePath。如果你画一个封闭图形但不调用closePath,它最后一条边就不会自动闭合。我曾经在一个仪表盘项目里漏了这个,指针尾部一直有个小缺口,找了半天才找到原因。
3.1.2 路径与填充
Canvas支持两种绘制模式:描边(stroke)和填充(fill)。描边就是画轮廓,填充就是涂颜色。你可以组合使用:
canvas.beginPath();
canvas.moveTo(50, 50);
canvas.lineTo(150, 50);
canvas.lineTo(100, 150);
canvas.closePath();
// 先填充,再描边
canvas.fill(Color::getColorFrom24BitRGB(0x34, 0x98, 0xDB));
canvas.stroke(Color::getColorFrom24BitRGB(0x29, 0x80, 0xB9));
注意顺序——先填充再描边,否则描边会被填充盖住。我刚开始做的时候顺序搞反了,描边线总是若隐若现,还以为是抗锯齿的问题。
3.1.3 线宽与样式
Canvas允许你设置线宽、线帽和连接样式:
| 属性 | 方法 | 说明 |
|---|---|---|
| 线宽 | setLineWidth(width) |
像素单位,默认1 |
| 线帽 | setLineCap(LineCap::ROUND) |
BUTT/ROUND/SQUARE |
| 连接样式 | setLineJoin(LineJoin::ROUND) |
MITER/BEVEL/ROUND |
3.2 颜色格式:别让颜色“失真”
TouchGFX支持多种颜色格式。你选错了格式,显示出来的颜色就会偏色、断层,甚至完全不对。我见过有人用RGB888的图片往RGB565的屏幕上怼,结果整个画面像蒙了一层纱。
3.2.1 常见颜色格式对比
| 格式 | 位数 | 颜色数 | 适用场景 |
|---|---|---|---|
| RGB565 | 16位 | 65,536 | 大多数MCU屏幕 |
| RGB888 | 24位 | 16,777,216 | 高性能MCU或RGB接口屏 |
| ARGB8888 | 32位 | 16,777,216 + Alpha | 需要透明效果的场景 |
| BW | 1位 | 2 | 低端单色屏 |
RGB565是嵌入式里最常用的。为什么?因为它占内存少,传输快。但代价是颜色精度低——你从RGB888转成RGB565,颜色会丢失一些细节。比如纯红色(0xFF0000)转成RGB565是0xF800,看起来没问题。但一些中间色,比如浅紫色,转过去可能就变成灰蒙蒙的了。
3.2.2 颜色转换与操作
TouchGFX提供了Color工具类来处理颜色转换:
// 从RGB888获取颜色
colortype c = Color::getColorFrom24BitRGB(0x34, 0x98, 0xDB);
// 从RGB565获取颜色
colortype c = Color::getColorFromRGB565(0x34DB);
// 获取颜色的RGB分量
uint8_t r = Color::getRedColor(c);
uint8_t g = Color::getGreenColor(c);
uint8_t b = Color::getBlueColor(c);
colortype类型。不要直接传RGB值。我见过有人写canvas.stroke(0xFF0000),结果画出来是黑色——因为colortype是16位的,0xFF0000被截断了。
3.2.3 颜色格式对性能的影响
选颜色格式不只是视觉问题,还直接影响性能。RGB565的Canvas缓冲区大小是:宽度 × 高度 × 2字节。RGB888是宽度 × 高度 × 3字节。如果你的MCU内存只有64KB,用RGB888画一个320×240的Canvas,缓冲区就要230KB——直接爆了。
我一般这样选:
- 内存紧张:RGB565,牺牲一点颜色精度
- 需要透明:ARGB8888,但只在小区域用
- 显示图片为主:RGB565,图片本身已经做了抖动优化
3.3 抗锯齿原理:让边缘不再“狗啃”
你想想看,屏幕上的像素是方形的。画一条斜线,如果不做处理,边缘就是锯齿状的——这就是“狗啃”效果。抗锯齿(Anti-Aliasing)就是通过模糊边缘像素来消除这种锯齿感。
3.3.1 抗锯齿是怎么工作的?
TouchGFX的Canvas默认开启了抗锯齿。它的原理很简单:
- 计算线条经过的像素区域
- 根据线条穿过像素的面积比例,决定该像素的透明度
- 边缘像素不是全黑或全白,而是半透明的过渡色
举个例子:一条45度斜线穿过一个像素,只覆盖了像素的30%。那么抗锯齿算法就会把这个像素填充为30%的线条颜色 + 70%的背景颜色。人眼看起来就是平滑的过渡。
3.3.2 抗锯齿的代价
抗锯齿不是免费的。它需要额外的计算和内存:
| 模式 | 计算量 | 内存占用 | 视觉效果 |
|---|---|---|---|
| 无抗锯齿 | 低 | 低 | 锯齿明显 |
| 2x抗锯齿 | 中 | 中 | 边缘平滑 |
| 4x抗锯齿 | 高 | 高 | 非常平滑 |
TouchGFX默认使用2x抗锯齿。对于大多数嵌入式屏幕来说,这个平衡点刚刚好。如果你发现绘制性能不够,可以考虑关闭抗锯齿:
canvas.setAntiAlias(false);
但我不建议这么做。除非你的屏幕分辨率极低(比如128×64),或者你画的是纯水平/垂直线——这些线本来就没有锯齿。
3.4 知识体系总览
下面这张图把本章的核心知识点串起来了。你可以看到Canvas绘图API、颜色格式、抗锯齿原理三者之间的关系:
3.5 实战:画一个带抗锯齿的圆弧
光说不练假把式。我们写一个完整的例子,画一个带抗锯齿的圆弧,同时演示颜色格式和Canvas API的配合:
// 假设屏幕颜色格式为RGB565
// Canvas缓冲区
uint16_t canvasBuffer[CANVAS_BUFFER_SIZE];
void drawArcWithAA(int16_t centerX, int16_t centerY, int16_t radius,
int16_t startAngle, int16_t endAngle, colortype color)
{
Canvas canvas(canvasBuffer, CANVAS_BUFFER_SIZE);
// 开启抗锯齿(默认就是开启的,但显式设置更安全)
canvas.setAntiAlias(true);
// 设置线宽和样式
canvas.setLineWidth(4);
canvas.setLineCap(Canvas::LineCap::ROUND);
canvas.setLineJoin(Canvas::LineJoin::ROUND);
// 画圆弧
canvas.beginPath();
canvas.arc(centerX, centerY, radius, startAngle, endAngle);
canvas.stroke(color);
// 刷新显示区域
HAL::getInstance()->flushFrameBuffer(
Rect(centerX - radius - 5, centerY - radius - 5,
radius * 2 + 10, radius * 2 + 10));
}