SVG擅长些啥?

1,439 阅读9分钟

上一篇我们说到SVG发展史时就讲到了,早期的Web领域,擅长图形展示的SVG被擅长图文展示的CSS+DIV按在地上摩擦,直到近些年进入了视觉效果大内卷时代,SVG才渐渐有了我命由我不由CSS的底气。

但这份底气不光是时势的造就,还要有足够的硬功夫,否则只能是绣花枕头华而不实了,接下来就让我们把傲娇SVG的外衣再拔下一层,看看到底是哪些肌肉,让它有胆量挑战前端三大剑客其中的两位呢?

SVG擅长点啥?

一、矢量

SVG的核心优势就是矢量,甚至可以说SVG就是为了实现可以无限放大却不失真的矢量图而应运而生的,那么何为是矢量图,而我们常用的 .jpg.png那些图片格式又是什么呢?

简单来讲呢,.jpg.png是位图(也叫点阵图或素图),.svg的则是矢量图,至于两者的区别嘛,主要就是位图放大会失真,而矢量图不会。

16.webp

至于造成这种问题的现象嘛,真要解释起来怕是比这个讲SVG的内容都要多了,不过简单总结一下就是:

位图:是由描述颜色的点组成的,所以位图通常用分辨率来表示,一个100px * 100px的位图就表示由10000个像素点组成的位图,这时试想一下,将这个位图平铺到一个1920 * 1080分辨率的屏幕下会是一个怎样的场景?10000个像素点平铺到200万像素屏幕上,那么特么哪够啊!

不过,没关系,像素不够插值来凑,于是,图形学中的插值算法出手了,至于怎么出手的呢,我们按下不表(其实我也不知道),总之,经过一系列牛B哄哄的数学计算,不够的那199万个点硬是给凑出来了,平铺是能平铺了,至于效果嘛,1万个点给你补到了200万还想要啥自行车啊,你说是吧。

这里补充一句,如果是一整张纯色填充的色块的话,即使补了199万个点,肉眼看上去也几乎不会感觉到失真,越丰富的色彩补充的越多插值算法也越发的力不从心。

矢量图:是由一堆几何对象组成(例如:点、线、面),对象信息中还包含了线条的粗细和颜色等信息。听上去有点抽象是不是,没关系,我给你简单粗暴的比喻一下你就懂了。

矢量图就是一个存储着算法的图片,而显示的尺寸就是算法的入参,输出的结果就是渲染时的图片。

这下懂了吧。所以为什么矢量图为什么不会失真是不是也懂了呢?人家每次渲染都是动态计算的尺寸,再失真才是见鬼了。

注意,这个算法只是一个帮助理解的比喻。

看到这里,聪明的同学可能会问,既然矢量图这么屌位图这么拉,那为啥不让矢量图一统天下呢?

答案就是矢量图它做不到啊。位图的优势在于色彩表现上,毕竟是一个个代表颜色的像素点组成的,在高分辨率的场景里,矢量图里那相对单一的颜色信息会被饱满丰富的位图色彩碾压到质疑人生,说的夸张一些,就相当于黑白电视要跟OLED比色彩一样滑稽。

二、重复且规则的图形

我们来想象这样一个场景,有很多一模一样的元素,想要保持它们彼此的间距相同,元素宽度自适应的话,应该如何实现呢?

方法有很多种,但是无疑的元素的宽度都是CSS根据剩余宽度计算的,然而这种动态计算就很难保持元素的宽度是整数,而浏览器对于CSS的多位小数的渲染精度却不尽人意,所以很容易出现重复元素宽度却不一致的问题,如果渲染的元素是正方形或圆形的话,效果更是惨不忍睹,而SVG则不会出现这种情况。

17.png

<svg width="20%" height="100" viewBox="0 0 216 100"
		 preserveAspectRatio="xMinYMid meet"
		 xmlns="http://www.w3.org/2000/svg">
    <rect x="0" y="10" width="1.6" height="80"/>
    <rect x="21.6" y="10" width="1.6" height="80"/>
    <rect x="43.2" y="10" width="1.6" height="80"/>
    <rect x="64.8" y="10" width="1.6" height="80"/>
    <rect x="86.4" y="10" width="1.6" height="80"/>
    <rect x="108" y="10" width="1.6" height="80"/>
    <rect x="129.6" y="10" width="1.6" height="80"/>
    <rect x="151.2" y="10" width="1.6" height="80"/>
    <rect x="172.8" y="10" width="1.6" height="80"/>
    <rect x="194.4" y="10" width="1.6" height="80"/>

  </svg>
  <div class="wrap" style="width: 20%;">
    <div class="item" style="width: calc((100% - 20px * 10) / 10);"></div>
    <div class="item" style="width: calc((100% - 20px * 10) / 10);"></div>
    <div class="item" style="width: calc((100% - 20px * 10) / 10);"></div>
    <div class="item" style="width: calc((100% - 20px * 10) / 10);"></div>
    <div class="item" style="width: calc((100% - 20px * 10) / 10);"></div>
    <div class="item" style="width: calc((100% - 20px * 10) / 10);"></div>
    <div class="item" style="width: calc((100% - 20px * 10) / 10);"></div>
    <div class="item" style="width: calc((100% - 20px * 10) / 10);"></div>
    <div class="item" style="width: calc((100% - 20px * 10) / 10);"></div>
    <div class="item" style="width: calc((100% - 20px * 10) / 10);"></div>
  </div>

