二维码

第四章 绘制曲线和路径

本章摘要

探索Canvas 2D API绘制曲线与路径的强大功能,从直线到贝塞尔曲线,再到SVG路径的解析。学习路径的基本概念及其作为图形构建单元的重要性,通过moveTo()、lineTo()等方法绘制基础图形。深入理解贝塞尔曲线在图形设计中的应用,并掌握SVG路径的绘制方法,将SVG图形转化为Canvas动态内容。这些技巧将极大提升你的图形开发能力。

图形系统开发实战课程 - 基础篇 绘制曲线和路径

第四章 绘制曲线和路径

  路径可以用来绘制各种形状和轮廓,例如直线、曲线、圆形、矩形等等。通过贝塞尔曲线等路径工具,可以创建出非常平滑和优美的曲线和形状,可以创建出各种复杂的形状和轮廓。本章的内容包括:

  • 绘制路径
  • 绘制曲线
  • 绘制复杂路径

1. 绘制路径

  Canvas路径概念非常重要。我们在绘制基本图形中讲述了矩形、折线、多边形、圆、椭圆等图形的绘制方法,这些基本图形除了矩形,其他都是以路径绘制方式绘制的,本章要讲的贝塞尔曲线也是按照路径规则绘制的。

基本过程

  定义一下路径的概念,路径是通过不同颜色和线宽的线段或曲线相连形成的不同形状的几何形状的集合。集合中包括了矩形、直线、圆弧或曲线,集合中的这些几何图形可称之为子路径,在绘制路径的时候,会遍历路径包含的所有子路径,然后绘制每一条子路径。通过路径我们可以完成几乎所有复杂图形的绘制。路径的绘制过程如下:

  1. 开始绘制路径:ctx.beginPath()
  2. 使用画图命令去定义路径,定义路径的命令包括:
    • 将路径移动到起始点:ctx.moveTo(x, y)
    • 添加直线路径:ctx.lineTo(x, y)
    • 添加矩形路径:ctx.rect(x, y, width, height)
    • 添加圆或圆弧路径: ctx.arc(x, y, radius, startAngle, endAngle, anticlockwise )
    • 添加椭圆或椭圆弧路径:ctx.ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle, anticlockwise)
    • 添加贝塞尔曲线路径:ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y)
    • 添加二次贝塞尔曲线路径:ctx.quadraticCurveTo(cpx, cpy, x, y)
    • 闭合路径(将路径移动到起始点): ctx.closePath()
  3. 设置路径样式:
    • 指定线宽:ctx.lineWidth = lineWidth
    • 指定边框颜色:ctx.strokeColor = color
    • 指定填充颜色:ctx.fillColor = color
    • 使用虚线模式:ctx.setLineDash([10,10])
  4. 对路径进行描边或填充
    • 描边:ctx.stroke()
    • 填充:ctx.fill();

扇形

  下面我们通过几个示例理解一下这个过程,首先学习一下扇形的绘制过程,其运行效果:

运行效果

在这个示例中,通过在路径中添加了直线和圆弧,并对齐路径进行了描边,从而实现了扇形的绘制。分解图示如下:

运行效果

具体实现过程为:

  1. 移动到点1
  2. 添加直线路径(从点1到点2)
  3. 添加圆弧路径(从点2到点3)
  4. 将路径移动到起始点(从点3至点1)

  其源代码如下:

{.line-numbers}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<script>
// 从页面中获取画布对象
let canvas = document.getElementById('canvas');
// 从画布中获取“2D渲染上下文”对象
let ctx = canvas.getContext('2d');
// 绘制背景网格线
drawGrid('lightgray', 10, 10);

// 设置路径属性
ctx.lineWidth = 4;
ctx.strokeStyle = "blue";

// 绘制一个圆弧
ctx.beginPath();
ctx.moveTo(50, 50);
ctx.lineTo(250, 50);
ctx.arc(50, 50, 200, 0, 90 * Math.PI / 180, false);
ctx.closePath();
ctx.stroke();

// 绘制另一个圆弧
ctx.beginPath();
ctx.moveTo(500, 50);
ctx.lineTo(700, 50);
ctx.arc(500, 50, 200, 0, 135 * Math.PI / 180, false);
ctx.closePath();
ctx.stroke();
</script>

正多边形

  接下来学习一下正多边形的绘制,其运行效果:

运行效果

  由于正多边形的各个顶点到中心点的距离均相等,我们可以使用三角函数公式来计算各个顶点的坐标。例如下图这个正六边形,各个顶点的坐标均可通过三角函数公式进行计算:

运行效果

正多边形的计算过程为:

  为此,我们可以编写一个通用的正多边形函数,实现各类正多边形的绘制,完整源代码如下:

{.line-numbers}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
<script>
// 从页面中获取画板对象
let canvas = document.getElementById('canvas');
// 从画板中获取“2D渲染上下文”对象
let ctx = canvas.getContext('2d');
// 绘制背景网格线
drawGrid('lightgray', 10, 10);

