SVG 入门与实战

714 阅读8分钟

此前对于 SVG 的认识很肤浅,开发上基本只是单纯的复制粘贴即可。最近在一番折腾之后算是对 SVG 有了进一步的认识,这里分享一下自己的经验,相信一定会对你有所帮助。

入门

Canvas or SVG?

CanvasSVG
图形类别柵格图形(raster images)矢量图形(vector images)
渲染模式立即模式(immediate mode)保留模式(retained mode)

图形类别比较好理解,一个放大会失真一个不会,这里重点讨论一下渲染模式。立即模式可以理解为胶片电影的播放,每次修改都是更新整个画面。而保留模式可以做到按需更新,只更新需要更新的元素,也就是对应了每个需要更新的 HTML 节点。总的来说,各有利弊,对于海量数据来说,Canvas 肯定是最好的选择。

Canvas / SVG 如何选择?

图片来自微软技术文档——如何为您的网站在 Canvas 和 SVG 之间做出选择,甚至还有混合使用的场景,过于高端了。

由于自身场景是由外部父容器节点控制的所有子元素的缩放,且通常来说 SVG 节点不会太多,所以 SVG 无疑是最好的选择。

SVG 入门

基本元素简介
path定义形状的通用元素。所有的基本形状都可以用 path 元素来创建。
circle用来创建圆,基于一个圆心和一个半径。
ellipse用来创建一个椭圆,基于一个中心坐标以及它们的 x 半径和 y 半径。
rect用来创建矩形,基于一个角位置以及它的宽和高。它还可以用来创建圆角矩形。
polyline用来创建一系列直线连接多个点。典型的一个 polyline 是用来创建一个开放的形状,最后一点不与第一点相连。

这里主要罗列了一些常见的基本元素,更多说明可参考 MDN 文档。值得注意的是 path 可以创建所有的基本形状,在实现一些通用的组件时,这是一个很重要的前提条件。

图形变换*

SVG transform 与 CSS transform 的异同

值得注意的区别有:

  • SVG 内部元素transform 不带单位和百分比
  • SVG 内部元素transform 默认是左上角 (0, 0) 作为变换原点,而 CSS transform 的变换原点为图形中心,可通过 transform-origintransform-box 调整变换效果。

基本变换

  • 平移 / translate
  • 旋转 / rotate
  • 斜切 / skew
  • 缩放 / scale
  • 矩阵变换 / matrix

可通过组合多个基本变换实现复合变换。值得注意的是,基本变换的排列顺序可能会影响最终渲染结果。MDN 中对此的描述如下:

The transform functions are multiplied in order from left to right, meaning that composite transforms are effectively applied in order from right to left.

变换函数按从左到右的顺序相乘,这意味着复合变换按从右到左的顺序有效地应用。

那么问题来了,从右到左有效应用如何理解?经过鄙人苦心搜索与理解,在 CSS3 transform order matters: rightmost operation first 的讨论中找到了自认为最合理的解释:

It all depends whether you consider your coordinates attached to your element (left to right) or fixed to the page based on the initial element position (right to left).

也有关联的文章 Chaining transforms 详细说明了这个问题,如果可以,非常建议阅读。一句话总结,可以理解为物理中学过的参考系不同导致观察到的行为不同。

transform.gif

如上图,CSS 中的两个动画,第一个是对 transform 从左到右的理解(变换原点跟着图形一起运动),第二个是从右到左理解(变换原点不随图形移动),可以看到由于 CSS 默认以图形中心作为变换原点,所以最终效果是一致的,但是过程是有所不同的。第一个变换过程比较容易理解,第二个变换读者一定要注意变换原点是不随图形移动的。

对于 SVG 的变换的例子,可以看到,仅仅是调换了变换顺序,最终效果便出现了不同,至于变换过程的正反向推导,读者可以自行尝试,注意 SVG 默认变换原点为 (0, 0) 即可,如果遇到什么问题,欢迎留言讨论。

通常来说,从左到右或者从右到左来理解都是可以的,但是在前文中提到的文章 Chaining transforms 中提及一个特殊情况:scaleY(.5) rotate(45deg),这种情况下,只有从右到左理解才能得到正确的结果,你学会了吗?

矩阵变换 / matrix

根据 MDN 我们知道 matrix(a, b, c, d, tx, ty) 意味着 matrix(scaleX(), skewY(), skewX(), scaleY(), translateX(), translateY())。但是我在实操中发现这个含义并不是严格对应的,除开 scale 和 translate 可直接迁移外,skew 并不能直接迁移,还有一个重要的问题:旋转变换呢?

