cover_image

LOFTER卡牌项目中SVG自定义路径及滤镜的使用实践

罗帅 LOFTER技术团队
2022年03月30日 06:54

引言

在前端开发中,我们偶尔会使用到svg。可能用它绘制icon,也可能会用来做一些图片处理。本文就将一些svg的滤镜和图片处理在项目中的实践做一些介绍,希望能帮助同学们快速理解svg滤镜的组合和使用方法。


先来看下我们做了什么~

图片这张图就是基于svg的一个效果。一个完成的图由25个碎片组成。用户每次获得一块新的碎片,就高亮显示,并和已有的其他合并成一块大的图像。直到全部收集齐后,才进行下一轮的收集。

为什么用SVG?

其实看我们现在的界面,很多同学会感觉这个div+css也是可以做到类似效果的。但是选用了svg,也是基于以下几点考虑:

  1. 考虑到后期如果需要支持不规则拼图碎片图形🧩,div就会比较吃力,canvas和svg都能做到,但是这个时候svg可能更容易通过滤镜去制作一些效果。以及后期svg转canvas也是可行,毕竟路径计算方法也是能复用的;
  2. svg的滤镜效果进行更多图片处理,我们是做活动的,一般视觉要求效果比较丰富,canvas操作比较复杂,svg的滤镜相对比较容易使用。

那我们讲什么呢?

本文倒不会一一细数svg的各种属性。这些可能大部分文档都会讲的更专业。我们更像是通过一些案例的实践过程,帮助同学们在那些琳琅满目的的svg标签和属性里,整理出一些制作svg的认知框架。我的案例里主要会涉及到 :

  1. path中 d 的使用,也就是如何绘制一个自定义的图形;
  2. filter 滤镜的叠加实例,帮助大家更好的理解svg滤镜的组合方法;
  3. feColorMatarix滤镜实践。该滤镜可以说是最有用的图片像素操作滤镜了。基本可能帮助我们完成绝大部分的图片处理效果。

本文后续还会同步到LOFTER技术团队公众号其其他平台~ 欢迎大家观看。

svg的视图和适配

为什么又要开始讲基础了呢,因为视图里有个很有意思的小技巧,且容我多讲一点。

我们看下是 svg 的viewBox属性

viewBox 容器大小

参数:viewBox="x, y, width, height"

viewBox="x, y, width, height" 中的xy,表示视图的起始位置。xy可以是负数(width, height 不可以)。如果我们把xy设置成负数,整个画布向右下方移动,就可以做到类似padding的效果,而不用去修改里面已经完成的路径了!如果你有一些外发光效果会出现在 0,0 以外,那这个将会起到一个良好的保护作用。

preserveAspectRatio 是否强制进行统一缩放

参数:preserveAspectRatio="xMidYMid meet"

  1. xMidYMid : 对齐位置,其他参数有 xMin、xMid、xMax、YMin、YMid、YMax;
  2. meet : 裁剪位置,其他参数有slice;
  3. preserveAspectRatio="none" : 即不设置,不保持宽高比的拉伸;

preserveAspectRatio顺带提一下,因为 svg会自动缩放,所以可以了解下,帮助我们做一些屏幕适配。

绘制图形

svg 绘制图形的标签有很多,line画直线、rect画方块、circle画圆。这些查文档即可了解全貌,基本不用特别的介绍。附文档地址 https://developer.mozilla.org/zh-CN/docs/Web/SVG

本文将重点提及path的路径的描述方法。因其自由度,同时也带来了复杂度。

path标签

就这个我们重点来分析d属性,也就是如何绘制路径

d属性 - 绘制路径

先来一个简单的例子

<path  d="M0,0 h100 v100 h-100 z"/>

划重点:每个路径是由空格或者空白符来分割的,逗号表示的是 x,y 即坐标。不要被看到的长长的path迷惑了,逐条分析即可得到结果。

那我们看下上面代码中绘制的图像的步骤是如何的:

  1. M0,0 移动到 x=0,y=0的坐标
  2. h100 横向绘制一条长度为100的直线, 此时终点为 x=100,y=0
  3. v100 纵向绘制一条长度为100的直线,此线的起点为 x=100,y=0,终点为 x=100,y=100
  4. h-100 横向绘制一条长度为-100的直线, -100表示从右向左画,即此线的起点为 x=100,y=100,终点为 x=0,y=100
  5. z 闭合图形,此时可以想象下,这个其实画了一个正方形,如下图:

图片

上述例子中已经使用 M,h,v,z, 那总共有哪些路径描述呢,如下:

  1. M/m - 移动到每个位置;
  2. H/h - 画一个横向的线;
  3. V/v - 画一个纵向的线;
  4. L/l - 画一条线到 x,y;
  5. C/c - 画一条贝塞尔曲线;
  6. A/a - 画一条弧线;
  7. z - 闭合;

可以看到有大写和小写之分。大写表示绝对坐标, 小写表示相对上一个点的坐标。比如我们从x=100,y=100的点开始画长度100的横线,用小写是 h100,大写是H200。

绘制镂空的蒙层 fill-rule="evenodd"

fill-rule也是我们绘制图形时需要关心的属性,特别是有交叉的路径的时候。镂空的关键点在于如何判定内容是内部还是外部,来看下这篇详细的介绍,可以帮助你理解:https://www.zhangxinxu.com/wordpress/2018/10/nonzero-evenodd-fill-mode-rule/

绘制高亮区域

梳理完基本原理后,我们就来实际操作一下。先看下效果图:图片
那我们的题目就是这样了:

已知后端数据为

[  [1, 1, 1, 0, 1],  [0, 0, 1, 0, 1],  [0, 0, 1, 1, 0],  [0, 1, 1, 1, 0],  [0, 0, 1, 1, 1]]

的二维数组。1表示获得,需要绘制为高亮。0表示未获得,需要加上白色蒙层。求如何绘制这个边界路径。

解题开始:

  1. 每个方块有4条线
    把4条线的数据添加到数组里。
  2. 相邻2个方块需要去掉相同的线。如
    其中B->D和D'->B'是同一条线,我们都移除。结果就会变成
    一个更大的方块。
  3. 重复1、2两步就可以获得包含整个拼图的路径数组。
  4. 从数组中任取一线。
  5. 查找另一条起点 等于 其终点的线,重复当前步骤直到无法找到下一个线,此时最后一个线的终点必然等于第一个线的起点。标记z,闭合当前路径。
  6. 查询数组是否已清空,若未清空,重复第4、5步。若已清空,则已完成path路径。

按照上述步骤你就可以获得一个完整的路径了,后续我们将继续讲解如何通过fill填充蒙层和图像filter组合滤镜完成发光效果。

fill 填充

绘制好路径我们就要给他填充上内容。

color 颜色

fill="#ffffff"颜色的填充就比较简单直观了,不做赘述了。

蒙层就可以用颜色加透明度来实现填充。

pattern 图案

pattern可以让你把图片作为填充物,也可以把一些svg内容作为填充物,非常好用。高亮部分就可以使用这个。也可以直接透明,用这个是为了刚好的实现后续的内外发光滤镜。

1. 添加图片

<pattern id="puzzleImage" x="0" y="0" patternUnits="userSpaceOnUse" primitiveUnits="userSpaceOnUse" width="495" height="1237">  <image x="0" y="0" width="495" height="1237" xlink:href="https://lofter.lf127.net/1645514196505/puzzle.jpg?imageView&crop=0_0_495_1237#pattern" /></pattern>

写法上也很方便,pattern里包裹image即可,其中patternUnits属性需要关注下 patternUnits 可选值为 userSpaceOnUse 和 objectBoundingBox。userSpaceOnUse表示用户空间也就是最外层svg上的viewBox。objectBoundingBox是相对与当前元素的。

我们这里是通过叠加多层图像来实现效果,所以选用 userSpaceOnUse,保证了填充的图片能完全一致。

2. 组合其他svg内容

<pattern id="puzzleImage" x="0" y="0" patternUnits="userSpaceOnUse" primitiveUnits="userSpaceOnUse" width="495" height="1237">  <image x="0" y="0" width="495" height="1237" xlink:href="https://lofter.lf127.net/1645514196505/puzzle.jpg?imageView&crop=0_0_495_1237#pattern" />  <line x1="99" y1="0" x2="99" y2="1237" stroke-width="1" stroke="#fff" />  <line x1="198" y1="0" x2="198" y2="1237" stroke-width="1" stroke="#fff" />  <line x1="297" y1="0" x2="297" y2="1237" stroke-width="1" stroke="#fff" /></pattern>

