我最近创建了一个砖墙图案,作为我的#PetitePatterns系列的一部分,这是一个挑战,我在560字节(或大约两篇推文的大小)内用SVG创建有机外观的图案或纹理。为了适应这种限制,我经历了一个旅程,它让我学会了一些优化SVG图案的激进方法,使它们在不影响整体图像质量的情况下尽可能少地包含代码。
我想通过这个过程来告诉你,我们是如何将一个从197个字节开始的SVG模式一直减少到只有44个字节的--减少了高达77.7%!这就是我的想法。
SVG模式
所谓的 "running bond "砖块模式。这是最常见的砖块图案,你肯定也见过这种图案:每行砖块的长度偏移一半,形成一个重复的交错图案。这种排列方式相当简单,使得SVG的<pattern> 元素非常适合在代码中再现它。
SVG<pattern> 元素使用了一个预定义的图形对象,它可以沿着水平和垂直轴以固定的间隔进行复制(或 "平铺")。从本质上讲,我们定义了一个矩形的瓦片图案,它被重复用来绘制填充区域。
首先,让我们设置砖块的尺寸和每个砖块之间的间隙。为了简单起见,让我们使用干净的圆形数字:砖头的宽度为100 ,高度为30 ,砖头之间的水平和垂直间隙为10 。
接下来,我们必须确定我们的 "基础 "瓦片。我所说的 "瓦片 "是指图案瓦片,而不是物理瓦片,不要与砖块混淆。让我们用上面图片的高亮部分作为我们的图案砖:第一行有两块整砖,第二行的两块半砖之间夹着一块整砖。请注意缝隙是如何被包括在内的,因为这些缝隙需要被包括在重复的图案瓦片中。
当使用<pattern> ,我们必须定义图案的width 和height ,它们对应于底砖的宽度和高度。为了得到这些尺寸,我们需要做一点数学运算。
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模式很有效,因为我们不必在代码中明确声明这些。
下面代码中的每一行都定义了一个矩形。width 和height 总是被设置,而x 和y 的位置只有在一个矩形偏离0 的位置时才被设置。
<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 ,值分别为55 和165 。
元素重复使用,(-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> 相似,用x 和y 属性作为偏移量。由于现在我们改用<use> ,每块砖都是全宽的(记住,我们明确地将图案瓦片第二行的砖减半),我们必须在第二行使用负的x ,然后确保最后一块砖从瓦片上溢出,以实现砖与砖之间的无缝连接。不过这些都是可以的,因为任何落在图案瓦片之外的东西都会被自动切断。
你能发现一些可以更有效编写的重复字符串吗?接下来让我们来研究一下这些。
重写到路径(-54B,共100B)
<path> 可能是SVG中最强大的元素。你可以画出几乎所有在其 属性中带有 "命令 "的形状。有20个命令可用,但我们只需要最简单的矩形命令。d
这就是我的落脚点。
<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)
我们的图案瓦片不是有重复的部分吗?很明显,在第一行,一整块砖都是重复的,但第二行呢?这有点难看,但如果我们把中间的砖头切成两半,就很明显了。
好吧,中间的砖头并没有完全切成两半。有一个轻微的偏移,因为我们还必须考虑到缝隙的问题。不管怎么说,我们刚刚找到了一个更简单的底层瓷砖图案,这意味着更少的字节!这也意味着我们必须将其减半。width 这也意味着我们必须将我们的<pattern> 元素的数量减半,从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中,这是不正确的。让我引用规范本身。
填充操作是通过执行填充操作来填充开放的子路径的,就好像在路径上添加了一个额外的 "closeepath "命令来连接子路径的最后一个点和子路径的第一个点一样。
这意味着我们可以省略关闭路径命令(z),因为子路径在填充时被视为自动关闭。
关于路径命令的另一个有用的东西是,它们有大写和小写之分。小写字母表示使用相对坐标;大写字母表示使用绝对坐标。
对于H 和V 命令来说,这就有点棘手了,因为它们只包括一个坐标。以下是我对这两个命令的描述。
- **
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个字节。我们的一个约束条件是:。
一个路径数据段(如果有的话)必须以 "moveto "命令开始。
但这也没关系。无论如何,第一条是最短的。如果我们交换行,我们可以得出一个路径定义,我们只需要在砖块之间进行水平或垂直的移动。如果我们在这里使用h 和v 命令而不是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模式。我很想看看我们是否能通过群众的智慧找到真正的全局最小值!