miniblink49 中的 Skia 绘制上下文:SkCanvas API 实战与源码级解析
2026/9/16 15:45:16 网站建设 项目流程

miniblink49 中的 Skia 绘制上下文:SkCanvas API 实战与源码级解析

【免费下载链接】miniblink49a lighter, faster browser kernel of blink to integrate HTML UI in your app. 一个小巧、轻量的浏览器内核,用来取代wke和libcef项目地址: https://gitcode.com/GitHub_Trending/mi/miniblink49

本文基于 miniblink49 仓库内自带的 Skia 文档 SkCanvas 指南 展开:SkCanvas 是 Skia 的绘制上下文,负责接收所有绘制命令、维护变换矩阵栈与裁剪栈。你将学会如何用它完成从清屏、仿射变换到各种几何图形与位图、文本绘制的完整流程,并了解 miniblink49 的 raster 光栅化与 cc 合成层是如何在真实浏览器内核场景中调用这些 API 的。

SkCanvas 的定位:只管理"画在哪",不管"怎么画"

skcanvas.md 对 SkCanvas 的官方定义是:它是 Skia 的 drawing context,知道绘制应指向哪里(屏幕或离屏像素),并维护一个矩阵栈(stack of matrices)与裁剪栈(stacks of clips)。

与 PostScript、cairo、awt 等类似上下文不同,Skia 刻意不把任何绘制属性(颜色、线宽等)保存在 canvas 上。每个绘制调用都通过独立的SkPaint对象显式指定样式。这一设计从源码结构上也能印证:SkCanvas.h 中所有drawXxx接口的最后一个参数都是const SkPaint&const SkPaint*,而 canvas 自身状态只有矩阵/裁剪相关的成员与save()/restore()栈。

这意味着:

  • canvas 管状态:当前变换矩阵、当前裁剪区域;
  • paint 管外观:颜色、填充/描边样式、线宽、抗锯齿、shader、xfermode 等。

快速上手:一段可复制的绘制代码

原文档给出的示例是一个填充七角星(heptagram),函数签名void draw(SkCanvas* canvas)正是 miniblink 内部各绘制回调用到的统一模式:

void draw(SkCanvas* canvas) { const SkScalar scale = 256.0f; const SkScalar R = 0.45f * scale; const SkScalar TAU = 6.2831853f; SkPath path; path.moveTo(R, 0.0f); for (int i = 1; i < 7; ++i) { SkScalar theta = 3 * i * TAU / 7; path.lineTo(R * cos(theta), R * sin(theta)); } path.close(); SkPaint p; p.setAntiAlias(true); canvas->clear(SK_ColorWHITE); canvas->translate(0.5f * scale, 0.5f * scale); canvas->drawPath(path, p); }

关键点:先用path.moveTo/lineTo/close构建路径,p.setAntiAlias(true)开启抗锯齿,clear()白屏后translate()将原点移到画布中心,最后drawPath落笔。注意此处translate之后没有restore()——如果后续还要以原坐标绘制其他内容,就必须成对调用 save/restore。

变换矩阵栈:save / restore 与 translate / rotate / scale

原文档第二个示例展示了经典的"保存—变换—绘制—恢复"模式,绘制一个旋转 45 度的蓝色矩形:

void draw(SkCanvas* canvas) { canvas->save(); canvas->translate(SkIntToScalar(128), SkIntToScalar(128)); canvas->rotate(SkIntToScalar(45)); SkRect rect = SkRect::MakeXYWH(-90.5f, -90.5f, 181.0f, 181.0f); SkPaint paint; paint.setColor(SK_ColorBLUE); canvas->drawRect(rect, paint); canvas->restore(); }

这里有两个容易踩坑的细节,对应 SkCanvas.h 中的注释:

  1. save/restore 必须配对restoreToCount(int saveCount)的文档明确写道:count 超过 saveCount 是错误用法,最低可以传saveCount == 1
  2. 旋转围绕当前原点。示例先把原点平移到 (128,128) 再旋转,矩形以-90.5, -90.5为中心,这样旋转后仍然居中。若不先 translate,旋转将围绕 (0,0) 进行。

从 SkCanvas.h 可以看到完整的仿射变换 API 集:translate(dx, dy)scale(sx, sy)rotate(degrees)(角度单位是度)、concat(const SkMatrix&)用于直接拼接任意矩阵。这些操作都是乘到"当前矩阵"上的,因此顺序很重要——先 translate 再 rotate 与先 rotate 再 translate 结果不同。

