跳转至

改图的六种常见手术

读图之后就到动手环节。改 SVG 的需求看似千变万化,归纳下来其实就六类操作。每一类都有固定套路,照做即可。


先理解:为什么一改就跑偏

Word 删一个元素后自动补位,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>
  <marker id="arrow">...</marker>
</defs>
<line ... marker-end="url(#arrow)"/>

如果删了 <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 虚
透明度 opacityfill-opacity 0~1
旋转某个角度 transform="rotate(角度 中心x 中心y)" 角度制,顺时针为正

线条两端与拐角

linecap=round 端点变圆、linejoin=round 拐角变柔

图:linecap=round 端点变圆、linejoin=round 拐角变柔

<path d="..." stroke-linecap="round" stroke-linejoin="round"/>
  • stroke-linecap端点形状(butt 平 / round 圆 / square 方)
  • stroke-linejoin拐角形状(miter 尖 / round 圆 / bevel 斜切)

示意图(同一条折线在三种 linecap 下的端点差异):

butt:    |————————|          端点齐平于路径终点
round:   (————————)          端点多出一个半圆(看起来更长、更柔和)
square:  [————————]          端点多出半个线宽(方头)

画流程图、示意线时,用 round 通常观感更好。

transform 的三种基本形式

transform="translate(dx, dy)"          <!-- 平移 -->
transform="rotate(角度, cx, cy)"        <!-- 绕 (cx,cy) 旋转,省略中心则绕原点 -->
transform="scale(sx, sy)"              <!-- 缩放;scale(-1,1) = 水平镜像 -->

多个变换可串联,从右往左依次应用:

transform="translate(100, 0) rotate(45)"

注意rotate 的角度顺时针为正(因为 y 轴朝下)。想逆时针转 30°,写 rotate(-30)


手术四:挪一整组(最重要的一招)

套 g transform 后整组一次性平移,子元素坐标不变

图:套 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 让文字垂直居中

图:y 是基线;dominant-baseline=middle 让文字垂直居中

<text x="85" y="48"
      font-size="14"
      font-weight="700"
      text-anchor="middle"
      fill="#1a73e8">模块一</text>
属性 作用 常用值
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。

文字重叠了怎么办

常见解法按优先级:

  1. 拉开行距:多行文字改 ydy
  2. 缩小字号1412,通常能立刻缓解。
  3. 改对齐:把 text-anchorstart 改成 middle,文字以锚点为中心展开,不容易撞到边界。
  4. 加背景衬底:在文字下方垫一个半透明 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> 上指定通用字体族:

<text font-family="Noto Sans SC, Microsoft YaHei, sans-serif">中文</text>

详细讨论见 SVG 排错清单


手术六:抽可复用图元

defs 定义一次,use 多处引用,改定义即全部生效

图: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 入门:读图与制作 · 术语速查 · 下一步:亮暗双主题与嵌入 → · 排错清单