当我发现这个问题之后这个问题就一直萦绕着我,最终在多方搜集资料后,2D 矩阵:这都是什么妖魔鬼怪啊!一文彻底为我解了惑,也算是明白了为啥叫矩阵变换了,虽然并不能完全看懂 _(:з」∠)_ 感兴趣的读者可自行探索 😂 这里简要总结一下,重点是结论。

矩阵是对一个点的操作,二维变换矩阵只对 x y 进行操作,所以只能表示 scale 和 skew:

image.png

当使用如下三维变换矩阵时,方可表示对 x y 的平移操作,可以理解为用三维坐标系中的一个切面来表示对原点的平移(至于原文中提到的齐次变换,我只能说很耳熟 😂):

image.png

最后,我们关心的斜切矩阵和旋转矩阵终于出来了:

image.png

因此,matrix(scaleX(), skewY(), skewX(), scaleY(), translateX(), translateY()) 中的 skew 相关的值其实是 tan(θ),而 rotate 是通过 scale 和 skew 实现的,你学会了吗?(我也只是明白了结论,理论推导也是一脸蒙逼 _| ̄|○

实战

通用图形绘制

这里的通用图形是指通过对单位图形做矩阵变换即可满足业务需求的图形。比如可通过正方形做矩阵变换绘制任意大小的正方形和矩形,通过正圆绘制椭圆等……所有的这些操作都是建立在单位图形都是通过 path 绘制的基础上,可以很方便的实现通用的工具函数实现各种操作。

值得注意的是,默认矩阵变换也会应用到线条上,可通过 vector-effect="non-scaling-stroke 使得线条不受影响。至此,万事俱备只欠东风。

这里展示下核心处理逻辑,基于(SVG.jsgsap):

onDrag() {
  // 在拖动时设置单位图形路径
  pencilPath.plot(pathD);
  // 得到拖动终点
  const endCoord = { x: this.endX, y: this.endY };
  // 因为用户可以往任何方向拖拽,因此拖拽时需要根据当前的起点和终点
  // 重新计算 SVG 绘制的起点坐标
  const relocateStartCoord = calcRelocatedStartCoord(startCoord, endCoord);

  const width = Math.abs(endCoord.x - startCoord.x);
  const height = Math.abs(endCoord.y - startCoord.y);

  if (relocateStartCoord) {
    // SVG.js 默认变换原点为图形中心。
    // ref: https://github.com/svgdotjs/svg.js/blob/2b028c35ab1a77c38a850c3c21082a5dacc18ac8/src/utils/utils.js#L81
    // pathCenter 表示单位图形的中心坐标,通常为 (0, 0)
    pencilPath.transform({
      translateX: relocateStartCoord.x + width / 2 - pathCenter.x,
      translateY: relocateStartCoord.y + height / 2 - pathCenter.y,
      scaleX: width / pathSize.width,
      scaleY: height / pathSize.height,
    });
  }

  this.update();
}

箭头绘制

假设需要绘制如下的箭头,起点 S,终点 E,关键点数据标注如下:

image.png

由于用户可以任意拖拽,所以需要根据起点终点实时计算出其他关键点以绘制该图形。这里说下主要用到的数学原理和方法及其作用:

原理和方法作用
直线方程(斜截式)得到 S E 两点所在的直线方程 L1
相似三角形得到 A B 两点坐标
两条垂直相交直线的斜率相乘积为 -1得到与直线 L1 垂直的直线 L2 的斜率,进而根据 B 点得出直线方程 L2
勾股定理(两点间距离公式)建立 B 点与两侧关键点的函数关系 F(圆方程)
求解一元二次方程联立 F 和 L2 求解得到 B 点两侧关键点坐标

此外,还需要考虑 S E 两点距离过近时需要重新计算 E 点坐标,即箭头的最短距离,更多边界情况需要在实操中自行探索了 👻

总结

实战方面只分享了自己的两个例子,更多侧重于 SVG 的入门上,更进一步是对基本变换的理解上,因为自己被这个问题困扰了很久,为了得到一个能够接受的答案,属实煞费苦心了。最有趣的便是对 transform 变换顺序的理解上,当我刚看罢 MDN 上的描述时,心里就一个想法:“怎么正反都给你说了?”在搞懂之后才明白“按从右到左的顺序有效地应用”属实太有含金量了。