SVG滤镜的艺术以及它为什么这么棒

编辑推荐: 掘金是一个高质量的技术社区,从 CSS 到 Vue.js,性能优化到开源类库,让你不错过前端开发的每一个技术干货。 点击链接查看最新前端内容,或到各大应用市场搜索「 掘金」下载APP,技术干货尽在掌握中。

经过近20年的发展,如今的网页排版,以其高密度的显示以及OpenType功能的支持,离现实世界的印刷排版仅是一步之遥。但是平面设计还是有一个领域,我们还在不断地fall back到使用位图,而不是使用单纯的文本:字体显示,如说明性的、绚丽的、戏剧性的、俏皮的、试验性的或料想不到的艺术字母。

在HTML中显示文本的案例

我们可以从成千上万的Web字体中挑选,还可以为它们添加CSS效果,一些具有广泛浏览器支持(如投影和三维变换),一些可能是更试验性的(如background-clip背景裁剪和text-stroke文本描边),但这都是基本的。如果我们希望能够在我们的网站上显示真正优秀的字体,我们通常会选择把它作为图像嵌入。

在Web上使用图像的缺点是显而易见的:文件的大小,对于经常改变的或用户生成的内容缺乏可行性,可访问性,以及时间损耗等等。

所以如果我们能够为字母编辑样式,就像我们经常使用CSS修饰文本那样,岂不是很棒吗?为多个边界应用不同的颜色?添加内斜面、外斜面?添加图案、纹理和3D效果?给它一个通用的样式?使用多种颜色还有扭曲样式?给它一个繁琐的样式?

复杂的SVG滤镜:CSS

这其中的大部分都是已经可以实现的:关键是要释放SVG滤镜的魔力。SVG滤镜(包括CSS滤镜)通常被认为是一种通过模糊效果或颜色处理来处理位图的方式。但它们其实不只是这样。像CSS规则,SVG滤镜可以是一组添加在传统文本顶部的可视化图层。有了CSS的filter属性,这些效果可以在SVG之外使用,然后直接应用到HTML内容上。

说到CSS和SVG中的滤镜可能有一点疑问:SVG滤镜可用一个SVG filter元素定义,而且可以在SVG文件中应用。CSS滤镜可以通过filter属性应用于任何HTML元素上。CSS滤镜如blur, contrasthue-rotate,都是预定义的快捷方式,也是常用的SVG滤镜效果。除此之外,规范还允许我们引用SVG文件中用户自定义的滤镜。还有一点困惑的是专有的-ms- filter标签,在IE9中已经被废弃,在IE10发布时就已经被删除。

本文主要涉及的是第一种情况:嵌入在HTML页面中的SVG文件中使用的滤镜,但后面我们会试着把SVG滤镜应用于HTML内容。

这篇文章中的插图都是SVG滤镜效果应用于文本的示例。点击图片可查看原文(在现代支持SVG的浏览器中查看)。我把他们称为“复杂的”SVG过滤器,因为实际上这些滤镜是多种效果的组合,然后结合到一个输出的。尽管字母的外观已经有了显著的改变,实际上文本仍然是可抓取并且可获得的,可以选中并复制。因为SVG滤镜在所有的现代浏览器中都是支持的,这些效果可以在IE10以上的浏览器中显示。

理解SVG滤镜是有一定挑战性的。即使是像投影这样简单的效果都需要复杂并且详细的语法。一些滤镜,如feColorMatrixfeComposite,没有对数学和色彩理论有一个透彻的理解的话是很难掌握的。本文不是一篇学习SVG滤镜的教程。相反,我将介绍一组标准构建模块,来完成一些效果,我会用尽量少的解释,重点在于记录完成这些效果的各个步骤。你看到的主要是关于如何完成,对于那些想要了解为什么的人,我在这篇文章的结尾处放了一个阅读列表。

构建滤镜

下面是一张复杂的SVG滤镜的构建图。滤镜输出的是风化的文本效果,我们将使用它作为示例,一步一步演练:

让我们把这个效果分解成几个部分:

  • 绿色文字
  • 红色投影
  • 文字和投影使用一个透明间隙隔开
  • 文字带有grungy和风化效果

我们的SVG滤镜是通过组合多个小模块构建而成的,也称为“滤镜原语”。每个模块都是由一组或更多原语构建而成的,然后再组合成统一的输出结果。下边的图片可以帮助你理解:

构建滤镜

构建复杂滤镜的处理步骤,最好的说明图

添加滤镜

我们从一个包含空滤镜和文本的模板SVG文件开始:

<svg version="1.1" id="Ebene_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
  <defs>
    <style type="text/css">
      <![CDATA[
        .filtered{
          filter: url(#myfilter);
          …
        }
      ]]>
    </style>

    <filter id="myfilter">
      <!-- filter stuff happening here -->
    </filter>
  </defs>

  <g class="filtered">
    <text x="0" y="200" transform="rotate(-12)">Petrol</text>
  </g>
</svg>

滤镜元素

我们从filter标签元素开始,在其开始和结束标签中间,我们可以放置变换、颜色、位图操作等等所有规则。滤镜可以作为目标元素的属性应用,也可以通过CSS应用。目标元素通常是SVG中的元素,但是后边我们将了解另一个有趣的选择:把SVG滤镜应用于HTML元素。

几个用来控制filter元素的属性:

  • xy位置(默认 -10%);
  • widthheight(默认 120%);
  • id属性,对于后边的引用是必需的;
  • filterRes,预定义解决方案(在“Filter Effects Module Level 1”规范中不建议使用);
  • 相对单位(默认objectBoundingBox)或绝对单位(默认userSpaceOnUsefilterUnits

关于滤镜原语

正如我们已经知道的,滤镜原语是SVG滤镜的组成部分。任何一种效果,都至少包含一个原语。一个原语通常包含一个或两个输入(inin2),以及一个输出(result)。原语输入包括模糊、移动、填充、结合或扭曲等等。

该规范允许我们采用滤镜元素的几个属性作为输入源。因为大多数的属性都不能跨浏览器运行,在这篇文章中我们会采用SourceGraphic(未添加滤镜的元素,有颜色、描边、图案填充等等)和SourceAlpha(alpha通道的不透明区域——即原图中填充黑色的部分),这两者都有非常好的浏览器支持。

如何加厚输入文本

我们要了解的第一个滤镜原语是feMorphology,一个用于把输入加厚(operator="dilate")或变薄(operator="erode")的原语——因此,非常适合用来创建轮廓和边界。

这是我们如何将SourceAlpha增粗4个像素:

如何加厚输入文本

增粗了4个像素的图

<feMorphology operator="dilate" radius="4" in="SourceAlpha" result="BEVEL_10" />

创建投影

下一步骤是在上一个原语的基础上创建一个3D的投影,结合feConvolveMatrix。这个滤镜原语是最强大也最难以掌握的一个。它主要是帮助你创建自己的滤镜。总之,你会定义一个会根据其相邻像素的值变化的像素栅格(一个内核矩阵)。这样一来,你就可以创建自己的滤镜效果,如模糊、锐化滤镜,或投影。

这是feConvolveMatrix创建的一个45deg3px的深度投影。order属性定义widthheight,这样原语才知道是应用3x3的矩阵,还是9x1的矩阵:

创建投影

使用feConvolveMatrix创建增粗的投影输入

<feConvolveMatrix order="3,3" kernelMatrix=
   "1 0 0 
   0 1 0
   0 0 1" in="BEVEL_10" result="BEVEL_20" />

考虑到IE11和Microsoft Edge无法处理大于8x8的矩阵,它们也无法很好地处理复杂矩阵,所以在部署这段代码之前先删除所有回车最好。

该原语同样可以应用于左、上、右、下各个方向。因为我们希望投影是往右下方的,我们需要修改结果。targetXtargetY这两个属性定义了效果的起点。可惜,IE对它们的解析不同于其它的浏览器。因此,要保持跨浏览器的兼容性,我们将使用另一个滤镜原语feOffset来处理。

OFFSETTING

顾名思义,feOffset需要一个输入值,如下:

<feOffset dx="4" dy="4" in="BEVEL_20" result="BEVEL_30"/>

裁剪投影部分

feComposite是为数不多的几个需要两个输入的滤镜原语之一。它运用了Porter-Duff合成来组合两张图像。feComposite可以用于掩蔽或裁剪元素。这是如何从feConvolveMatrix输出的结果中减去feMorphology的输出。

裁剪投影部分

从投影中裁剪掉第一个加粗的原语

<feComposite operator="out" in="BEVEL_20" in2="BEVEL_10" result="BEVEL_30"/>

为投影着色

这个过程包括两个步骤:

首先,我们使用feFlood创建一个着色区域。这个原语将会简单地在滤镜区域输出根据我们定义的颜色的矩形。

<feFlood flood-color="#582D1B" result="COLOR-red" />

然后我们再用一个feComposite裁剪掉BEVEL_30的透明部分:

为投影着色

为投影着色

<feComposite in="COLOR-red" in2="BEVEL_30" operator="in" result="BEVEL_40" />

将斜面和原图结合成一个输出

feMerge可以把斜面和源一起输出:

将斜面和原图结合成一个输出

斜面和原图混合成一个输出

<feMerge result="BEVEL_50">
   <feMergeNode in="BEVEL_40" />
   <feMergeNode in="SourceGraphic" />
</feMerge>

看起来像是我们期待的结果。让我们给它加一个风化的效果,看起来更逼真一些。

添加分形纹理

feTurbulence是最好玩的原语之一。但是,它可能融化你的多核CPU,让你的风扇像波音747的涡轮喷气发动机那样旋转。所以,谨慎使用,尤其是在移动设备上,因为这个原语对渲染性能有非常坏的影响。

feFlood, feTurbulence输出填充矩形,但使用的是杂乱的非结构化的纹理。

我们手头有几个值可用来改变纹理的质感和节奏。通过这种方式,我们可以创建像木头、沙子、水彩或破裂混凝土效果的表面。这些设置对滤镜的性能有直接的影响,所以测试要足够彻底。以下是如何创建一个类似描边画笔的纹理的代码:

<feTurbulence baseFrequency=".05,.004" width="200%" height="200%" top="-50%" type="fractalNoise" numOctaves="4" seed="0" result="FRACTAL-TEXTURE_10" />

默认情况下,feTurbulence输出的是彩色纹理——不是我们想要的那个。我们需要一个灰度alpha图;多一点对比的话会更好。通过feColorMatrix来增加对比度,同时将它转换为灰度图:

添加分形纹理

最后,加上分形纹理的效果

<feColorMatrix type="matrix" values=
   "0 0 0 0 0,
   0 0 0 0 0,
   0 0 0 0 0,
   0 0 0 -1.2 1.1"
   in="FRACTAL-TEXTURE_10" result="FRACTAL-TEXTURE_20" />

最后要做的就是将纹理alpha和文字组合,依然是使用我们的老朋友feComposite

<feComposite in="BEVEL_50" in2="FRACTAL-TEXTURE_20" operator="in"/>

终于完成啦O(∩_∩)O~

如何将SVG滤镜应用到SVG

以下是两种将SVG滤镜应用到SVGtext元素的方法:

通过CSS

.filtered {
   filter: url(#filter);
}

通过属性

<text filter="url(#filter)">Some text</text>

将SVG滤镜应用到HTML内容

滤镜最鸡冻人心的特性之一是,它可以嵌入SVG,在SVG中定义滤镜,并使用CSS把它应用到任何HTML元素中:

filter: url(#mySVGfilter);

在写这篇文章的时候,Blink和WebKit都需要添加前缀,如下:

-webkit-filter: url(#mySVGfilter);

这在理论上听起来很容易,但实际中却是一种黑暗艺术orz:

  • WebKit、Firefox和Blink目前都支持SVG滤镜应用于HTML内容。IE和Microsoft Edge却会显示未添加滤镜的元素,所以要确保默认样式看起来也非常OK~
  • 包含滤镜的SVG可能不会被设置为display: none。但是你可以自己设置visibility: hidden
  • 有时候SVG的大小会直接影响应用的目标元素的多少。
  • 我说过WebKit,Blink和Firefox理解这种语法吗?好吧,Safari(和它的小伙伴,Mobile Safari)是一个特例。你可以在Safari中跑一下这些demo,但是你很可能会抓狂。在写这篇文章的时候,我不建议在当前版本的Safari(8.0.6)中对HTML内容使用SVG滤镜。因为结果是不可预测的,技术并非刀枪不入。更糟糕的是,如果Safari因为某些原因无法渲染你的滤镜,它也不会显示目标HTML元素,噢噢噢噢噩梦:-(。基于经验法则,你增加你让Safari显示你的滤镜的机会,通过绝对定位和固定目标元素的大小。作为一个概念证明,我已经设置了一个“流行的”滤镜效果,针对桌面版Safari进行了优化。在Safari中,将feImage应用于HTML元素似乎是不可能的。

之前的DEMO,应用于HTML内容

在这些demo中,包裹元素都被设置为contenteditable = "true",方便进行文本编辑。(请注意,这些demo都是实验,在Safari、IE或Edge中都是不能运行的。)

自定义滤镜

根据其复杂程度,滤镜也可以是一个很复杂的东西。在制作滤镜的时候,你可以添加或移除规则、改变他们的顺序和值,但很快你就会变得混乱。这里有一些我自己写的规则,可以帮助我追踪发生的问题。因为人员和项目不同,在我看来逻辑和结构化的东西,在你看来可能是混乱和不知所云,所以采用并保留一下这些建议吧。

分组

我把滤镜原语根据它们自身的功能分成了几组——如:“border”、“fill”、“bevel”等等。在模块的开始和结束的地方,我会根据组名备注。

命名

良好的命名规则可以帮助你更好地组织滤镜,并且方便对原语内部和外部情况进行追踪。经过对BEM-like schemas的实验,我最终确定了一个非常简单的命名结构:

NAME-OF-GROUP_order-number

比如说,你可能使用像BEVEL_10, BEVEL_20, OUTLINE_10等等这样的命名。我从10开始,并使用10作为增量,方便调整原语的顺序,也方便在一组原语中间或开始的地方添加原语。我比较喜欢整块内容一起使用,因为它们能够帮我更快地扫描原内容。

保持声明输入和结果

尽管不是必要的,我通常都会声明一个“in”和“result”。(如果省略,原语的输出就默认是其继承者的输入)

一些构建模块

我们先看看单个技术能达到的效果。然后通过组合这些构建模块,我们可以创建新的复杂的滤镜效果。

文本描边

<!-- 1. Thicken the input with feMorphology: -->

<feMorphology operator="dilate" radius="2" 
in="SourceAlpha" result="thickened" />

<!-- 2. Cut off the SourceAlpha -->

<feComposite operator="out" in="SourceAlpha" in2="thickened" />

这个方法并不能保证结果是好看的。尤其是你将dilate与较大的radius值结合的时候,结果可能比通过stroke-width创建的几何体糟糕一些。根据不同的情况,比较好的选择是将文本存储在一个符号元素中,然后在需要的时候通过use插入,再通过CSS的stroke-width属性将其加厚。注意,stroke-width不能应用于HTML内容。

撕裂效果

<!-- 1. create an feTurbulence fractal fill -->

<feTurbulence result="TURBULENCE" baseFrequency="0.08"
numOctaves="1" seed="1" />

<!-- 2. create a displacement map that takes the fractal fill as an input to distort the target: -->

<feDisplacementMap in="SourceGraphic" in2="TURBULENCE" scale="9" />

颜色填充

<!-- 1. Create a colored filled area -->

<feFlood flood-color="#F79308" result="COLOR" />

<!-- 2. Cut off the SourceAlpha -->

<feComposite operator="in" in="COLOR" in2="SourceAlpha" />

有一点需要提到的是,除了feFloodfeColorMatrix是另一种能够改变原输入颜色的方法,尽管它本身的概念比较难以理解。

OFFSETTING

<!-- Offset the input graphic by the amount defined in its "dx" and "dy" attributes: -->

<feOffset in="SourceGraphic" dx="10" dy="10" />

投影

<!-- Define a convolve matrix that applies a bevel. -->

<!-- Order defines the depth of the extrusion; angle is defined by the position of "1" in the matrix. Here we see a 45-degree, 4-pixel deep extrusion: -->

<feConvolveMatrix order="4,4" 
   kernelMatrix="
   1 0 0 0
   0 1 0 0
   0 0 1 0 
   0 0 0 1" in="SourceAlpha" result="BEVEL" />

<!-- offset extrusion: -->

<feOffset dx="2" dy ="2" in="BEVEL" result="OFFSET" />

<!-- merge offset with Source: -->

<feMerge>
   <feMergeNode in="OFFSET" />
   <feMergeNode in="SourceGraphic" />
</feMerge>

噪点填充

feTurbulence滤镜原语可以通过应用Perlin噪声算法创建一个噪声文本(1981年,Ken Perlin在他研究TRON的工作期间发明的)。这可以生成一个填充了噪点的矩形,就像你在深夜时看旧电视机(有线电视发明之前)看到的画面。

噪点的外观可以通过以下几个参数进行设置:

  • type 默认状态会生成液体文本。
  • type 可以设置为fractalNoise,可以生成沙子文本。
  • baseFrequency 用于控制x和y方向图案的重复。
  • numOctaves 用于增加细节的层次,如果性能有影响的话,应该将其设置为一个较小的值。
  • seed 用于决定开始的随机数字。
<feTurbulence type="fractalNoise" baseFrequency="0.1" numOctaves="5" seed="2" />

图像填充

feImage的目的是为目标元素填充纹理。如果我们想要应用重复图案,必须和feTile结合使用。

<!-- The following code will create a 100 × 200-pixel square filled with "myfill.svg": -->

<feImage xlink:href="myfill.svg" x="0" y="0" width="100" height="200" result="IMAGEFILL"/>

<!-- We then use this fill as an input for feTile, creating a repeating pattern this way: -->

<feTile in="IMAGEFILL" resulte="TILEPATTERN"/>

<!-- Now we will use feComposite to "cut off" SourceAlpha's transparent areas from the fill: -->

<feComposite operator="in" in="TILEPATTERN" in2="SourceAlpha" />

滤镜有一个很酷的地方就是,规范允许我们使用任何SVG元素作为输入,并从它开始创建一个图案填充。所以,理论上,你可以从你SVG中的symbol、group或fragment创建图案填充,然后将它们作为纹理,填充到HTML元素。不幸的是,因为一些比较旧的bug,Firefox只能接受外部资源作为输入。如果你希望它保持自给自足,不想添加额外的HTTP请求,也可以:把图案填充以UTF-8 data URI嵌入:

<feImage xlink:href='data:image/svg+xml;charset=utf-8,<svg width="100" height="100"><rect width="50" height="50 /></svg>' />

有一些浏览器不能理解UTF-8 data URI,如果它们不是URL编码的话,所以把URL编码设置为默认:

<feImage xlink:href='data:image/svg+xml;charset=utf-8,%3Csvg%20width%3D%22100%22%20height%3D%22100%22%3E%3Crect%20width%3D%2250%22%20height%3D%2250%20%2F%3E%3C%2Fsvg%3E' />

如果你想要将feImage应用于HTML内容,需要注意尺寸的问题。包含滤镜的SVG需要覆盖它被应用的区域。最简单的方法就是将它设置为相对它被应用的块元素内的绝对定位的子元素。

<style>
  h1{
    position: relative;
    filter: url(#myImageFilter);
  }
  h1 svg{
    position: absolute;
    visibility: hidden;
    width: 100%;
    height: 100%;
    left: 0;
    top: 0;
  }
</style>
<h1>
  My Filtered Text
  <svg>
    <filter id="myImageFilter">…</filter>
  </svg>
</h1>

照明效果

这是一个令人惊叹的效果,但是使用过于频繁,很快就变得乏味。而且这个滤镜对性能有严重的影响,所以谨慎使用。

<!--We create a heightmap by blurring the source: -->

<feGaussianBlur stdDeviation="5" in="SourceAlpha" result="BLUR"/>

<!-- We then define a lighting effect with a point light that is positioned at virtual 3D coordinates x: 40px, y: -30px, z: 200px: -->

<feSpecularLighting surfaceScale="6" specularConstant="1" specularExponent="30" lighting-color="#white" in="BLUR" result="SPECULAR">
    <fePointLight x="40" y="-30" z="200" />
</feSpecularLighting>

<!-- We cut off the parts that overlap the source graphic… -->

<feComposite operator="in" in="SPECULAR" in2="SourceAlpha" result="COMPOSITE"/>

<!-- … and then merge source graphic and lighting effect: -->

<feMerge>
    <feMergeNode in="SourceGraphic" />
    <feMergeNode in="COMPOSITE"/>
</feMerge>

结论

纯CSS布局和在如Photoshop或Illustrator中创建的定制设计元素之间有一定的差距。外部嵌入的资源如背景图像、雪碧图图标和SVG symbol在网页设计中将会永远有它们的位置。但是复杂的SVG滤镜给予我们更多相对于第三方设计工具的独立性,直接在浏览器中创建视觉样式可以让我们弥补这种差距。

在这篇文章中我们已经看到了SVG滤镜如何帮助我们创建好玩的、装饰网页的排版。但没有人说我们必须就此停止。很快,浏览器支持将会变得更好,我们可以在每个HTML元素上很方便地使用这些效果,像我们今天使用CSS这样。尽管和原生CSS技术表现的效果不同(SVG滤镜不仅对元素有影响,对它的子元素也有影响),在不久的将来看到有创造力的网页设计师使用这些技术是非常令人激动的。

这篇文章中引用的资源

扩展阅读

本文根据@Dirk Weber的《The Art Of The SVG Filter And Why It Is Awesome》所译,整个译文带有我们自己的理解与思想,如果译得不好或有不对之处还请同行朋友指点。如需转载此译文,需注明英文出处:http://www.smashingmagazine.com/2015/05/26/why-the-svg-filter-is-awesome/

彦子

在校学生,本科计算机专业。逗比一枚,热爱前端热爱生活,喜欢CSS喜欢JavaScript喜欢SVG,爱玩PS玩AI玩啊逗比的软件。努力向上,厚积薄发。

如需转载,烦请注明出处:http://www.w3cplus.com/svg/why-the-svg-filter-is-awesome.html

返回顶部