在 miniblink 的真实合成流程里,这套 API 被大量使用。例如 CompositingLayer.cpp 的drawToCanvasChildren在绘制每个子层前,通过子层的currentTransform修改 canvas 矩阵、用clipRect设置子层边界裁剪,绘制完成后恢复;LayerTreeHost.cpp 的drawToCanvas入口处第一句就是canvas->save(),确保整棵层树的变换不会泄漏到外部 canvas 状态。

清屏与整屏填充:clear、drawPaint、drawColor

文档指出,清空画布最"笨"的办法是画一个巨大矩形,而正确姿势有两种:

// 方式一:用 SkPaint 填充整个 canvas(受当前 clip 约束) void draw(SkCanvas* canvas) { SkPaint paint; paint.setColor(SK_ColorWHITE); canvas->drawPaint(paint); } // 方式二:只想要纯色(可带 xfermode),直接 drawColor,省去分配 paint void draw(SkCanvas* canvas) { canvas->drawColor(SK_ColorWHITE); }

两者语义上有区别,源码签名也能印证:

  • drawPaint(const SkPaint& paint)(SkCanvas.h):用 paint 中指定的颜色、shader、xfermode 填充整个画布,尊重当前裁剪;若 paint 带 shader,shader 还会尊重 canvas 当前矩阵。
  • drawColor(SkColor color, SkXfermode::Mode mode = SkXfermode::kSrcOver_Mode)(SkCanvas.h):默认kSrcOver_Mode混合模式,适合快速清屏。
  • clear(SkColor color)(SkCanvas.h):非虚的便捷入口,示例代码里的canvas->clear(SK_ColorWHITE)走的就是它。

miniblink 的 LayerTreeHost.cpp 中clearCanvas实现展示了透明合成窗口下的真实清屏策略:构造一个SkPaint,当useLayeredBuffer为真时调用clearPaint.setARGB(0, 0xFF, 0xFF, 0xFF)填充全透明色,再交给 canvas——这正是"canvas 管画在哪、paint 管画成什么样"的典型案例。

完整绘制 API 一览:从矩形到文本

原文档给出一段覆盖主要 draw API 的完整示例,本文将其完整保留并按类别加注释:

SkBitmap source; void draw(SkCanvas* canvas) { canvas->drawColor(SK_ColorWHITE); // 清屏 SkPaint paint; paint.setStyle(SkPaint::kStroke_Style); // 描边模式 paint.setStrokeWidth(4); // 线宽 4 paint.setColor(SK_ColorRED); SkRect rect = SkRect::MakeXYWH(50, 50, 40, 60); canvas->drawRect(rect, paint); // 红色描边矩形 SkRRect oval; oval.setOval(rect); oval.offset(40, 60); paint.setColor(SK_ColorBLUE); canvas->drawRRect(oval, paint); // 蓝色椭圆(RRect 特例) paint.setColor(SK_ColorCYAN); canvas->drawCircle(180, 50, 25, paint); // 青色圆,圆心(180,50) 半径25 rect.offset(80, 0); paint.setColor(SK_ColorYELLOW); canvas->drawRoundRect(rect, 10, 10, paint); // 黄色圆角矩形,rx=ry=10 SkPath path; path.cubicTo(768, 0, -512, 256, 256, 256); // 三次贝塞尔曲线 paint.setColor(SK_ColorGREEN); canvas->drawPath(path, paint); // 绿色路径 canvas->drawBitmap(source, 128, 128, &paint); // 绘制位图(注意 paint 是指针,可为 null) SkRect rect2 = SkRect::MakeXYWH(0, 0, 40, 60); canvas->drawBitmapRect(source, rect2); // 位图按目标矩形绘制 SkPaint paint2; const char text[] = "Hello, Skia!"; canvas->drawText(text, strlen(text), 50, 25, paint2); // 在 (50,25) 起点处绘制文本 }

对应 SkCanvas.h 中的接口(行号可供检索):

类别接口源码位置
几何drawRectSkCanvas.h#L684
几何drawRRectSkCanvas.h#L722
几何drawCircle/drawArc/drawRoundRectSkCanvas.h#L738-L762
路径/线drawPath/drawLineSkCanvas.h#L676-L770
位图drawBitmapSkCanvas.h#L826
文本drawText/drawTextBlob/drawTextOnPathSkCanvas.h#L906-L965
回放drawPicture(const SkPicture*)SkCanvas.h#L973-L989

一个值得注意的约定:部分调用(如drawBitmap)传的是const SkPaint*指针,此时 paint允许为 null;其余情况 paint 参数是必须的。这一"指针即可空、引用则必传"的区分在 Skia 老版本 API 中普遍存在,阅读 SkCanvas.h 时看到paint&paint的差异要格外留心。

裁剪(Clip):与矩阵栈并行的第二状态

SkCanvas 维护的第二个状态是裁剪栈。裁剪 API 签名为clipRect / clipRRect / clipPath,均带SkRegion::Opantialias参数(SkCanvas.h#L1522-L1524)。裁剪与save()/restore()配合使用:save 压栈时同时保存当前裁剪,restore 时恢复。

miniblink 的合成器对裁剪的使用非常典型:CompositingLayer.cpp 在绘制子层前,用子层drawToCanvasProperties()->bounds构造SkRect并调用canvas->clipRect(...),保证子层内容不会越界绘制到父层区域——这就是文档中"drawPaint 会 respect the current clip"在实际窗口合成中的体现。

从 SkCanvas 对象到后端:Raster 与 Picture 两条落地路径

原文档末尾提到"如何创建 SkCanvas 对象"见 canvas.md。该文档列出了 Raster、Ganesh、SkPDF、SkPicture、NullCanvas 等多个后端,其中最常用的是 Raster(CPU 光栅化到内存)与 SkPicture(记录绘制命令的 display list)。

结合 miniblink49 源码可以印证这两条路径都在使用中:

路径一:Raster 表面 —— 光栅线程位图绘制。在 RasterTask.cpp 中,raster 任务通过SkSurface::NewRasterDirect(info, bitmap->getPixels(), stride, &surfaceProps)在客户端提供的内存上建表面,再取surface->getCanvas()得到 SkCanvas,然后用普通SkPaint绘制;同文件中还有一处直接用SkCanvas canvas(*filterBitmap);构造面向位图的 canvas,配合paint.setImageFilter(filter)+canvas.clear(SK_ColorTRANSPARENT)+canvas.drawBitmap(...)完成图像滤镜处理——这正是文档中"clear + drawBitmap + paint 属性"组合拳的真实工程化用法。

路径二:SkPicture 记录回放 —— 层缓存。RasterTask.cpp 顶部同时包含了 SkPicture.h 与 SkSurface.h,LayerTreeHost.cpp 则通过skia/ext/platform_canvas.h提供的平台工具函数创建 SkCanvas。从源码结构看,miniblink 的 cc 层(cc/layerscc/tilescc/blink下的各 LayerImpl)普遍以drawToCanvas(SkCanvas*, ...)形式的接口接收上层传入的 canvas,将"如何得到 canvas"与"往 canvas 上画什么"彻底解耦——这与文档强调的"SkCanvas 只是绘制上下文"的定位完全一致。

如果你需要自己写一段独立的 Raster 绘制代码,可参照 canvas.md 的推荐写法:SkSurface::NewRasterN32Premul(width, height)创建表面、getCanvas()取 canvas、绘制完成后newImageSnapshot()得到SkImageencode()导出 PNG。

小结

  • SkCanvas 是纯"上下文"对象:状态只有矩阵栈 + 裁剪栈,样式全部由每次调用传入的 SkPaint 决定;
  • 变换操作(translate/rotate/scale/concat)乘在当前矩阵上,务必以save()/restore()成对包裹,避免状态泄漏;
  • 清屏优先用clear()drawColor(),需要 shader/xfermode 时用drawPaint(),注意裁剪始终生效;
  • 所有drawXxxAPI 以const SkPaint&结尾,仅少数接受可空的const SkPaint*
  • 在 miniblink49 中,SkCanvas 是 raster 光栅化(cc/raster/RasterTask.cpp)与 cc 层合成(cc/layers/CompositingLayer.cpp、cc/trees/LayerTreeHost.cpp)共同的绘制入口,理解本文的 API 语义后可以直接阅读这些文件跟进真实调用链。

【免费下载链接】miniblink49a lighter, faster browser kernel of blink to integrate HTML UI in your app. 一个小巧、轻量的浏览器内核,用来取代wke和libcef项目地址: https://gitcode.com/GitHub_Trending/mi/miniblink49

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询