pattern除了图片还能添加其他svg标签,比如上图的,我们白色的分割线也放到了pattern里,这样填充的图片就自动带上了白色的分割线。我们高亮部分就是通过这种方式填充内容的。

Filter 滤镜组合演示

拼图高亮滤镜演示

我来看下一个高亮效果,一个内外发光的svg滤镜效果。图片

其实现代码代码如下:

<filter id="puzzleNormalShadow" x="-50%" y="-50%" width="200%" height="200%">  <feGaussianBlur in="SourceAlpha" stdDeviation="13" />  <feMorphology operator="dilate" radius="6" result="offsetblur" />  <feFlood flood-color="#ffe4b1" flood-opacity="0.75" />  <feComposite result="outblur" in2="offsetblur" operator="in" />
<feFlood flood-color="#ffe4b1" flood-opacity="0.75" /> <feComposite in2="SourceGraphic" operator="out" /> <feGaussianBlur stdDeviation="13" /> <feComposite result="inblur" in2="SourceGraphic" operator="atop" /> <feMerge> <feMergeNode in="outblur" /> <feMergeNode in="inblur" /> </feMerge></filter>

这是个比较典型的svg的多个滤镜组合后生成的效果,那它们又是怎么组合生效的呢?我们可以来看一下下方的gif,结合我们的分析来理解一下。图片现在我们逐条分析每一个滤镜的作用来加深理解:

  1. <feGaussianBlur in="SourceAlpha" stdDeviation="13" /> feGaussianBlur的滤镜作用是模糊,in="SourceAlpha"表示作用在透明区域上,stdDeviation="13"表示模糊的程度
  2. <feMorphology operator="dilate" radius="6" result="offsetblur" /> feMorphology是用来扩大或者缩小内容的。operator="dilate"表示扩大。result="offsetblur" result表示记录产物为offsetblur 可以用在后面指定的地方。
  3. <feFlood flood-color="#ffe4b1" flood-opacity="0.75" /> feFlood 表示填充颜色
  4. <feComposite result="outblur" in2="offsetblur" operator="in" /> feComposite 来合并图像,有inin22个属性。其中,in缺省则自动使用上一步的产物,in2="offsetblur"即使用之前第二步的产物。
  5. 至此完成了外发光的效果制作。
  6. 后面4步为内发光的效果制作,基本原理和外发光相同,同学们可以结合前面几步想象一下如何制作的。
  7. 最后:<feMerge> 把外发光内发光合并在一起,此时内外发光是可以用不同的颜色和效果的

虽然内外发光在我们看来是一个很常见的效果,但是真的去实现又需要多条滤镜相互结合才能完成。反复思考这个例子,相信可以帮助同学们理解滤镜是如何组合的,后面自己制作的时候也能梳理出思路,而不是只能搜索现成的。

feColorMatrix 色彩矩阵滤镜

什么是feColorMatrix?

feColorMatrix 是 svg的一个滤镜,主要作用是 通过一个计算方法,图像上的每个颜色转化成另一个颜色。比如红色 转 黄色,或者让红色变得更红,绿色更绿,都是可以做到的。可以看到feColorMatrix可能是我们最常用的图片处理滤镜了。

那我们先来看一下,一个 feColorMatrix 有哪些些属性:

<feColorMatrix  in="SourceGraphic"  type="matrix"  id="feColorMatrix"  values="0.2 0.2 0.2 0 0.3          0.2 0.2 0.2 0 0.3          0.17 0.17 0.17 0 0.49          0 0 0 1 0"/>

其中最重要的是 values 也就是我们的转换矩阵。

矩阵是怎么计算的?

计算公式如下:

图片


我们看下每个部分:

  1. 最左边的
    是图像上每个像素点,原图的RGBA值;
  2. feColorMatrix中的values属性;
  3. 处理后的图片RGBA值

最后,请大家多花费几分钟,再回归下我们的公式,理解一下。理解了才能进行我们实际的操作。

来做一个渐变映射的效果!

效果如下图,左边是处理完后的,右边是原图。运用feColorMatrix来处理吧!图片拿到换个效果的时候,我们一般也会先和视觉同学沟通下,了解他们如何用PS做出这个效果的。经过沟通我们知道他们做了2步操作:

  1. 把图片变成黑白的;
  2. 用一个紫色的做渐变映射。ps上配置如图:图片

