探索 CSS Paint API:图像碎片效果

Avatar of Temani Afif
Temani Afif

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

在我之前的文章 ,我使用 CSS 遮罩和自定义属性创建了碎片效果。这是一个不错的效果,但它有一个缺点:它使用了很多 CSS 代码(使用 Sass 生成)。这次我将使用新的 Paint API 重做同样的效果。这极大地减少了 CSS 的数量,并完全消除了对 Sass 的需求。

探索 CSS Paint API 系列


这就是我们要制作的效果。与之前的文章一样,目前只有 Chrome 和 Edge 支持。

看到了吗?不超过 5 个 CSS 声明,我们就得到了一个很酷的悬停动画。

什么是 Paint API?

Paint API 是 Houdini 项目的一部分。没错,“Houdini” 这个奇怪的术语,每个人都在谈论它。很多文章 已经介绍了它的理论方面,所以我不会再烦扰你。如果我必须用几句话来概括,我会简单地说:它是 CSS 的未来。Paint API(以及其他属于 Houdini 伞形下的 API)允许我们用自己的功能扩展 CSS。我们不再需要等待新功能的发布,因为我们可以自己做!

来自 规范

一个 API,允许 Web 开发人员使用 javascript [sic] 定义一个自定义 CSS <image>,它将响应样式和大小更改。

以及来自 解释器

CSS Paint API 正在开发中,以提高 CSS 的可扩展性。具体来说,这允许开发人员编写一个 paint 函数,该函数允许我们直接绘制到元素的 [sic] 背景、边框或内容中。

我认为这个想法很清楚。我们可以画出我们想要的任何东西。让我们从一个非常基本的背景颜色演示开始

  1. 我们使用 CSS.paintWorklet.addModule('your_js_file') 添加 paint worklet。
  2. 我们注册一个名为 draw 的新 paint 方法。
  3. 在里面,我们创建一个 paint() 函数,在那里我们完成所有工作。猜猜看?一切都与 <canvas> 相似。这个 ctx 是 2D 上下文,我简单地使用了一些众所周知的函数来绘制一个覆盖整个区域的红色矩形。

乍一看,这可能看起来不太直观,但请注意,主要结构始终相同:以上三个步骤是您在每个项目中重复的“复制/粘贴”部分。真正的工作是我们编写在 paint() 函数中的代码。

让我们添加一个变量

如您所见,逻辑非常简单。我们使用变量作为数组定义 getter inputProperties。我们将 properties 作为第三个参数添加到 paint() 中,稍后我们使用 properties.get() 获取变量。

就这样!现在我们拥有构建复杂碎片效果所需的一切。

构建遮罩

您可能想知道为什么使用 Paint API 来创建碎片效果。我们说它是一个用于绘制图像的工具,那么它如何让我们将图像碎片化呢?

在之前的文章中,我使用不同的遮罩层来实现效果,其中每个遮罩层都是一个用渐变定义的正方形(请记住,渐变是一种图像),因此我们得到了一种矩阵,技巧是单独调整每个遮罩层的 alpha 通道。

这次,我们将不再使用多个渐变,而是为我们的遮罩定义一个自定义图像,并且该自定义图像将由我们的 Paint API 处理。

请举个例子!

在上面,我创建了一个图像,其中一个不透明颜色覆盖左侧,而一个半透明颜色覆盖右侧。将此图像应用为遮罩,我们得到了一个半透明图像的逻辑结果。

现在我们所要做的就是将图像分成更多部分。让我们定义两个变量并更新我们的代码

代码的相关部分如下

const n = properties.get('--f-n');
const m = properties.get('--f-m');

const w = size.width/n;
const h = size.height/m;

for(var i=0;i<n;i++) {
  for(var j=0;j<m;j++) {
    ctx.fillStyle = 'rgba(0,0,0,'+(Math.random())+')';    
    ctx.fillRect(i*w, j*h, w, h);
}
}

NM 定义了矩形矩阵的维度。WH 是每个矩形的大小。然后我们有一个基本的 FOR 循环,用随机透明颜色填充每个矩形。

使用少量 JavaScript,我们得到了一个自定义遮罩,我们可以通过调整 CSS 变量来轻松控制。

