探索 CSS Paint API:Blob 动画
在碎片效果之后,我将处理另一个有趣的动画:斑点!我们都同意 CSS 很难实现这样的效果,所以我们通常会使用 SVG 来制作那些粘糊糊的形状。但是现在强大的 Paint API 可用,使用 CSS 不仅是可能的,而且一旦浏览器支持出现,它甚至可能是一种更好的方法。
这就是我们正在做的。目前仅支持 Chrome 和 Edge,因此请在我们继续的过程中在其中一个浏览器上查看。
现场演示 (仅限 Chrome 和 Edge)
构建 blob
让我们了解使用经典<canvas>元素绘制 blob 背后的逻辑,以更好地说明形状:
在谈论斑点时,我们也在谈论扭曲圆的一般形状,因此我们可以将其用作基础形状。我们定义N放置在圆圈周围的点(以绿色表示)。
const CenterX = 200;
const CenterY = 200;
const Radius = 150;
const N = 10;
var point = [];
for (var i = 0; i < N; i++) {
var x = Math.cos((i / N) * (2 * Math.PI)) * Radius + CenterX;
var y = Math.sin((i / N) * (2 * Math.PI)) * Radius + CenterY;
point[i] = [x, y];
}
考虑到中心点(由CenterX/定义CenterY)和半径,我们使用一些基本的三角学计算每个点的坐标。
之后,我们使用 绘制点之间的三次贝塞尔曲线quadraticCurveTo()。为此,我们引入了更多点(用红色表示),因为三次贝塞尔曲线需要一个起点、一个控制点和一个终点。
红点是起点和终点,绿点可以是控制点。每个红点位于两个绿点之间的中点。
ctx.beginPath(); /* start the path */
var xc1 = (point[0][0] + point[N - 1][0]) / 2;
var yc1 = (point[0][1] + point[N - 1][1]) / 2;
ctx.moveTo(xc1, yc1);
for (var i = 0; i < N - 1; i++) {
var xc = (point[i][0] + point[i + 1][0]) / 2;
var yc = (point[i][1] + point[i + 1][1]) / 2;
ctx.quadraticCurveTo(point[i][0], point[i][1], xc, yc);
}
ctx.quadraticCurveTo(point[N - 1][0], point[N - 1][1], xc1, yc1);
ctx.closePath(); /* end the path */
现在我们要做的就是更新控制点的位置以创建斑点形状。让我们通过添加以下内容来尝试一点:
point[3][0]= Math.cos((3 / N) * (2 * Math.PI)) * (Radius - 50) + CenterX;
point[3][1]= Math.sin((3 / N) * (2 * Math.PI)) * (Radius - 50) + CenterY;
第三个点最接近我们的圆的中心(大约 50 像素),我们的三次贝塞尔曲线完美地跟随运动以保持弯曲形状。
让我们对所有点都做同样的事情。我们可以使用相同的总体思路,更改这些现有的行:
var x = Math.cos((i / N) * (2 * Math.PI)) * Radius + CenterX;
var y = Math.sin((i / N) * (2 * Math.PI)) * Radius + CenterY;
…进入:
var r = 50*Math.random();
var x = Math.cos((i / N) * (2 * Math.PI)) * (Radius - r) + CenterX;
var y = Math.sin((i / N) * (2 * Math.PI)) * (Radius - r) + CenterY;
每个点都偏移 0 到 50 个像素之间的随机值,使每个点更靠近中心的量略有不同。结果我们得到了斑点形状!
现在我们使用CSS Paint API将该形状应用为图像上的蒙版。由于我们正在处理一个斑点形状,因此考虑方形元素(高度等于宽度)是合适的,其中半径等于宽度或高度的一半。
这里我们使用 CSS 变量 ( N) 来控制点数。
我强烈建议阅读我上一篇文章的第一部分,以了解 Paint API 的结构。
每次代码运行时,我们都会得到一个新的形状,这要归功于随机配置。
让我们动画这个!
画一个博客很好,但动画效果更好!毕竟,为 blob 设置动画实际上是本文的主要目的。我们将看到如何使用相同的代码基础创建不同类型的粘性 blob 动画。
主要思想是平滑地调整点的位置——无论是全部还是部分——以在两个形状之间过渡。让我们从基本的开始:通过改变一个点的位置,从一个圆到一个 blob 的过渡。
现场演示 (仅限 Chrome 和 Edge)
为此,我引入了一个新的 CSS 变量 B ,我正在对其应用 CSS 转换。
@property --b{
syntax: '<number>';
inherits: false;
initial-value: 0;
}
img {
--b:0;
transition:--b .5s;
}
img:hover {
--b:100
}
我在paint()函数内部获取这个变量的值并用它来定义我们点的位置。
如果您检查嵌入式链接演示中的代码,您会注意到:
if(i==0)
var r = RADIUS - B;
else
var r = RADIUS
所有的点都有一个固定的位置(由形状的半径定义),但第一个点的位置是可变的,(RADIUS - B)。在悬停时, 的值B从 0 变为 100,将我们的点移近中间,同时创造出很酷的效果。
让我们这样做以获得更多积分。不是全部,只有偶数。我将定位如下:
var r = RADIUS - B*(i%2);
现场演示(仅限 Chrome 和 Edge)
我们有了第一个 blob 动画!我们定义了 20 个点,并在悬停时使其中的一半更靠近中心。
只需调整 CSS 变量,我们就可以轻松获得不同的 blobby 变化。我们定义了B变量的点数和最终值。
现场演示 (仅限 Chrome 和 Edge)
现在让我们尝试一些随机的东西。与其用固定值移动我们的点,不如让该值随机移动它们。我们以前使用过这个:
var r = RADIUS - B*(i%2);
让我们把它改成这样:
var r = RADIUS - B*random();
... whererandom()给了我们一个范围内的值[0 1]。换句话说,每个点移动一个介于 0 和 之间的随机值B。这是我们得到的:
现场演示(仅限 Chrome 和 Edge)
看到了吗?我们得到了另一个具有相同代码结构的很酷的动画。我们只更改了一条指令。我们可以将该指令设置为变量,这样我们就可以在不更改 JavaScript 的情况下决定是使用统一配置还是随机配置。我们引入另一个变量 ,T它的行为类似于布尔值:
if(T == 0)
var r = RADIUS - B*(i%2);
else
var r = RADIUS - B*random();
我们有两个动画,多亏了T变量,我们可以决定使用哪一个。我们可以N使用变量来控制点数和距离V。是的,有很多变数,但别担心,我们会在最后总结一切。
那个random()函数在做什么?
它与我在上一篇文章中使用的功能相同。我们在那里看到我们不能依赖默认的内置函数,因为我们需要一个随机函数,我们能够控制种子以确保我们始终获得相同的随机值序列。所以种子值也是我们可以控制以获得不同斑点形状的另一个变量。手动更改该值并查看结果。
在上一篇文章中,我提到 Paint API 消除了 CSS 方面的所有复杂性,这使我们可以更灵活地创建复杂的动画。例如,我们可以将到目前为止我们所做的与关键帧和cubic-bezier():
现场演示(仅限 Chrome 和 Edge)
该演示包含另一个使用抛物线的示例,我在之前的文章中详细介绍过。
控制点的移动
在我们迄今为止创建的所有 blob 中,我们为我们的点考虑了相同的运动。无论我们使用统一配置还是随机配置,我们总是沿着一条线将点从边缘移动到圆的中心。
现在让我们看看如何控制该运动以获得更多动画。这个逻辑背后的想法很简单:我们以不同的方式移动x和y。
以前我们是这样做的:
var x = Math.cos((i / N) * (2 * Math.PI)) * (Radius - F(B)) + CenterX;
var y = Math.sin((i / N) * (2 * Math.PI)) * (Radius - F(B)) + CenterY;
... whereF(B)是一个基于B保持转换的变量的函数。
现在我们将有这样的东西:
var x = Math.cos((i / N) * (2 * Math.PI)) * (Radius - Fx(B)) + CenterX;
var y = Math.sin((i / N) * (2 * Math.PI)) * (Radius - Fy(B)) + CenterY;
...我们以不同的方式更新x和y变量以制作更多动画。让我们尝试一些。
一轴运动
对于这一点,我们将使其中一个函数等于 0,并使另一个函数保持与以前相同。换句话说,一个坐标在动画中保持固定
如果我们这样做:
Fy(B) = 0
……我们得到:
现场演示(仅限 Chrome 和 Edge)
这些点只是水平移动以获得另一种效果。我们可以通过制作Fx(B)=0(参见演示)轻松地对另一个轴执行相同的操作。
我想你应该已经明白了。我们所要做的就是调整每个轴的功能以获得不同的动画。
向左或向右移动
让我们尝试另一种运动。与其让点汇聚到中心,不如让它们朝同一方向(向右或向左)移动。我们需要一个基于由角度定义的点的位置的条件。