理解把图片变成黑白的

变黑白也就是图片只有黑白2色,图上只有白、灰、黑。做法其实就是吧 RGB 3个通道的颜色相加再平分。比如原来是 rgb(30, 60, 90) 转化后就变成了 rgb(60,60,60)。每个点都这么处理,图片就变黑白了。此时的feColorMatrix值应该是:

0.333 0.333 0.333 0 00.333 0.333 0.333 0 00.333 0.333 0.333 0 00 0 0 1 0

即通过设置每个通道的值都为R * 1/3 + G * 1/3 + B * 1/3实现黑白的变化。

理解渐变映射

渐变映射其实就是特殊的变黑白。用一个指定颜色到白色的过渡色来,填充 每个通道上0-255的变化。

比如我们这次的值为rgba(115,115,157,1)的紫色到白色到过渡图片。以R通道的计算为例,我们看下计算过程:

  1. 本次渐变的变化区间为115(紫的R通道值)-255(白的R通道值)等于140。原图的变化区间为0(黑)-255(白)等于255。所以可以得出 rR(参考上面公式) = 目标过渡变化区间/原图过渡变化区间 =  140 / 255
  2. 同时考虑要先进行黑白,即乘以1/3,rR(参考上面公式) = 140 / (255 * 3)。此时可以得出第一行是140 / (255 * 3) 140 / (255 * 3) 140 / (255 * 3) 0 X。X见下一条。
  3. 因我们是从**115(紫)**开始过渡的,所以 X是基础值,即 X = 115 / 255。最终结果就是140 / (255 * 3) 140 / (255 * 3) 140 / (255 * 3) 0 115/255
  4. 计算后为0.1830065359477124 0.1830065359477124 0.1830065359477124 0 0.45098039215686275

G和B 2个通道的转换矩阵,可以使用同样的方法计算得出。

最终生成的结果如下:

<feColorMatrixi  in="SourceGraphic"  type="matrix"   values="        0.1830065359477124 0.1830065359477124 0.1830065359477124 0 0.45098039215686275        0.1830065359477124 0.1830065359477124 0.1830065359477124 0 0.45098039215686275        0.12810457516339868 0.12810457516339868 0.12810457516339868 0 0.615686274509804        0 0 0 1 0" />

同学们也可以把这个计算过程梳理成JS函数,方便下次使用。其计算过程大致如下:

var originMax = 255;var total = originMax * 3;
var white = 255;var r = rInput.value;var g = gInput.value;var b = bInput.value;
var rp = (white - r) / total;var gp = (white - g) / total;var bp = (white - b) / total;
var matrix = ` ${rp} ${rp} ${rp} 0 ${r / originMax} ${gp} ${gp} ${gp} 0 ${g / originMax} ${bp} ${bp} ${bp} 0 ${b / originMax} 0 0 0 1 0`;

兼容性问题记录

这个是记录了下本次开发中几个兼容性问题,同学们可以方便查看~

Q: image 的href 和 xlink:href,该用哪个呢?

A: xlink:href 有更好的兼容性,推荐使用。低版本ios不支持href

Q: feGaussianBlur 做外发光滤镜后,左侧发光看上去被裁剪了怎么办?

A: 为 filter标签 添加下如下属性x="-50%" y="-50%" width="200%" height="200%"扩大下滤镜的作用范围,一般200%够用了,外发光不会有这么大

Q: ios safari 中 feFlood怎么不起作用了?

A: 为feFlood标签添加上 x="0" y="0" width="100%" height="100%" 属性,明确feFlood的范围

结束

本次开发也是作者开发使用和熟悉了解svg的一个过程。中间使用到了不少svg的特性。svg本身有一些便利性。通过预置的滤镜可以快速处理图像。大部分情况下使用svg滤镜处理图像能比canvas更快的看到结果。canvas需要先加载再处理,一般流程较长。但是svg依赖浏览器本身的优化,ios上就略有不足,还是会考虑使用上canvas的方案。但是用来做一些图片的快速处理倒是完全够用了。最后也欢迎大家留言讨论,有问题可以一起交流。


继续滑动看下一个
LOFTER技术团队
向上滑动看下一个