在前三篇文章中,我们先后学习了矩形、圆形和椭圆这三种基础图形。它们简单、直观,适合用来搭建规整的界面元素。但如果你需要绘制任意形状——一条蜿蜒的河流、一颗跳动的心脏、一个品牌的 Logo,或者一条平滑的数据趋势线——矩形和圆形就无能为力了。
这时,你需要的是 SVG 家族中的“瑞士军刀”——<path> 元素。
<path> 是 SVG 中最通用、最强大的图形元素。官方文档明确写道:“所有基本形状都可以用 path 元素创建。”换句话说,你学过的 <rect>、<circle>、<ellipse>,本质上都只是 <path> 的“特化版本”。
但强大的背后是复杂。<path> 的核心——d 属性——是一个由命令字母和数字组成的字符串,看起来像天书一样晦涩难懂。很多开发者看到 d="M10,30 A20,20 0,0,1 50,30 ..." 就望而却步了。
本文的目标,就是帮你彻底读懂这段“天书”。我们将从 SVGPathElement 接口入手,逐条拆解路径命令的含义,并通过 JavaScript 动态操控路径,让你真正“征服”这个强大的元素。
本文定位:本篇是“征服 SVGPathElement”的上半部分,聚焦于 SVGPathElement 接口概览和直线类路径命令。曲线命令(贝塞尔曲线、圆弧)将在下篇中深入讲解。
一、SVGPathElement:路径元素的 DOM 接口
在开始写路径命令之前,我们先来看看 <path> 元素在 JavaScript 中对应的接口——SVGPathElement。
1.1 继承体系回顾
回顾一下我们在第 2 篇中建立的继承链:
EventTarget → Node → Element → SVGElement → SVGGraphicsElement → SVGGeometryElement → SVGPathElementSVGPathElement 直接继承自 SVGGeometryElement。这意味着它天然拥有所有几何图形的“超能力”:
getTotalLength():获取路径的总长度。
getPointAtLength(distance):获取路径上指定距离处的坐标点。
getBBox():获取路径的边界框。
isPointInFill() / isPointInStroke():判断点是否在填充/描边区域内。
1.2 实例属性
SVGPathElement 自身只定义了一个只读属性:
属性 | 类型 | 说明 |
|---|---|---|
pathLength | SVGAnimatedNumber | 对应 <path> 标签上的 pathLength 属性,允许作者为路径指定一个总长度(以用户单位计) |
这个属性的用途比较特殊——它允许你“告诉”浏览器路径的总长度,从而影响虚线等基于长度的渲染计算。在大多数日常开发中,你更常用的是从父级继承来的 getTotalLength() 方法,它会由浏览器自动计算路径的真实长度。
1.3 实例方法
SVGPathElement 自身定义了以下方法:
方法 | 说明 | 状态 |
|---|---|---|
getTotalLength() | 返回浏览器计算出的路径总长度(用户单位) | 标准 |
getPointAtLength(distance) | 回路径上指定距离处的点(DOMPoint) | 标准 |
getPathData() | 返回路径数据的段序列,可选择标准化 | 实验性 |
setPathData(pathData) | 将路径段序列设置为新的路径数据 | 实验性 |
getPathSegmentAtLength(distance) | 返回路径上指定距离处的段 | 实验性 |
重要提示:在 SVG 2 中,getTotalLength() 和 getPointAtLength() 被移动到了父接口 SVGGeometryElement 中。但由于 SVGPathElement 继承自 SVGGeometryElement,所以你在 SVGPathElement 的实例上仍然可以调用这两个方法——它们只是“祖传”的能力而已。
1.4 已废弃的方法(了解即可)
在 SVG 1.1 时代,SVGPathElement 上有一系列 createSVGPathSeg* 方法和 getPathSegAtLength() 方法。这些在 SVG 2 中已被移除。如果你在老旧代码中看到它们,知道是历史遗留即可,不要在新项目中使用。
二、d 属性:路径的“灵魂”
<path> 元素最核心的属性就是 d(data 的缩写)。它定义了路径的实际形状。
d 属性的值是一个路径命令列表——每个命令由一个命令字母和若干数字参数组成。浏览器按照命令列表的顺序,从起点开始一笔一画地绘制出完整的图形。
2.1 命令字母的大小写含义
路径命令的字母有大写和小写两种形式,含义截然不同:
形式 | 含义 |
|---|---|
大写字母(如 M、L、C) | 绝对坐标——参数是相对于整个 SVG 坐标系的原点 |
小写字母(如 m、l、c) | 相对坐标——参数是相对于“当前点”的偏移量 |
记忆技巧:大写 = Absolute(绝对),小写 = relative(相对)。你可以在同一个 d 属性中混用大小写命令。
2.2 当前点(Current Point)
理解路径的关键是理解 “当前点” 的概念。浏览器在解析路径命令时,会始终维护一个“当前点”的位置:
初始状态下,当前点在原点 (0, 0)。
每执行一个绘图命令,当前点会更新为该命令的终点。
后续命令的起点,就是上一个命令的终点。
这个“当前点”就像一支画笔的笔尖——你告诉它“移动到这里”“画到那里”,它就会沿着你指定的轨迹移动。
三、直线类命令
我们先从最简单的命令开始——所有画直线的命令。它们是构建复杂路径的基石。
3.1 M / m:移动到(Move To)
M 命令的作用是将“笔尖”移动到指定位置,但不画任何东西。
命令 | 参数 | 说明 |
|---|---|---|
M x y | 绝对坐标 (x, y) | 移动到绝对位置 |
m dx dy | 相对偏移 (dx, dy) | 从当前点移动 dx、dy |
为什么需要 M?
因为路径的起点必须被明确指定。没有 M 命令,浏览器就不知道从哪里开始画。
示例:
d="M 100 50"这条命令只是把笔尖移动到 (100, 50),不画任何东西。后续的命令将从这里开始绘制。
注意:在同一个 d 属性中,如果出现多个 M 命令,就表示绘制多条不相连的子路径。比如 d="M 10 10 L 50 10 M 10 30 L 50 30" 会画出两条分离的横线。
3.2 L / l:画直线到(Line To)
L 命令从当前点画一条直线到指定位置。
命令 | 参数 | 说明 |
|---|---|---|
L x y | 绝对坐标 (x, y) | 从当前点画直线到绝对位置 |
l dx dy | 相对偏移 (dx, dy) | 从当前点画直线到偏移后的位置 |
示例:画一个三角形
<path d="M 50 10 L 90 90 L 10 90 Z" fill="blue" />解释:
M 50 10:将笔尖移动到 (50, 10)
L 90 90:从 (50, 10) 画直线到 (90, 90)
L 10 90:从 (90, 90) 画直线到 (10, 90)
Z:闭合路径,从 (10, 90) 画直线回到起点 (50, 10)
3.3 H / h:水平线到(Horizontal Line To)
H 命令画一条水平直线——只改变 x 坐标,y 坐标不变。
命令 | 参数 | 说明 |
|---|---|---|
H x | 绝对 x 坐标 | 从当前点画水平线到 x 位置 |
h dx | 相对偏移 | 从当前点画水平线,x 增加 dx |
示例:
d="M 10 50 H 100"解释:从 (10, 50) 画一条水平线到 (100, 50)。
3.4 V / v:垂直线到(Vertical Line To)
V 命令画一条垂直直线——只改变 y 坐标,x 坐标不变。
命令 | 参数 | 说明 |
|---|---|---|
V y | 绝对 y 坐标 | 从当前点画垂直线到 y 位置 |
v dy | 相对偏移 | 从当前点画垂直线,y 增加 dy |
示例:
d="M 50 10 V 90"解释:从 (50, 10) 画一条垂直线到 (50, 90)。
3.5 Z / z:闭合路径(Close Path)
Z 命令从当前点画一条直线回到路径的起点(即最近一个 M 或 m 命令的位置)。
命令 | 参数 | 说明 |
|---|---|---|
Z 或 z | 无 | 闭合当前子路径 |
Z 和 z 的效果完全相同——大小写没有区别。
示例:用 H 和 V 画一个矩形
<path d="M 20 20 H 120 V 80 H 20 Z" fill="red" />解释:
M 20 20:移动到左上角
H 120:画顶边到 (120, 20)
V 80:画右边到 (120, 80)
H 20:画底边到 (20, 80)
Z:闭合,画左边回到 (20, 20)
四、通过 JavaScript 操作路径
理解了路径命令的语法之后,我们来看看如何用 JavaScript 动态操控 <path> 元素。
4.1 读取和修改 d 属性
最直接的方式是使用 getAttribute 和 setAttribute:
<svg viewBox="0 0 200 200">
<path id="myPath" d="M 20 20 L 180 20 L 180 180 L 20 180 Z" fill="lightblue" stroke="blue" />
</svg>const path = document.getElementById('myPath');
// 读取当前的 d 属性值
const currentD = path.getAttribute('d');
console.log(currentD); // "M 20 20 L 180 20 L 180 180 L 20 180 Z"
// 修改 d 属性——画一个三角形
path.setAttribute('d', 'M 100 10 L 190 90 L 10 90 Z');常见陷阱:SVGPathElement 上 没有 d 这个实例属性可以直接读写。你可能会尝试 path.d = '...',但这不会生效。必须使用 setAttribute('d', ...)。
4.2 用 getTotalLength() 获取路径长度
getTotalLength() 返回浏览器计算出的路径总长度,以用户单位计。这个值在实现路径动画时非常关键。
const length = path.getTotalLength();
console.log(`路径总长度: ${length}`);4.3 用 getPointAtLength() 获取路径上的点
getPointAtLength(distance) 返回路径上指定距离处的坐标点,类型为 DOMPoint。
// 获取路径中点位置的坐标
const midLength = path.getTotalLength() / 2;
const midPoint = path.getPointAtLength(midLength);
console.log(`中点坐标: (${midPoint.x}, ${midPoint.y})`);这个方法的典型应用场景包括:
路径跟随动画:让一个物体沿着路径移动。
标注定位:在路径的特定位置添加文字或标记。
交互反馈:在鼠标悬停位置显示路径上的信息。
4.4 综合示例:动态绘制并分析路径
下面是一个完整的示例,演示如何用 JavaScript 动态创建路径、读取其长度,并获取中点坐标:
<svg viewBox="0 0 400 300" id="canvas">
<path id="dynamicPath" fill="none" stroke="#E67E22" stroke-width="3" />
<!-- 用来标记中点的圆 -->
<circle id="midMarker" r="6" fill="red" />
</svg>
<button id="drawBtn">绘制路径</button>
<button id="infoBtn">显示信息</button>
<div id="info"></div>const path = document.getElementById('dynamicPath');
const marker = document.getElementById('midMarker');
const info = document.getElementById('info');// 点击按钮绘制一条折线路径
document.getElementById('drawBtn').addEventListener('click', () => {
// 使用相对坐标和绝对坐标混合构建路径
const d = 'M 50 150 L 150 50 L 250 150 L 350 50 L 350 200';
path.setAttribute('d', d);
info.textContent = '路径已绘制!点击"显示信息"查看详情。';
});// 点击按钮显示路径信息
document.getElementById('infoBtn').addEventListener('click', () => {
try {
const totalLength = path.getTotalLength();
const midPoint = path.getPointAtLength(totalLength / 2);
// 将标记移动到中点位置
marker.setAttribute('cx', midPoint.x);
marker.setAttribute('cy', midPoint.y);
marker.setAttribute('r', '6');
marker.setAttribute('fill', 'red');
info.innerHTML = `
<strong>路径分析结果:</strong><br />
总长度:${totalLength.toFixed(2)} 用户单位<br />
中点坐标:(${midPoint.x.toFixed(2)}, ${midPoint.y.toFixed(2)})<br />
路径命令:${path.getAttribute('d')}
`;
} catch (e) {
info.textContent = '请先绘制路径!';
}
});五、d 作为 CSS 属性(前沿知识点)
在传统 SVG 中,d 是 <path> 元素的一个标签属性。但在现代 Web 标准中,d 也可以作为 CSS 属性使用。
这意味着你可以通过 CSS 来改变路径的形状,甚至实现路径的形变动画:
/* 悬停时改变路径形状 */
#svg_css_ex1:hover path {
d: path("M10,30 A20,20 0,0,1 50,30 A20,20 0,0,1 90,30 Q90,60 50,90 Q10,60 10,30 z M5,5 L90,90");
}应用场景:使用 CSS d 属性配合 transition,可以实现路径形状的平滑过渡动画——这是 SVG 1.1 时代需要用 SMIL 或 JavaScript 才能完成的效果。
不过需要注意的是,CSS d 属性只适用于 SVG <path> 元素,不适用于其他 SVG 元素或 HTML 元素。
六、常见错误与避坑指南
错误一:忘记 M 命令
<!-- 这样写是错的!没有指定起点 -->
<path d="L 100 100" />正确做法:每个路径必须以 M 或 m 开头。
错误二:混用绝对和相对坐标时计算错误
<!-- 从 (50, 50) 出发,想画到 (100, 80) -->
<path d="M 50 50 l 50 30" /> <!-- 正确:相对偏移 -->
<path d="M 50 50 L 100 80" /> <!-- 正确:绝对坐标 -->
<path d="M 50 50 l 100 80" /> <!-- 错误:实际到了 (150, 130) -->正确做法:使用相对坐标时,始终记得参数是偏移量而不是绝对位置。
错误三:通过 path.d 直接赋值
// 这样写不会生效!
path.d = 'M 10 10 L 90 90';正确做法:使用 setAttribute('d', ...)。
错误四:忘记 Z 导致路径不闭合
<!-- 三角形缺了一条边 -->
<path d="M 50 10 L 90 90 L 10 90" />虽然视觉上可能看起来是三角形(如果 fill 生效),但路径实际上没有闭合——最后一条边(从 (10, 90) 回到 (50, 10))是不存在的。如果设置了 fill,浏览器会自动填充封闭区域,但 stroke 会暴露出缺失的边。
正确做法:如果需要闭合路径,在末尾加上 Z。
七、本章总结
概念 | 要点 |
|---|---|
SVGPathElement | <path>的 DOM 接口,继承自SVGGeometryElement |
d属性 | 路径的核心,由命令字母 + 数字参数组成 |
大写 vs 小写 | 大写 = 绝对坐标,小写 = 相对坐标 |
当前点 | 浏览器维护的“笔尖”位置,每个命令的终点成为下个命令的起点 |
M / m | 移动到指定位置(不画线) |
L / l | 画直线到指定位置 |
H / h | 画水平线 |
V / v | 画垂直线 |
Z / z | 闭合路径——回到起点 |
getTotalLength() | 获取路径总长度 |
getPointAtLength() | 获取路径上指定距离处的坐标点 |
CSS d 属性 | 前沿特性,支持用 CSS 控制路径形状 |
三个核心认知:
路径的本质是一系列绘图指令——浏览器按照指令列表,从当前点开始逐条执行,最终形成完整的图形。
SVGPathElement 的能力来自继承——getTotalLength() 和 getPointAtLength() 实际上是 SVGGeometryElement 赋予的。
操作路径用 setAttribute——SVGPathElement 没有直接的 d 属性,必须通过 setAttribute('d', ...) 来修改。
下篇预告
在下一篇文章中,我们将进入路径命令的 高阶领域——曲线。你将学会:
二次贝塞尔曲线(Q / q):用一个控制点定义的平滑曲线。
三次贝塞尔曲线(C / c):用两个控制点定义的最灵活曲线。
平滑曲线(S / s、T / t):让曲线过渡更自然的快捷命令。
圆弧(A / a):用半径和角度定义的椭圆弧。
综合实战:用路径绘制一个品牌 Logo。
互动话题:尝试用本文学到的直线命令,在 SVG 中画一个你喜欢的几何图形(比如五角星、房子、箭头)。欢迎在评论区分享你的 d 属性代码和效果截图!