// 绘制从五边形至九角形的正多边形
for (let i = 5; i < 10; i++) {
// 绘制空心正多边形
drawRegularPolygon(ctx, 150 * (i - 5) + 100, 100, 50, i, { "color": "blue", "lineWidth": 4 });

// 绘制填充正多边形
drawRegularPolygon(ctx, 150 * (i - 5) + 100, 250, 50, i, { "fillColor": "red" });

// 绘制辅助文字
drawText(ctx, "正" + i + "边形", 150 * (i - 5) + 100, 340);
}

/**
* 绘制任意正多边形
* @param {CanvasRenderingContext2D} ctx
* @param {int} x 中心点X坐标
* @param {int} y 中心点Y坐标
* @param {int} size 顶点到中心点的距离
* @param {int} sideNum 边数
* @param {Object} style 渲染样式 {color, fillColor, angle}
*/
function drawRegularPolygon(ctx, x, y, size, sideNum, style) {
let coords = _getEdgeCoords(size, sideNum);
// 绘制多边形
let num = coords.length;
ctx.beginPath();
for (let i = 0; i < num; i++) {
let point = coords[i];
if (i == 0) {
ctx.moveTo(x + point[0], y + point[1]);
} else {
ctx.lineTo(x + point[0], y + point[1]);
}
}
ctx.closePath();
if (style.fillColor == null || style.fillColor === "none") {
ctx.strokeStyle = style.color;
ctx.lineWidth = style.lineWidth == null ? 1 : style.lineWidth;
ctx.stroke();
} else {
ctx.fillStyle = style.fillColor;
ctx.fill();
}
}

// 计算正多边形坐标
function _getEdgeCoords(size, sideNum) {
let vPoint = []; //vPoint为返回得到的多边形状的各顶点坐标
let arc = Math.PI / 2 - Math.PI / sideNum;
let r = size;
for (let i = 0; i < sideNum; i++) {
arc = arc - 2 * Math.PI / sideNum;
vPoint[i] = [r * Math.cos(arc), r * Math.sin(arc)];
}
return vPoint;
}

// 绘制文字
function drawText(ctx, text, x, y) {
ctx.save();
ctx.textAlign = "center";
ctx.baseAlignline = "bottom";
ctx.font = "bold 18px 黑体";
ctx.fillStyle = "black";
ctx.fillText(text, x, y);
ctx.restore();
}
</script>

地图

  我们在来学习一下,通过数据绘制地图,其运行效果如下图:

运行效果

  这么复杂的图形,其绘制过程依旧是上述绘制路径的那几个步骤,其源代码也非常简单,如下所示:

{.line-numbers}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<script>
// 从页面中获取画布对象
let canvas = document.getElementById('canvas');
// 从画布中获取“2D渲染上下文”对象
let ctx = canvas.getContext('2d');
// 绘制背景网格线
drawGrid('lightgray', 10, 10);

// 定义省界数据
let coords = [400,360,412,382,444,365,458,309,474,329,518,313,541,323,548,295,552,315,589,293,627,308,634,287,717,274,747,227,774,218,735,116,663,83,640,121,612,95,525,125,506,110,522,77,551,61,538,36,491,47,473,31,446,44,413,23,383,43,394,80,373,85,367,67,325,59,295,112,307,136,290,152,296,176,255,212,238,289,188,318,177,358,157,352,145,383,118,384,102,407,111,428,88,480,111,545,166,528,139,465,216,426,289,416,307,395,383,388,400,360];

// 绘制省界多边形
ctx.beginPath();
for (let i = 0, ii = coords.length; i < ii; i += 2) {
if (i === 0) {
ctx.moveTo(coords[i], coords[i + 1]);
} else {
ctx.lineTo(coords[i], coords[i + 1]);
}
}
ctx.closePath();
// 描边
ctx.strokeStyle = "black";
ctx.lineWidth = 2;
ctx.stroke();
// 填充多边形
ctx.fillStyle = "#DCFF7C";
ctx.fill();

// 绘制文字
ctx.font = "40px 黑体";
ctx.fillStyle = "black";
ctx.fillText("广东省", 400, 220);
</script>

2. 绘制曲线

  计算机图形学中绘制曲线通常是采用贝塞尔曲线来实现的,贝塞尔曲线(Bézier curve)又称贝兹曲线或贝济埃曲线,是应用于二维图形应用程序的数学曲线。

  贝兹曲线由线段与控制点组成,控制点是可拖动的支点,线段像可伸缩的皮筋,我们在绘图工具上看到的钢笔工具就是来做这种矢量曲线的。贝塞尔曲线又可包括二次贝塞尔曲线和三次吧贝塞尔曲线,二次贝塞尔曲线有一个控制点,而三次贝塞尔曲线则有两个控制点。

二次贝塞尔曲线

添加二次贝塞尔曲线至路径中:

1
ctx.quadraticCurveTo(cpx, cpy, x, y)

参数说明:

参数名 说明
cpx 控制点的 x 轴坐标
cpy 控制点的 y 轴坐标
x 终点的 x 轴坐标
y 终点的 y 轴坐标

  既然贝兹曲线由线段与控制点组成,而线段是由两个点所确定的,那么为什么这个方法只有终点的坐标,而没有起点坐标呢?

  这是因为贝塞尔曲线也是在路径中进行绘制的,路径是一组包含了直线、圆弧、和曲线的集合,也包括将路径移动到某个坐标位置的命令moveTo()。在绘制曲线时可以先将路径移动到某个位置,然后在执行绘制曲线,移动到的这个位置就是绘制曲线的起点,绘制曲线后,曲线的终点成为了路径新的起点。

  如果此时继续绘制另一端曲线,则可继续执行绘制曲线的命令,而不需要执行moveTo(),先看看以下运行效果:

运行效果

  在这个示例中绘制了两条曲线,绘制第一条曲线时先通过moveTo()将路径移动到了起点,而绘制第二条曲线时,由于是以第一条红色曲线的终点作为起点,因此可直接绘制曲线,而不需要执行moveTo(),其源代码如下:

{.line-numbers}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<script>
// 从页面中获取画布对象
let canvas = document.getElementById('canvas');
// 从画布中获取“2D渲染上下文”对象
let ctx = canvas.getContext('2d');
// 绘制背景网格线
drawGrid('lightgray', 10, 10);

// 红色曲线
ctx.save();
ctx.lineWidth = 4;
ctx.beginPath();
ctx.moveTo(50, 150)
ctx.quadraticCurveTo(150, 20, 250, 150)
ctx.strokeStyle = "red";
ctx.stroke();

// 蓝色曲线
ctx.beginPath();
ctx.moveTo(250, 150)
ctx.quadraticCurveTo(350, 280, 450, 150)
ctx.strokeStyle = "blue";
ctx.stroke();
ctx.restore();
</script>

  我们可以接着绘制第三条、第四条至第n条曲线,如下图所示:

运行效果

那么,控制点对绘制曲线有什么影响呢? 我们看一下以下的动画效果:

运行效果

  通过这个动画,我们了解到了控制点对贝塞尔曲线的影响,接下来我们看一个更加复杂的示例,在这个示例中将贝塞尔曲线与椭圆弧结合,形成了人人都喜欢的‘爱心’形状,运行效果如下图所示:

运行效果

  源代码非常简单,如下:

{.line-numbers}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<script>
// 从页面中获取画布对象
let canvas = document.getElementById('canvas');
// 从画布中获取“2D渲染上下文”对象
let ctx = canvas.getContext('2d');
// 绘制背景网格线
drawGrid('lightgray', 10, 10);

// 绘制爱心
ctx.beginPath();
ctx.moveTo(115, 121)
ctx.ellipse(189, 121, 74, 74, 0, Math.PI, 2 * Math.PI, false)
ctx.ellipse(337, 121, 74, 74, 0, Math.PI, 2 * Math.PI, false)
ctx.quadraticCurveTo(411, 232, 263, 343)
ctx.quadraticCurveTo(115, 232, 115, 121)
ctx.closePath()

// 描边
ctx.lineWidth = 4;
ctx.strokeStyle = "red";
ctx.stroke();
</script>

三次贝塞尔曲线

添加三次贝塞尔曲线至路径中:

1
ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y)

参数说明:

参数名 说明
cp1x 第一个控制点的 x 轴坐标
cp1y 第一个控制点的 y 轴坐标
cp2x 第二个控制点的 x 轴坐标
cp2y 第二个控制点的 y 轴坐标
x 终点的 x 轴坐标
y 终点的 y 轴坐标

  三次贝塞尔曲线有两个控制点,可对曲线进行更加复杂的变化,下图显示了三次贝塞尔曲线如何根据控制点的位置改变其形状

运行效果

  与两次贝塞尔曲线一样,三次贝塞尔曲线也是在路径中进行绘制,其参数也没有包括起点坐标,其绘制效果如下图:

运行效果

  源代码如下:

{.line-numbers}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<script>
// 从页面中获取画布对象
let canvas = document.getElementById('canvas');
// 从画布中获取“2D渲染上下文”对象
let ctx = canvas.getContext('2d');
// 绘制背景网格线
drawGrid('lightgray', 10, 10);

// 绘制三次贝塞尔曲线
ctx.beginPath();
ctx.moveTo(30, 300);
ctx.bezierCurveTo(90, 300, 80, 50, 160, 50);
ctx.lineWidth = 4;
ctx.strokeStyle = "red";
ctx.stroke();

ctx.beginPath();
ctx.moveTo(160, 50);
ctx.bezierCurveTo(230, 50, 220, 300, 280, 300);
ctx.strokeStyle = "blue";
ctx.stroke();
</script>

   贝塞尔的应用非常广泛,各种带有曲线的形状都离不开它的身影,下面这个示例是使用贝塞尔曲线绘制的花朵:

运行效果

  源代码如下:

{.line-numbers}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<script>
// 从页面中获取画布对象
let canvas = document.getElementById('canvas');
// 从画布中获取“2D渲染上下文”对象
let ctx = canvas.getContext('2d');
// 绘制背景网格线
drawGrid('lightgray', 10, 10);