三、异形

首先,这里说的异形不是雷德利的电影,也不是特别稀奇古怪的形状,而是指CSS+DIV这种常规布局实现起来没那么方便的图形,当然,CSS+DIV通过clip-path或者其他CSS属性还是可以实现特殊图形的,但即使不考虑clip-path的兼容性,CSS3相较于SVG,也只是形可得而神不备

18.png

<svg width="400" height="400" viewBox="-10 -10 320 120">
    <polygon points="50,0 21,90 98,35 2,35 79,90" fill-rule="evenodd" />
</svg>

四、进度条

如果要实现一个环形进度条,大家能想到哪些方案呢?是否第一顺位依旧考虑的是CSS呢?

如果将CSS拟人化的话,此时,一定会歪嘴一笑的说到,只需要将div实现成环形,然后左右两边各放一半的遮罩,再通过旋转将左边转到右边,右边转到左边,分别设置超出隐藏,之后再通过进度比率来计算出左右遮罩的旋转角度,就实现啦,一个环形进度条都想来挑战一下我的统治地位,How dare you!

然而,路过的SVG缓缓丢下一句,用stroke-dasharray不就够了。

19.gif

<svg width="400" height="400" viewBox="0 0 400 400">
    <circle cx="200" cy="200" r="150" stroke-width="50" stroke="#D1D3D7" fill="none">
    </circle>
    <circle cx="200" cy="200" r="150" stroke-width="50" stroke="#00A5E0" fill="none"
            transform="matrix(0,-1,1,0,0,400)" stroke-dasharray="0 943">
            <animate attributeName="stroke-dasharray"
                     values="0 943;943 0" dur="5s"
                     repeatCount="indefinite" />
    </circle>
  </svg>

再说实现原理之前,我们先来看看stroke-dasharray的描述:

MDNdasharray是一个<length><percentage>数列,数与数之间用逗号或者空白隔开,指定短划线和缺口的长度。如果提供了奇数个值,则这个值的数列重复一次,从而变成偶数个值。因此,5,3,2等同于5,3,2,5,3,2

简单来说,dasharray是由一组偶数个数字组成的数列,偶数位置的数值表示实线的长度,奇数位置的数值表示虚线的间隔长度,如果是奇数个值组成的数列会重复一次从而形成偶数个值的数列。

了解了属性,SVG的实现原理也就呼之欲出了,我们只要保证虚线+实线的长度≥圆周的长度(这样就不会出现有两段实线的情况),再动态的根据百分比来对实线与虚线进行公式计算即可,公式大体如下:

const line = perimeter * percent
const dashLine = perimeter * (1- percent)
const dasharray = `${line} ${dashLine}`

五、空心文字且虚线

20.png

试想一下CSS要如何实现这样的空心文字,而SVG只需要很简单的几行代码:

<svg width="200" height="100" viewBox="0 0 200 100">
  <text x="100" y="80" text-anchor="middle" font-size="80" stroke="black" fill="none">
    SVG
  </text>
</svg>

如果你想也可以实现一个空心的虚线文字,例如:

21.png

甚至还能让它动起来:

22.gif

<svg width="200" height="100" viewBox="0 0 200 100">
    <text x="100" y="80" text-anchor="middle" font-size="80" stroke="black" 
          fill="none" stroke-dasharray="30 10" stroke-dashoffset="10">
      SVG
      <animate attributeName="stroke-dashoffset" values="0;100" dur="5s"
               repeatCount="indefinite" />
    </text>
  </svg>

上面的代码中,又出现了一个陌生的属性text-anchor,它是用来设置文本水平对齐方式的。

23.png

<svg width="400" height="300" viewBox="0 0 400 300">
  <!-- start -->
  <text x="200" y="80" text-anchor="start" font-size="80" fill="black">
    SVG
  </text>
  
  <!-- middle -->
  <text x="200" y="180" text-anchor="middle" font-size="80" fill="black">
    SVG
  </text>
  
  <!-- end -->
  <text x="200" y="280" text-anchor="end" font-size="80" fill="black">
    SVG
  </text>
</svg>

六、不规则路径的文字排版

说到不规则的文字排版,我首先想到的是CSS中的float,这是一个专门为了实现文字环绕而生的属性,看看人家CSS在图文布局上的专业程度,怪不得SVG在那个年代被打压到无人问津呢。

24.png

但是float也仅仅能做到环绕而已,依旧无法按照不规则的路径对文字进行排列,这点对于SVG来讲,却是小菜一碟,只需要一个简简单单的textPath属性即可。

25.png

