我最近偶然发现了 Vasilis van Gemert 的 制造者地图集。它有趣而古怪的外观使我查看了其内部结构,这绝对值得!我发现它实际上是利用了过去几年中许多文章和演讲都讨论过的非常酷的功能构建的,但不知何故在实际应用中却很少使用——比如 CSS Grid、自定义属性、混合模式,甚至 SVG。
SVG 用于创建看起来像是用霓虹胶带粘贴到页面上的不规则图像。本文将解释如何以最简单的方式重新创建它,而无需离开浏览器。让我们开始吧!
我们首先选择一张开始使用的图片,例如,这只令人惊叹的雪豹

接下来,我们获取一个可以容纳猫的粗略多边形。为此,我们使用 Bennett Feely 的 Clippy。我们实际上不会使用 CSS clip-path
,因为它还没有跨浏览器兼容(但如果您希望它兼容,请 投票支持——无需登录),它只是为了快速获取多边形点的数值,而无需使用带有大量按钮和选项的实际图像编辑器,这些按钮和选项会导致您在开始之前就放弃。
我们为图像设置自定义 URL 并设置自定义尺寸。Clippy 根据视口大小限制这些尺寸,但对于我们来说,在这种情况下,图像的实际尺寸并不重要(尤其是在输出将只是 %
值的情况下),只有纵横比很重要,在我们的猫图片中,纵横比为 2:3
。

我们开启“显示 clip-path
外部区域”选项,以便更容易看到我们将要做什么。

clip-path
外部区域”选项。然后我们选择为裁剪路径使用自定义多边形,选择所有点,关闭路径,然后可能调整一些点的位置。
这为我们生成了 CSS clip-path
代码。我们只复制点的列表(作为 %
值),打开控制台并将此点列表粘贴为 JavaScript 字符串
let coords = '69% 89%, 84% 89%, 91% 54%, 79% 22%, 56% 14%, 45% 16%, 28% 0, 8% 0, 8% 10%, 33% 33%, 33% 70%, 47% 100%, 73% 100%';
我们去掉 %
字符并分割字符串
coords = coords.replace(/%/g, '').split(', ').map(c => c.split(' '));
然后我们设置图像的尺寸
let dims = [736, 1103];
之后,我们将获得的坐标缩放至图像的尺寸。我们还会对获得的值进行四舍五入,因为我们确定不需要小数来粗略地用多边形近似表示这么大的图像中的猫。
coords = coords.map(c => c.map((c, i) => Math.round(.01*dims[i]*c)));
最后,我们将它转换为可以从开发者工具复制的形式
`[${coords.map(c => `[${c.join(', ')}]`).join(', ')}]`;

