将 SVG 图案优化到最小尺寸

Avatar of Bence Szabó
Bence Szabó

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 200 美元的免费额度!

我最近在我的 #PetitePatterns 系列中创建了一个砖墙图案,这是一个挑战,我需要在 560 字节(或大约两条推文的大小)内用 SVG 创建自然外观的图案或纹理。为了满足这个限制,我经历了一段旅程,这段旅程教会了我一些优化 SVG 图案的激进方法,以便它们包含尽可能少的代码,而不会影响整体图像质量。

我想带您了解这个过程,并向您展示如何将一个最初为 197 字节的 SVG 图案一直缩小到只有 44 字节——减少了惊人的 77.7%!

SVG 图案

这被称为“纵向交错”砖图案。它是最常见的砖图案,您肯定以前见过:每一排砖块都相对于前一排偏移半个砖块的长度,形成重复的交错图案。这种排列非常简单,使 SVG 的<pattern> 元素非常适合在代码中重现它。

SVG 的<pattern> 元素使用预定义的图形对象,该对象可以在水平和垂直轴上的固定间隔处进行复制(或“平铺”)。从本质上讲,我们定义了一个矩形平铺图案,它会重复绘制填充区域。

首先,让我们设置砖块的尺寸以及每个砖块之间的间隙。为了简单起见,让我们使用整洁的圆形数字:砖块的宽度为100,高度为30,它们之间的水平和垂直间隙为10

Showing a highlighted portion of a brick wall pattern, which is the example we are using for optimizing SVG patterns.

接下来,我们必须识别我们的“基本”平铺。而我所说的“平铺”是指图案平铺,而不是物理平铺,不要与砖块混淆。让我们使用上面图像中突出显示的部分作为我们的图案平铺:第一行中的两个完整砖块,以及第二行中一个夹在两个半砖块之间的完整砖块。请注意间隙是如何以及在何处包含的,因为这些需要包含在重复的图案平铺中。

使用<pattern>时,我们必须定义图案的widthheight,它们分别对应于基本平铺的宽度和高度。要获取尺寸,我们需要进行一些计算

Tile Width  = 2(Brick Width) + 2(Gap) = 2(100) + 2(10) = 220
Tile Height = 2(Bright Height) + 2(Gap) = 2(30) + 2(10) = 80

好的,所以我们的图案平铺为220✕80。我们还必须设置patternUnits属性,其中值userSpaceOnUse本质上表示像素。最后,为图案添加id是必要的,以便在使用它绘制其他元素时可以引用它。

<pattern id="p" width="220" height="80" patternUnits="userSpaceOnUse">
  <!-- pattern content here -->
</pattern>

现在我们已经确定了平铺尺寸,挑战在于以尽可能少的字节数渲染图形的方式创建平铺的代码。这是我们希望在最后得到的结果

最终纵向交错图案的砖块(黑色)和间隙(白色)

初始标记(197 字节)

我想到的重现此图案最简单、最声明式的方法是绘制五个矩形。默认情况下,SVG 元素的fill为黑色,stroke为透明。这对于优化 SVG 图案非常有效,因为我们不必在代码中显式声明它们。

下面代码中的每一行都定义了一个矩形。始终设置widthheight,并且仅当矩形相对于0位置偏移时才设置xy位置。

<rect width="100" height="30"/>
<rect x="110" width="100" height="30"/>
<rect y="40" width="45" height="30"/>
<rect x="55" y="40" width="100" height="30"/>
<rect x="165" y="40" width="55" height="30"/>

平铺的顶行包含两个全宽砖块,第二个砖块定位到x="110",允许在砖块之前有10像素的间隙。类似地,之后也有10像素的间隙,因为砖块在水平轴上的210像素处结束(110 + 100 = 210),即使<pattern>宽度为220像素。我们需要那一点额外的空间;否则第二个砖块将与相邻平铺中的第一个砖块合并。

