svg 绘图

1,491 阅读8分钟

上篇文章我介绍了如何创建和使用 svg, 本文将进一步介绍如何使用 svg 绘制如下所示的图形,及相关知识点。(之后的文章会以这次绘制的静态图形为基础实现 MG 动画效果)

svg 元素

本案例选择直接在 html 中使用 <svg> 元素来绘制图形:

<!-- 代码片段 1 -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
      svg {
        border: 1px solid red;
      }
    </style>
  </head>
  <body>
    <!-- 定义 svg -->
    <svg width="100%" height="600" viewBox="0 0 800 600">
      <!-- 绘制内容 -->
    </svg>
  </body>
</html>

在代码片段 1 中,我通过给 <svg> 设置 viewBox,并让 width100% 以实现 svg 内容能够跟随页面大小自适应缩放(如果你不清楚原理可以去看看关于 viewBox 的介绍)。

绘制十字形

下面开始绘制位于图形中间的十字形。直接在 <svg> 里使用对应的元素,就能生成相应的图形,然后可以通过属性来定义图形的位置和大小颜色等。比如十字形我使用的是 <line> 来生成两根交叉的线条,属性 x1y1x2y2 用来定义线条的两个端点坐标。

组合元素的容器 <g>

因为两根线条的线宽 stroke-width 和颜色 stroke 是一样的,所以我把相同的属性定义在了用来组合元素的容器 <g> 元素上,然后让作为其子元素的 <line> 继承:

<!-- 代码片段 1.1 -->
<svg width="100%" height="600" viewBox="0 0 800 600">
  <!-- 十字 -->
  <g stroke-width="8" stroke="#FAB748">
    <!-- 横 -->
    <line x1="380" y1="300" x2="420" y2="300"></line>
    <!-- 竖 -->
    <line x1="400" y1="280" x2="400" y2="320"></line>
  </g>
</svg>

十字效果如下:
2023-11-02_162939.png

使用 css

在 SVG 中,有些属性,像 stroke-widthstroke 等属于 Presentation Attributes,除了可以直接在元素上通过属性定义,还可以直接使用 css 设置,比如:

<!-- 代码片段 1.1.1 -->
<head>
  <style>
    .cross {
      stroke-width: 8;
      stroke: red;
    }
  </style>
</head>
<body>
  <svg width="100%" height="600" viewBox="0 0 800 600">
    <g class="cross" stroke-width="8" stroke="#FAB748">
      <!-- 省略 -->
    </g>
  </svg>
</body>

得到的会是一个红色的十字,说明定义在 <head> 里的 <style> 的样式优先级高于通过元素属性定义的样式:
image.png
css 也可以定义在之后会介绍的 <defs> 中:

<!-- 代码片段 1.1.2 -->
<svg width="100%" height="600" viewBox="0 0 800 600">
  <defs>
    <style>
      .cross {
        stroke: skyblue;
      }
    </style>
  </defs>
  <!-- 十字 -->
  <g class="cross" stroke-width="8" stroke="#FAB748">
    <!-- 省略 -->
  </g>
</svg>

这样得到的就是蓝色十字,并且定义在 <defs> 里的 <style> 的优先级高于定义在 <head> 中的:
image.png
还可以直接在元素上通过 style 定义,并且优先级最高:

<!-- 代码片段 1.1.3 -->
<svg width="100%" height="600" viewBox="0 0 800 600">
  <!-- 十字 -->
  <g style="stroke: goldenrod" stroke-width="8">
    <!-- 省略 -->
  </g>
</svg>

绘制半圆

绘制半圆的方法不止一种,我选择的是直接使用路径元素 <path>。为了让半圆位于十字的下方,我们需要在定义十字的前面定义好半圆:

<!-- 代码片段 1.2 -->
<svg width="100%" height="600" viewBox="0 0 800 600">
  <!-- 中间的圆 -->
  <!-- 紫色大半圆 -->
  <path d="M 400 384 A 84 84 0 1 0 400 216" fill="#8052E8"></path>
  <!-- 蓝色大半圆 -->
  <path d="M 400 384 A 84 84 0 1 1 400 216" fill="#26AAD6"></path>
  <!-- 紫色小半圆 -->
  <path d="M 400 328 A 28 28 0 1 1 400 272" fill="#8052E8"></path>
  <!-- 蓝色小半圆 -->
  <path d="M 400 328 A 28 28 0 1 0 400 272" fill="#26AAD6"></path>

  <!-- 十字 -->
</svg>