<svg width="400" height="400" viewBox="0 0 100 100"
		 xmlns="http://www.w3.org/2000/svg">
    <path id="MyPath" fill="none" stroke="red"
          d="M10,90 Q90,90 90,45 Q90,10 50,10 Q10,10 10,40 Q10,70 45,70 Q70,70 75,50" />
    <text>
      <textPath href="#MyPath">
        黑云压城城欲摧,甲光向日金鳞开。下一句?
      </textPath>
    </text>
  </svg>

需要注意的是,这里的<path>是为了方便演示文字的排版路径而故意暴露出来的,如果想隐藏路径的话,只需要将<path>标签写到<des>中再引用即可。

七、内投影

虽然CSS中有box-shadow实现盒阴影,drop-shadow函数可以实现投影效果,filter滤镜或者backdrop-filter背景滤镜实现高斯模糊效果,但是依然存在某些场景,CSS是无能为力的,例如内投影(注意这里是内投影而不是内阴影)。

26.png

.shadow {
    filter: drop-shadow(2px 2px 6px #000a);
}
.inner-shadow {
    filter: url(#inset-shadow);
}
<h4>普通投影</h4>
<img src="fish.png" class="shadow">

<h4>内投影</h4>
<img src="fish.png" class="inner-shadow">

<svg width="300" height="300" viewBox="0 0 20 20"
		 style="position:absolute;left:-999px;">
  <filter id="inset-shadow">
    <!-- 投影偏移 -->
    <feOffset dx="0" dy="0"/>
    <!-- 投影模糊 -->
    <feGaussianBlur stdDeviation="6" result="offset-blur"/>
    <!-- 反转投影使其变成内投影 -->
    <feComposite operator="out" in="SourceGraphic" in2="offset-blur"
								 result="inverse"/>
    <!-- 内投影附加黑色 -->
    <feFlood flood-color="black" flood-opacity=".95" result="color"/>
    <feComposite operator="in" in="color" in2="inverse" result="shadow"/>
    <!-- 把内投影显示在图像上 -->
    <feComposite operator="over" in="shadow" in2="SourceGraphic"/> 
  </filter>
</svg>

可以看到,这里的SVG滤镜是通过CSS<img>标签上使用的,也就是说目前的浏览器渲染引擎已经可以做到CSSSVG互通使用了,这个后续也会讲到。

八、路径动画

27.gif

<svg width="400" height="200"viewBox="0 0 200 100">
  <path fill="none" stroke="lightgrey" d="M20,50 C20,-50 180,150 180,50 C180-50 20,150 20,50 z" />

  <circle r="5" fill="red">
    <animateMotion dur="10s" repeatCount="indefinite" path="M20,50 C20,-50 180,150 180,50 C180-50 20,150 20,50 z" />
  </circle>
</svg>

值得注意的是,就在几年前,在路径动画这个专项上,SVG是可以做到把CSS按在地上随便摩擦的程度,不过,Chrome可能是一直看SVG不爽,在SVG SMIL animation风头一时无两的情况下,就转投了CSS的旗下,并推出了offset-path这个时代新宠来实现了CSS版本的路径动画。

不过,之所以这里依旧把SVG SMIL animation列为擅长的项目,是因为单就从兼容性上来讲,offset-path依旧不足以匹敌SVG SMIL animation在此领域专研多年的而形成的壁垒。

28.png

29.png

30.png

不过,相信随着技术的日新月异,很多之前某项技术的独占就会慢慢的变得普遍起来,毕竟过去的异想天开,很多都变成了如今的习以为常。

九、补间动画

31.gif

<svg width="200" height="100" viewBox="0 0 200 100">
    <polygon points="50,10 150,10 190,90 10,90" fill="none" stroke="black">
      <animate attributeName="points" values="50,10 150,10 190,90 10,90; 0,0 100,0 200,0 100,100 " dur="5s" repeatCount="indefinite" />
    </polygon>
</svg>

我们要了解,无论是SVG animation 还是 CSS animation所实现的动画,其实都可以被称作补间动画,我们只需要定义关键帧时的状态和动画运行的时间,至于中间过程嘛,动画引擎会对比关键帧前后差异,然后对变化的数值做一系列连续处理。

除了路径动画之外,SVG动画在图形间的补间动画上相较于CSS3也是处于绝对优势的,虽然CSStransform可以实现很多变换,但也因此图形的变换能力除了基于图形的整体变换比如方形变为圆形外,也只能局限于旋转、缩放、位移和倾斜这些变换上了,而无法做到SVG的灵活多样。

如果,我上述给出的例子因为过于简单,可能clip-path基于CSS animation也能做到的话,那么试想一下下面的例子CSS animation是否还能胜任呢?

好了,到此为止我们盘点了一下SVG擅长些啥,可能总结的依旧不全(我是真想列10条啊,奈何能力有限,欢迎大佬评论区帮忙补充),但是结合上一篇《SVG到底是个啥》,大体应该对SVG有了一个新的认识,下一篇我们再来看看《SVG不擅长些啥》吧,就是这样,再见吧。

参考文档

  1. 实现SVG图标图形间的补形动画
  2. 借助SVG滤镜实现CSS无法实现的阴影和模糊效果