我们有两组点:[90deg 270deg]范围内的点(左侧),以及沿形状骑行侧的其余点。如果我们考虑索引,我们可以用不同的方式表达范围,比如点数[0.25N 0.75N]在哪里N。
诀窍是为每个组设置不同的符号:
var sign = 1;
if(i<0.75*N && i>0.25*N)
sign = -1; /* we invert the sign for the left group */
if(T == 0)
var r = RADIUS - B*sign*(i%2);
else
var r = RADIUS - B*sign*random();
var x = Math.cos((i / N) * (2 * Math.PI)) * r + cx;
我们得到:
现场演示(仅限 Chrome 和 Edge)
我们能够获得相同的方向,但有一个小缺点:一组点超出了遮罩区域,因为我们增加了某些点的距离,同时减少了其他点的距离。我们需要减小圆的大小,以便为所有点留出足够的空间。
我们只是使用V定义B变量最终值的值来减小圆的大小。换句话说,它是一个点可以达到的最大距离。

我们的初始形状(由灰色区域表示并用绿点定义)将覆盖较小的区域,因为我们将用 值减小半径值V:
const V = parseFloat(properties.get('--v'));
const RADIUS = size.width/2 - V;
现场演示(仅限 Chrome 和 Edge)
我们修复了点在外面的问题,但我们还有另一个小缺点:可悬停区域是相同的,所以效果甚至在光标击中图像之前就开始了。如果我们也可以减少那个区域,让一切都保持一致,那就太好了。
我们可以使用额外的包装器和负边距技巧。这是演示。诀窍很简单:
.box {
display: inline-block;
border-radius: 50%;
cursor: pointer;
margin: calc(var(--v) * 1px);
--t: 0;
}
img {
display: block;
margin: calc(var(--v) * -1px);
pointer-events: none;
-webkit-mask: paint(blob);
--b: 0;
transition:--b .5s;
}
.box:hover img {
--b: var(--v)
}
额外的包装器是一个inline-block元素。它内部的图像具有等于V变量的负边距,该变量减少了形状框的整体大小。然后我们禁用图像元素上的悬停效果(使用pointer-events: none),因此只有框元素触发过渡。最后,我们为 box 元素添加一些边距以避免任何重叠。
和之前的效果一样,这个效果也可以结合cubic-bezier()和 关键帧来获得更酷的动画。下面是一个使用我的正弦曲线在悬停时产生摆动效果的示例。
现场演示(仅限 Chrome 和 Edge)
如果我们添加一些变换,我们可以创建一种奇怪(但很酷)的滑动动画:
现场演示(仅限 Chrome 和 Edge)
圆周运动
让我们处理另一个有趣的运动,它允许我们创建无限的“逼真”的斑点动画。我们不会将我们的点从一个位置移动到另一个位置,而是将它们围绕一个轨道旋转以进行连续运动。