第二(底部)行中的砖块是偏移的,因此该行包含两个半砖块和一个完整砖块。在这种情况下,我们希望半宽砖块合并,因此在开头或结尾处没有间隙,允许它们与相邻图案平铺中的砖块无缝连接。偏移这些砖块时,我们还必须包含一半的间隙,因此x值分别为55165

元素重用(-43B,共 154B)

如此明确地定义每个砖块似乎效率低下。有没有办法通过重用形状来优化 SVG 图案?

我认为 SVG 有一个<use>元素并不是广为人知。您可以使用它引用另一个元素,并在使用<use>的任何位置呈现该引用的元素。这可以节省相当多的字节,因为我们可以省略指定每个砖块的宽度和高度,除了第一个之外。

也就是说,<use>确实需要付出一点代价。也就是说,我们必须为我们要重用的元素添加一个id

<rect id="b" width="100" height="30"/>
<use href="#b" x="110"/>
<use href="#b" x="-55" y="40"/>
<use href="#b" x="55" y="40"/>
<use href="#b" x="165" y="40"/>

最短的id可能是一个字符,所以我为砖块选择“b”。<use>元素可以像<rect>一样定位,使用xy属性作为偏移量。由于每个砖块现在都是全宽(记住,我们在图案平铺的第二行中明确地将砖块减半),因此我们必须在第二行中使用负x值,然后确保最后一个砖块超出平铺,以便在砖块之间实现无缝连接。这些是可以的,因为任何超出图案平铺范围的内容都会自动被裁剪掉。

您能发现一些可以更有效地编写的重复字符串吗?接下来让我们处理这些。

重写为路径(-54B,共 100B)

<path>可能是 SVG 中功能最强大的元素。您可以使用其d属性中的“命令”绘制几乎任何形状。有 20 个可用命令,但对于矩形,我们只需要最简单的命令。

这是我的最终结果

<path d="M0 0h100v30h-100z
         M110 0h100v30h-100
         M0 40h45v30h-45z
         M55 40h100v30h-100z
         M165 40h55v30h-55z"/>

我知道,超级奇怪的数字和字母!它们当然都有意义。以下是此特定情况下的情况

  • M{x} {y}根据坐标移动到一个点。
  • z关闭当前段。
  • h{x}从当前点绘制一条水平线,长度为x,方向由x的符号定义。小写x表示相对坐标。
  • v{y}从当前点绘制一条垂直线,长度为y,方向由y的符号定义。小写y表示相对坐标。

此标记比上一个标记简洁得多(换行符和缩进空格仅用于可读性)。而且,嘿,我们已经设法减少了一半的初始大小,达到 100 字节。但是,有些事情让我觉得这可以更小……

平铺修订(-38B,共 62B)

我们的图案平铺是否有重复的部分?很明显,在第一行中重复了一个完整的砖块,但第二行呢?有点难以看出,但如果我们将中间的砖块切成两半,就会变得很明显。

The left half preceding the red line is the same as the right side.

好吧,中间的砖块并没有完全切成两半。存在轻微的偏移,因为我们还必须考虑间隙。无论如何,我们刚刚找到了一个更简单的基本平铺图案,这意味着更少的字节!这也意味着我们必须将<pattern>元素的width从 220 减半到 110。

<pattern id="p" width="110" height="80" patternUnits="userSpaceOnUse">
  <!-- pattern content here -->
</pattern>

现在让我们看看如何使用<path>绘制简化的平铺

<path d="M0 0h100v30h-100z
         M0 40h45v30h-45z
         M55 40h55v30h-55z"/>

尺寸减小到 62 字节,这已经不到原始尺寸的三分之一了!但是,当我们还可以做更多事情时,为什么要停在这里呢!

缩短路径命令(-9B,共 53B)

值得更深入地了解一下<path>元素,因为它提供了更多优化 SVG 图案的提示。我在使用<path>时曾有一个误解,那就是关于fill属性是如何工作的。小时候玩了很多 MS Paint,我了解到任何我想用纯色填充的形状都必须是封闭的,即没有开放点。否则,油漆会从形状中泄漏并溢出到所有地方。

