☰
SkiaSharp 路径基础:用 SKPath 组合连接线与曲线
2026/10/13 2:28:01 网站建设 项目流程
  • 图形学
  • 图像处理
  • 跨平台

【免费下载链接】SkiaSharp

SkiaSharp is a cross-platform 2D graphics API for .NET platforms based on Google's Skia Graphics Library. It provides a comprehensive 2D API that can be used across mobile, server and desktop models to render images.

项目地址:https://gitcode.com/gh_mirrors/sk/SkiaSharp
点击查看免费下载

导读:本文围绕 SkiaSharp 的SKPath对象,系统讲解图形路径(Graphics Path)的核心概念——轮廓(Contour)、当前点(Current Point)、五种线段/曲线追加方法及其相对坐标变体、Close闭合语义、批量添加几何轮廓的Add*方法,以及通过SKPaint.StrokeJoin与StrokeMiter控制拐角外观的机制。读完本文,你将能够在 .NET MAUI、WPF、WinUI 等任何 SkiaSharp 场景中,用纯几何方式构建并渲染可复用的复杂矢量图形。

为什么"是否连接"是路径的第一要义

图形路径(Graphics Path)最重要的能力之一,是精确定义哪些线应该相连、哪些线应该断开。这个区别在视觉上可能非常显著——例如两个三角形顶部顶点的处理:一条"看似相连"的折线与一条真正闭合的轮廓,在端点处的渲染结果完全不同。

在 SkiaSharp 中,图形路径由SKPath对象封装。一个路径是一个或多个轮廓(Contour)的集合;每个轮廓是一组相互连接的直线与曲线。轮廓与轮廓之间彼此不相连,但它们可能在视觉上相互重叠;有时单个轮廓也会自交。

using var path = new SKPath(); // 第一个轮廓(开放) path.MoveTo(100, 100); path.LineTo(200, 100); path.LineTo(150, 200); // 第二个轮廓(闭合) path.MoveTo(300, 100); path.LineTo(400, 100); path.LineTo(350, 200); path.Close();

轮廓的起点:MoveTo 与当前点

每个轮廓一般以SKPath的MoveTo方法开始,其参数是一个点,既可以传SKPoint值,也可以直接传独立的 X、Y 坐标。MoveTo调用会确立轮廓的起点,同时设置一个初始的当前点(Current Point)——它是后续所有追加操作隐含的起点。

从源码可以看到,SKPath为MoveTo提供了两个重载,并在当前仓库版本中被标记为[Obsolete],实际委托给内部的SKPathBuilder完成(见 SKPath.cs):

[Obsolete ("Use SKPathBuilder instead.")] public void MoveTo (SKPoint point) { EnsureBuilder (); _builder.MoveTo (point); } [Obsolete ("Use SKPathBuilder instead.")] public void MoveTo (float x, float y) { EnsureBuilder (); _builder.MoveTo (x, y); }

SKPathBuilder是 SkiaSharp 当前推荐的增量式路径构造入口,其底层直接调用SkiaApi.sk_pathbuilder_move_to(见 SKPathBuilder.cs),支持Snapshot()取得当前路径副本、Detach()移交路径所有权并重置构造器(SKPathBuilder.cs)。官方推荐的新写法如下:

using var builder = new SKPathBuilder(); builder.MoveTo(0, 0); builder.LineTo(100, 0); builder.LineTo(50, 100); builder.Close(); using var path = builder.Detach();

追加线段与曲线的五种方法

从当前点出发,可以用下列方法为轮廓追加一段线或曲线,终点会顺理成章地成为新的当前点:

方法作用关键参数
LineTo追加一条直线终点(1 个点)
ArcTo追加圆弧——圆周或椭圆周上的一段线半径、x 轴旋转角、大/小弧、扫掠方向、终点
CubicTo追加三次贝塞尔样条2 个控制点 + 1 个终点
QuadTo追加二次贝塞尔样条1 个控制点 + 1 个终点
ConicTo追加有理二次贝塞尔样条,可精确渲染圆锥曲线(椭圆、抛物线、双曲线)1 个控制点 + 终点 + 权重w

