多年来,我一直无法在 CSS 中创建看起来比较自然的图案,这让我很头疼。我的意思是,有时我只需要一个木纹图案。我所知道的唯一可行的解决方案是使用外部图像,但外部图像是一种额外的依赖项,还会引入新的复杂性。
我现在知道,这些问题中很大一部分可以用几行 SVG 代码来解决。
TL;DR: 带我到画廊!
SVG 中有一个称为 <feTurbulence>
的过滤器基元。它很特别,因为它不需要任何输入图像——过滤器基元本身会生成一个图像。它产生所谓的 Perlin 噪声,这是一种噪声梯度。Perlin 噪声被广泛用于计算机生成的图形中,用于创建各种纹理。<feTurbulence>
提供选项来创建多种类型的噪声纹理,每种类型有数百万种变体。
<feTurbulence>
生成的。“所以呢?”你可能会问。这些纹理当然很吵,但它们也包含隐藏的图案,我们可以通过将它们与其他过滤器配对来发现这些图案!这正是我们接下来要深入研究的内容。
创建 SVG 过滤器
自定义过滤器通常由多个过滤器基元链接在一起组成,以实现所需的效果。在 SVG 中,我们可以使用 <filter>
元素和许多 <fe{PrimitiveName}>
元素以声明的方式描述它们。然后,可以将声明的过滤器应用于可渲染元素(如 <rect>
、<circle>
、<path>
、<text>
等)——通过引用过滤器的 id
。以下代码段显示了一个标识为 coolEffect
的空过滤器,它应用于一个全宽全高的 <rect>
。
<svg xmlns="http://www.w3.org/2000/svg">
<filter id="coolEffect">
<!-- Filter primitives will be written here -->
</filter>
<rect width="100%" height="100%" filter="url(#coolEffect)"/>
</svg>
SVG 提供了十多种不同的过滤器基元,但让我们从一个相对简单的基元 <feFlood>
开始。它完全按字面意思执行:它会淹没目标区域。(这也无需输入图像。)目标区域在技术上是过滤器基元在 <filter>
区域内的子区域。
过滤器区域和过滤器基元的子区域都可以自定义。但是,在本文中,我们将始终使用默认值,这实际上是我们矩形的整个区域。以下代码段通过设置 flood-color
(red
) 和 flood-opacity
(0.5
) 属性使我们的矩形变为红色且半透明。
<svg xmlns="http://www.w3.org/2000/svg">
<filter id="coolEffect">
<feFlood flood-color="red" flood-opacity="0.5"/>
</filter>
<rect width="100%" height="100%" filter="url(#coolEffect)"/>
</svg>
现在让我们看看 <feBlend>
基元。它用于混合多个输入。我们的一个输入可以是 SourceGraphic
,它是一个表示应用过滤器的原始图形的关键字。
我们的原始图形是一个黑色矩形——这是因为我们没有在 <rect>
上指定 fill
,而默认填充颜色为黑色。我们的另一个输入是 <feFlood>
基元的结果。正如您在下面看到的,我们在 <feFlood>
中添加了 result
属性来命名其输出。我们使用 in
属性在 <feBlend>
中引用此输出,并使用 in2
属性引用 SourceGraphic
。
默认混合模式为 normal
,并且输入顺序很重要。我将我们的混合操作描述为将半透明红色矩形放在黑色矩形之上。
<svg xmlns="http://www.w3.org/2000/svg">
<filter id="coolEffect">
<feFlood flood-color="red" flood-opacity="0.5" result="flood"/>
<feBlend in="flood" in2="SourceGraphic"/>
</filter>
<rect width="100%" height="100%" filter="url(#coolEffect)"/>
</svg>
<feFlood>
叠放在黑色 <rect>
上,并将两者使用 <feBlend>
混合在一起,我们链接了 <feFlood>
的结果到 <feBlend>
。链接过滤器基元是一个相当频繁的操作,幸运的是,它具有已标准化的有用默认值。在上面的示例中,我们可以省略 <feFlood>
中的 result
属性以及 <feBlend>
中的 in
属性,因为任何后续过滤器都将使用前一个过滤器的结果作为其输入。在本文中,我们将经常使用此快捷方式。
feTurbulence
生成随机图案
使用 <feTurbulence>
有几个属性,这些属性决定它产生的噪声模式。让我们逐个介绍它们。
baseFrequency
这是最重要的属性,因为它是创建图案所必需的。它接受一个或两个数值。指定两个数字分别定义沿 x 轴和 y 轴的频率。如果只提供一个数字,则它定义沿两个轴的频率。这些值的合理区间介于 0.001
和 1
之间,其中低值会导致较大的“特征”,而高值会导致较小的“特征”。x 和 y 频率之间的差异越大,图案的“拉伸”程度就越大。
<svg xmlns="http://www.w3.org/2000/svg">
<filter id="coolEffect">
<feTurbulence baseFrequency="0.001 1"/>
</filter>
<rect width="100%" height="100%" filter="url(#coolEffect)"/>
</svg>
baseFrequency
值:0.01
、0.1
、1
。下面一行:0.01 0.1
、0.1 0.01
、0.001 1
。type
type
属性接受两个值之一:turbulence
(默认值)或 fractalNoise
,这通常是我使用的值。fractalNoise
在红色、绿色、蓝色和 alpha(RGBA)通道上产生相同类型的图案,而 turbulence
在 alpha 通道中的图案与 RGB 中的图案不同。我发现很难描述差异,但比较视觉结果时会更容易看到。
<svg xmlns="http://www.w3.org/2000/svg">
<filter id="coolEffect">
<feTurbulence baseFrequency="0.1" type="fractalNoise"/>
</filter>
<rect width="100%" height="100%" filter="url(#coolEffect)"/>
</svg>
turbulence
类型(左)与 fractalNoise
类型(右)的比较numOctaves
您可能对来自音乐或物理学的八度音阶概念很熟悉。高八度音阶使频率加倍。对于 SVG 中的 <feTurbulence>
,numOctaves
属性定义了在 baseFrequency
上渲染的八度音阶数量。
默认的 numOctaves
值为 1
,这意味着它以基频渲染噪声。任何额外的八度音阶都会使频率加倍,并将幅度减半。此数字越大,其效果就越不明显。此外,更多的八度音阶意味着更多的计算,可能会影响性能。我通常使用 1-5 之间的数值,并且只将其用于细化图案。
<svg xmlns="http://www.w3.org/2000/svg">
<filter id="coolEffect">
<feTurbulence baseFrequency="0.1" type="fractalNoise" numOctaves="2"/>
</filter>
<rect width="100%" height="100%" filter="url(#coolEffect)"/>
</svg>
numOctaves
值比较:1
(左)、2
(中)和 5
(右)seed
seed
属性创建不同的噪声实例,并用作噪声生成器的起始数字,该生成器在幕后产生伪随机数。如果定义了种子值,则会出现不同的噪声实例,但具有相同的属性。它的默认值为 0
,正整数会被解释(尽管 0
和 1
被认为是相同的种子)。浮点数会被截断。
此属性最适合为图案添加独特的风格。例如,可以在访问页面时生成随机种子,这样每个访问者都会看到略微不同的图案。由于一些 技术细节 和 单精度浮点数 的原因,生成随机种子的实际区间是 0
到 9999999
。但这仍然是 1000 万个不同的实例,希望这可以涵盖大多数情况。
<svg xmlns="http://www.w3.org/2000/svg">
<filter id="coolEffect">
<feTurbulence baseFrequency="0.1" type="fractalNoise" numOctaves="2" seed="7329663"/>
</filter>
<rect width="100%" height="100%" filter="url(#coolEffect)"/>
</svg>
seed
值比较:1
(左)、2
(中)和 7329663
(右)stitchTiles
我们可以像在 CSS 中使用 background-repeat: repeat
一样平铺图案!我们所需要的只是 stitchTiles
属性,它接受两个关键字值之一:noStitch
和 stitch
,其中 noStitch
是默认值。stitch
将图案沿两个轴无缝重复。

