使用胶带粘贴跨浏览器响应式不规则图像

Avatar of Ana Tudor
Ana Tudor

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

我最近偶然发现了 Vasilis van Gemert制造者地图集。它有趣而古怪的外观使我查看了其内部结构,这绝对值得!我发现它实际上是利用了过去几年中许多文章和演讲都讨论过的非常酷的功能构建的,但不知何故在实际应用中却很少使用——比如 CSS Grid、自定义属性、混合模式,甚至 SVG。

SVG 用于创建看起来像是用霓虹胶带粘贴到页面上的不规则图像。本文将解释如何以最简单的方式重新创建它,而无需离开浏览器。让我们开始吧!

我们首先选择一张开始使用的图片,例如,这只令人惊叹的雪豹

Fluffy snow leopard walking through the snow, looking up at the camera with an inquisitive face.
我们将使用的图片:一只毛茸茸的雪豹。

接下来,我们获取一个可以容纳猫的粗略多边形。为此,我们使用 Bennett FeelyClippy。我们实际上不会使用 CSS clip-path,因为它还没有跨浏览器兼容(但如果您希望它兼容,请 投票支持——无需登录),它只是为了快速获取多边形点的数值,而无需使用带有大量按钮和选项的实际图像编辑器,这些按钮和选项会导致您在开始之前就放弃。

我们为图像设置自定义 URL 并设置自定义尺寸。Clippy 根据视口大小限制这些尺寸,但对于我们来说,在这种情况下,图像的实际尺寸并不重要(尤其是在输出将只是 % 值的情况下),只有纵横比很重要,在我们的猫图片中,纵横比为 2:3

Clippy screenshot showing from where to set custom dimensions and URL. The custom dimensions need to satisfy a 2:3 ratio so we pick 540 = 2*270 for the width and 810 = 3*270 for the height.
Clippy:设置自定义尺寸和 URL。

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

Animated gif. Illustrates turning on the 'Show outside clip-path' option. This is going to be useful later, while picking the vertices of the polygon that roughly clips the image around the cat, so that we can see outside the partial polygon we have before we get all the points.
Clippy:开启“显示 clip-path 外部区域”选项。

然后我们选择为裁剪路径使用自定义多边形,选择所有点,关闭路径,然后可能调整一些点的位置。

Clippy:选择一个大致近似于猫形状的自定义多边形的点。

这为我们生成了 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(', ')}]`;
Screenshot of the steps described above in the dev tools console.
开发者工具控制台中的上述步骤截图。

现在我们继续使用 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]}`)
  - }
- }

乍一看,这 似乎什么也没做

但是,这是因为默认的 strokenone。使此 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() 而被丢弃,因此我们 以这种方式获得了我们想要的结果

Screenshot of the Chrome (left) and Firefox (right) dev tools showing how the second stroke rule is seen as invalid and discarded by both browsers
在 Chrome(左)和 Firefox(右)开发者工具中看到的第二个 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 的矩形。如果我们画出这个矩形对角线,我们会看到我们可以使用勾股定理在黄色高亮三角形中计算它。

SVG illustration showing the SVG rectangle and highlighting half of it - a right triangle where the catheti are the SVG viewBox width and height and the hypotenuse is the SVG diagonal.
SVG 矩形对角线可以从一个直角三角形计算,其中直角边是 SVG 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%

任何 pathstroke 一半在路径线内,一半在路径线外。但是,我们需要将 viewBox 空间增加超过 stroke-width 的一半。我们还需要考虑到 stroke-linecappath 的端点之外扩展了 stroke-width 的一半

SVG illustration showing how the stroke is drawn half on one side of the path line, half on the other side and how a square linecap causes it to expand by half a stroke-width beyond its endpoints.
stroke-widthstroke-linecap: square 的效果。

现在让我们考虑裁剪多边形的一个点正好位于原始 image 边缘的情况。为了简化问题,我们只考虑具有一个端点在此点上的多边形边之一(另一个边的处理方式完全相同)。

我们以沿着多边形边的一个条带为例,该条带的一个端点 E 位于原始图像(以及 SVG)的顶部边缘。

Screenshot showing the result of the latest demo and highlighting an edge which has an end on the top boundary of the original image.
突出显示一个多边形边,该边的一个端点位于原始图像的顶部边界。

我们想知道当使用 stroke 创建此条带并且 stroke-linecap 设置为 square 时,此条带可以在图像顶部边缘之外扩展多少。这取决于与顶部边缘形成的角度,我们感兴趣的是找到顶部边界之外需要多少额外的空间,以便我们的条带的任何部分都不会被溢出裁剪掉。

为了更好地理解这一点,下面的交互式演示允许我们旋转条带并了解创建此条带的 stroke(包括 square 线帽)的外角可以扩展到多远

查看 thebabydino (@thebabydino) 在 CodePen 上的 Pen

如上图所示,通过跟踪 stroke(包括 stroke-linecap)的外角的位置,边界之外需要的最大额外空间是当端点在边缘 E 上到 stroke(包括线帽)在该端点处的外部角之间的线段垂直时,此空间等于线段的长度。

鉴于 stroke 在切线方向和法线方向上都超出端点 stroke-width 的一半,因此该线段的长度是直角等腰三角形的斜边,其直角边分别等于 stroke-width 的一半

SVG illustration showing how to get the segment between the path's endpoint and one of the outer corners of the stroke including the linecap: it's the hypotenuse in an isosceles triangle where the catheti are half a stroke width, along the normal direction because half the stroke is on one side of the path line while the other half is on the other and along the tangent direction because we have square linecap.
连接端点到包括线帽的 stroke 外角的线段是直角等腰三角形的斜边,其中直角边是 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