第 7 章 路径

所有基本形状都是 <path> 元素的简写形式

<path> 元素更通用,它通过指定一系列相互连接的线、圆弧、曲线绘制任意形状的轮廓

所有描述轮廓的数据都放在 <path> 元素的 d 属性中(data 的缩写),路径数据包括单个字符的命令,如 M 表示 movetoL 表示 lineto,接着是该命令的坐标信息

moveto、lineto、closepath

每个路径都必须以 moveto 命令开始

  • 命令字母为 M,紧跟着一个使用逗号或空格分隔的 xy 坐标。这个命令用来设置绘制轮廓的“笔”的当前位置
  • moveto 命令后面紧跟着一个或多个 lineto 命令,L 表示,它的后面也是由逗号或空格分割的 xy 坐标
  • 每次使用新的 moveto 命令时,都会重新开始一条新的子路径
<!-- @format -->

<svg width="150px" height="150px" xmlns="http://www.w3.org/2000/svg">
    <g style="stroke: black; fill: none">
        <path d="M 10,10 L 100,10"></path>
        <path d="M 10,20 L 100,20 L 100,50"></path>
        <path d="M 40,60 L 10,60 L 40,42.68  M 60,60 L 90,60 L 60,42.68"></path>
    </g>
</svg>

如果要绘制封闭的图形,可以使用 closepath 命令,Z 表示。使用 closepath 命令绘制出来的封闭图形,开始线和结束线会被连接到一起,形成一个连续的形状,如果线条宽度更粗或者设置了 stroke-linecapstroke-linejoin 效果时,区别更明显

<!-- @format -->

<svg width="150px" height="150px" xmlns="http://www.w3.org/2000/svg">
    <g style="stroke: black; fill: none">
        <!-- 四条线形式的矩形 -->
        <path d="M 10,10 L 40,10 L 40,30 L 10,30 L 10,10"></path>
        <!-- closepath 绘制的矩形 -->
        <path d="M 60,10 L 90,10, L 90,30 L 60,30 Z"></path>
        <!-- 两个 30 度角 -->
        <path
            d="M 40,60 L 10,60 L 40,42.68 Z M 60,60 L 90,60 L 60,42.68 Z"
        ></path>
    </g>
</svg>

相对 moveto 和 lineto

  • 小写字母 l 会被解析为相对于当前的画笔位置
  • 小写字母 m 会被解析为相对位置,相对于上一个坐标的,如果是第一个,则是相对于坐标原点

本章中所有其他命令都有同样的大小写差别,大写为绝对位置,小写为相对位置。closepath 命令除外,因为它没有坐标,因此大小写效果相同

路径的快捷方式

水平和垂直 lineto 命令

简写形式等价的冗长形式效果
H 20L 20 y绘制一条到绝对位置 (20, y) 的线(即 y 不变,画横线)
h 20l 20 0绘制一条,相对上一个点到 (x + 20, y) 的线
V 20L x 20绘制一条到绝对位置 (x, 20) 的线 (即 x 不变,画竖线)
v 20l 0 20绘制一条,相对上一个点到 (x, y + 20) 的线

路径快捷方式表示法

  • 可以在 L 或者 l 后面放多组坐标,如 <polyline> 元素中那样
  • 如在 M 或者 m 后面放多组坐标,则除了第一对坐标外,其他坐标会被解析为跟在一个 lineto 后面
  • 所有不必要的空白都可以消除(为了代码可读性,请自行斟酌是否需要消除)
    • 命令字母后面的空白
    • 数字和命令之间的空白
    • 正数和负数之间的空白

还可在水平和垂直 lineto 命令后面放置多个坐标轴,但只在使用线标记时才会看到效果,目前还没讨论过线标记。H 25 35 45H 45 相同,V 11 13 15V 39 相同

椭圆弧

