想画复杂形状?用 Path 自由组合直线和曲线

前面几篇我们画了矩形、圆、椭圆——都是"固定形状"。但如果你要做一个绘画 APP,用户想画什么就画什么,光靠这些固定形状是不够的。你需要 Path(路径)

Path 是什么?简单说,它就是一条"由你定义的线"。你可以用直线段、弧线、贝塞尔曲线把它们连起来,组成任意形状。画完之后,你可以用 Canvas 的 drawPath 把它画出来。

下面是 Path 绘制的整体流程:

创建空 Path

moveTo 设置起点

选择绘制方式

lineTo 画直线

arcTo 画弧线

quadTo 二阶贝塞尔

cubicTo 三阶贝塞尔

addRect/addCircle 添加标准形状

需要闭合路径?

close 闭合

保持开放路径

attachBrush/Pen 到 Canvas

canvas.drawPath 绘制

创建 Path

import { drawing } from '@kit.ArkGraphics2D';

let path = new drawing.Path();

一个空的 Path,什么都没有。接下来我们要往里面"添加"各种线段。

moveTo:抬笔移动

path.moveTo(100, 100);

moveTo 的意思是"把笔抬起来,移动到这个位置"。它不会画任何东西,只是设置起点。你可以把它想象成:你拿起笔,在纸上点了一个点,准备从这里开始画。

为什么要 moveTo?因为 Path 可以有多条不相连的线段。画完一条线后,用 moveTo 跳到一个新的位置,再画下一条。

lineTo:画直线

path.moveTo(10, 10);    // 从 (10,10) 开始
path.lineTo(100, 10);   // 画到 (100,10)
path.lineTo(100, 100);  // 再画到 (100,100)
path.lineTo(10, 100);   // 再画到 (10,100)
path.close();            // 闭合路径(回到起点)

lineTo 从当前位置画一条直线到目标位置。连续调用 lineTo 就能画出折线。

close() 是什么意思?它会自动画一条从当前位置回到 moveTo 起点的线段,让路径闭合。上面这段代码画的是一个矩形轮廓——四条直线围成的封闭形状。

如果不调用 close(),路径就是"开放"的,首尾不相连。

arcTo:画弧线

path.arcTo(50, 50, 200, 200, 0, 180);

arcTo 画一段弧线。它的工作方式是:指定一个矩形区域(左上角 x1,y1 到右下角 x2,y2),取这个矩形的内切椭圆,然后从起始角度扫过指定度数,截取一段弧。

参数说明:

  • x1, y1, x2, y2:矩形区域
  • startDeg:起始角度(度数),0° 是 x 轴正方向(3 点钟方向)
  • sweepDeg:扫描度数,正数顺时针,负数逆时针

注意:arcTo 会自动画一条从当前位置到弧线起点的直线。如果你不想画这条"连接线",需要先用 moveTo 跳到弧线的起点。

quadTo:二阶贝塞尔曲线

贝塞尔曲线是什么?你可以把它想象成一根有弹性的绳子。你拉住绳子的两端,中间用一个"控制点"把绳子往某个方向拉,绳子就会弯曲成一条平滑的曲线。

二阶贝塞尔有一个控制点:

path.moveTo(10, 10);
path.quadTo(100, 0, 200, 100);  // 控制点(100,0),终点(200,100)

从当前位置 (10,10) 到终点 (200,100),曲线会被控制点 (100,0) "拉"过去。控制点离得越远,曲线弯曲得越厉害。

cubicTo:三阶贝塞尔曲线

三阶贝塞尔有两个控制点,能画出更复杂的曲线:

path.moveTo(10, 10);
path.cubicTo(50, 0, 150, 200, 200, 100);  // 控制点1(50,0),控制点2(150,200),终点(200,100)

两个控制点分别控制曲线前半段和后半段的弯曲方向。三阶贝塞尔是绘图软件里最常用的曲线类型——Photoshop 里的"钢笔工具"画的就是三阶贝塞尔。

addRect、addCircle:快速添加形状

Path 不只是画自定义线条,也可以直接添加标准形状:

// 添加矩形
path.addRect({ left: 10, top: 10, right: 200, bottom: 100 }, drawing.PathDirection.CLOCKWISE);

// 添加圆形
path.addCircle(150, 150, 50);

// 添加椭圆
path.addOval({ left: 10, top: 10, right: 300, bottom: 200 });

// 添加圆角矩形
let roundRect = new drawing.RoundRect(
  { left: 10, top: 10, right: 200, bottom: 100 },
  20, 20
);
path.addRoundRect(roundRect);

// 添加弧线
path.addArc({ left: 10, top: 10, right: 200, bottom: 200 }, 0, 180);

pathDirection 参数指定添加方向:CLOCKWISE(顺时针)或 COUNTER_CLOCKWISE(逆时针)。方向会影响填充规则(Winding/EvenOdd),一般用默认的就行。

路径布尔运算

