改图的六种常见手术¶
读图之后就到动手环节。改 SVG 的需求看似千变万化,归纳下来其实就六类操作。每一类都有固定套路,照做即可。
先理解:为什么一改就跑偏¶
图:Word 删一个元素后自动补位,SVG 原地留洞——坐标绝对最直观的代价
这是改 SVG 最核心的一条原理,先讲清楚,后面六种手术才好理解。
SVG 里每个元素的坐标都是绝对写死的,元素之间没有任何"相对关系"。
对比一下:
| 场景 | 插入/删除一个元素后 |
|---|---|
| Word 文档 | 后面的内容自动前移/后移排版 |
| SVG | 其余元素纹丝不动,原地不动 |
举例:
<rect x="20" y="20" width="100" height="40"/> <!-- 元素 A -->
<rect x="20" y="70" width="100" height="40"/> <!-- 元素 B -->
<rect x="20" y="120" width="100" height="40"/> <!-- 元素 C -->
现在删掉 B,结果是:A 不动、C 不动,中间留下一个 50px 的空洞。B 的消失不会让 C 自动上移。
所以——
任何"插入"或"删除",实际工作量 = 操作本身 + 手工重排所有受影响的元素。
唯一的例外是用分组规避:把需要整体移动的元素包进一个 <g>,之后移动这个 <g> 就能一次性带走所有子元素(见手术四)。
手术一:加一个元素¶
图:复制相近元素并改坐标,加出一个新模块
套路:复制一个长得像的现存元素 → 改坐标 → 改内容/属性。
不要从零写。从图里找一个结构相近的元素复制,改起来最不容易出错,还能顺带继承正确的样式(描边色、字号等)。
<!-- 现存:一个方框 + 居中文字 -->
<g>
<rect x="20" y="20" width="130" height="60" rx="10"
fill="#e8f0fe" stroke="#1a73e8" stroke-width="2"/>
<text x="85" y="48" text-anchor="middle" font-size="14"
font-weight="700" fill="#1a73e8">模块一</text>
</g>
<!-- 复制后改:整体下移 120(注意 rect 和 text 的 y 都要改!) -->
<g>
<rect x="20" y="140" width="130" height="60" rx="10"
fill="#e8f0fe" stroke="#1a73e8" stroke-width="2"/>
<text x="85" y="168" text-anchor="middle" font-size="14"
font-weight="700" fill="#1a73e8">模块二</text>
</g>
最容易漏的:一个视觉元素 = 多个 SVG 元素
上面例子里,"一个带文字的方框"由 2 个 SVG 元素组成。只改 <rect> 的 y 而忘了 <text> 的 y,文字就会跑到框外面去。 做法:复制时先数清楚目标由几个元素构成(回看 读图第 ③ 步),每个都要改。
要插在中间而不是末尾? 先给后面的元素"让位"——手工把它们的 y 全部加上间距;或者更省事的做法是,把后面的元素整体包一个 <g transform="translate(0, 120)">(见手术四)。
手术二:删一个元素¶
图:删掉元素后留下空洞,其余元素纹丝不动
套路:定位 → 整组删除 → 检查是否留空洞 → 决定是否重排。
删除本身很简单,真正的坑有两个:
坑 1:删了元素,留下了被别处引用的定义¶
如果删了 <defs> 里的 arrow,所有引用它的 line 都会失去箭头(不报错,静默失效)。
做法:删 <defs> 里的东西之前,先搜一下它的 id 在别处有没有 url(#...) 引用。
坑 2:删了元素,留下视觉空洞¶
见前面"为什么一改就跑偏"。决定是否要把后面的元素补上来。
更稳妥的替代:先隐藏,别急着删¶
如果只是暂时不想看到,用属性隐藏比删除安全:
<rect ... display="none"/> <!-- 完全不渲染,也不占布局 -->
<rect ... visibility="hidden"/><!-- 不渲染,但保留位置 -->
<rect ... opacity="0"/> <!-- 完全透明,仍可被点击/响应事件 -->
三者的区别:
| 属性 | 是否显示 | 是否占位 | 能否响应事件 |
|---|---|---|---|
display="none" | 否 | 否 | 否 |
visibility="hidden" | 否 | 是 | 否 |
opacity="0" | 否(透明) | 是 | 是 |
不确定还要不要的时候,先 display="none",反悔时删掉属性即可。
手术三:改一个元素(属性级)¶
套路:直接改属性。这是最简单的一种,但要清楚常见属性对应什么。
| 想改什么 | 改哪个属性 | 备注 |
|---|---|---|
| 位置(矩形/图像左上角) | x / y | 圆是 cx / cy |
| 大小 | width / height | 圆是 r |
| 圆角 | rx(/ ry) | 只用于 rect |
| 填充色 | fill | fill="none" = 不填充 |
| 描边色 | stroke | |
| 描边粗细 | stroke-width | |
| 虚线 | stroke-dasharray | 如 "6 4" = 6 实 4 虚 |
| 透明度 | opacity 或 fill-opacity | 0~1 |
| 旋转某个角度 | transform="rotate(角度 中心x 中心y)" | 角度制,顺时针为正 |
线条两端与拐角¶
图:linecap=round 端点变圆、linejoin=round 拐角变柔
stroke-linecap:端点形状(butt平 /round圆 /square方)stroke-linejoin:拐角形状(miter尖 /round圆 /bevel斜切)
示意图(同一条折线在三种 linecap 下的端点差异):
画流程图、示意线时,用 round 通常观感更好。
transform 的三种基本形式¶
transform="translate(dx, dy)" <!-- 平移 -->
transform="rotate(角度, cx, cy)" <!-- 绕 (cx,cy) 旋转,省略中心则绕原点 -->
transform="scale(sx, sy)" <!-- 缩放;scale(-1,1) = 水平镜像 -->
多个变换可串联,从右往左依次应用:
注意:
rotate的角度顺时针为正(因为 y 轴朝下)。想逆时针转 30°,写rotate(-30)。
手术四:挪一整组(最重要的一招)¶
图:套 g transform 后整组一次性平移,子元素坐标不变
适用场景:要把某个模块整体移动、缩放、旋转。
套路:给这一组元素套一个 <g transform="...">。
<!-- 改前:三个元素,各写各的坐标 -->
<rect x="20" y="20" width="100" height="40"/>
<text x="70" y="45">标题</text>
<line x1="20" y1="70" x2="120" y2="70"/>
<!-- 改后:套一个 g,整体下移 100、右移 30 -->
<g transform="translate(30, 100)">
<rect x="20" y="20" width="100" height="40"/>
<text x="70" y="45">标题</text>
<line x1="20" y1="70" x2="120" y2="70"/>
</g>
子元素坐标一个都不用改——这就是分组的价值。
为什么这招特别重要¶
回到"插入元素"的困境:要在图中间插一个新模块,按手术一得把后面所有元素的坐标逐个改一遍。用分组的话:
<g id="part1">...</g> <!-- 前面的,不动 -->
<g id="new-module">...</g> <!-- 新插入的 -->
<g id="part2" transform="translate(0, 120)"> <!-- 后面的,整体上移/下移让位 -->
...
</g>
一行搞定,而且可逆(删掉 transform 就还原)。
嵌套分组的坐标叠加¶
<g> 可以嵌套,transform 会累加:
<g transform="translate(100, 0)"> <!-- 外层:右移 100 -->
<g transform="translate(0, 50)"> <!-- 内层:再下移 50 -->
<rect x="0" y="0" width="10" height="10"/>
<!-- 这个 rect 实际出现在 (100, 50) -->
</g>
</g>
排查"元素跑到了奇怪的地方"时,记得沿着父级 <g> 一路把 transform 加起来。
手术五:改文字¶
文字相关的需求通常是:改内容、调字号、修重叠、改对齐。
基本属性¶
图:y 是基线;dominant-baseline=middle 让文字垂直居中
| 属性 | 作用 | 常用值 |
|---|---|---|
x / y | 文字基线位置(注意:不是文字框左上角) | |
font-size | 字号(单位即 viewBox 单位) | 10–16 |
font-weight | 字重 | 400 常规 / 700 粗体 |
text-anchor | 水平对齐 | start(默认,左)/ middle(中)/ end(右) |
dominant-baseline | 垂直对齐 | middle / hanging / auto |
fill | 文字颜色 |
y 是基线,不是顶部
y="48" 指的是字母底部所在的线,不是文字块的上边缘。所以文字看起来会比预期"高一点"。 想让文字在框里垂直居中,最简单的是加 dominant-baseline="middle",然后把 y 设成框的中心 y。
文字重叠了怎么办¶
常见解法按优先级:
- 拉开行距:多行文字改
y或dy。 - 缩小字号:
14→12,通常能立刻缓解。 - 改对齐:把
text-anchor从start改成middle,文字以锚点为中心展开,不容易撞到边界。 - 加背景衬底:在文字下方垫一个半透明
rect,提升可读性。
多行文字推荐用 <tspan>(共享父级样式,只改位置):
<text x="20" y="300" font-size="12" fill="#3c4043">
<tspan x="20" dy="0">第一行</tspan>
<tspan x="20" dy="16">第二行</tspan> <!-- dy 是相对上一行的偏移 -->
<tspan x="20" dy="16">第三行</tspan>
</text>
中文显示为方块(豆腐块)¶
SVG 里的 <text> 依赖渲染环境的字体。如果环境缺中文字体,就会显示成 □□□。
最稳的解法是把文字转成路径(转曲),代价是失去可编辑性与可搜索性;折中方案是在 <text> 上指定通用字体族:
详细讨论见 SVG 排错清单。
手术六:抽可复用图元¶
图:defs 定义一次,use 多处引用,改定义即全部生效
适用场景:同一个图形要出现多次(比如电路图里的两颗相同 MOS 管、流程图里的多个相同节点)。
套路:定义一次 → 多处引用。
<defs>
<!-- 定义:一个带标记的圆点 -->
<g id="node">
<circle cx="0" cy="0" r="6" fill="#1a73e8"/>
<circle cx="0" cy="0" r="2" fill="#fff"/>
</g>
</defs>
<!-- 引用三次,各自定位 -->
<use href="#node" x="50" y="100"/>
<use href="#node" x="150" y="100"/>
<use href="#node" x="250" y="100"/>
好处:
- 改一处,全部生效——把
<defs>里r="6"改成r="8",三个引用同时变大。 - 文件更短、结构更清晰。
注意事项:
| 注意点 | 说明 |
|---|---|
href vs xlink:href | 现代浏览器支持 href;要兼容老环境需同时写 xlink:href(并在 <svg> 上声明 xmlns:xlink) |
<use> 的 x/y 是平移 | 等价于 transform="translate(x,y)" |
| 定义区的坐标建议以原点为中心 | 如上例 cx="0" cy="0",引用时定位最直观 |
| 引用不能改内部细节 | <use> 只能整体缩放/平移,不能改其中某个圆的颜色。要差异化就复制一份出来单独改 |
改造现成 SVG 时慎用这一招
把别人的图"重构成可复用图元"看起来很优雅,但风险不小:一旦判断错"哪几个元素真的相同",就会出现一个引用改动波及多处、而其中某处本不该变的情况。 建议:以改完为目的的图,宁可冗余也不强行抽象;只有在确定要长期维护、且会反复新增同类元素时,才值得抽图元。
六招速查¶
| 需求 | 招式 | 关键提醒 |
|---|---|---|
| 加元素 | 复制相近元素 + 改坐标 + 改内容 | 一个视觉元素可能是多个 SVG 元素,别漏改 |
| 删元素 | 定位后整组删;不确定就先 display="none" | 删 <defs> 前先搜有没有 url(#id) 引用 |
| 改属性 | 直接改 x/y/width/fill/stroke… | rotate 顺时针为正 |
| 挪整组 | 套 <g transform="translate(...)"> | 嵌套 <g> 的 transform 会累加 |
| 改文字 | 改 font-size / text-anchor / dy | y 是基线不是顶部 |
| 抽图元 | <defs> 定义 + <use> 引用 | 图会以改完为目的时,别过度抽象 |
相关:← SVG 入门:读图与制作 · 术语速查 · 下一步:亮暗双主题与嵌入 → · 排错清单