WhatsCanvas —— 一个介于 NanoVG 和 Skia 之间的 C++17 2D 渲染库¶
本文介绍 WhatsCanvas,一个面向原生应用的可嵌入式 2D 渲染库。如果你正在寻找一个 API 风格接近 HTML Canvas、支持多后端渲染、具备完整文本排版和图层滤镜能力的 C++ 绘图方案,这篇文章值得一读。
一、背景:为什么需要 WhatsCanvas?¶
在原生 C++ 2D 渲染领域,开发者通常面临这样的选择:
| 方案 | 优点 | 缺点 |
|---|---|---|
| NanoVG | 轻量、易集成 | 无多语言文本、无图层滤镜、无像素回归 |
| Skia | 功能完整、工业级 | 体量大、构建复杂、API 面广 |
| Cairo | 成熟稳定 | 文本能力有限、缺少 GPU 后端 |
WhatsCanvas 的定位正好填补了 NanoVG 和 Skia 之间的空白:
- 提供 Canvas / Paint / Path 三件套 API(类似 HTML Canvas,对前端开发者友好)
- 支持 多语言文本 (CJK、RTL、双向文本、HarfBuzz shaping)
- 内置 图层滤镜 (模糊、内阴影、毛玻璃效果)
- 支持 5 种渲染后端 (Software、OpenGL、OpenGL ES、Vulkan、Metal)
- 覆盖 6 个平台 (Windows、Linux、macOS、Android、iOS、Web)
- 配备完整的 像素回归测试 基础设施
二、快速上手:60 秒画出第一帧¶
WhatsCanvas 提供 Software 后端,无需 GPU、窗口或图形上下文,非常适合快速验证:
#include <wsc/wsc.h>
int main()
{
// 创建 256x256 的 Software Canvas
auto canvas = wsc::Canvas::create(wsc::Canvas::Backend::Software, 256, 256);
if (!canvas || !canvas->initializeContext()) {
return 1;
}
canvas->beginFrame();
// 设置画笔:蓝色填充 + 抗锯齿
wsc::Paint fill;
fill.setColor(wsc::Color(40, 120, 240, 255));
fill.setAntiAlias(true);
// 绘制圆角矩形
canvas->drawRoundRect(wsc::RectF(40, 40, 176, 176), 24.0f, fill);
canvas->endFrame();
// 输出到 PPM 文件
return canvas->savePixelsPPM("first.ppm") ? 0 : 1;
}
仅 24 行代码,就能完成一次完整的离屏渲染。CMake 配置也非常简洁:
cmake_minimum_required(VERSION 3.16)
project(MyApp LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
find_package(WhatsCanvas 1.2.1 CONFIG REQUIRED)
add_executable(MyApp main.cpp)
target_link_libraries(MyApp PRIVATE WhatsCanvas::Software)
2.1 先确定物理尺寸、逻辑尺寸和 DPR¶
上面的离屏示例没有设置 DPR,因此 1 个绘制单位对应 1 个输出像素。这适合生成小图,但不应直接照搬到高密度窗口或手机界面。
WhatsCanvas 把三个尺寸概念分开处理:
| 概念 | 含义 | 示例 |
|---|---|---|
| 物理尺寸 | Canvas 缓冲区或 framebuffer 的像素宽高 | 720 × 820 px |
| DPR | 一个逻辑单位对应多少物理像素 | 2.0 |
| 逻辑尺寸 | 业务布局使用的宽高 | 360 × 410 |
constexpr int physicalWidth = 720;
constexpr int physicalHeight = 820;
constexpr float dpr = 2.0f;
auto canvas = wsc::Canvas::create(
wsc::Canvas::Backend::Software, physicalWidth, physicalHeight);
canvas->setDevicePixelRatio(dpr);
const float logicalWidth = physicalWidth / dpr;
const float logicalHeight = physicalHeight / dpr;
设置 DPR 后,坐标、圆角、描边和文字大小都使用逻辑单位,Canvas 在输出时统一映射到物理像素。getWidth() 和 getHeight() 仍返回物理尺寸,不能直接拿它们作为逻辑布局宽高。
Android 宿主通常把 DisplayMetrics.density 传给 setDevicePixelRatio,于是一个逻辑单位可以按 1 dp 使用。Paint::setTextSize 本身不认识 sp;文字需要由 Android 宿主使用 TypedValue.applyDimension(COMPLEX_UNIT_SP, ...) 换算,再除以 density 得到 Canvas 逻辑字号。完整约定和示例见教程首页的尺寸说明。
三、核心架构与 API 设计¶
3.1 三大核心对象¶
WhatsCanvas 的 API 设计围绕三个核心概念:
这种设计对有 HTML Canvas 或 Android Canvas 使用经验的开发者来说极其自然。
3.2 Canvas:绘制 API 一览¶
// 基础图形
canvas->drawRect(rect, paint);
canvas->drawRoundRect(rect, radius, paint);
canvas->drawCircle(cx, cy, radius, paint);
canvas->drawOval(bounds, paint);
canvas->drawPath(path, paint);
canvas->drawLine(x1, y1, x2, y2, paint);
canvas->drawBoxShadow(rect, radius, spread, blur, dx, dy, color);
// 图片绘制
canvas->drawImage(image, x, y, paint);
canvas->drawImageFit(image, dst, ImageFit::COVER, paint);
canvas->drawImageRounded(image, dst, radius, paint);
canvas->drawImageNinePatch(image, centerSrc, dst, paint);
canvas->drawImageTiled(image, dst, paint);
// 文本绘制
canvas->drawText("Hello", x, y, paint);
canvas->drawTextBox(text, bounds, lineHeight, maxLines, ellipsize, paint);
canvas->drawTextOnPath(text, path, hOffset, vOffset, paint);
float width = canvas->measureText(text, paint);
// 状态管理
canvas->save();
canvas->translate(dx, dy);
canvas->scale(sx, sy);
canvas->rotate(radians);
canvas->clipPath(path);
canvas->restore();
3.3 Paint:丰富的绘制属性¶
wsc::Paint paint;
// 颜色和样式
paint.setColor(wsc::Color(40, 120, 240, 255));
paint.setStyle(wsc::Paint::Style::FILL); // FILL / STROKE / FILL_AND_STROKE
paint.setStrokeWidth(2.0f);
paint.setAntiAlias(true);
// 渐变
paint.setLinearGradient(x1, y1, x2, y2, {
{0.0f, Color::RED}, {0.5f, Color::GREEN}, {1.0f, Color::BLUE}
});
paint.setRadialGradient(cx, cy, radius, startColor, endColor);
// 阴影
paint.setShadowLayer(radius, dx, dy, shadowColor);
// 混合模式(14 种)
paint.setBlendMode(wsc::Paint::BlendMode::MULTIPLY);
// 文本
paint.setTextSize(24.0f);
paint.setFontFamily("Roboto");
paint.setFontWeight(700);
paint.setTextAlign(wsc::Paint::TextAlign::CENTER);
// 路径效果
paint.setDashPathEffect({10.0f, 5.0f}, 0.0f);
paint.setCornerPathEffect(8.0f);
3.4 Path:路径构造与查询¶
wsc::Path path;
path.moveTo(10, 10);
path.lineTo(100, 10);
path.quadTo(controlX, controlY, endX, endY);
path.cubicTo(c1x, c1y, c2x, c2y, ex, ey);
path.close();
// 形状辅助
path.addRoundRect(rect, radius);
path.addCircle(cx, cy, radius);
// 查询与 hit-testing
bool hit = path.contains(x, y);
bool strokeHit = path.strokeContains(x, y, strokeWidth);
RectF bounds = path.getBounds();
四、图层滤镜:毛玻璃效果开箱即用¶
WhatsCanvas 内置了一套完整的图层滤镜系统,支持 content blur、backdrop blur、内阴影、毛玻璃等常见 UI 效果:
// 创建毛玻璃滤镜
auto frosted = wsc::ImageFilter::frostedGlass(
8.0f, // 模糊半径
1.18f, // 饱和度
1.04f, // 亮度
1.02f, // 对比度
0.012f // 颗粒感
);
// 创建带背景模糊的图层
canvas->saveLayer(bounds, paint, wsc::LayerOptions()
.setBackdropFilter(frosted));
// 在模糊背景上绘制前景内容
canvas->drawRoundRect(rect, radius, foregroundPaint);
canvas->restore();
支持的滤镜类型包括:
- ImageFilter::blur() — 高斯模糊
- ImageFilter::innerShadow() — 内阴影
- ImageFilter::frostedGlass() — 毛玻璃(模糊 + 饱和度 + 亮度 + 对比度 + 颗粒)
- ImageFilterChain — 多滤镜链式组合
- 颜色矩阵变换
五、多后端架构¶
WhatsCanvas 通过统一的 Canvas::create() API 支持多种渲染后端:
using Backend = wsc::Canvas::Backend;
// 按优先级尝试多个后端,自动 fallback
auto canvas = wsc::Canvas::create(
{Backend::Vulkan, Backend::Metal, Backend::OpenGL, Backend::Software},
width, height);
| 后端 | CMake Target | 适用场景 |
|---|---|---|
| Software | WhatsCanvas::Software |
测试、headless、截图、CI 环境 |
| OpenGL 3.3 | WhatsCanvas::OpenGL |
桌面应用主力路径 |
| OpenGL ES 3.0 | WhatsCanvas::OpenGLES |
移动端、WebGL |
| Vulkan | 编入 OpenGL target | 高性能、低开销渲染 |
| Metal | WhatsCanvas::Metal |
macOS / iOS 原生 |
后端可在构建时按需裁剪,不需要的后端完全不参与编译和链接:
六、文本排版:不只是画字¶
WhatsCanvas 的文本系统远超一般 2D 绘图库的水平:
- 字体发现与 fallback 链:自动发现系统字体,支持多级回退
- CJK 排版:无空格换行、正确的标点处理
- RTL 与双向文本:完整 UAX #9 实现(861,948 例测试全部通过)
- HarfBuzz shaping:阿拉伯文、天城文等复杂文字正确成形
- 字体特性:weight / slant / OpenType features / 可变轴
- COLR/CPAL 彩色 emoji:支持 v0 和常见 COLRv1 paint graph
- 平台原生后端:Windows 可选 DirectWrite,Apple 可选 CoreText
// 文本绘制示例
wsc::Paint textPaint;
textPaint.setTextSize(18.0f);
textPaint.setFontFamily("Noto Sans SC");
textPaint.setColor(wsc::Color(33, 33, 33, 255));
// 多行文本 + 省略号
canvas->drawTextBox(
u8"WhatsCanvas 支持中日韩文本排版,包括自动换行和省略号处理。",
wsc::RectF(20, 20, 300, 200),
1.5f, // 行高倍数
3, // 最大行数
true, // 启用省略号
textPaint
);
// 文字沿路径绘制
wsc::Path arc;
arc.addArc(bounds, 0.0f, 3.14f);
canvas->drawTextOnPath(u8"Text on Path", arc, 0, 0, textPaint);
七、跨平台支持¶
| 平台 | 自动化状态 | 渲染后端 |
|---|---|---|
| Windows x64 | CI 单元测试 + 像素回归 + 包消费验证 | OpenGL, GLES, Software, Vulkan |
| Linux x64 | CI 构建 + 单元测试 + 像素门禁 | OpenGL, GLES, Software |
| macOS | CI 单元测试 + Metal 像素门禁 + universal 发布包 | Metal, OpenGL, Software |
| Android | 三 ABI (armeabi-v7a, arm64-v8a, x86_64) | OpenGL ES |
| iOS | 真机 + 模拟器 Metal/CoreText | Metal |
| Web | Emscripten/WebGL 2 + 浏览器自动化测试 | OpenGL ES (编译为 WebGL 2) |
八、性能表现¶
WhatsCanvas 仓库内归档了与 NanoVG 的详细对比基准测试(Windows / Core i7-8700 / GTX 1060 / 1920x1080 / OpenGL):
| 场景 | 结果 |
|---|---|
| 抗锯齿几何(256~4096 图形) | 8 项领先、1 项持平,最大帧时间下降 26.7% |
| 图片绘制(64~1024 张) | 9/9 领先,最大帧时间下降 58.5% |
| 动态文字(64~1024 次绘制) | 9/9 领先,最大帧时间下降 32.0% |
总计 26 项领先、0 项落后、1 项持平,27 项像素质量验证全部通过。
测试方法:每进程预热 5 帧、测量 30 帧,每个 cell 使用 2 个 ABBA block、每端 4 个新进程和 10,000 次 bootstrap,保证统计可信度。
九、接入方式¶
WhatsCanvas 提供多种接入方式,适应不同工程实践:
方式 1:GitHub Release 预编译包¶
# 下载 whatscanvas-win64-release-1.2.1.zip
# 解压后通过 CMAKE_PREFIX_PATH 指向安装目录
cmake -S . -B build -DCMAKE_PREFIX_PATH=/path/to/whatscanvas
方式 2:作为 CMake 子目录¶
方式 3:从源码构建¶
git clone --recursive https://github.com/ClarkWain/WhatsCanvas.git
cd WhatsCanvas
# Windows
build.bat --release --package --no-run
# Linux / macOS
sh ./build.sh --release --package --no-run
十、项目结构概览¶
WhatsCanvas/
├── include/wsc/ # 16 个公开头文件(~74 KiB)
├── src/ # 实现代码
│ ├── canvas/ # Canvas 实现、命令录制
│ ├── opengl/ # OpenGL/GLES/Vulkan/Metal 后端
│ ├── render/ # 渲染器接口、帧编译
│ └── text/ # 文本 shaping、布局、字体后端
├── examples/ # 使用示例
│ ├── hello_world/ # 最小示例(24 行代码)
│ ├── game/tetris/ # 俄罗斯方块游戏
│ └── game/racer/ # 赛车游戏
├── platforms/ # 平台宿主
│ ├── android/ # Android GLSurfaceView/JNI
│ ├── ios/ # iOS UIKit/Metal/CoreText
│ ├── desktop/ # GLFW 桌面宿主
│ └── wasm/ # Emscripten/WebGL 2
├── tests/ # 单元测试 + 像素回归
├── benchmarks/ # 性能基线(NanoVG 对比矩阵)
├── doc/ # 50+ 文档文件
└── third_party/ # FreeType, HarfBuzz, GLAD, GLFW, stb 等
十一、适用场景总结¶
推荐使用 WhatsCanvas 的场景:
- 原生应用自定义 UI 渲染(非标准控件)
- 数据可视化、图表、HUD
- 2D 游戏渲染层
- 服务端离屏图片生成
- 跨平台 UI 框架的渲染底座
- 需要像素级回归测试的渲染管线
不建议使用的场景:
- 需要现成 UI 控件体系(考虑基于 WhatsCanvas 的 WhatsUI)
- 必须使用 WebGPU
- 需要严格色彩管理或 PDF 生成
- 需要复杂富文本编辑器
- 需要稳定 ABI 的插件系统
十二、与同类方案对比¶
| 特性 | WhatsCanvas | NanoVG | Skia | Cairo |
|---|---|---|---|---|
| 多语言文本(CJK/RTL/Bidi) | ✅ | ❌ | ✅ | 有限 |
| 图层滤镜(模糊/毛玻璃) | ✅ | ❌ | ✅ | ❌ |
| 多渲染后端 | 5 种 | 仅 GL | 多种 | 2 种 |
| 像素回归测试 | ✅ | ❌ | ✅ | ✅ |
| 构建复杂度 | 中等 | 极低 | 极高 | 中等 |
| 库体量(静态) | ~5-8 MiB | <1 MiB | >30 MiB | ~5 MiB |
| API 学习曲线 | 低 | 低 | 高 | 中等 |
| 移动端支持 | ✅ | 需自行集成 | ✅ | 有限 |
十三、Retained Rendering:Picture 录制与缓存¶
对于需要反复绘制的静态内容,WhatsCanvas 提供 Picture 机制减少重复开销:
// 录制绘制命令
auto picture = canvas->recordPicture([](wsc::Canvas& c) {
wsc::Paint p;
p.setColor(wsc::Color(255, 100, 50, 255));
c.drawCircle(128, 128, 100, p);
// ... 更多复杂绘制 ...
});
// 多次重放
canvas->drawPicture(*picture);
// 带 GPU 缓存的重放(光栅化到 texture 后复用)
canvas->drawPictureRasterized(*picture);
十四、总结¶
WhatsCanvas 是一个 定位精准 的 C++17 2D 渲染库:
- API 友好 —— Canvas/Paint/Path 三件套,上手门槛低
- 功能完备 —— 文本、图片、滤镜、路径一应俱全
- 跨平台 —— 6 平台 5 后端,按需裁剪
- 工程严谨 —— 像素回归、性能基线、CI 全覆盖
- 体量适中 —— 比 Skia 轻得多,比 NanoVG 强得多
如果你的项目需要一个 "比 NanoVG 强、比 Skia 轻" 的原生 2D 渲染引擎,WhatsCanvas 值得一试。
项目地址:https://github.com/ClarkWain/WhatsCanvas 在线文档:https://clarkwain.github.io/WhatsCanvas/ 许可证:MIT
如果这篇文章对你有帮助,欢迎点赞、收藏、关注,你的支持是我持续输出的动力!