这五种方法都不包含描述完整线/曲线的全部信息:它们必须与紧邻的上一次方法调用所确立的当前点协同工作。例如LineTo只接受一个终点,因为起点就是当前点。

仓库源码完整实现了这些方法的所有重载,包括SKPoint与裸float坐标两种形态(见 SKPath.cs)。其中 SVG 风格圆弧重载ArcTo(SKPoint r, float xAxisRotate, SKPathArcSize largeArc, SKPathDirection sweep, SKPoint xy)支持通过SKPathArcSize枚举选择绘制最小弧还是最大弧、通过SKPathDirection控制顺时针/逆时针扫掠;而ArcTo(SKRect oval, float startAngle, float sweepAngle, bool forceMoveTo)则以角度制描述弧段(SKPath.cs)。

ConicTo的权重w决定了曲线"拉向"控制点的程度:w = 1时等价于二次贝塞尔;w < 1趋向椭圆弧;w > 1趋向双曲线。

相对坐标变体:R 开头方法族

SKPath还定义了与上述六个方法同名、但以R开头的一组方法:

  • RMoveTo
  • RLineTo
  • RArcTo
  • RCubicTo
  • RQuadTo
  • RConicTo

R代表relative(相对)。这些方法与不带R的对应方法语法完全相同,但坐标都被解释为相对于当前点的偏移量。它们非常适合在可多次调用的方法中绘制路径中重复出现的相似部分——同一个"图元"函数可以在不同位置反复调用,而不必每次都计算绝对坐标。

一个值得注意的语义细节:在Close之后调用R*方法,偏移量是相对闭合所回到的起点计算的,而不是相对闭合前的最后一点。仓库测试 PathConsistentAfterClose 专门验证了这一点(基于 Skia 上游test_path_close_issue1474):

using var builder = new SKPathBuilder(); builder.RLineTo(0, 100); builder.RLineTo(100, 0); builder.Close(); // 回到 (0,0) builder.RLineTo(50, 50); // 应到达 (50,50),而不是 (150,50) using var path = builder.Detach(); Assert.Equal(50, path.LastPoint.X); Assert.Equal(50, path.LastPoint.Y);

轮廓的结束:MoveTo 与 Close 的语义差异

一个轮廓以以下两种方式之一结束:

  • 再次调用MoveTo/RMoveTo,开始一个新轮廓;
  • 调用Close,闭合当前轮廓。

Close会自动从当前点向轮廓的第一个点追加一条直线,并把路径标记为闭合——闭合路径在渲染时不会绘制任何线帽(stroke cap)。从源码看,Close在 SKPath.cs 中定义,其注释明确指出:"若当前点与轮廓起点不重合,会自动追加一条线段"。

仓库测试 PathConsistentAfterClose 还验证了Close后R*相对方法基于闭合起点的行为,说明该语义在 Skia 原生层与托管绑定层均保持一致。

完整示例:两个三角形轮廓

下面的示例来自原文档的Two Triangle Contours页面,使用一个SKPath的两个轮廓渲染两个三角形——第一个轮廓开放、第二个轮廓闭合:

void OnCanvasViewPaintSurface(object? sender, SKPaintSurfaceEventArgs args) { SKImageInfo info = args.Info; SKSurface surface = args.Surface; SKCanvas canvas = surface.Canvas; canvas.Clear(); // 创建路径 SKPath path = new SKPath(); // 定义第一个轮廓(开放三角形) path.MoveTo(0.5f * info.Width, 0.1f * info.Height); path.LineTo(0.2f * info.Width, 0.4f * info.Height); path.LineTo(0.8f * info.Width, 0.4f * info.Height); path.LineTo(0.5f * info.Width, 0.1f * info.Height); // 定义第二个轮廓(闭合三角形) path.MoveTo(0.5f * info.Width, 0.6f * info.Height); path.LineTo(0.2f * info.Width, 0.9f * info.Height); path.LineTo(0.8f * info.Width, 0.9f * info.Height); path.Close(); // 创建两个 SKPaint 对象 SKPaint strokePaint = new SKPaint { Style = SKPaintStyle.Stroke, Color = SKColors.Magenta, StrokeWidth = 50 }; SKPaint fillPaint = new SKPaint { Style = SKPaintStyle.Fill, Color = SKColors.Cyan }; // 填充并描边路径 canvas.DrawPath(path, fillPaint); canvas.DrawPath(path, strokePaint); }

第一个轮廓由一次MoveTo(使用裸 X、Y 坐标而非SKPoint)加三次LineTo构成三角形三条边;第二个轮廓只有两次LineTo,但以Close收尾自动补上最后一条边。差异非常显著:第一个轮廓看起来是三条相连的线,但末端并未连回起点,两条线在顶部互相重叠;第二个轮廓则明显闭合,且少调用了一次LineTo。

填充不闭合轮廓的规则

SKCanvas只定义了一个DrawPath方法,本示例对其调用了两次以分别填充和描边。所有轮廓都会被填充,即使它们并未闭合——在填充未闭合路径时,Skia 会假定轮廓起点与终点之间存在一条直线。因此,如果你删掉第一个轮廓的最后一次LineTo,或者删掉第二个轮廓的Close调用,每个轮廓将只剩两条边,但仍会像三角形一样被填充。这一点对理解路径几何与填充渲染的区别至关重要。

批量添加完整轮廓:Add 方法族

SKPath还定义了若干一次性添加整个轮廓的方法,这些轮廓是否闭合取决于具体方法:

方法行为
AddRect添加闭合矩形轮廓;可通过SKPathDirection指定绕行方向,还可通过startIndex指定起点角标(0..3)
AddRoundedRect添加闭合圆角矩形轮廓(SKRoundRect或SKRect + rx/ry两种重载)
AddCircle添加闭合圆形轮廓(圆心 X/Y + 半径 + 方向)
AddOval添加闭合椭圆轮廓(边界SKRect+ 方向)
AddArc在椭圆周上添加一段曲线(边界SKRect+ 起始角 + 扫掠角,均为角度制)
AddPath将另一条路径追加到当前路径,可按(dx, dy)平移或应用SKMatrix变换;SKPathAddMode.Append作为新轮廓追加,Extend则延伸当前最后一个轮廓
AddPathReverse以相反顺序添加另一条路径

源码中这些方法均以[Obsolete]兼容形式存在于 SKPath.cs,并委托给SKPathBuilder的同名方法(SKPathBuilder.cs)。其中AddPath的Append/Extend模式由SKPathAddMode枚举控制;AddPathReverse对应 builder 的ReverseAddPath,用于反转轮廓走向——这在实现"双边描边""路径镜像"等效果时非常实用。

值得注意的是方向参数:SKPathDirection.Clockwise(顺时针,默认值)与SKPathDirection.CounterClockwise(逆时针)会影响填充规则的判定结果(与FillType配合时尤其重要),枚举定义见 SKPathDirection.generated.cs。

路径只描述几何,渲染交给 SKPaint

务必牢记:SKPath对象只定义几何——一系列点及其连接关系。只有当SKPath与SKPaint对象结合时,路径才会以特定颜色、线宽等样式渲染出来。

同时,传给DrawPath的SKPaint对象定义了整条路径的外观特性。如果你需要绘制多种颜色的内容,就必须为每种颜色使用独立的路径(或至少独立的绘制调用),因为单个SKPaint无法为路径的不同部分分别着色。

拐角外观:StrokeJoin 与 StrokeMiter

正如线条的起止外观由线帽(stroke cap)定义,两条线连接处的外观由**拐角连接(stroke join)**定义。通过设置SKPaint的StrokeJoin属性为SKStrokeJoin枚举成员来控制:

枚举成员效果
Miter尖锐的拐角(默认)
Round圆角连接
Bevel削平/截断式连接

枚举定义见 SKStrokeJoin.generated.cs,其底层对应 Skia 的sk_stroke_join_t:Miter = 0、Round = 1、Bevel = 2。SKPaint.StrokeJoin属性直接映射到原生sk_paint_set_stroke_join调用(SKPaint.cs)。

Stroke Joins 示例

原文档的Stroke Joins页面循环遍历三个枚举成员,每个都绘制一条粗线(50 像素)并在其上叠加一条细线(2 像素)以标示几何端点:

void OnCanvasViewPaintSurface(object? sender, SKPaintSurfaceEventArgs args) { SKImageInfo info = args.Info; SKSurface surface = args.Surface; SKCanvas canvas = surface.Canvas; canvas.Clear(); SKPaint textPaint = new SKPaint { Color = SKColors.Black }; SKFont font = new SKFont { Size = 75 }; SKPaint thickLinePaint = new SKPaint { Style = SKPaintStyle.Stroke, Color = SKColors.Orange, StrokeWidth = 50 }; SKPaint thinLinePaint = new SKPaint { Style = SKPaintStyle.Stroke, Color = SKColors.Black, StrokeWidth = 2 }; float xText = info.Width - 100; float xLine1 = 100; float xLine2 = info.Width - xLine1; float y = 2 * font.Spacing; string[] strStrokeJoins = { "Miter", "Round", "Bevel" }; foreach (string strStrokeJoin in strStrokeJoins) { // 显示文字 canvas.DrawText(strStrokeJoin, xText, y, SKTextAlign.Right, font, textPaint); // 解析枚举值 SKStrokeJoin strokeJoin; Enum.TryParse(strStrokeJoin, out strokeJoin); // 创建路径(一个折角) SKPath path = new SKPath(); path.MoveTo(xLine1, y - 80); path.LineTo(xLine1, y + 80); path.LineTo(xLine2, y + 80); // 绘制粗线与细线 thickLinePaint.StrokeJoin = strokeJoin; canvas.DrawPath(path, thickLinePaint); canvas.DrawPath(path, thinLinePaint); y += 3 * font.Spacing; } }

Miter 连接的长度限制

Miter 连接由两条线交汇处的一个尖锐尖角构成。当两条线以很小的夹角连接时,miter 尖角会变得非常长。为防止出现过度细长的尖角,miter 的长度受SKPaint.StrokeMiter属性限制(SKPaint.cs,映射到原生sk_paint_get/set_stroke_miter)。一旦 miter 长度超过该阈值,就会被削平,退化为 bevel 连接——这是 Skia 遵循 SVG/PostScript 规范的默认保护行为,在绘制锐角折线(如星形、闪电符号)时尤其需要注意调整该值。

与相邻主题的衔接

路径基础是整个 SkiaSharp 图形绘制体系的地基:

  • 相邻文档 Lines and Stroke Caps 讲解了单条直线渲染与SKStrokeCap(Butt/Square/Round)的差异,并指出SKPointMode.Points/Lines/Polygon三种模式渲染多个点的方式——其中Polygon模式绘制的各段线并不真正相连,这正是理解"路径连接"意义的关键伏笔;
  • The Path Fill Types 深入探讨SKPathFillType填充规则;
  • 后续的 SkiaSharp Curves and Paths 系列则专门讨论SKPath支持的各种曲线类型。

相关资源

  • SKPath 核心实现
  • SKPathBuilder 增量构造器
  • SKPaint 描边属性(StrokeJoin / StrokeMiter / StrokeCap)
  • SKStrokeJoin 枚举定义
  • SKPath 行为测试
  • 图形学
  • 图像处理
  • 跨平台

【免费下载链接】SkiaSharp

SkiaSharp is a cross-platform 2D graphics API for .NET platforms based on Google's Skia Graphics Library. It provides a comprehensive 2D API that can be used across mobile, server and desktop models to render images.

项目地址:https://gitcode.com/gh_mirrors/sk/SkiaSharp
点击查看免费下载
上一篇:Sollumz:3步在Blender中制作GTA V游戏模组的完整指南
下一篇:Moonlight-Switch终极指南:如何将任天堂Switch变身高性能游戏串流终端

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

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

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

立即咨询