圆弧命令以 A 或者 a 开始,后面紧跟以下 7 个参数

  • 点所在椭圆的 x 半径和 y 半径
  • x-axis-rotation:椭圆的 x 轴旋转角度(默认为 0,与坐标系统的 x 轴平行)
  • large-arc-flag:如果需要圆弧的角度小于 180 度,则为 0,否则为 1
  • sweep-flag:如果需要圆弧以负角度绘制则为 0,否则为 1
  • 终点的 x 坐标和 y 坐标(起点由前一个绘制的点或者最后一个 moveto 命令确定)

<!-- @format -->

<svg width="400px" height="200px" xmlns="http://www.w3.org/2000/svg">
    <g style="fill: none">
        <!-- b -->
        <path d="M 125,75 A100,50 0 0,0 225,125" style="stroke: red" />
        <!-- c -->
        <path d="M 125,75 A100,50 0 0,1 225,125" style="stroke: blue" />
        <!-- d -->
        <path d="M 125,75 A100,50 0 1,0 225,125" style="stroke: black" />
        <!-- e -->
        <path d="M 125,75 A100,50 0 1,1 225,125" style="stroke: orange" />
    </g>
</svg>

一个太极图形

<!-- @format -->

<svg width="400px" height="300px" xmlns="http://www.w3.org/2000/svg">
    <!-- 灰色投影 -->
    <ellipse cx="154" cy="154" rx="150" ry="120" style="fill: #999999" />
    <!-- 浅蓝色椭圆 -->
    <ellipse cx="152" cy="152" rx="150" ry="120" style="fill: #cceeff" />
    <!-- 浅红色大半圆填充符号的上半部分,其下方“浸入"符号左下方的浅红色小半圆 -->
    <path
        d="M 302 152 A 150 120, 0, 1, 0, 2 152 
 A 75 60, 0, 1, 0, 152 152"
        style="fill: #ffcccc"
    />
    <!-- 浅蓝色小半圆,填充符号右上方 -->
    <path d="M 152 152 A 75 60, 0, 1, 1, 302 152" style="fill: #cceeff" />
</svg>
  • 不能使用一个 <path> 命令绘制一个完整的椭圆。如果弧形的起点和终点相同,则有无数种方式定位椭圆。SVG 阅读器会跳过这样的圆弧命令。如果指定的椭圆半径太小,导致不能覆盖起点和终点,则 SVG 阅读器会扩大椭圆知道它足够覆盖起点和终点
  • 对于如何处理超出范围的参数,请参考 弧形实现说明规范open in new window

从其他弧线格式转换

SVG 之所以选择这种看似古怪的方法来实现弧形,是因为:在 SVG 中,弧形并不能单独存在,它要成为线和曲线连接路径的一部分(如,一个圆角矩形就是由一系列的线和椭圆弧组成的)

假设有一个按照如下方式指定的椭圆

<ellipse cx="cx" cy="cy" rx="rx" ry="ry" />

下面是绘制 4 种可能的半个椭圆的路径(括号中是要计算的代数表达式)

<!-- 北半球 -->
<path d="M (cx - rx) cy A rx ry 0 1 1 (cx + rx) cy" />
<!-- 南半球 -->
<path d="M (cx - rx) cy A rx ry 0 1 0 (cx + rx) cy" />
<!-- 东半球 -->
<path d="M cx (cy - ry) A rx ry 0 1 1 cx (cy + ry)" />
<!-- 西半球 -->
<path d="M cx (cy - ry) A rx ry 0 1 0 cx (cy + ry)" />
  • 有时候想要绘制一个指定了中心和角度的任意弧,并且想要将它转换为 SVG 的终点和范围格式
  • 有时候还可能希望弧形由 SVG 格式转换为中点和角度格式

第二种情况的数学运算相当复杂,详细信息可查看 SVG 规范,在附录 F 中可以看到这些转换方式的 JavaScript 版本

贝塞尔曲线

二次贝塞尔曲线

Qq 命令指定一个二次曲线,后面紧跟 2 组坐标:控制点、终点(起点由前一个绘制的点或者最后一个 moveto 命令确定)

<!-- @format -->

