超详细SVG实战——徒手画pipeline,带你玩转SVG!

1,895 阅读12分钟

记录个 svg 实战应用~最近断断续续在搞公司的前端发布平台,本想搞 pipeline ,结果先给 svg 给拦下了。基于发布平台没有同时多项目发布的能力,so~笔者决定搞个 pipelinejenkinsfreeStyle job 给串起来,实现并、串行前端发布的能力。本着身为前端开发的角色,界面肯定是不能少的,于是就有了以下的故事...

笔者也不卖关子了,直接上图!如图,这就是流水线板块的成品样子了。哈哈哈,丑是丑了点,凑合看吧~本文将以笔者自己的业务场景为案例,记录一下如何使用 svg实现直、曲线段,箭头,圆圈来实现这种流水线效果

  • image.png

熟悉 DevOps 的同学这种界面一定不会陌生,笔者也是照猫画虎搞了个。其实就是把每一个 card 通过各种线段连接起来了,前端看起来会协调一点。这里笔者把 线段部分 给干掉,这样上下一对比,果然还是差了点呀~

  • image.png

回到主题 svg ,笔者做前端这么久了也只知道有 svg 的存在,实际并没有在实战中自己玩过,最最最接近实战 svg 的时刻,应该就是导出个 svg图标 应用在项目里了~趁着这次搞 pipeline 的机会,直接搞搞 svg ,来个顺手牵羊,美哉美哉~

从笔者的学习到应用来说,使用 svg 做图标、画线并不困难,可能更多时候你不是在搞svg,而是在计算各种点位,就是数学计算而已!本文不是文档,不会直接丢出一大堆api,防止出现看了跟没看一样的效果。本文就用笔者的实现,带着大家过一遍实战。所以大家不用抗拒,花个10来20分钟了解下实战应用,一定对 svg 有自己的理解。接下来,马上进入正题!

一、svg 基本使用

首先看看 MDN 对 svg 的定义:一种基于 XML 的标记语言,用于描述基于二维的矢量图形。其放大、缩小都不会失真,还算是很不错的。只要掌握了 svg 的使用方法,我们可以轻松的用它来实现一些小图标,小logo,还是非常方便的。

首先看一下本文所需要用到的 svg 相关标签元素

  • <svg> 用于包裹着整个矢量图。像 <line>:直线、<circle>:圆、<rect>:矩形等各种svg图形都是被包裹在 <svg> 标签内的。

  • <path> 定义路径。这是本文的重点!可以理解为:指定坐标点,指定他们的连接方式。以此,我们就能构建出任何图形,比如一些曲线,各种折线、曲直线结合。

  • <circle> 圆形元素。这个我们仅记一点即可:圆心(cx, cy) + 半径(r)~

  • <marker> 标记。我们可以用它,在 <path> 的起点、终点添加一些图形。比如案例中的小圆圈,小箭头,都是用这个标签实现的。它需要包裹在 <defs> 元素中。

  • <defs> 用于实现svg的复用,其内容不使用时是不可见的。将需要复用的图形用该标签包裹,通过 <use> 标签来使用即可。这个跟下面结合 <g> 标签一起举个例子就明了了。

  • <g> 其实就是 group 的意思,也就是一个集合。我们可以把图形组合用 <g> 进行包裹,做一个装箱的包装。

以下,直接通过制作一个 “+” 的图标来加深对 <defs><g> 标签的理解:

<svg>
  <!-- 代码可以不用细看,知道就是一个十字架的坐标就ok了(可以copy到本地自己试试)~ -->
  <line x1="0" y1="10" x2="20" y2="10" 
        style="stroke: #000; stroke-width: 2px;" 
  />
  <line x1="10" y1="0" x2="10" y2="20" 
        style="stroke: #000; stroke-width: 2px;" 
  />
</svg>

这里我们用两个 <line> 实现一个“+”,接下来我们用 <g> 包裹起两个 <line> 并放在 <defs> 中实现复用

  • image.png