我们点的初始位置(绿色)将成为一个轨道,红色圆圈是我们的点将采取的路径。换句话说,每个点将沿着半径为 r 的圆围绕其初始位置旋转。
我们需要做的就是确保两条相邻路径之间没有重叠,因此半径需要有一个最大允许值。

我不会详细说明数学,但最大值等于:
const r = 2*Radius*Math.sin(Math.PI/(2*N));
现场演示(仅限 Chrome 和 Edge)
这是代码的相关部分:
var r = (size.width)*Math.sin(Math.PI/(N*2));
const RADIUS = size.width/2 - r;
// ...
for(var i = 0; i < N; i++) {
var rr = r*random();
var xx = rr*Math.cos(B * (2 * Math.PI));
var yy = rr*Math.sin(B * (2 * Math.PI));
var x = Math.cos((i / N) * (2 * Math.PI)) * RADIUS + xx + cx;
var y = Math.sin((i / N) * (2 * Math.PI)) * RADIUS + yy + cy;
point[i] = [x,y];
}
我们得到半径的最大值,然后从主半径减小该值。请记住,我们需要为我们的点留出足够的空间,因此我们需要像之前的动画一样减少遮罩区域。然后对于每个点,我们得到一个随机半径rr(介于 0 和 之间r)。然后我们使用xx和计算圆形路径内的位置yy。最后,我们将围绕其轨道路径,并获得最后的位置(x,y值)。
请注意B,像往常一样,带有转换的值。这一次,我们将有一个从 0 到 1 的过渡,以便围绕轨道进行完整的转弯。
螺旋运动
再给你一份!这个是前两个的组合。
我们看到了如何围绕固定轨道移动点以及如何将点从圆的边缘移动到中心。我们可以将两者结合起来,让我们的点围绕一个轨道移动,我们通过将它从边缘移动到中心来对轨道做同样的事情。
让我们在现有代码中添加一个额外的变量:
for(var i = 0; i < N; i++) {
var rr = r*random();
var xx = rr*Math.cos(B * (2 * Math.PI));
var yy = rr*Math.sin(B * (2 * Math.PI));
var ro = RADIUS - Bo*random();
var x = Math.cos((i / N) * (2 * Math.PI)) * ro + xx + cx;
var y = Math.sin((i / N) * (2 * Math.PI)) * ro + yy + cy;
point[i] = [x,y];
}
如您所见,我使用了与我们看到的第一个动画完全相同的逻辑。我们用一个随机值(Bo在这种情况下控制)来减小半径。
现场演示(仅限 Chrome 和 Edge)
又一个花哨的斑点动画!现在每个元素都有两个动画:一个动画轨道 ( Bo),另一个动画其圆形路径中的点 ( B)。想象一下通过简单地调整动画值(持续时间、轻松等)可以获得的所有效果!
把所有东西放在一起
Oof,我们已经完成了所有的动画!我知道你们中的一些人可能已经迷失了我们引入的所有变化和所有变量,但不用担心!我们现在将总结所有内容,您会发现它比预期的要容易。
我还想强调的是,我所做的并不是所有可能动画的详尽列表。我只解决了其中的几个。我们可以定义更多,但本文的主要目的是了解整体结构并能够根据需要对其进行扩展。
让我们总结一下我们所做的以及主要的几点:
-
点数(
N):此变量是控制 blob 形状粒度的变量。我们在 CSS 中定义它,稍后用于定义控制点的数量。 -
运动的类型(
T):在我们看过的几乎所有动画中,我总是考虑两种动画:“均匀”动画和“随机”动画。我称之为我们可以使用TCSS 中设置的变量来控制的运动类型。我们将在代码中的某个地方根据该T变量执行 if-else 。 -
随机配置:在处理随机运动时,我们需要使用我们自己的
random()函数来控制种子,以便每个元素具有相同的随机序列。种子也可以被认为是一个变量,一个产生不同形状的变量。 -
运动的本质:
这是点所走的路径。我们可以有很多变化,例如:
- 从圆的边缘到中心
- 单轴运动(x 轴或 y 轴)
- 圆周运动
- 螺旋运动
- 和许多其他人……
与运动的类型一样,运动的性质也可以通过引入另一个变量来有条件,这里可以做的事情没有限制。我们所要做的就是找到数学公式来创建另一个动画。
- 动画变量(
B):这是包含过渡/动画的 CSS 变量。我们通常应用从 0 到某个值的过渡/动画(在所有带有变量的示例中定义V)。该变量用于表示我们点的位置。更新该变量在逻辑上更新位置;因此我们得到了动画。在大多数情况下,我们只需要为一个变量设置动画,但我们可以根据运动的性质设置更多的动画(例如我们使用两个变量的螺旋运动)。 - **形状区域:**默认情况下,我们的形状覆盖整个元素区域,但我们看到一些移动需要点到形状之外。这就是为什么我们必须减少面积。我们通常通过
B(由 定义V)的最大值或基于运动性质的不同值来执行此操作。
我们的代码结构如下:
var point = [];
/* The center of the element */
const cx = size.width/2;
const cy = size.height/2;
/* We read all of the CSS variables */
const N = parseInt(properties.get('--n')); /* number of points */
const T = parseInt(properties.get('--t')); /* type of movement */
const Na = parseInt(properties.get('--na')); /* nature of movement */
const B = parseFloat(properties.get('--b')); /* animation variable */
const V = parseInt(properties.get('--v')); /* max value of B */
const seed = parseInt(properties.get('--seed')); /* the seed */
// ...
/* The radius of the shape */
const RADIUS = size.width/2 - A(V,T,Na);
/* Our random() function */
let random = function() {
// ...
}
/* we define the position of our points */
for(var i = 0; i < N; i++) {
var x = Fx[N,T,Na](B) + cx;
var y = Fy[N,T,Na](B) + cy;
point[i] = [x,y];
}
/* We draw the shape, this part is always the same */
ctx.beginPath();
// ...
ctx.closePath();
/* We fill it with a solid color */
ctx.fillStyle = '#000';
ctx.fill();
如您所见,代码并不像您预期的那么复杂。所有的工作都在那些函数Fx和中Fy,它定义了基于N,T和的运动Na。我们还有A减少形状大小的功能,以防止动画期间点溢出形状。
让我们检查一下 CSS:
@property --b {
syntax: '<number>';
inherits: false;
initial-value: 0;
}
img {
-webkit-mask:paint(blob);
--n: 20;
--t: 0;
--na: 1;
--v: 50;
--seed: 125;
--b: 0;
transition: --b .5s;
}
img:hover {
--b: var(--v);
}
我认为代码是不言自明的。您可以定义变量、应用蒙版并B使用过渡或关键帧为变量设置动画。就这样!
我将以最后的演示结束本文,我将所有变体放在一起。您所要做的就是使用 CSS 变量