<svg>
    <path d="M30 75 Q240 30, 300 120" style="stroke: black; fill: none" />
</svg>

还可以在二次曲线命令后面指定多组坐标,这样会生成一个多边贝塞尔曲线

<!-- @format -->

<svg>
    <path
        d="M30 100 Q 80 30 100 100 130 65 200 80"
        style="stroke: black; fill: none"
    />
</svg>

使用 Tt 可以实现平滑的二次贝塞尔曲线,其会自动计算新的控制点,方法为:使新的控制点与上一条命令中的控制点相对于当前点中心对称

<!-- @format -->

<svg>
    <path
        d="M30 100 Q 80 30 100 100 T 200 80"
        style="stroke: black; fill: none"
    />
</svg>

从数学角度来讲,新的控制点的坐标 (x2, y2) 基于上一条线段的端点坐标 (x, y) 和上一个控制点的坐标 (x1, y1) ,按照如下规则计算

  • x2 = x + (x - x1) = 2 * x - x1
  • y2 = y + (y - y1) = 2 * y - y1

三次贝塞尔曲线

使用 C 或者 c 命令,后面紧跟 3 组坐标:起点的控制点、终点的控制点、终点

<!-- @format -->

<svg>
    <path
        d="M20 80 C 50 20, 150 60, 200 120"
        style="stroke: black; fill: none"
    />
</svg>

三次贝塞尔曲线也可以通过在三次曲线命令后面指定多组坐标,来构建多条连接在一起的三次曲线,第一条取消的最后一个点会变成下一条曲线的第一个点,以此类推。

使用 Ss 命令可以实现平滑的三次贝塞尔曲线,其规则与二次贝塞尔曲线的 T 命令类似,会自动计算第一个控制点,因此只需提供另外两组坐标即可

<!-- @format -->

<svg>
    <path
        d="M30 100 C 50 30 70 50 100 100 S 150 40 200 80"
        style="stroke: black; fill: none"
    />
</svg>

路径总结

大写命令使用绝对坐标,小写命令使用相对坐标

命令参数效果
M mx y移动到给定坐标
L lx y绘制一条到给定坐标的线,可以提供多组坐标来绘制折线
H hx绘制一条到给定 x 坐标的横线
V vy绘制一条到给定 y 坐标的横线
A arx ry x-axis-ratation large-arc sweep x y绘制一个从当前点到 (x, y) 的椭圆弧。椭圆上的 x 半径为 rxy 半径为 ry。椭圆旋转 x-axis-ratation 度。如果圆弧小于 180 度,则 large-arc 为 0,反之为 1,如果圆弧按顺时针方向绘制,则 sweep 为 1,反之为 0
Q qx y x1 y1绘制一条从当前点到 (x, y),控制点为 (x1, y1) 的二次贝塞尔曲线
T tx y绘制一条从当前点到 (x, y) 的二次贝塞尔曲线。控制点是前一个 Q 命令的控制点的中心对称点。如果没有前一条曲线,当前点会被用作控制点
C cx y x1 y1 x2 y2绘制一条从当前点到 (x, y) 的三次贝塞尔曲线,(x1, y1) 为曲线的开始控制点,(x2, y2) 为曲线的终点控制点
S sx y x2 y2绘制一条从当前点到 (x, y) 的三次贝塞尔曲线,(x2, y2) 为曲线的终点控制点。第一个控制点是前一个 C 命令的终点控制点的中心对称点。如果前一个曲线不存在,当前点会被用作第一个控制点

路径和填充

第 4 章介绍的填充规则同样适用于路径,路径中不仅可以有相交线,还可以有“缺口”

<!-- @format -->