// 绘制花朵1
drawFlower(ctx, 120, 120, 150, 5, { "color": "red", "fillColor": "green" })
// 绘制花朵2
drawFlower(ctx, 340, 120, 150, 6, { "color": "red", "fillColor": "green" })
// 绘制花朵3
drawFlower(ctx, 560, 120, 150, 7, { "color": "red", "fillColor": "green" })

/**
* 绘制花朵
*/
function drawFlower(ctx, x, y, size, sideNum, style) {
if (sideNum < 3) sideNum = 4;
if (sideNum > 8) sideNum = 6;
size--;
ctx.beginPath();

// 绘制花瓣
for (let n = 0; n < sideNum; n++) {
let theta1 = ((Math.PI * 2) / sideNum) * (n + 1);
let theta2 = ((Math.PI * 2) / sideNum) * (n);
let x1 = (size * Math.sin(theta1)) + x;
let y1 = (size * Math.cos(theta1)) + y;
let x2 = (size * Math.sin(theta2)) + x;
let y2 = (size * Math.cos(theta2)) + y;
ctx.moveTo(x, y);
ctx.bezierCurveTo(x1, y1, x2, y2, x, y);
}
ctx.closePath();
ctx.fillStyle = "red";
ctx.fill();

// 绘制花蕾
ctx.beginPath();
ctx.arc(x, y, size / 5, 0, 2 * Math.PI, false);
ctx.fillStyle = "yellow";
ctx.fill();
ctx.closePath();
}
</script>

3.绘制复杂路径

  路径绘制是HTML5 Canvas功能中最重要的功能,通过绘制路径,我们可以在画布上绘制出基本几何形状(线条、圆、矩形和曲线),并且通过组合不同形状创造出各种复杂的图形。

  在开始绘制路径时,记住每个新的路径均应使用beginPath()方法声明,在其之后开始添加基本形状至路径中,最后通过描边或填充将其绘制出来。

复杂路径

  接下来我们通过绘制下面这个更复杂一点的图形,再次学习路径的绘制过程:

运行效果

  在这个示例中,通过直线和贝塞尔曲线定义了一个两个英文字母,其源代码如下:

{.line-numbers}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
<script>
// 从页面中获取画布对象
let canvas = document.getElementById('canvas');
// 从画布中获取“2D渲染上下文”对象
let ctx = canvas.getContext('2d');
// 绘制背景网格线
drawGrid('lightgray', 10, 10);

// 开始路径
ctx.beginPath();

// 字母E
ctx.moveTo(506, 91)
ctx.bezierCurveTo(475, 91, 457, 115, 453, 140)
ctx.lineTo(560, 140)
ctx.bezierCurveTo(557, 116, 540, 91, 506, 91)
ctx.moveTo(454, 175)
ctx.bezierCurveTo(460, 202, 482, 220, 513, 220)
ctx.bezierCurveTo(529, 220, 553, 214, 568, 199)
ctx.lineTo(595, 230)
ctx.bezierCurveTo(570, 256, 534, 263, 508, 263)
ctx.bezierCurveTo(448, 263, 400, 219, 400, 157)
ctx.bezierCurveTo(400, 99, 444, 50, 506, 50)
ctx.bezierCurveTo(565, 50, 609, 96, 609, 157)
ctx.lineTo(609, 174)
ctx.lineTo(454, 174)
ctx.lineTo(454, 175)
ctx.closePath()

// 字母M
ctx.moveTo(305, 263)
ctx.lineTo(305, 132)
ctx.bezierCurveTo(305, 110, 294, 97, 274, 97)
ctx.bezierCurveTo(255, 97, 242, 110, 234, 120)
ctx.lineTo(234, 264)
ctx.lineTo(180, 264)
ctx.lineTo(180, 132)
ctx.bezierCurveTo(180, 110, 170, 97, 149, 97)
ctx.bezierCurveTo(131, 97, 118, 110, 110, 120)
ctx.lineTo(110, 264)
ctx.lineTo(56, 264)
ctx.lineTo(56, 54)
ctx.lineTo(110, 54)
ctx.lineTo(110, 80)
ctx.bezierCurveTo(119, 69, 140, 51, 172, 51)
ctx.bezierCurveTo(201, 51, 221, 63, 229, 88)
ctx.bezierCurveTo(242, 70, 264, 51, 297, 51)
ctx.bezierCurveTo(336, 51, 359, 74, 359, 119)
ctx.lineTo(359, 263)
ctx.lineTo(304, 263)
ctx.lineTo(305, 263)
ctx.closePath()

// 填充路径
ctx.fill();
</script>

SVG路径

   SVG路径是SVG中用于定义形状或线条的元素之一,在SVG中路径使用<path>元素来定义,并且<path>元素中有一个d属性,这个d属性是一系列命令的集合。每个命令对应一个字母,字母区分大小写。例如,M(MoveTo)命令表示将当前点移动到某处,L(LineTo)命令表示从当前点画线段到某处,C(CurveTo)命令表示绘制贝塞尔曲线等等,SVG就是通过组合使用这些命令来确定路径,描述各种复杂的形状。

