WhatsCanvas — A C++17 2D Rendering Library Between NanoVG and Skia¶
This article introduces WhatsCanvas, an embeddable 2D rendering library for native applications. If you are looking for a C++ drawing solution whose API style is close to HTML Canvas, that supports multi-backend rendering, and that ships complete text layout and layer filter capabilities, this article is worth reading.
For the Chinese version, see zh/00-whatscanvas-intro.md.
1. Background: Why WhatsCanvas?¶
In the native C++ 2D rendering space, developers usually face this trade-off:
| Option | Pros | Cons |
|---|---|---|
| NanoVG | Lightweight, easy to integrate | No multilingual text, no layer filters, no pixel regression |
| Skia | Full-featured, industrial grade | Large, complex to build, wide API surface |
| Cairo | Mature and stable | Limited text capabilities, no GPU backend |
WhatsCanvas fits into exactly the gap between NanoVG and Skia:
- Ships a Canvas / Paint / Path trio of APIs (similar to HTML Canvas, familiar to front-end developers)
- Supports multilingual text (CJK, RTL, bidirectional text, HarfBuzz shaping)
- Provides built-in layer filters (blur, inner shadow, frosted glass)
- Supports 5 render backends (Software, OpenGL, OpenGL ES, Vulkan, Metal)
- Covers 6 platforms (Windows, Linux, macOS, Android, iOS, Web)
- Ships a complete pixel regression testing infrastructure
2. Quick Start: Draw Your First Frame in 60 Seconds¶
WhatsCanvas ships a Software backend that does not require a GPU, window, or graphics context, which makes it ideal for quick validation:
#include <wsc/wsc.h>
int main()
{
// Create a 256x256 Software Canvas
auto canvas = wsc::Canvas::create(wsc::Canvas::Backend::Software, 256, 256);
if (!canvas || !canvas->initializeContext()) {
return 1;
}
canvas->beginFrame();
// Configure the paint: blue fill + anti-aliasing
wsc::Paint fill;
fill.setColor(wsc::Color(40, 120, 240, 255));
fill.setAntiAlias(true);
// Draw a rounded rectangle
canvas->drawRoundRect(wsc::RectF(40, 40, 176, 176), 24.0f, fill);
canvas->endFrame();
// Write out to a PPM file
return canvas->savePixelsPPM("first.ppm") ? 0 : 1;
}
Twenty-four lines are enough for a complete offscreen render. The CMake setup is equally minimal:
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 Nail Down Physical Size, Logical Size, and DPR First¶
The offscreen example above does not set a DPR, so one drawing unit equals one output pixel. That is fine for producing small images, but you should not copy the pattern directly into high-density windows or mobile UIs.
WhatsCanvas treats three sizing concepts separately:
| Concept | Meaning | Example |
|---|---|---|
| Physical size | Pixel width and height of the Canvas buffer or framebuffer | 720 × 820 px |
| DPR | How many physical pixels one logical unit maps to | 2.0 |
| Logical size | Width and height your layout code uses | 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;
Once you set a DPR, coordinates, corner radii, stroke widths, and text sizes all use logical units, and the Canvas maps them to physical pixels at output time. getWidth() and getHeight() still return the physical dimensions; do not use them directly as your logical layout width and height.
Android hosts typically pass DisplayMetrics.density to setDevicePixelRatio, so one logical unit can be treated as 1 dp. Paint::setTextSize itself does not recognize sp; the Android host must convert with TypedValue.applyDimension(COMPLEX_UNIT_SP, ...) and then divide by density to get the Canvas logical font size. The full contract and examples live in the sizing note on the tutorials home page.
3. Core Architecture and API Design¶
3.1 Three Core Objects¶
The WhatsCanvas API is organized around three core concepts:
Canvas ── Drawing surface, manages frame lifecycle and state stack
Paint ── Drawing attributes (color, gradient, text, blend mode, ...)
Path ── 2D geometric path (Bezier curves, hit-testing, ...)
The design is immediately familiar if you have used HTML Canvas or Android Canvas.
3.2 Canvas: Drawing API Overview¶
// Basic shapes
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);
// Image drawing
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);
// Text drawing
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);
// State management
canvas->save();
canvas->translate(dx, dy);
canvas->scale(sx, sy);
canvas->rotate(radians);
canvas->clipPath(path);
canvas->restore();
3.3 Paint: Rich Drawing Attributes¶
wsc::Paint paint;
// Color and style
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);
// Gradients
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);
// Shadow
paint.setShadowLayer(radius, dx, dy, shadowColor);
// Blend modes (14 total)
paint.setBlendMode(wsc::Paint::BlendMode::MULTIPLY);
// Text
paint.setTextSize(24.0f);
paint.setFontFamily("Roboto");
paint.setFontWeight(700);
paint.setTextAlign(wsc::Paint::TextAlign::CENTER);
// Path effects
paint.setDashPathEffect({10.0f, 5.0f}, 0.0f);
paint.setCornerPathEffect(8.0f);
3.4 Path: Construction and Queries¶
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();
// Shape helpers
path.addRoundRect(rect, radius);
path.addCircle(cx, cy, radius);
// Queries and hit-testing
bool hit = path.contains(x, y);
bool strokeHit = path.strokeContains(x, y, strokeWidth);
RectF bounds = path.getBounds();
4. Layer Filters: Frosted Glass Out of the Box¶
WhatsCanvas ships a complete layer filter system that supports content blur, backdrop blur, inner shadow, frosted glass, and other common UI effects:
// Build a frosted glass filter
auto frosted = wsc::ImageFilter::frostedGlass(
8.0f, // Blur radius
1.18f, // Saturation
1.04f, // Brightness
1.02f, // Contrast
0.012f // Grain
);
// Create a layer with backdrop blur
canvas->saveLayer(bounds, paint, wsc::LayerOptions()
.setBackdropFilter(frosted));
// Draw the foreground content on top of the blurred background
canvas->drawRoundRect(rect, radius, foregroundPaint);
canvas->restore();
Supported filter types include:
- ImageFilter::blur() — Gaussian blur
- ImageFilter::innerShadow() — Inner shadow
- ImageFilter::frostedGlass() — Frosted glass (blur + saturation + brightness + contrast + grain)
- ImageFilterChain — Chained composition of multiple filters
- Color matrix transforms
5. Multi-Backend Architecture¶
WhatsCanvas supports multiple render backends through a unified Canvas::create() API:
using Backend = wsc::Canvas::Backend;
// Try backends in priority order and fall back automatically
auto canvas = wsc::Canvas::create(
{Backend::Vulkan, Backend::Metal, Backend::OpenGL, Backend::Software},
width, height);
| Backend | CMake Target | Use Case |
|---|---|---|
| Software | WhatsCanvas::Software |
Tests, headless, screenshots, CI environments |
| OpenGL 3.3 | WhatsCanvas::OpenGL |
Primary path for desktop apps |
| OpenGL ES 3.0 | WhatsCanvas::OpenGLES |
Mobile, WebGL |
| Vulkan | Built into the OpenGL target | High-performance, low-overhead rendering |
| Metal | WhatsCanvas::Metal |
macOS / iOS native |
Backends can be trimmed at build time; the ones you do not need are excluded from compilation and linking entirely:
# CPU rendering only, zero GPU dependency
set(WHATSCANVAS_BUILD_OPENGL OFF)
set(WHATSCANVAS_BUILD_SOFTWARE ON)
6. Text Layout: More Than Drawing Characters¶
The WhatsCanvas text stack goes well beyond what a typical 2D drawing library ships:
- Font discovery and fallback chain: automatic system font discovery with multi-level fallback
- CJK layout: no-space line breaking, correct punctuation handling
- RTL and bidirectional text: full UAX #9 implementation (861,948 test cases all pass)
- HarfBuzz shaping: correct shaping of Arabic, Devanagari, and other complex scripts
- Font features: weight / slant / OpenType features / variable axes
- COLR/CPAL color emoji: supports v0 and common COLRv1 paint graphs
- Native platform backends: optional DirectWrite on Windows, optional CoreText on Apple
// Text drawing example
wsc::Paint textPaint;
textPaint.setTextSize(18.0f);
textPaint.setFontFamily("Noto Sans SC");
textPaint.setColor(wsc::Color(33, 33, 33, 255));
// Multi-line text + ellipsis
canvas->drawTextBox(
u8"WhatsCanvas supports CJK text layout, including automatic line breaking and ellipsis handling.",
wsc::RectF(20, 20, 300, 200),
1.5f, // Line-height multiplier
3, // Max lines
true, // Enable ellipsis
textPaint
);
// Text along a path
wsc::Path arc;
arc.addArc(bounds, 0.0f, 3.14f);
canvas->drawTextOnPath(u8"Text on Path", arc, 0, 0, textPaint);
7. Cross-Platform Support¶
| Platform | Automation Status | Render Backends |
|---|---|---|
| Windows x64 | CI unit tests + pixel regression + package-consumer validation | OpenGL, GLES, Software, Vulkan |
| Linux x64 | CI build + unit tests + pixel gates | OpenGL, GLES, Software |
| macOS | CI unit tests + Metal pixel gates + universal release package | Metal, OpenGL, Software |
| Android | Three ABIs (armeabi-v7a, arm64-v8a, x86_64) | OpenGL ES |
| iOS | Metal/CoreText on device and simulator | Metal |
| Web | Emscripten/WebGL 2 + browser automation | OpenGL ES (compiled to WebGL 2) |
8. Performance¶
The repository archives a detailed benchmark comparison against NanoVG (Windows / Core i7-8700 / GTX 1060 / 1920×1080 / OpenGL):
| Scenario | Result |
|---|---|
| Anti-aliased geometry (256–4096 shapes) | 8 wins, 1 tie; max frame time down 26.7% |
| Image drawing (64–1024 images) | 9/9 wins; max frame time down 58.5% |
| Dynamic text (64–1024 draws) | 9/9 wins; max frame time down 32.0% |
26 wins, 0 losses, 1 tie total, with all 27 pixel-quality checks passing.
Method: each process warms up for 5 frames and measures 30; each cell uses 2 ABBA blocks, 4 fresh processes per side, and 10,000 bootstrap resamples for statistical confidence.
9. Integration Options¶
WhatsCanvas offers several integration paths to fit different engineering workflows:
Option 1: Precompiled GitHub Release Package¶
# Download whatscanvas-win64-release-1.2.1.zip
# Extract and point CMAKE_PREFIX_PATH at the install directory
cmake -S . -B build -DCMAKE_PREFIX_PATH=/path/to/whatscanvas