noStitch
(上)和 stitch
(下)请注意,<feTurbulence>
还会在 Alpha 通道中产生噪声,这意味着图像不是完全不透明的,而是半透明的。
图案库
让我们看看一些用 SVG 滤镜制作的炫酷图案,并了解它们的原理!

星空
这个图案由两个链接的滤镜效果组成,应用于全宽高矩形。<feTurbulence>
是第一个滤镜,负责生成噪声。<feColorMatrix>
是第二个滤镜效果,它逐像素地改变输入图像。我们可以根据一个常数和像素中所有输入通道值来指定每个输出通道值应该是什么。每个通道的公式如下
是输出通道值
是输入通道值
是权重
因此,例如,我们可以为红色通道编写一个仅考虑绿色通道的公式,方法是将 设置为
1
,并将其他权重设置为 0
。我们可以为绿色通道和蓝色通道编写类似的公式,它们分别仅考虑蓝色通道和红色通道。对于 Alpha 通道,我们可以将 (常数)设置为
1
,并将其他权重设置为 0
,以创建一个完全不透明的图像。这四个公式执行色调旋转。
这些公式也可以写成矩阵乘法,这也是 <feColorMatrix>
名称的由来。虽然 <feColorMatrix>
可以不用理解矩阵运算就使用,但我们要记住,我们的 4×5 矩阵是四个公式的 4×5 权重。
是红色通道对红色通道贡献的权重。
是红色通道对绿色通道贡献的权重。
是绿色通道对红色通道贡献的权重。
是绿色通道对绿色通道贡献的权重。
- 为了简洁起见,省略了剩余 16 个权重的描述
上面提到的色调旋转写成这样
需要注意的是,RGBA 值是介于 0 到 1(含)之间的浮点数(而不是像你预期的那样介于 0 到 255 之间的整数)。权重可以是任何浮点数,尽管在计算结束时,任何小于 0 的结果都会被钳制到 0,任何大于 1 的结果都会被钳制到 1。星空图案依赖于这种钳制,因为它的矩阵是

