SVG的filter使用经验

早就知道SVG的filter可以实现很厉害的效果,但入门起来有点困难。一个重要的原因我觉得是,网上没找到很好的教程。连我最信任的MDN都没有把它讲明白。

使用SVG filter的缘由

为什么我会用到SVG filter呢,这还是工作上因为要给leaflet做的地图的边框添加效果,所以才用到的。大家都知道,leaflet默认的renderer是在SVG上绘制地图的。而SVG可以应用CSS以及filter来改变其显示效果。

我们不可能期待一个以精简为目标的地图库还提供绘制复杂边框效果的选项。在地图上直接overlay一张图片的解决方案固然能解决问题,但如果涉及到地图的放大缩小,问题就会变得明显。因为地图是投影坐标系,图片放大缩小无法正确地保持地图的投影关系,所以覆盖图片只有在单一比例尺下可用。另外一个使用图片的小问题是,在leaflet中图片缩放不如地图缩放流畅,所以能够明显感觉到一个图片盖在上面的感觉。想要与地图完美契合,还是用SVG filter比较好。

另外我还有考虑CSS filter。CSS filter确实比SVG filter好写太多了。因为它都是一些预定义的filter。这些预定义的filter可以调整颜色,甚至能够给元素加阴影。但它的灵活性比SVG filter要差很多,一般无法完成复杂的效果。另外有一个重要的原因是,CSS filter在chrome系列浏览器中无法作用于SVG里的path,只能作用于SVG图像整体(在Firefox上可以用于path)。由于用户大多使用chrome,这对于实际使用来说很有影响,所以放弃CSS filter。

SVG filter能做的事情

SVG filter是在现有图形基础上修饰,而不是凭空创造。这一点要心里有底。Filter这个词,在中文中常常翻译为“滤镜”,它的作用跟你手机相机里的滤镜的作用差不多。

具体而言,你可以添加高斯模糊、重新映射颜色、给区域上色、平移、添加纹理、侵蚀与扩张,甚至图片混合、给图片打光。而且重要的是,你可以把多个效果的结果结合起来,产生想要的效果。

用SVG filter产生想要的效果

我不是SVG filter的行家,只能说刚刚会用而已。所以我这里所说的经验也就是对不会用的朋友有点帮助。我前面也提到了,我的目标是修饰边框,所以我会以修饰边框为目标来介绍如何使用这些filter。

在用SVG filter的时候,你需要理解输入SourceGraphic或SourceAlpha的含义。虽然我们操作的是SVG,但是我们不能把这两个输入想成是SVG的元素,而应当想成是它呈现的样子,也就是一张再普通不过的PNG图片,它无法区分你选中的部分的轮廓与内部,它是作为一个整体接收的。

举个简单的例子,当你操作一个path的时候,如果你原始的元素是内部有颜色的,那么SourceGraphic就是包含内部的;但如果内部是透明的,那么SourceGraphic就会只包含你这一个轮廓。由于SourceGraphic定义了你可以操作的基础,你需要想清楚你是需要对一个区域(下文称为面)操作,还是只是轮廓本身操作。

我尝试过,如果你事先提供的SourceGraphic是只有边框的,那么你无论如何也无法把多边形的内部染色,因为你的基础已经定下来是这个轮廓了,那么你用feFlood染色也只是给这个轮廓染色,而不是给轮廓包围住的区域染色。这也是我选择预先给我的地图染个颜色的原因,我需要SourceGraphic给我的是一个面,而不是一个轮廓。

下面我来说说为什么我本来要修饰轮廓,但我必须选一个面的原因。假如我只是需要改变轮廓的颜色、粗细,以及辉光效果的高斯模糊,那么我的确不需要选一个面。但因为我需要在它的内部和外部添加一些圈层,假如没有一个面的话,我想不到如何通过加减运算来很好地确定内外圈层的范围。

说回效果本身。先说最简单的辉光效果。还是假设我们只是输入一个轮廓吧,这样好描述一些。不需要位移,直接加一个feGaussianBlur就有了。

但是实际运用就容易发现,blur后的亮度太弱了,叠在地图上隐隐约约,能看出来但又不明显。这时我们就不用纠结feGaussianBlur的那个stdDeviation参数了,因为再怎么调它也不能很好地变亮。这时候就该搬出feComponentTransfer了。这个原语的名字起得一点也不直白。它的意思就是,你可以自由地调整图片的RGBA中任意一个部分。有些博主告诉我这个原语几乎不会用到,但我恰恰觉得它是相当好用的。我就调了调feFuncA就解决了问题。

再说第二个效果,就是我前面说的附加一些圈层。设计师设计出来的好看的边框一般都不会一层解决。这里我就用到了传说中的feMorphology原语。前面提到我输入的是一个面,所以使用feMorphology我可以轻易地把它侵蚀成比原来“小一圈”以及“大一圈”的面。再把面之间做做减法,就可以拿到某一圈的区域了。

如何做减法?这可能也是初学者不知道的一个事情。答案就是feComposite原语。它能提供一些常见的集合运算。这里我推荐看MDN文档,它展示了不同的组合方式的效果。我们只需要最简单的operation为out的运算

就获得了biggerSource比SourceAlpha大出的部分,然后用feFlood染成需要的颜色就好啦,这个不用教吧。

原语级联以及效果叠加

上面我只提到了用这个用那个原语,但没提到它们是如何连接起来的。也没提如何把两个成型的效果叠加在一起。现在我就来介绍这些。

想要级联原语,其实只要让下一个的输入等于上一个的输出。我们需要用result属性来将结果存到缓冲区里。然后下一个原语的输入里我们再指定它就行。比如我们在高斯模糊后面接一个位移,就可以这样写:

一个小提示,如果不指定in参数,那就会默认使用SourceGraphic来输入。在网上很多例子中,都能看到不写in参数的情况。这可能让初学者很迷惑。

然后我们就要说说效果叠加。这需要用到feMerge原语。想要把offsetBlur和litPaint合并起来,就像下面这样做。

在合并方面我也想给一个小提示,就是合并是有顺序的。就像HTML里面一样,后面的是叠在前面的上面的。如果你以不正确的顺序合并,那可能会挡住你想要加上去的效果。

结束语

关于SVG filter还有很多更深入的知识,我也没有学到。我希望这篇文章达到的效果就是,能够了解到这个工具如何入门,如何实现我们想要的效果。