使用 CSS Masks 和自定义属性实现图像碎片化效果

Avatar of Temani Afif
Temani Afif

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

Geoff 分享了一个关于 棋盘格揭示图像 的想法,其中瓷砖一个接一个地消失以揭示图像。在这个想法中,一个元素有一个背景图像,然后 CSS 网格布局包含从填充背景色到透明的“瓷砖”,从而揭示图像。轻微的 SCSS 触碰使动画交错进行。

我有一个类似的想法,但采用不同的方法。与其揭示图像,不如从完全揭示图像开始,然后将其一块一块地消失,就像它漂浮成微小的碎片一样。

这是一个结果的工作演示。没有 JavaScript 处理,也没有 SVG 花招。只有一个 <img> 和一些 SCSS 魔法。

很酷,对吧?当然,但问题来了。您需要在 Chrome、Edge 或 Opera 中查看此内容,因为目前只有这些浏览器支持 @property,而这是这个想法的关键组成部分。我们不会让它阻止我们,因为这是一个绝佳的机会,让我们可以接触到一些很酷的 CSS 功能,例如遮罩和在 @property 的帮助下动画化线性渐变。

遮罩元素

遮罩有时难以概念化,并且经常与剪裁混淆。底线:遮罩是图像。当将图像应用为元素的遮罩时,图像的任何透明部分都允许我们透过元素看到。任何不透明的部分都会使元素完全可见。

遮罩的工作方式与不透明度相同,但作用于同一元素的不同部分。这与剪裁不同,剪裁是所有路径外部的内容都被隐藏的路径。遮罩的优点是我们可以在同一元素上拥有任意数量的遮罩层——类似于我们在 background-image 上链接多个图像的方式。

并且由于遮罩是图像,因此我们可以使用 CSS 渐变来创建它们。让我们举一个简单的例子来更好地理解这个技巧。

img {
  mask:
    linear-gradient(rgba(0,0,0,0.8) 0 0) left,  /* 1 */
    linear-gradient(rgba(0,0,0,0.5) 0 0) right; /* 2 */
  mask-size: 50% 100%;
  mask-repeat: no-repeat;
}

在这里,我们正在图像上定义两个遮罩层。它们都是纯色,但 alpha 透明度值不同。上面的语法可能看起来很奇怪,但它是编写 linear-gradient(rgba(0,0,0,0.8), rgba(0,0,0,0.8)) 的简化方式。

值得注意的是,我们使用的颜色无关紧要,因为默认的 mask-modealpha。alpha 值是唯一相关的东西。我们的渐变可以是 linear-gradient(rgba(X,Y,Z,0.8) 0 0),其中 XYZ 是随机值。

每个遮罩层等于 50% 100%(或图像的半宽和全高)。一个遮罩覆盖左侧,另一个覆盖右侧。最后,我们有两个不重叠的遮罩覆盖图像的整个区域,并且,正如我们之前讨论的那样,每个遮罩都具有不同的 alpha 透明度值。

我们正在查看使用两个线性渐变创建的两个遮罩层。第一个渐变(左侧)的 alpha 值为 0.8。第二个渐变(右侧)的 alpha 值为 0.5。第一个渐变更不透明,这意味着显示的图像更多。第二个渐变更透明,这意味着显示的背景更多。

动画化线性渐变

我们要做的是对遮罩的线性渐变 alpha 值应用动画以创建透明度动画。稍后,我们将将其制成异步动画,从而创建碎片化效果。

动画化渐变是我们一直无法在 CSS 中做到的事情。也就是说,直到我们获得了对 @property 的有限支持。Jhey Tompkins 深入探讨了 @property 的强大动画能力,演示了如何使用它来过渡渐变。同样,您需要在 Chrome 或其他基于 Blink 的浏览器中查看此内容。

简而言之,@property 允许我们创建自定义 CSS 属性,在其中我们可以通过指定类型来定义语法。让我们创建两个属性 --c-0--c-1,它们都接收一个数字,初始值为 1

@property --c-0 {
   syntax: "<number>";
   initial-value: 1;
   inherits: false;
}
@property --c-1 {
   syntax: "<number>";
   initial-value: 1;
   inherits: false;
}

这些属性将表示 CSS 遮罩中的 alpha 值。并且由于它们都默认为完全不透明(即 1),因此整个图像都显示在遮罩中。以下是我们可以使用自定义属性重写遮罩的方式

/* Omitting the @property blocks above for brevity */

img {
  mask:
    linear-gradient(rgba(0,0,0,var(--c-0)) 0 0) left,  /* 1 */
    linear-gradient(rgba(0,0,0,var(--c-1)) 0 0) right; /* 2 */
  mask-size: 50% 100%;
  mask-repeat: no-repeat;
  transition: --c-0 0.5s, --c-1 0.3s 0.4s;
}

img:hover {
  --c-0:0;
  --c-1:0;
}