现在,我们需要控制 alpha 通道,以便创建每个矩形的淡出效果,并构建碎片效果。

让我们引入第三个变量,用于 alpha 通道,我们也会在悬停时更改该变量。

我们定义了一个 CSS 自定义属性作为 <number>,它从 1 过渡到 0,并且同一个属性用于定义矩形的 alpha 通道。悬停时不会发生任何奇特的事情,因为所有矩形将以相同的方式淡出。

我们需要一个技巧来防止所有矩形同时淡出,而是让它们之间产生延迟。以下是一个说明我将要使用的想法的插图

上面显示了两个矩形的 alpha 动画。首先,我们定义一个应该大于或等于 1 的变量 L,然后对于矩阵的每个矩形(即,对于每个 alpha 通道),我们执行从 XY 的过渡,其中 X - Y = L,因此所有 alpha 通道的总持续时间相同。X 应该大于或等于 1,而 Y 应该小于或等于 0。

等等,alpha 值不应在 [1 0] 范围内,对吗?

是的,应该!我们正在执行的所有技巧都依赖于此。在上面,alpha 从 8 动画到 -2,这意味着我们在 [8 1] 范围内有一个不透明颜色,在 [0 -2] 范围内有一个透明颜色,并且在 [1 0] 范围内有一个动画。换句话说,任何大于 1 的值都将与 1 具有相同的效果,而任何小于 0 的值都将与 0 具有相同的效果。

[1 0] 范围内的动画不会同时发生在我们两个矩形上。矩形 2 将比矩形 1 更早到达 [1 0]。我们将此应用于所有 alpha 通道,以获得我们的延迟动画。

在我们的代码中,我们将更新此

rgba(0,0,0,'+(o)+')

…为

rgba(0,0,0,'+((Math.random()*(l-1) + 1) - (1-o)*l)+')

L 是前面说明的变量,O 是我们从 1 过渡到 0 的 CSS 变量的值

O=1 时,我们有 (Math.random()*(l-1) + 1)。考虑到 random() 函数为我们提供 [0 1] 范围内的值,最终值将在 [L 1] 范围内。

O=0 时,我们有 (Math.random()*(l-1) + 1 - l)[0 1-L] 范围内的值。

L 是我们用于控制延迟的变量。

让我们看看实际效果

我们越来越接近了。我们得到了一个很酷的碎片效果,但不是我们在文章开头看到的那个。这个效果不够流畅。

问题与 random() 函数有关。我们说过每个 alpha 通道都需要在 XY 之间动画,因此逻辑上这些值需要保持不变。但 paint() 函数在过渡期间被多次调用,因此每次 random() 函数都会为每个 alpha 通道提供不同的 XY 值;因此,我们得到了“随机”效果。

为了解决这个问题,我们需要找到一种方法来存储生成的 value,以便它们在每次调用 paint() 函数时都保持不变。让我们考虑一个伪随机函数,一个始终生成相同值序列的函数。换句话说,我们希望控制种子

不幸的是,我们无法使用 JavaScript 内置的 random() 函数来做到这一点,因此,像任何优秀的开发人员一样,让我们从 Stack Overflow 中选择一个

const mask = 0xffffffff;
const seed = 30; /* update this to change the generated sequence */
let m_w  = (123456789 + seed) & mask;
let m_z  = (987654321 - seed) & mask;

let random =  function() {
  m_z = (36969 * (m_z & 65535) + (m_z >>> 16)) & mask;
  m_w = (18000 * (m_w & 65535) + (m_w >>> 16)) & mask;
  var result = ((m_z << 16) + (m_w & 65535)) >>> 0;
  result /= 4294967296;
  return result;
}

结果变为

我们得到了碎片效果,而代码并不复杂

  • 一个基本的嵌套循环,用于创建 NxM 个矩形
  • 一个巧妙的 alpha 通道公式,用于创建过渡延迟
  • 一个从网络上获得的现成的 random() 函数

就是这样!您所要做的就是将 mask 属性应用于任何元素并调整 CSS 变量。

对抗间隙!

如果您使用上面的演示进行操作,您会注意到,在某些特定情况下,矩形之间会出现奇怪的间隙

为了避免这种情况,我们可以用一个小偏移量扩展每个矩形的区域。