下面我们看一个SVG文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?xml version="1.0" encoding="UTF-8"?>
<svg viewBox="0 0 793.7 1122.5" xmlns="http://www.w3.org/2000/svg">
<g fill="none" stroke="#000" stroke-width="3.125">
<path d="m782.98 577.54a387.3 397.46 0 1 0 -774.61 0 387.3 397.46 0 1 0 774.61 0z"/>
<g fill-opacity=".75">
<path d="m378.74 182.34s-109.95 45.977-112.21 398.28c-2.2584 352.3 123.5 394.4 123.5 394.4"/>
<path d="m378.74 182.34s338.75 45.166 336.49 397.46c-2.2583 352.3-325.2 395.21-325.2 395.21"/>
<path d="m378.74 182.34s121.21 47.46 118.95 399.76c-2.2584 352.3-107.66 392.91-107.66 392.91"/>
<path d="m378.74 182.34s-307.84 40.684-310.1 392.98c-2.2583 352.3 321.39 399.69 321.39 399.69"/>
<path d="m378.74 182.34s225.83 40.65 223.57 392.95c-2.2582 352.3-212.28 399.72-212.28 399.72"/>
<path d="m378.74 182.34s-227.72 42.236-229.98 394.54c-2.2584 352.3 241.27 398.14 241.27 398.14"/>
<path d="m378.74 182.34s6.775 33.875 4.5166 386.17c-2.2582 352.3 6.7749 406.5 6.7749 406.5"/>
<path d="m240.98 213.95s9.0333 51.942 140.02 51.942c130.98 0 169.37-47.425 169.37-47.425"/>
<path d="m113.88 306.54s6.4347 79.041 265.11 79.041 307.51-74.525 302.99-72.266"/>
<path d="m48.895 399.13s29.877 101.62 332.11 101.62 363.72-92.591 363.72-92.591"/>
<path d="m13.009 523.34s49.446 83.558 373.46 83.558 391.88-79.041 391.88-79.041"/>
<path d="m13.012 615.93s47.121 94.85 370.17 94.85 392.9-90.333 392.9-90.333"/>
<path d="m35.368 715.3s45.75 88.075 348.92 88.075 367.19-83.558 367.19-83.558"/>
<path d="m84.569 812.41s38.426 76.783 300.89 76.783 319.07-72.266 319.07-72.266"/>
<path d="m156.1 884.67s31.99 72.267 233.89 72.267 243.01-67.75 243.01-67.75"/>
</g>
</g>
</svg>

在这个SVG中使用<path>定义地球轮廓和经纬线,使用浏览器显示该文件的图形如下图所示:

运行效果

   从SVG的文件内容我们可以看出,这是一种使用“数据”定义图形内容的方式,这种数据文件称为矢量数据文件,矢量数据文件存储了空间对象的几何属性(如点、线、面等)和属性信息(如对象颜色、材质等)。矢量数据文件格式有很多类型,常见的有AutoCAD图形交换格式(.dxf)、可缩放矢量图(.svg)、GeoJSON(.json)、Shapefile(.shp)等等。

   我们刚刚学习了Canvas中绘制路径的方法,很高兴的告诉大家,Canvas路径可以完全实现SVG路径的功能,上面这个地球轮廓使用Canvas路径绘制的效果如下图所示:(为了区别于直接使用浏览器显示SVG图形,我在这张Canvas绘制的SVG路径图中加上了标志性的网格线)

运行效果

   实现的逻辑也并不复杂,逐条读取SVG路径d属性数据,解析出d属性中命令集合,然后使用Canvas绘制路径的方法创建路径,并按顺序逐个命令的添加到路径中,最后绘制出来。下面这段源码实现了这个过程,有关这部分的详细说明可以继续关注我们图形开发学院的实战课程。

{.line-numbers}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
<script>
// 从页面中获取画板对象
let canvas = document.getElementById('canvas');
// 从画板中获取“2D渲染上下文”对象
let ctx = canvas.getContext('2d');
// 绘制背景网格线
drawGrid('lightgray', 10, 10);