<feColorMatrix>
为 R、G 和 B 通道描述的传递函数。输入始终为 Alpha。我们对 RGB 通道使用相同的公式,这意味着我们正在生成灰度图像。该公式将 Alpha 通道的值乘以 9,然后减去 4。请记住,即使 Alpha 值在 <feTurbulence>
的输出中也会有所不同。大多数结果值将不在 0 到 1 范围内;因此它们将被钳制。因此,我们的图像主要是黑色或白色——黑色是天空,白色是最亮的星星;其余几个介于两者之间的值是暗淡的星星。我们在第四行将 Alpha 通道设置为常数 1
,这意味着图像完全不透明。
松木
这段代码与我们在星空图案中看到的代码差别不大。它只是做了一些噪声生成和颜色矩阵变换。典型的木质图案的特点是,在一个维度上的特征比另一个维度上的特征更长。为了模拟这种效果,我们通过设置 baseFrequency="0.1 0.01"
来创建“拉伸”的噪声。此外,我们还设置了 type="fractalNoise"
。
在 <feColorMatrix>
中,我们只是重新着色了更长的图案。而且,与之前一样,Alpha 通道用作方差的输入。但是,这次我们通过比应用于 Alpha 输入的权重更大的常数权重来偏移 RGB 通道。这确保了图像的所有像素都保持在特定的颜色范围内。找到最佳颜色范围需要稍微调整一下值。

