遗憾的是,clip-path: path() 仍然不可行

Avatar of Ana Tudor
Ana Tudor

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

当我第一次听说clip-path: path()即将进入 Firefox 时,我非常兴奋。 想象一下,只需一个 HTML 元素和很少的 CSS 就能轻松地编码一个像下面这样的呼吸框,而无需 SVG 或多边形函数中的大量点列表!

Chris 也对最初的实现感到兴奋

这将多么有趣

Animated gif. Shows a square breathing in and out - its waistline smoothly contracts and then expands.
呼吸框。

我决定试一试。 我进入 CodePen,在 HTML 面板中放置了一个<div>,以视口单位为其设置尺寸,以便它可以很好地缩放,并添加了一个background以便我可以看到它。 然后,我访问 MDN 以查看一些使用示例…… 而我的蓬松云朵般的梦想开始破灭了!

请注意,clip-path: path()仅在 Firefox 63-70 中有效,并且在about:config中将layout.css.clip-path-path.enabled标志设置为true,以及在 Firefox 71+ 中无需启用任何标志。(来源:MDN。)

这些是我找到的示例

path('M0 200L0 110A110 90 0 0 1 240 100L 200 340z')
path('M.5 1C.5 1 0 .7 0 .3A.25 .25 1 1 1 .5 .3 .25 .25 1 1 1 1 .3C1 .7 .5 1 .5 1Z')

这些坐标是什么? 可悲的答案是**像素值**! 使用它们是因为path()函数采用 SVG <path>字符串作为参数,该字符串(就像 SVG d属性在<path>元素上的值一样)仅包含一种坐标值:无单位像素。 在 SVG 案例中,这些像素随<svg>元素的viewBox缩放,但它们在 CSS path()函数内部根本不会缩放!

这意味着如果我们有一个具有clip-path属性的path()值的响应式元素,则该元素始终会被裁剪到相同的固定区域。 例如,考虑一个边长为35vw的正方形.box。 我们使用path()函数将其裁剪成心形

clip-path: path('M256 203C150 309 150 309 44 203 15 174 15 126 44 97 73 68 121 68 150 97 179 68 227 68 256 97 285 126 285 174 256 203')

当我们实际的.box元素的尺寸随视口变化时,这个心形保持相同的大小

Animated gif. Shows how the heart clipped using a fixed pixel path() doesn't fit within the element's bounding rectangle when its viewport-depending size goes down at small screen sizes.
固定像素path()的问题。

在 2020 年,响应式设计是标准而不是例外,这是一个坏消息。 除了我们想要裁剪的元素实际上具有固定像素大小的奇特情况外,path()函数完全没用! 我们今天仍然最好使用实际的 SVG,甚至使用clip-pathpolygon()近似值。 简而言之,尽管path()已经起步,但它仍然需要改进。

Amelia Bellamy-Royds 在这里提出了两种可能性

选项 1:允许在路径数据内使用calc()值/单位。 这可能是在一般扩展 SVG path语法时完成的。

选项 2:在clip-path声明中指定viewBox,缩放路径以适应。

我个人更喜欢第一个选项。 第二个选项相对于使用 SVG 唯一提供的优势是,我们不必包含实际的 SVG。 也就是说,包含实际的 SVG 将始终具有更好的支持。

但是,第一个选项可以比使用 SVG 有很大的改进——至少足以证明在 HTML 元素上使用clip-path而不是在其中包含 SVG。 让我们考虑一下这篇文章顶部的呼吸框。 使用 SVG,我们有以下标记

<svg viewBox='-75 -50 150 100'>
  <path/>
</svg>

请注意,viewBox的设置使得0,0点正好位于中间。 这意味着我们必须使左上角的坐标(即前两个viewBox值)等于减去一半的viewBox尺寸(即最后两个viewBox值)。

在 SCSS 中,我们将初始正方形框的边长($l)设置为最小的viewBox尺寸(即最后两个值中最小的一个)。 在我们的例子中,它是100

我们从正方形框的左上角开始路径。 这意味着移动到M命令到该点,其坐标都等于边长的一半的负数。

然后我们向下移动到左下角。 这需要绘制一条长度等于边长($l)且向下(在y轴的正方向上)的垂直线。 因此,我们将使用v命令。

接下来,我们移动到右下角。 我们将绘制一条长度等于边长($l)且向右(在x轴的正方向上)的水平线。 我们将使用h命令来实现这一点。

移动到右上角意味着绘制另一条长度等于边长($l)的垂直线,因此我们将再次使用v命令——只是这次的区别在于我们沿着y轴的反方向移动,这意味着我们使用相同的坐标,但带负号。

将所有内容放在一起,我们就有了一个允许我们创建初始正方形框的 SCSS

.box {
  d: path('M#{-.5*$l},#{-.5*$l} v#{$l} h#{$l} v#{-$l}');
  fill: darkorange
}

生成的 CSS(其中$l被替换为100)如下所示

.box {
  d: path('M-50,-50 v100 h100 v-100');
  fill: darkorange;
}

结果可以在下面的交互式演示中看到,其中将鼠标悬停在路径数据的一部分上会突出显示生成的 SVG 中的对应部分,反之亦然

请参阅 thebabydino 在 CodePen 上的 Pen@thebabydino)。