// 地球轮廓和经纬线数据
let datas = [
"m782.98 577.54a387.3 397.46 0 1 0 -774.61 0 387.3 397.46 0 1 0 774.61 0z",
"m378.74 182.34s -109.95 45.977 -112.21 398.28 c -2.2584 352.3 123.5 394.4 123.5 394.4",
"m378.74 182.34s338.75 45.166 336.49 397.46 c -2.2583 352.3 -325.2 395.21 -325.2 395.21",
"m378.74 182.34s121.21 47.46 118.95 399.76 c -2.2584 352.3 -107.66 392.91 -107.66 392.91",
"m378.74 182.34s -307.84 40.684 -310.1 392.98 c -2.2583 352.3 321.39 399.69 321.39 399.69",
"m378.74 182.34s225.83 40.65 223.57 392.95 c -2.2582 352.3 -212.28 399.72 -212.28 399.72",
"m378.74 182.34s -227.72 42.236 -229.98 394.54 c -2.2584 352.3 241.27 398.14 241.27 398.14",
"m378.74 182.34s6.775 33.875 4.5166 386.17 c -2.2582 352.3 6.7749 406.5 6.7749 406.5",
"m240.98 213.95s9.0333 51.942 140.02 51.942c130.98 0 169.37 -47.425 169.37 -47.425",
"m113.88 306.54s6.4347 79.041 265.11 79.041 307.51 -74.525 302.99 -72.266",
"m48.895 399.13s29.877 101.62 332.11 101.62 363.72 -92.591 363.72 -92.591",
"m13.009 523.34s49.446 83.558 373.46 83.558 391.88 -79.041 391.88 -79.041",
"m13.012 615.93s47.121 94.85 370.17 94.85 392.9 -90.333 392.9 -90.333",
"m35.368 715.3s45.75 88.075 348.92 88.075 367.19 -83.558 367.19 -83.558",
"m84.569 812.41s38.426 76.783 300.89 76.783 319.07 -72.266 319.07 -72.266",
"m156.1 884.67s31.99 72.267 233.89 72.267 243.01 -67.75 243.01 -67.75"];
ctx.scale(0.6, 0.6);
ctx.translate(100, -140);
ctx.strokeStyle = "black";
ctx.lineWidth = 4;

// 逐条路径分析和解析
for (let i = 0; i < datas.length; i++) {
// 将字符串转换为命令和坐标数据
let pathData = pathParse(datas[i]);
let lastPoint = [0, 0];
let c_lastControlPoint = [0, 0]; // 三次贝塞尔曲线控制点
let q_lastControlPoint = [0, 0]; // 二次贝塞尔曲线控制点

// 开始绘制多边形
ctx.beginPath();

// 生成多边形路径
for (let j = 0; j < pathData.length; j++) {
// 移动位置 (x,y)
if (pathData[j][0] == "M") {
ctx.moveTo(pathData[j][1], pathData[j][2]);
lastPoint = [pathData[j][1], pathData[j][2]];
}
// 直线 (x,y)
else if (pathData[j][0] == "L") {
ctx.lineTo(pathData[j][1], pathData[j][2]);
lastPoint = [pathData[j][1], pathData[j][2]];
}
// 水平直线 (x)
else if (pathData[j][0] == "H"){
ctx.lineTo(pathData[j][1], lastPoint[1]);
lastPoint = [pathData[j][1], lastPoint[1]];
}
// 垂直直线 (y)
else if (pathData[j][0] == "V"){
ctx.lineTo(lastPoint[0], pathData[j][1]);
lastPoint = [lastPoint[0], pathData[j][1]];
}
// 椭圆曲线 (rx ry angle large-arc-flag sweep-flag x y)+
else if (pathData[j][0] == "A") {
let arcArray = fromArcToBeziers(lastPoint[0], lastPoint[1], pathData[j]);
for(let idx = 0; idx<arcArray.length; idx ++) {
ctx.bezierCurveTo(arcArray[idx][1], arcArray[idx][2], arcArray[idx][3], arcArray[idx][4], arcArray[idx][5], arcArray[idx][6]);
lastPoint = [arcArray[idx][5], arcArray[idx][6]];
}
}
// 三次贝塞尔曲线 (x2,y2,x,y)
else if (pathData[j][0] == "S") {
if (c_lastControlPoint[0] == 0 && c_lastControlPoint[1] == 0) {
c_lastControlPoint = lastPoint.slice();
} else {
c_lastControlPoint = getSymmetricPointRelative(c_lastControlPoint, lastPoint); // 控制点1
}
ctx.bezierCurveTo(c_lastControlPoint[0], c_lastControlPoint[1], pathData[j][1], pathData[j][2], pathData[j][3], pathData[j][4]);
c_lastControlPoint = [pathData[j][1], pathData[j][2]]
lastPoint = [pathData[j][3], pathData[j][4]];
}
// 三次贝塞尔曲线 (x1,y1,x2,y2,x,y)
else if (pathData[j][0] == "C") {
ctx.bezierCurveTo(pathData[j][1], pathData[j][2], pathData[j][3], pathData[j][4], pathData[j][5], pathData[j][6]);
c_lastControlPoint = [pathData[j][3], pathData[j][4]];
lastPoint = [pathData[j][5], pathData[j][6]];
}
// 二次贝塞尔曲线 (x1,y1,x,y)
else if (pathData[j][0] == "Q") {
ctx.quadraticCurveTo(pathData[j][1], pathData[j][2], pathData[j][3], pathData[j][4]);
q_lastControlPoint = [pathData[j][1], pathData[j][2]];
lastPoint = [pathData[j][3], pathData[j][4]];
}
// 二次贝塞尔曲线 (x,y)
else if (pathData[j][0] == "T") {
if (q_lastControlPoint[0] == 0 && q_lastControlPoint[1] == 0) {
q_lastControlPoint = lastPoint.slice();
} else {
q_lastControlPoint = getSymmetricPointRelative(q_lastControlPoint, lastPoint);
}
lastPoint = [pathData[j][1], pathData[j][2]];
}
// 移动到初始为止
else if (pathData[j][0] == "Z" || pathData[j][0] == "z") {
ctx.closePath();
}
}

ctx.stroke();
}
</script>

