SVG 基础形状中的路径 Path
路径 Path 是 SVG 基础图形中最强大的元素,它只有一个属性 d
,属性里包含了命令和命令的参数。
直线
用 Path 画直线有下面五个命令,大写字母命令后面参数是绝对坐标,小写字母后面跟着的参数是相对位置,即偏移量。
M
和m
:“Move to”,在 path 的开头,用于指定绘图的起始位置;L
和l
:“Line to”,从当前位置到参数指定位置绘制一条直线;H
和h
:“Horizontal”,从当前位置画一条水平线,参数只需要指定x
的位置或者是水平移动的距离;V
和v
:“Vertical”,从当前位置画一条竖直线;Z
和z
:从当前位置画回到起点的直线,大小写字母是一样的。
上面图形由 path 构建,从 (80,50)
开始画一条水平线到 x 为 220 的位置,再画一条线到 (260,150)
,再画一条水平线到 x 为 40 的位置,最后画一条直线到起始点。
<svg width="300" height="200" xmlns="http://www.w3.org/2000/svg">
<path d="M 80 50 H 220 L 260 150 H 40 Z" />
</svg>
上图这种组合图形在一个 path 里也可以实现,不过还是写成多个 path 的表达方式更好。
<svg width="300" height="200" xmlns="http://www.w3.org/2000/svg">
<path d="M 80 50 H 260 V 150 Z M 80 70 V 150 H 260 Z" stroke="#FF4500" stroke-width="1"/>
</svg>
曲线
SVG 里有三种平滑曲线:
- 二次贝塞尔曲线;
- 三次贝塞尔曲线;
- 圆弧;
二次贝塞尔曲线 Q
二次贝塞尔曲线的命令用的字母 Q
,后面再跟着两个点的坐标,第一个点是控制点,第二个点是曲线终点,控制点控制着与与起点和终点的斜率。同样的,小写字母 q
后面跟的两个差值,差值是与上一个点的差值。
Q x1 y1, x y
(or)
q dx1 dy1, dx dy
下图的二次贝塞尔曲线从起始点 M(10,80)
开始,控制点是 (95,10)
,曲线终点是 (180,80)
。
下面是上图的源代码,第一个 path
画的就是二次贝塞尔曲线,其他内容是做标记解释用的。
<svg width="300" height="100" xmlns="http://www.w3.org/2000/svg">
<path d="M 10 80 Q 95 10, 180 80" stroke="black" fill="none" />
<circle cx="10" cy="80" r="3" fill="red" />
<text x="5" y="95" font-size="10">M(10,80)</text>
<circle cx="95" cy="10" r="3" fill="red" />
<text x="100" y="10" font-size="10">(95,10)</text>
<circle cx="180" cy="80" r="3" fill="red" />
<text x="170" y="95" font-size="10">(180,80)</text>
<path d="M 10 80 L 95 10 L 180 80" stroke="blue" stroke-width="1" fill="none" />
</svg>
用小写字母 q
绘制曲线有个好处,需要移动曲线的时候只需要移动起始点即可保持形状不变,因为小写 q
后面跟着的是相对位置,只要起始点移动,后续曲线的控制点和终点都会自动移动,如下曲线所示。
如下代码所示,上面的图就是改了起始点 M
的值就移动了曲线。
<svg width="600" height="100" xmlns="http://www.w3.org/2000/svg">
<path d="M 10 80 q 85 -70, 170 0" stroke="black" fill="none" />
<path d="M 105 80 q 85 -70, 170 0" stroke="black" fill="none" />
<path d="M 200 80 q 85 -70, 170 0" stroke="black" fill="none" />
</svg>
二次曲线还有一个 T
命令,是自动生成上一个控制点的镜像,续上二次贝塞尔曲线,因此这个命令只需要一个曲线终点的参数。T
命令自动生成控制点的可以用公式 P + (P - C) = 2P - C
计算得出,其中 P
是上一个曲线终点,C
是上一个控制点。
T x y
(or)
t dx dy
上图用了命令 T
来自动生成两个控制点,分别是 C2 = 2P1 - C1 = (2×95−52.5,2×80−10) = (137.5,150)
和 C3 = 2P2 - C2 = (2×120−137.5,2×80−150) = (102.5,10)
<svg width="250" height="180" xmlns="http://www.w3.org/2000/svg">
<path d="M 10 80 Q 52.5 10, 95 80 T 120 80 T 180 80" stroke="black" fill="transparent" />
<circle cx="10" cy="80" r="3" fill="red" />
<text x="5" y="95" font-size="10">M(10,80)</text>
<circle cx="52.5" cy="10" r="3" fill="red" />
<text x="20" y="10" font-size="10">C1(52.5,10)</text>
<circle cx="95" cy="80" r="3" fill="red" />
<text x="60" y="95" font-size="10">P1(95,80)</text>
<circle cx="137.5" cy="150" r="3" fill="#07c160" />
<text x="130" y="165" font-size="10">C2(137.5,150)</text>
<circle cx="120" cy="80" r="3" fill="red" />
<text x="100" y="70" font-size="10">P2(120,80)</text>
<circle cx="102.5" cy="10" r="3" fill="#07c160" />
<text x="105" y="10" font-size="10">C3(102.5,10)</text>
<circle cx="180" cy="80" r="3" fill="red" />
<text x="165" y="95" font-size="10">P3(180,80)</text>
<path d="M 10 80 L 52.5 10 L 95 80" stroke="blue" stroke-width="1" fill="none" />
<path d="M 95 80 L 137.5 150 L 120 80" stroke="blue" stroke-width="1" fill="none" />
<path d="M 120 80 L 102.5 10 L 180 80" stroke="blue" stroke-width="1" fill="none" />
</svg>
下图是用小写 t
命令生成,参数是终点相对于上一个终点的偏移量。
<svg width="800" height="160" xmlns="http://www.w3.org/2000/svg">
<path d="M 10 80 Q 52.5 10, 95 80 t 85 0 t 85 0" stroke="black" fill="transparent" />
<path d="M 10 80 l 255 0" stroke="blue" stroke-width="1"/>
</svg>
三次贝塞尔曲线 C
三次贝塞尔曲线有两个控制点,分别控制起始点和终点的切线方向,因此需要三个点作为参数。
C x1 y1, x2 y2, x y
(or)
c dx1 dy1, dx2 dy2, dx dy
如下图所示,两个控制点的位置不同则切线方向不同,控制点与端点的距离不同则曲率不同:
- 控制点离端点越近,曲线的长度越短,直觉上曲线更直,但其实弯更急,数学上定义的曲率更大;
- 相反的,控制点离端点越远,曲线的长度越长,弯更缓,数学上定义的曲率更小;
类似二次曲线的命令 T
,三次贝塞尔曲线也有命令 S
和 s
,它指定了第二个控制点和终点的坐标,第一个控制点是由上一个曲线的第二控制点的镜像。
S x2 y2, x y
(or)
s dx2 dy2, dx dy
如上图的绿点就是自动生成的控制点。
<svg width="190" height="160" xmlns="http://www.w3.org/2000/svg">
<path d="M 10 80 C 20 10, 30 10, 95 80 S 140 150, 180 80" stroke="black" fill="transparent" />
<circle cx="10" cy="80" r="2" fill="red" />
<circle cx="20" cy="10" r="2" fill="red" />
<circle cx="30" cy="10" r="2" fill="red" />
<circle cx="95" cy="80" r="2" fill="red" />
<circle cx="160" cy="150" r="2" fill="#07c160" />
<circle cx="170" cy="150" r="2" fill="red" />
<circle cx="180" cy="80" r="2" fill="red" />
<path d="M 10 80 L 20 10" stroke="blue" stroke-width="1" fill="none" />
<path d="M 30 10 L 95 80 L 160 150" stroke="blue" stroke-width="1" fill="none" />
<path d="M 170 150 L 180 80" stroke="blue" stroke-width="1" fill="none" />
</svg>
小写 s
命令两个点的差值坐标都是相对于上一个节点的终点,下面命令会生成与上面相同的曲线,第一个参数为 (dx2,dy2) = (140,150) - (95,80) = (45,70)
,第二个参数为 (dx,dy) = (180,80) - (95,80) = (85,0)
。
<svg width="190" height="160" xmlns="http://www.w3.org/2000/svg">
<path d="M 10 80 C 20 10, 30 10, 95 80 s 45 70, 85 0" stroke="black" fill="transparent" />
</svg>
圆弧曲线 A
这里圆弧是指椭圆的一段,命令是 A
,这个命令比较复杂。它需要下面的参数,里面的 rx
,ry
是椭圆两个轴的半径,x y
是曲线终点的坐标,曲线起点是上一个命令的终点,x-axis-rotation
是以椭圆中心为支点顺时针旋转的角度,这个参数为正值,这样椭圆的大小固定,倾斜角度也固定了,这样穿过起点和终点曲线就只有四条,再有这两个参数 large-arc-flag
,sweep-flag
确定画哪条曲线,large-arc-flag
为 1 是指定用大弧(大于等于 180° 的弧长),0 指小弧长,sweep-flag
设置 1 是指从起点到终点是顺时针方向,0 是逆时针方向。
A rx ry x-axis-rotation large-arc-flag sweep-flag x y
a rx ry x-axis-rotation large-arc-flag sweep-flag dx dy
下图的圆弧起点 M(100,100)
,终点 (150,100)
,椭圆 x
,y
轴分别是 60,30,椭圆顺时针旋转 30 度,经过这两点的四段圆弧如下。
下面是上图的代码。
<svg width="320" height="320" xmlns="http://www.w3.org/2000/svg">
<path
d="M 100 100 A 60 30 30 1 1 150 100"
stroke="black"
fill="none"
stroke-width="1" />
<path
d="M 100 100 A 60 30 30 0 1 150 100"
stroke="black"
fill="none"
stroke-width="1" />
<path
d="M 100 100 A 60 30 30 0 0 150 100"
stroke="black"
fill="none"
stroke-width="1" />
<path
d="M 100 100 A 60 30 30 1 0 150 100"
stroke="black"
fill="none"
stroke-width="1" />
<circle cx="100" cy="100" r="2" fill="red" />
<circle cx="150" cy="100" r="2" fill="red" />
<text x="10" y="20" font-size="10">large-arc-flag=1, sweep-flag=1</text>
<text x="50" y="80" font-size="10">large-arc-flag=0, sweep-flag=1</text>
<text x="70" y="120" font-size="10">large-arc-flag=0, sweep-flag=0</text>
<text x="100" y="190" font-size="10">large-arc-flag=1, sweep-flag=0</text>
</svg>
上面介绍了通过两个参数 large-arc-flag
,sweep-flag
可以确定选取哪一段椭圆曲线,但是通过起始点和终点的两个椭圆是怎么画出来的呢?下面介绍一种几何方法。
我们现在有两个点(起始点 S
和终点 E
),我们也知道椭圆的两个轴长 rx,ry
,以及椭圆顺时针旋转的角度 x-axis-rotation
,把这个角度简记为 xrt
。以 S
和 E
为椭圆圆心,先分别画两个辅助椭圆,轴长 rx,ry
,角度为 xrt
,假设有两个交点 C1,C2
,这两个就是我们要找的目标椭圆的圆心,再结合 rx,ry
和倾斜角度 xrt
就可以画出我们要找的两个椭圆。
以上面的圆弧曲线为例,先以起始点 S(100,100)
和终点 E(150,100)
为椭圆圆心, 轴长 rx=60,ry=30
,顺时针旋转 30 度,画出两个辅助椭圆(图中虚线椭圆),得到两个交点(图中绿点),交点的坐标可以通过 Mathematica 计算得出,C1=(100.42, 66.89), C2=(149.58, 133.11)
,以这两个交点为中心就可以画出我们的目标椭圆(图中蓝色椭圆),和上面圆弧画出的曲线是一致的。
<svg width="250" height="200" xmlns="http://www.w3.org/2000/svg">
<ellipse cx="100" cy="100" rx="60" ry="30"
transform="rotate(30 100 100)"
fill="none" stroke="black" stroke-width="1"
stroke-dasharray="4 2"/>
<ellipse cx="150" cy="100" rx="60" ry="30"
transform="rotate(30 150 100)"
fill="none" stroke="black" stroke-width="1"
stroke-dasharray="4 2"/>
<circle cx="100" cy="100" r="2" fill="red" />
<circle cx="150" cy="100" r="2" fill="red" />
<circle cx="100.42" cy="66.89" r="2" fill="#07c160" />
<circle cx="149.58" cy="133.11" r="2" fill="#07c160" />
<text x="80" y="115" font-size="10">S(100,100)</text>
<text x="140" y="115" font-size="10">E(150,100)</text>
<text x="70" y="55" font-size="10">C1(100.42, 66.89)</text>
<text x="130" y="150" font-size="10">C2(149.58, 133.11)</text>
<ellipse cx="100.42" cy="66.89" rx="60" ry="30"
transform="rotate(30 100.42 66.89)"
fill="none" stroke="blue" stroke-width="1"/>
<ellipse cx="149.58" cy="133.11" rx="60" ry="30"
transform="rotate(30 149.58 133.11)"
fill="none" stroke="blue" stroke-width="1"/>
</svg>
为什么上面的方法是可行的?下面简单解释一下。对于圆心固定,两个轴长固定的椭圆有多少个?有无穷多个,固定轴长的椭圆绕着圆心可以旋转一周,每个角度都是一个椭圆。
所以只要再固定一个旋转角度就可以唯一确定一个椭圆,选择角度为 0 的形式就是如下的椭圆标准方程,(h,k)
是椭圆圆心,rx,ry
是两个轴半径,这个角度的椭圆方程式最简洁,我们后面就先假设要求的椭圆的旋转角度是 0。
我们要找到两个椭圆经过起始点 S
和终点 E
,也就是只要找到两个椭圆的圆心就行。把 (h,k)
为变量,S
和 E
点的坐标带入下面公式就可以得到两个方程,这样就是求解一个二元二次方程组,得到的两个解就是我们要求的椭圆圆心。这两个方程同时表达的几何含义就是把这起始点和终点分别作为圆心的椭圆,也就是我们上面第一步划出的两个辅助椭圆,两个椭圆的交点也就是这个方程组的解。
上面讲的都是有两个椭圆的情形,此外还可能只有一个椭圆,这样椭圆圆心就在曲线起始点和终点连线的中心,没有长短弧长的区别了,large-arc-flag
取值 0 或 1 结果都是一样的,弧线由 sweep-flag
参数决定,如下图所示。
还有一种情况,如果没有能满足条件的椭圆后这个命令 A
会怎么样?这些边界情况在 SVG 标准规范中给出了说明:
- 终点如果和起点相同,那么这相当于完全省略椭圆弧段;
- 如果
rx
或ry
为 0,那么该圆弧将被当作直线段连接两个端点; - 如果
rx
或ry
为负,则去掉负号使用他们的绝对值; - 如果椭圆不够大,无法通过两个端点,则成比例的放大
rx
,ry
直到有圆弧可以穿过两个端点。
下面的命令从起始点 (40,100)
到终点 (100,100)
距离是 60,而 rx=20
,直径 40,所以要够到两个端点需要按照 1.5 倍比例放大 rx,ry
,所以实际的椭圆的半径是 rx=30, ry=75
,如下图所示和圆心 (70,100)
半径 rx=30 ry=75
的椭圆的曲线完全重合。
<svg width="200" height="300" xmlns="http://www.w3.org/2000/svg">
<ellipse cx="70" cy="100" rx="30" ry="75" stroke="yellow" stroke-width="3" fill="none" />
<path
d="M40 100 A 20 50 0 0 0 100 100"
stroke="black"
fill="none"
stroke-width="1" />
</svg>