然而,在 SVG 中,情况并非如此。让我引用规范本身的内容。

填充操作通过执行填充操作来填充开放的子路径,就好像一个额外的“闭合路径”命令被添加到路径中以连接子路径的最后一个点和子路径的第一个点。

这意味着我们可以省略闭合路径命令 (z),因为在填充时会自动将子路径视为已关闭。

关于路径命令,另一个有用的知识是它们有大小写两种形式。小写字母表示使用相对坐标;大写字母表示使用绝对坐标。

对于HV命令来说,情况稍微复杂一些,因为它们只包含一个坐标。以下是我的描述方式

  • H{x}: 从当前点绘制一条水平线到坐标x
  • V{y}: 从当前点绘制一条垂直线到坐标y

当我们绘制图案块中的第一块砖时,我们从(0,0)坐标开始。然后我们绘制一条水平线到(100,0)和一条垂直线到(100,30),最后,绘制一条水平线到(0,30)。我们在最后一行使用了h-100命令,但它等同于H0,后者是两个字节而不是五个字节。我们可以替换两个类似的出现,并将我们的<path>代码缩减到以下内容

<path d="M0 0h100v30H0
         M0 40h45v30H0
         M55 40h55v30H55"/>

又减少了 9 个字节——我们还能缩小多少?

桥接 (-5B,共 48B)

阻碍我们获得完全优化的 SVG 图案的最长命令是“移动到”命令,它们分别占用 4、5 和 6 个字节。我们有一个限制是

路径数据段(如果存在)必须以“移动到”命令开头。

但这没关系。第一个是最短的。如果我们交换行,我们可以想出一个路径定义,其中我们只需要在砖块之间水平或垂直移动。如果我们可以在那里使用hv命令而不是M呢?

路径从左上角的红点开始。红色是支持的路径命令,带箭头,黑色是箭头指向的坐标。

上图显示了如何用一条路径绘制这三个形状。请注意,我们利用了fill操作自动关闭(110,0)(0,0)之间开放部分的事实。通过这种重新排列,我们还将间隙移动到了第二行全宽砖块的左侧。以下是代码的外观,仍然每行一块砖

<path d="M0 0v30h50V0
         h10v30h50
         v10H10v30h100V0"/>

当然,我们现在已经找到了绝对最小的解决方案,因为我们已经减少到 48 个字节了,对吧?!好吧……

数字修剪 (-4B,共 44B)

如果您对尺寸有一点灵活性,那么我们还可以通过另一种方法优化 SVG 图案。我们一直在使用 100 像素的砖宽,但这需要三个字节。将其更改为 90 意味着每当我们需要写入它时都会减少一个字节。同样,我们使用了 10 像素的间隙——但如果我们将其更改为 8,则每次出现都会节省一个字节。

<path d="M0 0v30h45V0
         h8v30h45
         v8H8v30h90V0"/>

当然,这也意味着我们必须相应地调整图案尺寸。以下是最终优化的 SVG 图案代码

<pattern id="p" width="98" height="76" patternUnits="userSpaceOnUse">
  <path d="M0 0v30h45V0h8v30h45v8H8v30h90V0"/>
</pattern>

上面代码片段中的第二行(不包括缩进)是44 个字节。我们从 197 个字节经过六次迭代才得到这个结果。这是一个巨大的77.7% 的尺寸缩减

不过,我想知道……这真的是可能的最小尺寸吗?我们是否已经查看了所有可能的 SVG 图案优化方法?

我邀请您尝试进一步缩小此代码,甚至尝试使用其他方法来优化 SVG 图案。我很想知道我们是否可以通过集思广益找到真正的全局最小值!

更多关于创建和优化 SVG 图案的信息

如果您有兴趣了解有关创建和优化 SVG 图案的更多信息,请阅读我关于使用 SVG 滤镜创建图案的文章。或者,如果您想查看 60 多个图案的图库,您可以查看PetitePatterns CodePen 集合。最后,欢迎您观看我在 YouTube 上的教程,以帮助您更深入地了解 SVG 图案。