4 本章小结

  在学习了曲线的绘制后,我们完成了定义路径所有命令,如下表所示:

方法名 说明
beginPath 新建一条路径
closePath 将路径移动到起始点
moveTo 将路径移动到指定的坐标(x, y)
lineTo 添加直线至路径
rect 添加矩形至路径
arc 添加圆形至路径
ellipse 添加椭圆至路径
bezierCurveTo 添加三次贝塞尔曲线至路径
quadraticCurveTo 添加二次贝塞尔曲线至路径
stroke 通过线条来绘制图形轮廓
fill 通过填充路径的内容区域生成实心的图形

  文中还讲述了绘制复杂的曲线和绘制SVG曲线的方法,并介绍了矢量图形数据文件,矢量数据文件是一种使用“数据”定义图形内容的方式,其应用非常广泛,

练习一下

简单路径练习

  橡皮、尺子、三角板和量角器都是很常用的数学工具,分别绘制一个吧。

SVG路径练习

  以下的SVG文件将会渲染出一匹马,如下图所示:

运行效果

SVG文件内容如下:

1
2
3
4
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" data-name="Layer 1" viewBox="0 0 2200.08 1876">
<path d="M753.06,142.4c36.23,15.31,71.38,30.66,106,49.49,32,17.4,61.47,37.54,92.9,55.84,33.54,19.53,64.27,43.94,95.14,67.34,15.46,11.71,36.73,23.46,49.47,38.11,3.28,3.77,4.54,9.22,7.69,13.11,6.19,7.64,16,13.13,23.86,18.91,32.59,23.86,63.06,43.86,102.1,55.37s66.53,36.58,101,56.8c68.72,40.35,146.08,49.09,224.29,44.82,80.57-4.4,158-24.79,236.63-41,75.7-15.64,151-19.93,227.19-5.26,38.9,7.49,77.55,16.49,116,26.15,39.89,10,77.42,25.36,115.64,40.41,64.72,25.48,140.5,73.08,179,132.78,43.15,66.91,69.37,145.28,74.58,224.43,2.67,40.62,2.92,84.17,3.34,123.67.47,44.23-3.61,8.74-4.65,27.33-.48,8.55.28,18.49-1.63,26.82-1.51,6.57-4.35,10.95-4.11,18.25l.18,5.44c.72,22.25,1.42,44.75-3.06,66.56-6.53,31.82-23.65,60.3-40.49,88.07-1.38-4.19-2.66-8.41-4-12.62.54,4.82.25,10-1.74,14.37,1.06-2.32-3.06-9.45-4.28-11.14-6.64,7.73-14.17,14.79-21.31,22.07a391.6,391.6,0,0,1-.69-62.33c-1.71,4.89-9.61,1.07-10.9-3.95s-.61-11.29-4.78-14.36l-8.47,18.54a162.66,162.66,0,0,0-9.65-38.53,30.76,30.76,0,0,1-15,4.38c-9.52-109.66-16.68-62.33-18.74-172.38l-5.52,11.69c-19.35-95-4.77-193.9-17.61-290-3.22-24.09-8.34-48.46-20.7-69.39s-33-38.12-57.14-41.07c34.14,63,36.23,138.65,26.28,209.59s-31,140-40.41,211c-2.7,20.34-4.43,41-2.2,61.36,2.53,23.08,10.08,45.33,19,66.75a518.15,518.15,0,0,0,52.42,95.64c10.46,15.07,24,28.5,33.94,43.77,7.91,12.18,6.13,30,6.15,44.06.06,42.59,4.28,84.76,8.76,127.1,4.41,41.63-12.94,53.07-4.14,94q7.11,33.09,13.33,66.38c3.55,18.83,9.15,38.88,5,58.06-3.18,14.79-15,18.84-22.22,31.43-8.37,14.53-16.48,35.56-7.7,51.37,5.74,10.35,17,17.94,18.05,31,.64,8.13-3.84,12.38-6,19.58-3.36,10.94-4.5,7.07,4.82,16.72-27.8,23.08-67.68,16.93-101.57,14.8-8.61-.54-29.93.28-34.56-9.86-5.08-11.12,8-27.53,12.9-36.19,18.07-31.82,35.53-61.43,41.21-98.21,5.55-35.89.21-76.26-2.8-112.39-6.14-73.78,9.28-116.92-26-183.3-17.72-33.37-34.57-71-57.37-101.08-23.14-30.57-48.62-59.62-72.89-89.3,11.49,26.72,23.39,52.28,35.95,78.32,13.56,28.1,2.45,59.61-3.71,88.24-14,65.13-25.09,129.3-29.51,195.84-2.06,31-3.73,62-6.14,93-1.17,14.95-3.3,28.71-9.13,42.65-6.4,15.29-10.48,11.43-23.81,17.88-15.24,7.37-38.33,30.35-35.44,48.9,1.07,6.89,6.53,11.57,8.2,18.1s-1,10.52-1,16.26c.14,14.43,7,20.71-10.9,26.27-26.72,8.31-60.35,1.49-87.33-1.9-18-2.26-54.27-3.26-36.61-28.79,14.13-20.42,30.72-43,49.83-58.63,20.57-16.84,35.45-35.05,45.23-60,23.33-59.5,29.82-122.41,36.08-185.62,3-30.43,6.3-59.17-.6-89.36-6.85-30-11.12-61.29-19.1-90.91-7.65-28.4-22.48-55-32.51-82.66-11.28-31.14-18.63-62.33-24.64-94.81-5.1-27.57-19.64-48-32.46-72.45-6-11.46-12.56-22.29-19.53-33.18s-14.89-32.43-24.93-39.92c-8.91-6.65-18.82-2-28.83-3.91-10.65-2-18.56-11-28.26-15.25-27.45-12.14-64.29-4.14-91.63,5.3-61,21.06-117.74,44.12-182.1,54.35-31.63,5-63.19,6.35-95.06,8.77-30.72,2.33-60.5,7.69-91.45,7.46a827.49,827.49,0,0,0-94.39,4.94c-17.23,1.84-34,3.29-51.38,2.93-14.32-.3-23.14-3.71-29.8,10.13-12.54,26.08-4.17,67.68-3,95.2,1.46,34.4,4.32,68.8,4.45,103.24a512.35,512.35,0,0,0,6.95,82.92c5.37,32.57-2.15,63.5,2.42,95.71,5.34,37.68,16.48,75.17,24.8,112.29,7,31.45,22,70.92,15.25,103.16-3.22,15.42-13.86,14.29-23.63,25.2-7.14,8-16,24.18-18.48,34.65-4.16,17.3,10.52,25.63,11.56,41.8.73,11.37-7.71,31-17.49,37.67-8.9,6.07-22.25,4.35-32.36,4.21-18.21-.25-36.41-1-54.6-1.78-14.91-.68-33.25-5-46.87,3-13.4,7.89-10,15.88-26.27,18.25-18.29,2.67-37.86,1.91-56.24.8-14-.85-40.3.35-52.65-7-3.72-2.21-7.16-1.75-6.9-7.35.39-8.33,62.63-77.89,73.25-87.62,29.83-27.31,33.83-60.1,40.7-98a443,443,0,0,0,3.63-131.12c-2.25-17.8-9.66-33.81-12-51.51-2.7-20.18-5.4-40.84-5.88-61.21a1143.58,1143.58,0,0,0-10.45-126c-5.94-44.09-21.21-85.31-29.32-128.85-5.56-29.82-1.91-77.17-27.08-97.84-14.64-12-28.54-22.73-40.46-37.77-5.38-6.79-9.8-17.28-16-23-8.39-14-13.2-30-16.09-42-4.45-18.53-8-36.9-9.28-55.93-2.27-34.43-2.48-69.89-7.91-104-5.2-32.62-26.85-56.61-45.08-82.76-19.51-28-36.35-57.74-51.37-88.33-30.66-62.42-49.41-137.44-91.59-193.16-17.61-23.26-36-46.13-67.25-38.7-34.79,8.28-62.81,21.88-95.12,37C495.1,449.06,452.77,458.9,432.71,482c-8.37,9.63-10.59,21.31-23.48,26.39-11.34,4.47-30.22,4.74-41.93,1.81-12.66-3.17-29.55-12.31-39.13-21.33-10.66-10-10.56-22.52-14.79-35.64-11.42-35.35-5-58.48,15.76-89,19.79-29.06,44.15-54.51,65.3-82.51,10.16-13.46,19.54-27.49,29-41.42,8.65-12.72,13.73-26.66,22.35-39.25,8.92-13,20-24.44,30.4-36.23C489.33,150,488,145.72,489.28,127c1.77-26.46,18.19-44.27,13.52-70.7-1-5.68-11.52-47.49-2.49-46.82,6.55.49,22.93,34.74,29.68,40.43C576,88.68,543.56,12.18,554.49.8c9.12-9.49,54.95,68.38,65.27,78.35,13.45,13,37.81,19.09,54.67,26.47q30.13,13.18,60.22,26.48m394.32,1340c-19.27-1.23-.72,178.59.27,199,1,19.63,5.42,64.57-15.13,76.62-4.27,2.5-9.59.83-13.82,2.73-7.08,3.18-10.66,8-16,13.88-13.69,14.94-21.13,18.83-12.06,38.43,10.8,23.37,14.62,11.43,27.18-4.86,9.12-11.83,17.45-24.35,26.91-35.93,21.26-26,36.91-43.23,38-78.41,1.11-34.92,1.48-69.56-4.12-104.18C1154.18,1542.11,1139,1508.26,1129,1472.12Z" transform="translate(-307.77 0)"/>
</svg>

使用我们这堂课学习的路径绘制方法也可以将其绘制出来,动手试一试吧~~

本文为“图形开发学院”(www.graphanywhere.com)网站原创文章,遵循CC BY-NC-ND 4.0版权协议,商业转载请联系作者获得授权,非商业转载请附上原文出处链接及本声明。

历史发布版本

第1版发布时间:2023-11-01
第2版发布时间:2024-06-06