效果如下:
image.png
我们以位于十字下方的紫色小半圆的绘制为例,解释 <path> 的用法。它有一个基本属性 d,用来设置路径点的位置,d 的值是 "命令 + 参数" 的序列,命令都是区分大小写的,大写代表绝对定位,小写表示从上一个点开始计算的相对定位:

  • 其必须以 M(Move To) 命令开头,指示解析器从哪个点开始绘制,M 400 328 即表示先移动到 (400, 328) 这个点,也就是圆弧的起点;
  • A 28 28 0 1 1 400 272A 命令为弧形命令,有 7 个参数:
    • 前 2 个参数分别是 x 轴半径(rx)和 y 轴半径(ry),因为是半圆,所以应该是相等的,我设置为 28;
    • 第 3 个参数为旋转角度(x-axis-rotation,值为正数时顺时针旋转),当 rx 和 ry 相等时设置无效,所以为 0;
    • 第 4 个参数(large-arc-flag)有两个可选值,0 代表取小角度弧线,1 代表取大角度弧线;
    • 第 5 个参数(sweep-flag)也是有两个可选值,0 代表逆时针,1 代表顺时针;
    • 最后 2 个参数为圆弧终点的 x、y 坐标,因为起点在 (400, 328),半径为 28,所以终点 x 为 400,y 为 328 - 28 * 2 = 272;

image.png

  • fill 作为通用的属性,给画好的半圆填充颜色,否则默认填充为黑色。

d 属性支持的命令还有很多,比如绘制水平线的 H/h 等,从阿里图标库下载的图标,几乎都是使用 <path> 绘制,可以看出 <path> 的强大。

绘制矩形

现在来绘制位于圆形上下两端的 4 个小正方形:

image.png

定义重复元素 <defs>

可以看到,如果我们绘制好了一端的小正方形,在另一端只需要改一下定位是可以复用的,所以我们在 <defs> 内定义好正方形:

<!-- 代码片段 1.3 -->
<svg width="100%" height="600" viewBox="0 0 800 600">
  <defs>
    <!-- 蓝色正方形 -->
    <rect id="blueRect" width="42" height="42" fill="#26AAD6"></rect>
    <!-- 绿色正方形 -->
    <rect
      id="greenRect"
      width="34"
      height="34"
      fill="none"
      stroke-width="8"
      stroke="#51E88D"
      ></rect>
  </defs>
  <!-- 省略 -->
</svg>

正方形使用绘制矩形的 <rect> 来绘制,widthheight 定义了宽高,蓝色正方形为实心的,所以用 fill 填充,绿色正方形为空心的,所以使用 stroke 描边。

引用元素 <use>

<defs> 里定义的可复用元素是看不到的,需要使用 <use> 来引用并显示:

<!-- 代码片段 1.3.1 -->
<svg width="100%" height="600" viewBox="0 0 800 600">
  <defs>
    <!-- 定义正方形 -->
  </defs>

  <!-- 显示正方形 -->
  <!-- 左上角蓝色 x = 400 - 42; y = 300 - 84 - 42  -->
  <use x="358" y="174" href="#blueRect"></use>
  <!-- 右下角蓝色 x = 400; y = 300 + 84 -->
  <use x="400" y="384" href="#blueRect"></use>
  <!-- 右上角绿色 x = 400 + 4; y = 300 - 84 - (42 - 4) -->
  <use x="404" y="178" href="#greenRect"></use>
  <!-- 左下角绿色 x = 400 - 42 + 4; y = 300 + 84 + 4 -->
  <use x="362" y="388" href="#greenRect"></use>
</svg>

引用时通过 xy 设置位置,href 的值为引用对象的 id 或 url。<defs> 里除了可以定义基本图形,也可以定义组合图形(<g>)或是样式(<style>)等。在 <use> 上设置属性 widthheight是无效的,除非引用的元素具有 viewBox 属性,比如是另一个 <svg> 或是下面介绍的 <symbol> 元素。

symbol 元素

这里顺便介绍下和 <defs> 类似的 <symbol><defs> 元素本身是没有专有属性的,使用时一般也不会添加属性。而 <symbol> 则提供了 viewBoxxywidthheight 等属性:

<!-- 代码片段 1.3.2 -->
<body>
  <svg>
    <symbol id="myRect" viewBox="0 0 100 100">
      <rect width="100" height="100"></rect>
    </symbol>
  </svg>
  <svg>
    <use href="#myRect" width="50" height="50"></use>
  </svg>
  <svg width="50" height="50">
    <use href="#myRect"></use>
  </svg>
</body>

通常是在一个 <svg> 内使用 <symbol> 定义好要复用的内容,这些内容不会显示。然后在别的 <svg> 内使用 <use> 引用并显示,并且可以通过给 <svg><use> 设置宽高来实现对复用图形的缩放。 <symbol> 常见的应用场景是定义图标,比如阿里图标库的图标就可以通过 symbol 引用的方式来使用:

image.png

绘制三角形

我使用前面介绍过的 <path> 绘制位于中心圆两侧的三角形,用到了 2 个新命令:

  • l(Line to)用于绘制线段,其参数就是线段终点的坐标,因为是小写的,所以采用的是相对定位;
  • Z 用于闭合路径,即从当前点绘制一条直线到路径起点,不区分大小写。