我们将此

ctx.fillRect(i*w, j*h, w, h);

…更新为

ctx.fillRect(i*w-.5, j*h-.5, w+.5, h+.5);

这会在矩形之间创建一个小重叠,以补偿它们之间的间隙。我使用的值 0.5 没有特别的逻辑。您可以根据您的用例调整大小。

想要更多形状吗?

以上内容可以扩展到考虑矩形以外的形状吗?当然可以!别忘了,我们可以用 Canvas 来绘制任何形状,不像 纯 CSS 形状,有时需要一些技巧性的代码。让我们尝试构建那个三角形碎片效果。

在网上搜索后,我发现了一种名为 Delaunay 三角剖分 的方法。我不会深入讲解其背后的理论,但它是一种针对一组点绘制具有特定属性的连接三角形的算法。有很多现成的实现,但 我们会选择 Delaunator,因为它应该是其中速度最快的。

我们首先定义一组点(这里我们将使用 random()),然后运行 Delauntor 为我们生成三角形。在这种情况下,我们只需要一个定义点数的变量。

const n = properties.get('--f-n');
const o = properties.get('--f-o');
const w = size.width;
const h = size.height;
const l = 7; 

var dots = [[0,0],[0,w],[h,0],[w,h]]; /* we always include the corners */
/* we generate N random points within the area of the element */
for (var i = 0; i < n; i++) {
  dots.push([random() * w, random() * h]);
}
/**/
/* We call Delaunator to generate the triangles*/
var delaunay = Delaunator.from(dots);
var triangles = delaunay.triangles;
/**/
for (var i = 0; i < triangles.length; i += 3) { /* we loop the triangles points */
  /* we draw the path of the triangles */
  ctx.beginPath();
  ctx.moveTo(dots[triangles[i]][0]    , dots[triangles[i]][1]);
  ctx.lineTo(dots[triangles[i + 1]][0], dots[triangles[i + 1]][1]);
  ctx.lineTo(dots[triangles[i + 2]][0], dots[triangles[i + 2]][1]);  
  ctx.closePath();
  /**/
  var alpha = (random()*(l-1) + 1) - (1-o)*l; /* the alpha value */
  /* we fill the area of triangle with the semi-transparent color */
  ctx.fillStyle = 'rgba(0,0,0,'+alpha+')';
  /* we consider stroke to fight the gaps */
  ctx.strokeStyle = 'rgba(0,0,0,'+alpha+')';
  ctx.stroke();
  ctx.fill();
} 

我对以上代码中的注释没有更多要补充的。我只是使用了一些基本的 JavaScript 和 Canvas 功能,但我们却得到了一个很酷的效果。

我们可以制作更多形状!我们只需要为其找到一个算法。

我不能不制作六边形效果!

我从 Izan Pérez Cosano 写的 这篇文章 中获取了代码。现在我们的变量是 R,它将定义一个六边形的尺寸。

下一步是什么?

现在我们已经构建了我们的碎片效果,让我们专注于 CSS。请注意,效果与更改元素悬停状态下的 opacity 值(或您正在使用的任何属性的值)一样简单。

不透明度动画

img {
  opacity:1;
  transition:opacity 1s;
}

img:hover {
  opacity:0;
}

碎片效果

img {
  -webkit-mask: paint(fragmentation);
  --f-o:1;
  transition:--f-o 1s;
}

img:hover {
  --f-o:0;
}

这意味着我们可以轻松地整合这种效果来创建更复杂的动画。这里有一些想法!

响应式图片滑块

同一个滑块的另一个版本

噪声效果

加载屏幕

卡片悬停效果

总结

所有这些仅仅是使用 Paint API 可以实现的功能的冰山一角。最后,我想强调两点。

  • Paint API 90% 是 <canvas>,所以您对 <canvas> 了解得越多,您就可以实现越花哨的功能。Canvas 被广泛使用,这意味着有大量文档和文章可以帮助您快速入门。嘿,这里就在 CSS-Tricks 上
  • Paint API 消除了 CSS 方面的所有复杂性。您无需处理复杂的技巧性代码来绘制酷炫的东西。这使得 CSS 代码更容易维护,更不用说错误更少了。

探索 CSS Paint API 系列