路径布尔运算可以将两个形状组合成新的形状,选择不同的运算方式会得到不同结果:

Path A + Path B

选择布尔运算

UNION 并集

INTERSECT 交集

DIFFERENCE 差集

XOR 异或

两个形状合并为一个

只保留重叠部分

A 减去 B 的部分

只保留不重叠部分

你可以对两个 Path 做布尔运算,合并成新的形状:

let path1 = new drawing.Path();
path1.addCircle(100, 100, 80);

let path2 = new drawing.Path();
path2.addCircle(150, 100, 80);

// 并集:两个圆合并
path1.op(path2, drawing.PathOp.UNION);

// 交集:两个圆重叠的部分
path1.op(path2, drawing.PathOp.INTERSECT);

// 差集:path1 减去 path2 的部分
path1.op(path2, drawing.PathOp.DIFFERENCE);

// 异或:两个圆不重叠的部分
path1.op(path2, drawing.PathOp.XOR);

布尔运算能做出很多有趣的形状。比如两个圆的交集就是一个"透镜"形状,差集就是"月牙"形状。

在 Canvas 上画 Path

class DrawingRenderNode extends RenderNode {
  draw(context: DrawContext) {
    const canvas = context.canvas;

    let path = new drawing.Path();
    path.moveTo(50, 50);
    path.lineTo(200, 50);
    path.quadTo(250, 100, 200, 150);
    path.lineTo(50, 150);
    path.close();

    // 用 Brush 填充
    const brush = new drawing.Brush();
    brush.setColor(255, 100, 200, 255);
    canvas.attachBrush(brush);

    // 用 Pen 描边
    const pen = new drawing.Pen();
    pen.setColor(255, 0, 0, 0);
    pen.setStrokeWidth(2);
    canvas.attachPen(pen);

    canvas.drawPath(path);

    canvas.detachBrush();
    canvas.detachPen();
  }
}

这段代码画了一个自定义形状:上面是平的,右边有一个曲线凹进去,下面也是平的,左边用直线闭合。蓝色填充,黑色描边。

填充规则

Path 的填充规则决定了"哪些区域算内部"。有两个选项:

path.setFillType(drawing.PathFillType.WINDING);   // 默认
path.setFillType(drawing.PathFillType.EVEN_ODD);
  • Winding(缠绕规则):根据路径的环绕方向判断。如果一个点被顺时针环绕的次数减去逆时针环绕的次数不为零,这个点就在内部。
  • EvenOdd(奇偶规则):不管方向,只数一个点被穿过的次数。奇数次在内部,偶数次在外部。

什么时候有区别?当两条路径交叉形成"环"的时候。比如两个重叠的圆:

  • Winding:两个圆的并集(全部填充)
  • EvenOdd:只有重叠的部分不填充(形成"甜甜圈"效果)

一般情况下用默认的 Winding 就行。如果你发现填充效果不对,试试切换到 EvenOdd。

其他常用方法

reset:清空路径,回到初始状态:

path.reset();

isEmpty:判断路径是否为空:

let empty = path.isEmpty();  // true 或 false

拷贝构造

let path2 = new drawing.Path(path);  // 复制一份

set:用另一个路径更新当前路径:

path.set(anotherPath);

完整示例:画一个心形

来个有趣的例子——用 Path 画一个心形:

import { RenderNode } from '@kit.ArkUI';
import { common2D, drawing } from '@kit.ArkGraphics2D';

class HeartRenderNode extends RenderNode {
  draw(context: DrawContext) {
    const canvas = context.canvas;

    let path = new drawing.Path();
    // 从底部尖端开始
    path.moveTo(150, 250);
    // 左半边心形:用三阶贝塞尔画弧线
    path.cubicTo(50, 200, 0, 100, 75, 50);
    // 左上角圆弧
    path.arcTo(50, 20, 150, 80, 180, 180);
    // 右上角圆弧
    path.arcTo(150, 20, 250, 80, 180, 180);
    // 右半边心形
    path.cubicTo(300, 100, 250, 200, 150, 250);
    path.close();

    // 红色填充
    const brush = new drawing.Brush();
    brush.setColor(255, 255, 50, 50);
    canvas.attachBrush(brush);

    canvas.drawPath(path);
    canvas.detachBrush();
  }
}

这段代码用 cubicToarcTo 组合出了一个心形。你可以调整控制点的位置来改变心形的"胖瘦"。

小结

Path 是 2D 绘制中最灵活的工具:

  • moveTo:抬笔移动
  • lineTo:画直线
  • arcTo:画弧线
  • quadTo:二阶贝塞尔曲线(1 个控制点)
  • cubicTo:三阶贝塞尔曲线(2 个控制点)
  • close:闭合路径
  • addRect/addCircle/addOval/addRoundRect:快速添加标准形状
  • op:路径布尔运算(并集/交集/差集/异或)

用这些基础元素,你可以画出任何你能想到的形状。

下一篇我们来看 ShadowLayer——怎么给绘制内容加阴影效果。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