<feColorMatrix>
虽然非常细微,但第二条是渐变。必须了解的是,矩阵默认情况下在线性化的 RGB 颜色空间中运行。例如,紫色 (#800080
) 由值 、
和
表示。乍一看可能很奇怪,但使用线性化 RGB 进行某些变换是有充分理由的。这篇文章很好地解释了 为什么,这篇文章非常适合深入研究 如何。
归根结底,这意味着我们需要将我们通常的 #RRGGBB
值转换为线性化的 RGB 空间。我使用 这个颜色空间工具来执行此操作。在第一行输入 RGB 值,然后使用第三行中的值。在紫色示例中,我们会在第一行输入 、
、
,然后点击
sRGB8
按钮,在第三行中获取线性化的值。
如果我们正确选择值并正确执行转换,我们最终会得到类似松木颜色的东西。
斑点狗斑点
这个示例通过引入 <feComponentTransfer>
滤镜来增加一些难度。这种效果使我们能够为每个颜色通道(也称为颜色分量)定义自定义传递函数。在本演示中,我们只为 Alpha 通道定义一个自定义传递函数,并将其他通道保留为未定义(这意味着将应用恒等函数)。我们使用 discrete
类型来设置一个阶梯函数。阶梯由 tableValues
属性中的空格分隔的数值描述。tableValues
控制阶梯的数量和每个阶梯的高度。
让我们考虑一些我们对 tableValues
值进行调整的示例。我们的目标是从噪声中创建一个“有斑点”的图案。以下是我们所知道的
tableValues="1"
将每个值传递到1
。tableValues="0"
将每个值传递到0
。tableValues="0 1"
将小于0.5
的值传递到0
,并将介于0.5
到1
之间的值传递到1
。tableValues="1 0"
将小于0.5
的值传递到1
,并将介于0.5
到1
之间的值传递到0
。

值得对这个属性进行一些调整,以便更好地了解它的功能和我们噪声的质量。经过一些实验,我们得到了 tableValues="0 1 0"
,它将中等值转换为 1,并将其他值转换为 0。
本示例中的最后一个滤镜效果是 <feColorMatrix>
,它用于重新着色图案。具体来说,它使透明部分 (Alpha = 0
) 为黑色,不透明部分 (Alpha = 1
) 为白色。
最后,我们使用 <feTurbulence>
对图案进行微调。设置 numOctaves="2"
有助于使斑点更加“参差不齐”,并减少细长的斑点。baseFrequency="0.06"
基本上设置了一个缩放级别,我认为这最适合这种图案。
ERDL 迷彩
ERDL 图案是为掩盖军事人员、设备和设施而开发的。近几十年来,它被应用于服装。该图案由四种颜色组成:深绿色作为背景色,棕色作为形状颜色,黄绿色作为色块,黑色散布在其中形成一些小斑点。
与我们看到的斑点狗斑点示例类似,我们将 <feComponentTransfer>
链接到噪声——尽管这次 discrete
函数是为 RGB 通道定义的。
想象一下 RGBA 通道是图像的四层。我们通过定义单步函数在三个层上创建斑点。每个函数的步骤从不同的位置开始,在每个层上产生不同数量的斑点。红色、绿色和蓝色的切割分别为 66.67%、60% 和 50%。
<feFuncR type="discrete" tableValues="0 0 0 0 1 1"/>
<feFuncG type="discrete" tableValues="0 0 0 1 1"/>
<feFuncB type="discrete" tableValues="0 1"/>
此时,各层上的斑点在某些地方重叠,导致我们不想要的颜色。这些其他颜色使将我们的图案转换为 ERDL 迷彩变得更加困难,因此让我们消除它们。
- 对于红色,我们定义恒等函数。
- 对于绿色,我们的起点是恒等函数,但我们从其中减去红色。
- 对于蓝色,我们的起点也是恒等函数,但我们从其中减去红色和绿色。
这些规则意味着红色保留在红色、绿色和/或蓝色曾经重叠的地方;绿色保留在绿色和蓝色重叠的地方。生成的图像包含四种类型的像素:红色、绿色、蓝色或黑色。
第二个链接的 <feColorMatrix>
重新着色所有内容。
- 黑色部分通过常数权重变为深绿色。
- 红色部分通过否定常数权重变为黑色。
- 绿色部分通过来自绿色通道的附加权重变为那种黄绿色。
- 蓝色部分通过来自蓝色通道的附加权重变为棕色。
岛屿群
这个例子基本上是一个 高度图。使用 <feTurbulence>
生成逼真的高度图非常容易——我们只需要关注一个颜色通道,我们已经有了它。让我们关注红色通道。借助 <feColorMatrix>
,我们将彩色噪声转换为灰度高度图,方法是将绿色和蓝色通道的值覆盖为红色通道的值。
现在,我们可以依赖每个像素的每个颜色通道的相同值。这使得我们可以使用 <feComponentTransfer>
轻松地逐级重新着色我们的图像,不过这次我们使用的是 table
类型的函数。table
类似于 discrete
,但每个步骤都是到下一步的斜坡。这允许在级别之间进行更平滑的过渡。