<!-- 代码片段 1.4 -->
<svg width="100%" height="600" viewBox="0 0 800 600">
  <!-- 省略 -->
  <!-- 亮黄色三角形 -->
  <g fill="#FFED5D">
    <!-- 左 -->
    <path d="M 316 300 l 0 -84 l -84 84 Z"></path>
    <!-- 右 -->
    <path d="M 484 300 l 0 84 l 84 -84 Z"></path>
  </g>
  <!-- 橘色三角形 -->
  <g fill="#FAB748">
    <!-- 左 -->
    <path d="M 316 300 l 0 84 l -84 -84 Z"></path>
    <!-- 右 -->
    <path d="M 484 300 l 0 -84 l 84 84 Z"></path>
  </g>
</svg>

至此,图形绘制成果如下:
image.png

剪切

接着绘制如下图箭头所指的具有剪切效果的 4 个小三角形:

image.png

先在 <defs> 内使用 <clipPath> 定义好作为剪切轮廓的三角形,此处三角形的绘制采用的是用于绘制多边形的 <polygon>,它的属性 points 的值为各个点的坐标,最后一个点会自动与第一个点连线闭合:

<!-- 代码片段 1.5 -->
<defs>
  <!-- 用于剪切的三角形 -->
  <!-- 左上 -->
  <clipPath id="cut-off-left-top">
    <polygon points="316 216, 358 216, 358 174" />
  </clipPath>
  <!-- 右上 -->
  <clipPath id="cut-off-right-top">
    <polygon points="442 216, 484 216, 442 174" />
  </clipPath>
  <!-- 左下 -->
  <clipPath id="cut-off-left-bottom">
    <polygon points="316 384, 358 384, 358 426" />
  </clipPath>
  <!-- 右下 -->
  <clipPath id="cut-off-right-bottom">
    <polygon points="442 384, 484 384, 442 426" />
  </clipPath>
</defs>

然后以左上角的小剪切三角形为例说明如何应用剪切图形:

<!-- 代码片段 1.5.1 -->
<!-- 左上角剪切三角 -->
<g clip-path="url(#cut-off-left-top)" fill="#FAB748">
  <g transform="rotate(45, 337, 195)">
    <rect x="316" y="162" width="50" height="6"></rect>
    <rect x="316" y="174" width="50" height="6"></rect>
    <rect x="316" y="186" width="50" height="6"></rect>
    <rect x="316" y="198" width="50" height="6"></rect>
    <rect x="316" y="210" width="50" height="6"></rect>
    <rect x="316" y="222" width="50" height="6"></rect>
  </g>
</g>

我使用 <rect> 定义了多个矩形,并把它们放入 <g> 内归为一组:

image.png

然后通过给形变属性 transform 赋值 rotate(45, 337, 195),使得它们一起顺时针旋转了 45°,旋转中心点坐标为 (337, 195):

image.png

再在外面包裹上一个 <g>,通过 clip-path 引用代码片段 1.5 定义好的三角形作为剪切轮廓并使用 fill 上色:

image.png

绘制圆形

绘制位于四周的小圆比较简单,在 <defs> 内使用 <circle> 定义好圆形,r 为半径:

<!-- 代码片段 1.6 -->
<defs>
  <!-- 四个角的蓝色圆 -->
  <circle id="blueCircle" r="20" fill="#26AAD6"></circle>
</defs>

然后进行引用:

<!-- 代码片段 1.6.1 -->
<!-- 四周小圆 -->
<!-- 左上 x = 400 - 84 - 84; y = 300 - 84 - 84 -->
<use x="232" y="132" href="#blueCircle"></use>
<!-- 左下 y = 300 + 84 + 84 -->
<use x="232" y="468" href="#blueCircle"></use>
<!-- 右上 x = 400 + 168 -->
<use x="568" y="132" href="#blueCircle"></use>
<!-- 右下 -->
<use x="568" y="468" href="#blueCircle"></use>

绘制图片

使用 <image> 元素绘制图片,注意是通过 href 属性引用,而不是像 html 中的 <img> 使用 src

<!-- 代码片段 1.7 -->
<image x="214" y="286" href="../imgs/juejin.png" width="40"></image>

通过 xy 定义图片位置,如果不写则值默认为 0;width 指定图片的宽度,高度会按照图片原宽高比自动调整,如果宽高均不设置则默认为原图大小。

绘制文字

最后在右下角使用 <text> 签个名,在 <text> 内还可以使用 <tspan> 对部分文字设置单独的样式:

<!-- 代码片段 1.8 -->
<text x="600" y="580" font-size="12" fill="#26AAD6">
  by:
  <tspan font-size="14">亦黑迷失</tspan>
</text>

SVG 的优缺点

至此,绘制完毕。回顾绘制过程,不难发现 svg 的优点有很多,比如绘制都是声明式地直接使用对应的元素,并且可以通过 css 和 js 修改,也就更利于创建动画。放大浏览器的页面缩放比例,可以看到图形并不会像使用 canvas 绘制的图形那样出现失真: image.png

查看源代码,可以看到所有使用 svg 绘制的代码,这也就利于 SEO:

image.png

当然 svg 也有缺点,比如当 svg 很复杂时,DOM 变得复杂,渲染就会变得比较慢。

感谢.gif 点赞.png