但是,如果我们希望侧边“呼吸”,则不能使用直线。 让我们用二次贝塞尔曲线(q)替换它们。 终点保持不变,即沿同一条垂直线向下移动一个边长。 我们通过0,#{$l}到达那里。

但是我们在之前需要指定的控制点呢? 我们将该点垂直放置在起点和终点之间的中间,这意味着我们向下移动到它,移动距离为到达终点距离的一半。

假设在水平方向上,我们将它放置在边长四分之一处的一个方向或另一个方向上。 如果我们希望线条突出以拓宽框或将其挤压以缩窄它,我们需要执行以下操作

d: path('M#{-.5*$l},#{-.5*$l} 
         q#{-.25*$l},#{.5*$l} 0,#{$l} 
         h#{$l} 
         v#{-$l}'); /* swollen box */

d: path('M#{-.5*$l},#{-.5*$l} 
         q#{.25*$l},#{.5*$l} 0,#{$l} 
         h#{$l} 
         v#{-$l}'); /* squished box */

这编译成以下 CSS

d: path('M-50,-50 
         q-25,50 0,100 
         h100 
         v-100'); /* swollen box */

d: path('M-50,-50 
         q25,50 0,100 
         h100 
         v-100'); /* squished box */

下面的交互式演示显示了此path的工作原理。 您可以将鼠标悬停在路径数据组件上以查看它们在 SVG 图形上的突出显示。 您还可以切换膨胀和收缩版本。

请参阅 thebabydino 在 CodePen 上的 Pen@thebabydino)。

这仅是左边缘。 我们也需要对右边缘执行相同的操作。 此处的区别在于我们是从右下角到右上角而不是向下(在y轴的负方向上)。 我们将控制点放置在框外以获得宽框效果,这也意味着将其放置在其端点的右侧(在x轴的正方向上)。 同时,我们将控制点放置在内部以获得窄框效果,这意味着将其放置在其端点的左侧(在x轴的负方向上)。

d: path('M#{-.5*$l},#{-.5*$l} 
         q#{-.25*$l},#{.5*$l} 0,#{$l} 
         h#{$l} 
         q#{.25*$l},#{-.5*$l} 0,#{-$l}'); /* swollen box */

d: path('M#{-.5*$l},#{-.5*$l} 
         q#{.25*$l},#{.5*$l} 0,#{$l} 
         h#{$l} 
         q#{-.25*$l},#{-.5*$l} 0,#{-$l}'); /* squished box */

上面的 SCSS 生成以下 CSS

d: path('M-50,-50 
         q-25,50 0,100 
         h100 
         q25,-50 0,100'); /* swollen box */

d: path('M-50,-50 
         q25,50 0,100 
         h100 
         q-25,-50 0,-100'); /* squished box */

请参阅 thebabydino 在 CodePen 上的 Pen@thebabydino)。

为了获得呼吸效果,我们在膨胀状态和收缩状态之间进行动画切换

.box {
  d: path('M#{-.5*$l},#{-.5*$l} 
           q#{-.25*$l},#{.5*$l} 0,#{$l} 
           h#{$l} 
           q#{.25*$l},#{-.5*$l} 0,#{-$l}'); /* swollen box */
  animation: breathe .5s ease-in-out infinite alternate
}

@keyframes breathe {
  to {
    d: path('M#{-.5*$l},#{-.5*$l} 
             q#{.25*$l},#{.5*$l} 0,#{$l} 
             h#{$l} 
             q#{-.25*$l},#{-.5*$l} 0,#{-$l}'); /* squished box */
  }
}

由于两种状态之间唯一不同的只是控制点水平差的符号(二次贝塞尔曲线q命令后第一个数字的符号),因此我们可以使用混合宏简化操作

@mixin pdata($s: 1) {
  d: path('M#{-.5*$l},#{-.5*$l} 
           q#{-.25*$s*$l},#{.5*$l} 0,#{$l} 
           h#{$l} 
           q#{.25*$s*$l},#{-.5*$l} 0,#{-$l}')
}

.box {
  @include pdata();
  animation: breathe .5s ease-in-out infinite alternate
}

@keyframes breathe { to { @include pdata(-1) } }

这几乎就是我为实际的呼吸框演示所做的工作,尽管运动稍微更谨慎。 尽管如此,这对生成的 CSS 没有任何影响——我们仍然在编译后的代码中拥有两个长而丑陋且几乎相同的路径。

但是,如果我们能够使用一个<div>,并使用支持各种值(包括内部的calc()值)的clip-path: path()进行裁剪,那么我们可以将符号设置为自定义属性--sgn,然后借助 Houdini 在-11之间对其进行动画。

div.box {
  width: 40vmin; height: 20vmin;
  background: darkorange;
  --sgn: 1;
  clip-path: path(M 25%,0%
                  q calc(var(--sgn)*-25%),50% 0,100%
                  h 50%
                  q calc(var(--sgn)*25%),-50% 0,-100%);
  animation: breathe .5s ease-in-out infinite alternate
}

@keyframes breathe { to { --sgn: -1 } }

能够做到这一点将产生天壤之别。 我们的元素将随视口很好地缩放,我们从中裁剪出的呼吸框也是如此。 而且,最重要的是,我们不需要重复此裁剪路径以获取其两个不同版本(膨胀版本和收缩版本),因为在calc()值内使用符号自定义属性(--sgn)将可以解决问题。 但是就目前而言,clip-path: path()几乎毫无用处。