现在我们用 <g> 包裹好,放在 <defs> 里,页面其实是没有内容的(也就是defs中的内容默认不显示的),如下图:

  • image.png

最后,我们只需要使用一个 <use href="#..."> 使用即可~效果就不贴了,大家可以复制代码到页面试试。

<svg>
  <defs>
    <g id="icon">
      <line x1="0" y1="10" x2="20" y2="10"
            style="stroke: #000; stroke-width: 2px;"
      />
      <line x1="10" y1="0" x2="10" y2="20"
            style="stroke: #000; stroke-width: 2px;"
      />
    </g>
  </defs>
  <use href="#icon"></use> <!-- 用 use标签+id 来使用我们的图标组合 -->
</svg>

哈哈哈,其实讲了点跟本文实现无关的,不过也没关系,多了解点也不错~如果感兴趣的话,大家自己去找找资料看看哈,本文讲的只是冰山一角。在掌握了 <svg> 基本的用法后,我们接着往下,看看如何实现这种流水线效果~

二、svg 实现曲线

笔者会用从简到繁的方式,一步一步的把曲线画出来,不会直接堆代码和一堆坐标,所以大家跟着走,绝对轻松get。本文主要通过 <path> 标签来实现曲线效果,所以我们对这个标签要进行深入一点的了解。

1. path的属性 —— “d”

<path> 路径绘制的属性 d :d 这个 属性值其实就是由一堆 “英文字母 + 数字” 组合成的,字母代表指令数字表示坐标。简单点理解:

<!-- 
  坐标(0,1):直线 
  坐标(1,2):曲线
  坐标(2,3):直线
-->
<path d="直线 0 1 曲线1 2 直线2 3">

下面看看本文用到的字母指令:

指令名称参数
Mmoveto 移动,用于起始点x y
Llineto 画直线x y
Qquadratic Bézier curve 二阶贝塞尔曲线x1 y1 x y

这里最头疼的可能就是贝塞尔曲线了,还分阶。不过也不用紧张,我们用二阶的就够了,二阶无非也就是在直线的基础上,多了一个偏移点而已。去维基百科搞了个二阶的gif,其实二阶曲线就是在 P0 - P2 这个直线段之间有一个偏移点 P1 ,高阶的贝塞尔曲线也就是直线段中间存在多个偏移点而已,所以二阶的还是比较好理解的。

Bézier_2_big.gif

2. step by step 画曲线

接下来,我们一起画一个这样的曲线:

  • image.png

为了好理解,这个demo在 100*100 的正方形里面画~

  1. 首先我们确定一下对角的两个点,先画根直线。起点(0, 0)——终点(100, 100)
<path d="M0 0 L100 100" style="stroke: #000; stroke-width: 2px; fill: none;" />
  • image.png
  1. 上半段路径变成曲线。这个我们不急着动手,先在一个坐标图中画出来就很清晰了
  • image.png 根据坐标图,我们上半段曲线的偏移点就确定下来了,就是(50, 0)这个点,笔者用深色的虚线模拟了偏移后的曲线,我们把它用 path 的 d 表达出来:

  • 首先起点:M 0 0

  • 偏移点在(50, 0),在(50, 50)结束,我们用二阶贝塞尔曲线表达出来:Q 50 0 50 50

  • 结束点还是:L 100 100

<path d="M0 0 Q50 0 50 50 L100 100" style="stroke: #000; stroke-width: 2px; fill: none;" />

这时候我们看看效果:

  • image.png 这里细心的朋友可能看到了,曲线的开头比后半段的线段要细,这是因为stroke-width为 2px ,所以我们需要让起始点往下多1px,防止线段宽度给吃了。调整后就正常了~
  • image.png
  1. 下半段曲线跟步骤2其实是异曲同工的,偏移点是(50, 100),这里就不再演示了,贴出最终的代码:
<path d="M0 1 Q50 1 50 50 Q50 100 100 100" style="stroke: #000; stroke-width: 2px; fill: none;" />

三、实战的实现

首先分析实战中所用到的相关图形:

  1. 直线
  2. 曲直线(基于二、svg实现曲线的两段二阶贝塞尔曲线变化而来)
  3. 路径的起始位置圆圈
  4. 路径的结束位置箭头
  5. 其中还有虚线、直线的效果

1. 实现流水线中的曲线效果

案例实现的效果是基于上述曲线的实现进行了一些拓展。因为业务上并行流水线可以有很多,所以每条这种曲线的起始点到终点的距离是变化的,直接应用步骤二的曲线就会出现以下这种情况,并行的任务越多,线段曲度给拉得越平,不是特别美观。

  • image.png

一个解决的方案就是path中的曲线的曲折程度固定,曲线段之间通过直线连接。也就是伸缩的地方都是直线,曲线的长度、偏折度是固定的。

整体曲线思路大致是这样的:直线 - 圆角曲线 - 直线 - 圆角曲线 - 直线。大致如下图所示

  • image.png

在 path 中进行表示也就是: M - L - Q - L - Q - L。接下来,笔者在上述曲线的基础上,实现一下这种方式。跟实现曲线时候的步骤一样,我们不急着直接撸码,先通过画图,看看坐标点,捋清晰了坐标再实现就很容易了。

  • image.png

跟图所示一样,我们的偏折点依然是(50, 0),但是在第一段曲线的起始点(0, 0)和终点(50, 50)之间多了两段标注绿色的平行线,而这就是要被我们用直线替换的地方。他们分别(0,0 - 25,0)(50,25 - 50,50)。接着我们在代码上实现如下:

<path d="M0 1
         L25 1
         Q50 0 50 25
         L50 50
         Q50 100 100 100" style="stroke: #000; stroke-width: 2px; fill: none;" />

线段效果就出来!这里还得再提醒一下,线段宽度是2px,我们的起始点的y值要是1,不然直线会被吃掉~基于这样,只要我们保证我们曲线段的直线长度、偏折度不变,就能适配案例中的并行流水线的场景啦。接着的下半段线段笔者就不再赘述了,大家有兴趣的话,可以自己把下半段给撸出来哈~

  • image.png

这里带一笔,案例中实现的虚线是通过css样式控制的:stroke-dasharray。svg的属性,还有一些相关的控制样式,这里不会展开,一方面是太多了,笔者也没精力去整理;一方面是直接丢 api 没场景应用,很容易就忘了,没什么学习效果,就好比背文档没有意义一样。相信有了实现思路,具体实现时再去查找对应的配置、api就行了,实现起来都不会有太大问题~

我们就着上面的曲折线段,搞个虚线试试。给 path 添加样式代码 stroke-dasharray: 6 6;(它是一个<length><percentage>数列,数与数之间用逗号或者空白隔开,指定短划线和缺口的长度,哈哈哈,具体大家去看MDN吧)

  • image.png

接下来,看看 <marker> 怎么用来做箭头!

2. 使用 <marker> 在起始、结束点加icon