现在我们继续使用 Pug 生成 SVG。在这里,我们使用我们在上一步获得的坐标数组
- var coords = [[508, 982], [618, 982], [670, 596], [581, 243], [412, 154], [331, 176], [206, 0], [59, 0], [59, 110], [243, 364], [243, 772], [346, 1103], [537, 1103]];
- var w = 736, h = 1103;
svg(viewBox=[0, 0, w, h].join(' '))
clipPath#cp
polygon(points=coords.join(' '))
image(xlink:href='snow_derpard.jpg'
width=w height=h
clip-path='url(#cp)')
这给了我们我们一直在寻找的不规则形状的图像
查看 thebabydino (@thebabydino) 在 CodePen 上的 Pen。
现在让我们继续讨论胶带的部分。为了生成它们,我们使用相同的坐标数组。在此步骤执行任何其他操作之前,我们读取它的长度,以便我们可以遍历它
-// same as before
- var n = coords.length;
svg(viewBox=[0, 0, w, h].join(' '))
-// same as before
- for(var i = 0; i < n; i++) {
- }
接下来,在这个循环中,我们进行一个随机测试来决定从当前点到下一点的胶带条是否存在
- for(var i = 0; i < n; i++) {
- if(Math.random() > .5) {
path(d=`M${coords[i]} ${coords[(i + 1)%n]}`)
- }
- }
乍一看,这 似乎什么也没做。
但是,这是因为默认的 stroke
为 none
。使此 stroke
可见(通过将其设置为具有随机生成的色相的 hsl()
值)并加粗即可显示我们的胶带
stroke: hsl(random(360), 90%, 60%);
stroke-width: 5%;
mix-blend-mode: multiply
我们还在其上设置了 mix-blend-mode: multiply
,以便重叠更加明显。
查看 thebabydino (@thebabydino) 在 CodePen 上的 Pen。
看起来不错,但我们这里仍然有一些问题。
第一个也是最明显的问题是它不是跨浏览器的。mix-blend-mode
在 Edge 中不起作用(如果您希望它起作用,请不要忘记 投票支持)。我们可以获得足够接近的效果的方法是,仅对 Edge 使 stroke
半透明。
我最初的想法是以一种目前仅在 Edge 中受支持的方式来做到这一点:使用一个结果不是整数的 calc()
值作为 RGB 分量。问题是我们有一个 hsl()
值,而不是 rgb()
值。但由于我们使用的是 Sass,因此我们可以 提取 RGB 分量
$c: hsl(random(360), 90%, 60%);
stroke: $c;
stroke: rgba(calc(#{red($c)} - .5), green($c), blue($c), .5)
最后一个规则是在 Edge 中应用的规则,但在 Chrome 中由于 calc()
的结果而被丢弃,在 Firefox 中也由于使用了 calc()
而被丢弃,因此我们 以这种方式获得了我们想要的结果。

stroke
规则无效。但是,如果其他浏览器赶上 Edge 的步伐,这种情况将不再存在。
因此,一个更 面向未来的解决方案 是使用 @supports
path {
$c: hsl(random(360), 90%, 60%);
stroke: rgba($c, .5);
@supports (mix-blend-mode: multiply) { stroke: $c }
}
第二个问题是我们希望胶带条超出其端点稍微扩展一点。幸运的是,这个问题有一个简单的解决方法:将 stroke-linecap
设置为 square
。这实际上使我们的条带在每个端点的两端超出 stroke-width
的一半。
查看 thebabydino (@thebabydino) 在 CodePen 上的 Pen。
最后一个问题是我们的胶带条在 SVG 的边缘被截断。即使我们将 overflow
属性设置为 SVG 上的 visible
,包含 SVG 的容器也可能会将其截断,或者紧随其后的元素可能会与之重叠。
因此,我们可以尝试做的是在 image
周围增加 viewBox
空间,增加的量我们称之为 p
,这个量足以容纳我们的胶带条。
-// same as before
- var w1 = w + 2*p, h1 = h + 2*p;
svg(viewBox=[-p, -p, w1, h1].join(' '))
-// same as before
这里的问题是……p
的值是多少?
嗯,为了获得该值,我们需要考虑到 stroke-width
是一个 %
值。在 SVG 中,类似于 stroke-width
的 %
值是相对于 SVG 区域的对角线计算的。在我们的例子中,这个 SVG 区域是一个宽度为 w
、高度为 h
的矩形。如果我们画出这个矩形对角线,我们会看到我们可以使用勾股定理在黄色高亮三角形中计算它。
viewBox
的宽度和高度。所以我们的对角线是
- var d = Math.sqrt(w*w + h*h);
从这里,我们可以将 stroke-width
计算为对角线 (d
) 的 5%
。这等效于将对角线 (d
) 乘以 .05
因子
- var f = .05, sw = f*d;
请注意,这是从 %
值 (5%
) 变为用户单位 (.05*d
) 的值。这将非常方便,因为通过增加 viewBox
的尺寸,我们也会增加对角线,因此也会增加对角线的 5%
。
任何 path
的 stroke
一半在路径线内,一半在路径线外。但是,我们需要将 viewBox
空间增加超过 stroke-width
的一半。我们还需要考虑到 stroke-linecap
在 path
的端点之外扩展了 stroke-width
的一半
stroke-width
和 stroke-linecap: square
的效果。现在让我们考虑裁剪多边形的一个点正好位于原始 image
边缘的情况。为了简化问题,我们只考虑具有一个端点在此点上的多边形边之一(另一个边的处理方式完全相同)。
我们以沿着多边形边的一个条带为例,该条带的一个端点 E 位于原始图像(以及 SVG)的顶部边缘。

我们想知道当使用 stroke
创建此条带并且 stroke-linecap
设置为 square
时,此条带可以在图像顶部边缘之外扩展多少。这取决于与顶部边缘形成的角度,我们感兴趣的是找到顶部边界之外需要多少额外的空间,以便我们的条带的任何部分都不会被溢出裁剪掉。
为了更好地理解这一点,下面的交互式演示允许我们旋转条带并了解创建此条带的 stroke
(包括 square
线帽)的外角可以扩展到多远
查看 thebabydino (@thebabydino) 在 CodePen 上的 Pen。
如上图所示,通过跟踪 stroke
(包括 stroke-linecap
)的外角的位置,边界之外需要的最大额外空间是当端点在边缘 E 上到 stroke
(包括线帽)在该端点处的外部角之间的线段垂直时,此空间等于线段的长度。
鉴于 stroke 在切线方向和法线方向上都超出端点 stroke-width
的一半,因此该线段的长度是直角等腰三角形的斜边,其直角边分别等于 stroke-width
的一半
stroke-width
的一半。在这个三角形中使用勾股定理,我们有
- var hw = .5*sw;
- var p = Math.sqrt(hw*hw + hw*hw) = hw*Math.sqrt(2);
综合起来,我们的 Pug 代码变为
/* same coordinates and initial dimensions as before */
- var f = .05, d = Math.sqrt(w*w + h*h);
- var sw = f*d, hw = .5*sw;
- var p = +(hw*Math.sqrt(2)).toFixed(2);
- var w1 = w + 2*p, h1 = h + 2*p;
svg(viewBox=[-p, -p, w1, h1].join(' ')
style=`--sw: ${+sw.toFixed(2)}px`)
/* same as before */
而在 CSS 中,我们需要调整胶带条的 stroke-width
stroke-width: var(--sw);
请注意,我们不能使 --sw
为无单位值,然后将 stroke-width
设置为 calc(var(--sw)*1px)
——虽然理论上这应该可以工作,但实际上 Firefox 和 Edge 尚未支持对 stroke-*
属性使用 calc()
值。
最终结果可以在以下 Pen 中看到
查看 thebabydino (@thebabydino) 在 CodePen 上的 Pen。
这太棒了,谢谢分享!
简化此方法的一种方式可能是使用相同的
<polygon>
轮廓,并使用“随机”stroke-dasharray
来创建“胶带”。我不太熟悉 Pug,但你应该能够使用 Pug 中的坐标随机化 stroke-dasharray 以获得稍好一些的排列。
啊!不敢相信我在文章中忘记提到这一点,但有一个原因是我避免这样做。当使用单个元素来表示所有条带时,你无法在两条条带相遇的拐角处获得效果。这就是我的意思
啊,当然。你可以用两个
<polygon>
和不同的stroke-dasharray
来做到这一点,但是为了获得最佳效果,你仍然需要使用你的多边形坐标数组来很好地间隔它们,并将它们与线条匹配,而不会包裹拐角。无论如何,很棒的技术。感谢撰写和回复!