我们在这里所做的只是为每个自定义变量应用不同的过渡持续时间和延迟。继续并悬停图像。遮罩的第一个渐变将淡出到 alpha 值为 0 以使图像完全透明,然后是第二个渐变。

更多遮罩!

到目前为止,我们只在遮罩和两个自定义属性上使用两个线性渐变。要创建平铺或碎片化效果,我们需要更多瓷砖,这意味着需要更多渐变和许多自定义属性!

SCSS 使这项任务相当简单,因此从现在开始我们将转向它来编写样式。正如我们在第一个示例中看到的,我们有一种瓷砖矩阵。我们可以将其视为行和列,因此让我们定义两个 SCSS 变量 $x$y 来表示它们。

自定义属性

我们将需要每个属性的 @property 定义。但是,没有人希望手动编写所有这些内容,因此让我们允许 SCSS 为我们完成繁重的工作,通过循环运行我们的属性

@for $i from 0 through ($x - 1) {
  @for $j from 0 through ($y - 1) {
    @property --c-#{$i}-#{$j} {
      syntax: "<number>";
      initial-value: 1;
      inherits: false;
    }
  }
}

然后我们使它们全部在悬停时变为 0

img:hover {
  @for $i from 0 through ($x - 1) {
    @for $j from 0 through ($y - 1) {
      --c-#{$i}-#{$j}: 0;
    }
  }
}

渐变

我们将编写一个 @mixin 来为我们生成它们

@mixin image() {
  $all_t: (); // Transition
  $all_m: (); // Mask
  @for $i from 0 through ($x - 1) {
    @for $j from 0 through ($y - 1) {
      $all_t: append($all_t, --c-#{$i}-#{$j} transition($i,$j), comma);
      $all_m: append($all_m, linear-gradient(rgba(0,0,0,var(--c-#{$i}-#{$j})) 0 0) calc(#{$i}*100%/(#{$x} - 1)) calc(#{$j}*100%/(#{$y} - 1)), comma);
    }
  }
  transition: $all_t;
  mask: $all_m;
}

我们所有的遮罩层大小都相同,因此我们只需要一个属性,依赖于 $x$y 变量以及 calc()

mask-size: calc(100%/#{$x}) calc(100%/#{$y})

您可能也注意到了这一行

$all_t: append($all_t, --c-#{$i}-#{$j} transition($i,$j), comma);

在同一个混合中,我们还生成了包含所有先前定义的自定义属性的 transition 属性。

最后,由于 SCSS 中的 random() 函数,我们为每个属性生成了不同的持续时间/延迟。

@function transition($i,$j) {
  @return $s*random()+s $s*random()+s;
}

现在我们要做的就是调整 $x$y 变量以控制碎片化的粒度。

使用动画

我们还可以更改随机配置以考虑不同类型的动画。

在上面的代码中,我定义了如下所示的 transition() 函数

// Uncomment one to use it
@function transition($i,$j) {
  // @return (($s*($i+$j))/($x+$y))+s (($s*($i+$j))/($x+$y))+s; /* diagonal */
  // @return (($s*$i)/$x)+s (($s*$j)/$y)+s; /* left to right */
  // @return (($s*$j)/$y)+s (($s*$i)/$x)+s; /* top to bottom */
  // @return  ($s*random())+s (($s*$j)/$y)+s; /* top to bottom random */
  @return  ($s*random())+s (($s*$i)/$y)+s; /* left to right random */
  // @return  ($s*random())+s (($s*($i+$j))/($x+$y))+s; /* diagonal random */
  // @return ($s*random())+s ($s*random())+s; /* full random*/
}

通过调整公式,我们可以获得不同类型的动画。只需取消注释您要使用的那个。此列表并不详尽——我们可以通过考虑更多公式来获得任何组合。(如果您添加高级数学函数,例如 sin()sqrt() 等,您可以想象一下可能实现的功能。)

使用渐变

我们仍然可以通过调整渐变来修改我们的代码,以便在动画化 alpha 值而不是动画化颜色停止点。我们的渐变将如下所示

linear-gradient(white var(--c-#{$i}-#{$j}),transparent 0)

然后我们将变量从 100% 动画化为 0%。而且,嘿,我们不必坚持使用线性渐变。为什么不使用径向渐变呢?

像过渡一样,我们可以定义任何我们想要的渐变——组合是无限的!

使用重叠进行操作

让我们引入另一个变量来控制渐变蒙版之间的重叠。此变量将像这样设置mask-size

calc(#{$o}*100%/#{$x}) calc(#{$o}*100%/#{$y})

如果它等于1,则没有重叠。如果它更大,那么我们确实会得到重叠。这使我们能够制作更多种类的动画

就是这样!

我们所要做的就是找到变量和公式之间的完美组合,以创建令人惊叹和疯狂的图像碎片效果。