前面用了点篇幅去描述 defs ,这里算是派上用场了。在本文的场景中,线段的开头,结尾添加了圆圈和箭头。主要实现是把圆圈、箭头实现,再用 <marker> 进行包裹,添加id,再放在 defs 中。需要添加箭头、圆圈时,通过给标签写样式 marker-end: url(#id) 去使用。大概就是下面这个样子

<svg>
  <defs>
    <marker id="triangle">
      <!-- 实现三角形  -->
    </marker>
  </defs>
  <path d="曲线" style="marker-end: url(#triangle)" />
</svg>

对的,有了 marker 实现这种需求场景非常的便捷。那紧接着,跟大家一起实现一个箭头+圆圈的效果~

  1. 使用 <path> 实现三角箭头
  2. 使用两个 <circle> 实现圆圈

实现三角形,我们还是现在坐标轴表示出来。这里我们在尺寸 10 * 10 的正方形中实现~如图所示,我们为三角形找出三个点,接着笔者用 path 中的 d 表示这三个点位

  • image.png
<path d="M0 0 L10, 5 L0 10" style="fill: #000;"></path>

这里的样式跟我们画线的时候有点不同,这里就不是用 stroke 去画的了,而是在标注了三个点位后用了 fill ~这样,我们的等腰三角形就出来!

  • image.png

紧接着,我们把圆圈也实现一下,其实比起三角就更简单了,两个 circle 拼合起来即可。回顾一下我们属性:圆心(cx, cy) + 半径(r)。这个我们直接撸代码。圆心都是(5, 5),一个半径3,一个半径4。

<!-- 黑色圆 -->
<circle cx="5" cy="5" r="4" style="fill: #000;"></circle>
<!-- 白色圆 -->
<circle cx="5" cy="5" r="3" style="fill: #fff;"></circle>

圆圈效果就出来了~

  • image.png

最后一步,我们把使用 <marker> 把我们的箭头和圆圈放在我们的曲线上。let'go!

我们把三角形的 <path> 放到 <marker> 中,并在我们的曲线 <path> 中添加样式属性: marker-end: url(#triangle)。代码如下

<svg>
      <defs>
        <!-- 根据三角形的大小,这里设置 markerWidth,markerHeight的值为10 -->
        <marker id="triangle" markerWidth="10" markerHeight="10">
          <path d="M0 0 L10, 5 L0 10" style="fill: #000;"></path>
        </marker>
      </defs>
      <path d="M0 1
         L25 1
         Q50 0 50 25
         L50 50
         Q50 100 100 100" 
         style="
           stroke: #000; 
           stroke-width: 2px; 
           fill: none; 
           marker-end: url(#triangle);
         "
         />
    </svg>

这时候看下页面效果:

  • image.png 箭头是出现了,但是它并不是对齐终点的正中间的。这时候我们就需要 <marker> 的另外两个属性 refXrefY 进行位置上的调整。此时,我们只需要把三角上移一半,也就是5px即可。我们调整下 refY
  • image.png 嗯,没错,这就是我们想要的效果了~

这里,笔者还想引入一个定向 orient="auto" 的属性,虽然在本文中没用到,但是也是个比较方便省事的属性。怎么理解呢,我们在上述demo中,让结尾处线段不水平,让他有一定的水平偏移度,这下箭头还是会指向水平方向。比如下图:

  • image.png

这里笔者调整下了曲线的终点的Y轴坐标 160(由于超出默认高度,笔者把svg的高度设置成200px),这下子,箭头跟线段的终点在角度上就对不上了,有点奇怪。这时候,我们给 marker 加入 orient="auto" 属性试试效果~

  • image.png

好了,基本上讲到这里就差不多了,开头圆圈的实现也是异曲同工的,也就不再进行演示了~大家有兴趣可以自己尝试一下,加在线段开头需要使用 marker-start ,其他都是一样的用法。


其实整个流水线的ui做出来也并不困难,还是那句,更多是在点位的取值、计算当中度过的。没错!!!笔者觉得比较麻烦的就是各种计算,path的路径d的计算都是放在js中,但是其跟一些css样式是有关联的。比如我的一个card多高,path的终点就需要计算到这个card的高度的一半再加上各种边距。基于这样,怎么做到只调整一个地方的尺寸,就能自动计算到最终的位点也是比较关键的,毕竟很多时候我自己会对一些宽高做微调~

基于这样!笔者用的css变量的方式,将js、css的尺寸相关的数值进行了统一,也不知道还有没有什么更好的方案和办法,哈哈哈期待评论区的你们。本文就不再展开如何统一js、css中的一些样式值了,看看最近有没时间,再单独写一篇文章来进行分享吧~