<svg >
    <defs>
        <g id="g" style="stroke: red">
            <!-- 顺时针方向的路径 -->
            <path
                d="M 0 0, 60 0, 60 60, 0 60 Z M 15 15, 45 15, 45 45, 15 45Z"
            />
        </g>
        <g id="g2" style="stroke: red">
            <!-- 外部路径为顺时针方向,内部路径为逆时针方向 -->
            <path
                d="M 0 0, 60 0, 60 60, 0 60 Z M 15 15, 15 45, 45 45, 45 15Z"
            />
        </g>
    </defs>

    <use xlink:href="#g" style="fill-rule: nonzero" />
    <use xlink:href="#g2" style="fill-rule: nonzero" x="100" />
    <use xlink:href="#g" style="fill-rule: evenodd" y="80" />
    <use xlink:href="#g2" style="fill-rule: evenodd" x="100" y="80" />
</svg>

<marker> 元素

用于给图形添加起终点标记和绘制方向标记

<!-- @format -->

<svg>
    <defs>
        <marker
            id="mCircle"
            markerWidth="10"
            markerHeight="10"
            refX="5"
            refY="5"
        >
            <circle cx="5" cy="5" r="4" style="fill: none; stroke: black" />
        </marker>

        <marker
            id="mArrow"
            markerWidth="4"
            and
            markerHeight="8"
            refX="0"
            refY="4"
            orient="auto"
        >
            <path d="M 0 0 4 4 0 8" style="fill: none; stroke: black" />
        </marker>

        <marker
            id="mTriangle"
            markerWidth="5"
            markerHeight="10"
            refX="5"
            refY="5"
            orient="auto"
        >
            <path d="M 0 0 5 5 0 10 Z" style="fill: black" />
        </marker>
    </defs>
    <path
        d="M 10 20 100 20 A 20 30 0 0 1 120 50 L 120 110"
        style="
            marker-start: url(#mCircle);
            marker-mid: url(#mArrow);
            marker-end: url(#mTriangle);
            fill: none;
            stroke: black;
        "
    />
</svg>
  • <marker> 自身不会显示,但是可以把它放到 <defs> 元素中,因为它是存放可复用元素的
  • 一个 <marker> 是一个独立的图形,它拥有自己私有的坐标,因此必须指定它的 markerWidthmarkerHeight
  • <marker> 开始和结束标签之间绘制标记图形
  • <path> 中,通过 marker-start 属性引用 <marker>(标记被认为是表现而不是结构的一部分,这是灰色地带,可以支持任何一种观点)
  • 默认情况下,开始标记的 (0, 0) 与路径的开始坐标对齐,<marker> 中的 refXrefY 属性指定哪个坐标(标记的坐标系统中)与路径的开始坐标对齐
  • 必须明确设置标记的 orient 属性为 auto,这会让标记自动旋转以匹配路径的方向(确切地讲,旋转角度是线条进入标记图形和线条退出标记图形时的角度平均值。也可以指定度数,此时标记始终按照指定的度数旋转)
  • 如果将 markerUnits 属性
    • strokeWidth(默认值):标记的坐标系统会被设置为单位等于笔画宽度,标记会与笔画宽度成正比,通常也是我们想要的效果
    • userSpaceOnUse:标记的坐标系统会被假定为引用该标记的对象的坐标系统一样,不管笔画宽度为多少,标记都会保持相同的尺寸

标记记录

  • 如果想要路径的起点、中间、终点都使用相同的标记,只需使用 marker 属性引用想要的标记即可
  • 可以在 <marker> 元素上设置 viewBoxpreserveAspectRatio 属性,以更好地控制它的显示效果。例如可以使用 viewBox 定义网格,让坐标原点位于标记的中心,而不是使用 refXrefY
  • 可以在 <polygon><polyline><line><path> 中引用 <marker>
  • 可以在标记中的路径添加标记,但是不建议。此时第二个标记必须适配由第一个标记的 markerWidth markerHeight 建立的矩形。如果要实现这种效果,最好将从属标记也作为主标记的一部分,而不是嵌套
  • 确保没有为标记定义自己作为从属标记。使用如下 CSS 规则可能会发生这种情况
path {
    marker: url(#start);
}
/* 为了防止出现自己定义自己,添加如下样式 */
marker#start path {
    marker: none;
}
Last Updated:
Contributors: af