<feComponentTransfer>
中定义的 RGB 传输函数tableValues
的数量决定了传输函数中有多少个斜坡。我们必须考虑两件事才能找到最佳的斜坡数量。一个是图像中强度分布。不同的强度分布不均匀,尽管斜坡宽度始终相等。另一件要考虑的事情是我们希望看到的级别数量。当然,我们还需要记住我们是在线性化 RGB 空间中。我们可以深入研究所有这些的数学问题,但只需玩一玩,感受出正确的数值就容易得多。

<feComponentTransfer>
将灰度映射到颜色我们从最低强度到中间某个地方使用深蓝色和水绿色值来表示水。然后,我们使用几种黄色的色调来表示沙质部分。最后,绿色和深绿色在最高强度下创造出森林。
我们在这些示例中都没有看到 seed
属性,但我建议您尝试通过添加它来使用它。想一个 1 到 1000 万之间的随机数,然后在 <feTurbulence>
中将该数字用作 seed
属性值,例如 <feTurbulence seed="3761593"
... >
现在您有了自己独特的图案变体!
生产使用
到目前为止,我们所做的就是看看一堆很酷的 SVG 图案以及它们是如何制作的。我们看到的大部分内容都是很好的概念证明,但真正的好处是能够以负责任的方式在生产中使用这些图案。
我认为,有三种基本路径可供选择。
方法 1:在 CSS 或 HTML 中使用内联数据 URI
我最喜欢使用 SVG 的方法是将它们内联,前提是它们足够小。对我来说,“足够小”意味着几 KB 或更少,但这确实取决于具体的用例。内联的好处是,该图像保证在您的 CSS 或 HTML 文件中存在,这意味着无需等待它下载。
缺点是必须对标记进行编码。幸运的是,有一些专门为此目的而制作的出色工具。 Yoksel 的 SVG URL 编码器 就是这样一种工具,它提供了一个复制粘贴的方式 UI 来生成代码。如果您正在寻找一种编程方法——例如作为构建过程的一部分——我建议您研究 mini-svg-data-uri。我个人没有使用它,但它似乎很受欢迎。
无论采用哪种方法,编码后的数据 URI 都会直接放在您的 CSS 或 HTML(甚至 JavaScript)中。CSS 更好,因为它可重用,但 HTML 的交付时间最短。如果您正在使用某种服务器端渲染技术,您还可以将一个随机的 seed
值滑入数据 URI 内的 <feTurbulence>
中,以便为每个用户显示唯一的变体。
以下是星空示例用作背景图像,并在 CSS 中使用其内联数据 URI
.your-selector {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='filter'%3E%3CfeTurbulence baseFrequency='0.2'/%3E%3CfeColorMatrix values='0 0 0 9 -4 0 0 0 9 -4 0 0 0 9 -4 0 0 0 0 1'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23filter)'/%3E%3C/svg%3E%0A");
}
这就是它在 HTML 中用作 <img>
的样子
<img src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='filter'%3E%3CfeTurbulence baseFrequency='0.2'/%3E%3CfeColorMatrix values='0 0 0 9 -4 0 0 0 9 -4 0 0 0 9 -4 0 0 0 0 1'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23filter)'/%3E%3C/svg%3E%0A"/>
方法 2:在 HTML 中使用 SVG 标记
我们可以简单地将 SVG 代码本身放入 HTML 中。以下是星空的标记,可以将其放入 HTML 文件中
<div>
<svg xmlns="http://www.w3.org/2000/svg">
<filter id="filter">
<feTurbulence baseFrequency="0.2"/>
<feColorMatrix values="0 0 0 9 -4
0 0 0 9 -4
0 0 0 9 -4
0 0 0 0 1"/>
</filter>
<rect width="100%" height="100%" filter="url(#filter)"/>
</svg>
</div>
这是一个非常简单的方法,但它有一个巨大的缺点——尤其是对于我们看到的示例。
那个缺点是什么?ID 冲突。
请注意,SVG 标记使用 #filter
ID。想象一下在同一个 HTML 文件中添加其他示例。如果它们也使用 #filter
ID,那么这将导致 ID 冲突,第一个实例将覆盖其他实例。
我个人只会在手工制作的页面中使用这种技术,其中范围足够小,可以了解所有包含的 SVG 及其 ID。可以在构建过程中生成唯一 ID,但这又是另一个故事。
方法 3:使用独立的 SVG
这是使用 SVG 的“经典”方法。实际上,它就像使用任何其他图像文件一样。将 SVG 文件放到服务器上,然后在 HTML <img>
标签中或 CSS 中的某个地方(如背景图像)使用 URL。
因此,回到星空示例。以下是 SVG 文件的内容,但这次文件本身放在服务器上。
<svg xmlns="http://www.w3.org/2000/svg">
<filter id="filter">
<feTurbulence baseFrequency="0.2"/>
<feColorMatrix values="0 0 0 9 -4
0 0 0 9 -4
0 0 0 9 -4
0 0 0 0 1"/>
</filter>
<rect width="100%" height="100%" filter="url(#filter)"/>
</svg>
现在我们可以在 HTML 中使用它,例如作为图像
<img src="https://example.com/starry-sky.svg"/>
在 CSS 中使用 URL(如背景图像)也同样方便
.your-selector {
background-image: url("https://example.com/starry-sky.svg");
}
考虑到当今的 HTTP2 支持以及与光栅图像相比,SVG 文件的大小相对较小,这根本不是一个糟糕的解决方案。或者,该文件可以放置在 CDN 上,以实现更好的交付。将 SVG 作为单独的文件的好处是,它们可以在多层中进行缓存。
注意事项
虽然我真的很喜欢制作这些小图案,但我也要承认它们的一些缺陷。
最重要的缺陷是,它们很快就会创建一个计算密集的“怪物”过滤器链。单个过滤器效果非常类似于照片编辑软件中的单次操作。我们基本上是用代码“photoshop”,并且每次浏览器显示这种 SVG 时,它都必须渲染每个操作。因此,如果您最终拥有一个很长的过滤器链,那么最好将结果捕获并作为 JPEG 或 PNG 提供,以节省用户的 CPU 时间。Taylor Hunt 的“提高 SVG 运行时性能”包含许多其他关于如何充分利用 SVG 性能的出色技巧。
其次,我们必须讨论浏览器支持。一般来说,SVG 支持良好,尤其是在现代浏览器中。但是,我在使用这些图案时遇到了 Safari 的一个问题。我尝试使用 <radialGradient>
与 spreadMethod="repeat"
创建一个重复的圆形图案。它在 Chrome 和 Firefox 中运行良好,但 Safari 不喜欢它。Safari 显示径向渐变,就好像它的 spreadMethod
设置为 pad
一样。您可以在 MDN 的文档中验证它。
您可能会发现不同的浏览器以不同的方式渲染相同的 SVG。考虑到渲染 SVG 的所有复杂性,几乎不可能实现完美的一致性。也就是说,我只发现浏览器之间有一个值得一提的差异,那就是切换到“全屏”视图时。当 Firefox 进入全屏模式时,它不会在视窗的扩展部分渲染 SVG。Chrome 和 Safari 很好。您可以通过在选择的浏览器中打开此笔,然后进入全屏模式来验证它。这是一个罕见的边缘情况,可能可以通过一些 JavaScript 和全屏 API 来解决。
谢谢!
呼,终于结束了!我们不仅看到了很多很酷的图案,还学到了很多关于 SVG 中的<feTurbulence>
的知识,包括它接受的各种过滤器以及如何以有趣的方式操作它们。
像网络上的大多数东西一样,我们一起讨论的概念也存在缺点和潜在弊端,但希望你现在对可能发生的事情以及需要注意的事项有了一些了解。你有能力在 SVG 中创建一些很棒的图案!
感谢您介绍这些概念,它们远非直观。幸运的是,现在网络上有许多例子,可以普及这种方法,再加上您这样的深入文章。