CSS Paint API(神奇的 Houdini 家族的一部分)为 CSS 设计开启了一个激动人心的新世界。使用 Paint API,我们可以创建自定义形状、复杂图案和精美的动画——所有这些都带有一丝随机性——以一种可移植、快速且响应迅速的方式。
我们将深入到生成 CSS 魔法的沸腾大锅中,创建我最喜欢的形状,**Blob**。随机 Blob 对于任何刚接触生成艺术/设计的人来说都是一个很好的起点,并且我们将在学习过程中学习 CSS Paint API,因此对于刚接触这个领域的人来说,这是一个理想的起点。您很快就会成为生成 CSS 方面的魔法师!
让我们跳上我们的扫帚,召唤一些形状。
生成?
对于一些阅读本文的人来说,生成艺术可能是一个陌生的主题。如果您已经熟悉生成艺术/设计,可以随意跳到下一节。如果没有,这里有一个小例子
想象一下,您正坐在桌子旁。您有三个印章、一些骰子和一张纸。每个印章上都有不同的形状。有一个正方形、一条线和一个圆圈。您掷骰子。如果骰子落在**一**上,您就在纸上使用**正方形**印章。如果骰子落在**二**上,您就使用**线条**印章。如果它落在**三**上,您就使用圆圈印章。如果骰子显示四、五或六,则什么也不做。您重复掷骰子和盖章的过程,直到页面上充满形状——这就是生成艺术!
一开始可能看起来有点吓人,但实际上,“生成”的含义就是这样——用随机/不可预测的元素创造的东西。我们定义一些规则,并让随机源引导我们走向结果。在上面的“模拟”示例中,随机源是一些骰子。当我们在浏览器中工作时,它可能是Math.random()
或其他类似函数。
为了暂时将事物带回 0 和 1 的世界,以下是如果用代码编写上述示例的样子
很酷吧?通过定义一些简单的规则并随机执行它们,我们创建了一个独特的图案。**在本教程系列中,我们将使用类似的生成技术来创建令人兴奋的用户界面。**
什么是 CSS Paint API,什么是 Worklet?
CSS Paint API 允许我们通过类似 HTML5 <canvas>
的绘图 API 来访问 CSS 本身(!) 。我们可以通过称为Worklet的东西来利用这种能力。
Worklet简而言之,是 JavaScript 类。每个 Worklet 类必须具有一个paint()
函数。Worklet 的paint()
函数可以以编程方式为任何期望图像的 CSS 属性创建图像。
例如
.my-element {
background-image: paint(texture);
}
这里,我们有一个虚构的texture
Worklet,它生成一个美丽的(我把它留给你的想象力)程序纹理。在通常为background-image
属性分配url(...)
值的地方,我们改为调用paint(worklet_name)
——这将运行 Worklet 的paint()
函数并将输出呈现到目标元素。
我们很快就会详细介绍如何编写 Worklet,但我希望在开始讨论它们之前先向您简要介绍一下它们是什么。
我们正在构建什么
因此,在本教程中,我们将构建一个生成 Blob Worklet。我们的 Worklet 将获取一些输入参数(作为 CSS 自定义属性,稍后会详细介绍),并返回一个美丽、随机的 Blob 形状。
让我们首先查看一些已完成 Worklet 的示例——如果一张图片胜过千言万语,那么一个 CodePen 必须胜过百万,对吧?
Blob Worklet 作为背景图像
首先,这是一个 Blob Worklet 自行运行的演示,它为元素的background-image
属性生成一个值
我鼓励您查看上面 CodePen 的 CSS,更改自定义属性,调整元素大小,并查看会发生什么。看看形状如何流畅地调整大小并在自定义属性更改时更新?现在不用担心理解如何做到这一点。在这个阶段,我们只关心我们正在构建的什么。
生成图像蒙版,一个实际用例
太棒了,现在我们已经看到了“独立”的 Worklet,让我们看看如何使用它。在此示例中,Worklet 充当生成图像蒙版
结果(我认为)相当醒目。Worklet 为设计添加了自然的、引人注目的曲线。此外,每次页面加载时蒙版形状都不同,这是一种保持 UI 新鲜和令人兴奋的绝佳方式——单击上面 CodePen 上的“重新运行”以查看此效果。这种不断变化的行为非常微妙,当然,但我希望它能给注意到它的人带来一点快乐。网络可能是一个非常冷冰冰、毫无生机的地方,像这样的生成触摸可以使它感觉更具活力!
注意:我当然不是建议我们都开始让我们的整个界面随机变化。这对可用性来说将是可怕的!这种行为最适合谨慎地应用,并且仅应用于网站或应用程序的展示元素。例如,考虑博客文章标题、英雄图像、微妙的背景图案等。
现在,这只是一个示例(而且很简单),但我希望它能为您提供一些关于如何在您自己的设计和开发中使用 Blob Worklet 的想法。对于任何寻找额外灵感的人来说,快速Dribbble 搜索“Blob”应该会给你很多想法!
等等,我需要 CSS Paint API 来制作 Blob 吗?
简而言之,不需要!
实际上,有很多方法可以在 UI 设计中制作 Blob。您可以使用像Blobmaker这样的工具,使用border-radius
进行一些魔法,使用常规的<canvas>
元素,任何方式都可以!通往 Blob 城市的道路很多。
然而,这些都不完全等同于使用 CSS Paint API。为什么?
好吧,列举几个原因……
它允许我们在 CSS 中表达自己
与其拖动滑块、调整半径或不断点击“重新生成”以期获得完美的 Blob,我们可以使用几个易于理解的值来获得我们所需的内容。
例如,我们将在本教程中构建的 Blob Worklet 将获取以下输入属性
.worklet-target {
--blob-seed: 123456;
--blob-num-points: 8;
--blob-variance: 0.375;
--blob-smoothness: 1;
--blob-fill: #000;
}
需要您的 Blob 非常微妙和简约?减少--blob-variance
自定义属性。需要它们详细且夸张?把它调高!
想以更粗野主义的方向重新设计您的网站?没问题!无需重新导出数百个资产或自定义编码一堆border-radius
属性,只需将--blob-smoothness
自定义属性减少到零即可
方便吧?CSS Paint API 通过 Worklet 允许我们创建与设计系统完美契合的独特 UI 元素。
注意:在上面的示例中,我使用了GSAP来为我们在本教程中构建的绘制工作线程的输入属性添加动画。
它性能极佳
恰好生成式工作在计算方面可能会变得稍微繁重一些。我们经常发现自己需要循环遍历大量元素,执行计算以及其他有趣的事情。当我们考虑到可能需要在一个页面上创建多个程序化的生成式视觉效果时,性能问题可能会成为风险。
幸运的是,CSS 绘制 API 工作线程在主浏览器线程之外执行所有操作。主浏览器线程是我们通常编写的 JavaScript 代码存在并执行的地方。以这种方式编写代码完全没问题(通常也是首选),**但它可能存在局限性。当我们试图在主浏览器线程上做太多事情时,UI 可能会变得迟缓甚至被阻塞。**
由于工作线程在与主网站或应用程序不同的线程上运行,因此它们不会“阻塞”或减慢界面速度。此外,这意味着浏览器可以启动许多独立的工作线程实例,并在需要时调用它们——这类似于容器化,并带来极快的性能提升!
它不会使 DOM 混乱
因为 CSS 绘制 API 本质上是在 CSS 属性中添加一个图像,所以它不会在 DOM 中添加任何额外的元素。对我来说,这感觉像是创建生成式视觉元素的一种非常简洁的方法。您的 HTML 结构保持清晰、语义化且不受污染,而您的 CSS 则处理外观。
浏览器支持
值得注意的是,CSS 绘制 API 是一项相对较新的技术,尽管支持度正在增长,但在一些主流浏览器中它仍然不可用。这是一个浏览器支持表
此浏览器支持数据来自Caniuse,其中包含更多详细信息。数字表示浏览器从该版本开始支持该功能。
桌面
Chrome | Firefox | IE | Edge | Safari |
---|---|---|---|---|
65 | 否 | 否 | 79 | 否 |
移动/平板电脑
Android Chrome | Android Firefox | Android | iOS Safari |
---|---|---|---|
127 | 否 | 127 | 否 |
尽管浏览器支持度仍然有些不足——在本教程中,我们将了解如何使用由GoogleChromeLabs维护的css-paint-polyfill,以确保所有浏览器的用户都能享受我们的创作。
此外,我们将了解如何在 CSS 绘制 API 不受支持时“优雅降级”。polyfill 意味着额外的 JavaScript 负担,因此对于某些人来说,它不是一个可行的解决方案。如果你是这种情况,请不要担心。我们将探索所有人的浏览器支持选项。
让我们开始编码!
好的,好的!我们知道我们要构建什么,以及为什么 CSS 绘制 API 很棒——现在让我们开始编码!首先,让我们启动一个开发环境。
注意:如果您在本教程中的任何时候感到有点迷茫,您可以查看工作线程的完成版本。
简单的开发环境
为了开始,我创建了一个worklet-starter-kit 仓库。第一步,访问 GitHub 并克隆它。克隆并导航到仓库内部后,运行
npm install
然后运行
npm run start
运行上述命令后,当前目录中会启动一个简单的开发服务器,并且您的默认浏览器会打开。由于工作线程必须通过 HTTPS 或从localhost
加载——此设置确保我们可以在没有任何 CORS 问题的情况下使用我们的工作线程。启动工具包还处理在进行任何更改时自动刷新浏览器。
除了提供内容服务和基本的实时重载之外,此仓库还提供了一个简单的构建步骤。由esbuild提供支持,此过程会捆绑工作线程中任何 JavaScript 的import
,并将结果输出到worklet.bundle.js
文件中。在worklet.js
中进行的任何更改都会自动反映在worklet.bundle.js
中。
如果您仔细查看仓库,可能会注意到已经有一些 HTML 和 CSS 代码。我们有一个简单的index.html
文件,其中包含一个worklet-canvas
div,以及一些 CSS 代码将其居中于页面上并将其缩放至视口。可以将其视为所有工作线程实验的空白画布!
初始化我们的工作线程
好的,现在我们的开发环境已经启动并运行,是时候创建我们的工作线程了。让我们从导航到worklet.js
文件开始。
注意:请记住,worklet.bundle.js
是由我们的构建步骤自动生成的。我们永远不想直接编辑此文件。
在我们的worklet.js
文件中,我们可以定义我们的Blob
类并使用registerPaint
函数将其注册。我们向registerPaint
传递两个值——我们希望工作线程具有的名称(在我们的例子中为 blob)以及定义它的类
class Blob {}
registerPaint("blob", Blob);
太棒了!我们刚刚朝着创建 blob迈出了第一步!
paint()
函数
添加现在,还没有发生太多事情,所以让我们向我们的Blob
类添加一个简单的paint()
函数来检查一切是否正常。
paint(ctx, geometry, properties) {
console.log(`Element size is ${geometry.width}x${geometry.height}`);
ctx.fillStyle = "tomato";
ctx.fillRect(0, 0, geometry.width, geometry.height);
}
我们可以将此paint()
函数视为回调。它最初在工作线程的目标元素首次渲染时运行。在此之后,每当元素的尺寸发生变化或工作线程的输入属性更新时,它都会再次运行。
当调用paint()
函数时,它会自动传递一些值。在本教程中,我们使用了前三个
context
——一个类似于<canvas>
元素的 2D 绘制上下文,我们用它来绘制东西。geometry
——一个包含目标元素宽度和高度的对象properties
——一个自定义属性数组
现在我们已经定义了一个简单的paint()
函数,让我们转到index.html
文件并加载我们的工作线程。为此,我们将在结束的</body>
标记之前添加一个新的<script>
<script>
if (CSS["paintWorklet"] !== undefined) {
CSS.paintWorklet.addModule("./worklet.bundle.js");
}
</script>
注意:我们正在注册工作线程的捆绑版本!
太棒了。我们的blob
工作线程现在已加载并准备在我们的 CSS 中使用。让我们用它来为我们的worklet-canvas
类生成一个background-image
.worklet-canvas {
background-image: paint(blob);
}
添加上述代码段后,您应该会看到一个红色正方形。我们的工作线程启动了!干得好。如果调整浏览器窗口大小,您应该会看到浏览器控制台中打印的worklet-canvas
元素的尺寸。请记住,每当工作线程目标的尺寸发生变化时,paint()
函数都会运行。
定义工作线程的输入属性
为了让我们的工作线程能够生成漂亮的 blob,我们需要帮助它并传递一些属性。我们需要以下属性:
--blob-seed
——伪随机数生成器的“种子”值;稍后会详细介绍--blob-num-points
——根据沿形状使用的点数确定 blob 的细节程度--blob-variance
——blob 控制点的变化程度--blob-smoothness
——blob 边缘的平滑度/锐利度--blob-fill
——blob 的填充颜色
让我们告诉我们的工作线程它将接收这些属性,并且需要监视这些属性的变化。为此,我们可以回到我们的Blob
类并添加一个inputProperties
getter
static get inputProperties() {
return [
"--blob-seed",
"--blob-num-points",
"--blob-variance",
"--blob-smoothness",
"--blob-fill",
];
}
不错。现在我们的工作线程知道要期望哪些输入属性了,我们应该将它们添加到我们的 CSS 中
.worklet-canvas {
--blob-seed: 123456;
--blob-num-points: 8;
--blob-variance: 0.375;
--blob-smoothness: 1;
--blob-fill: #000;
}
现在,在这一点上,我们可以使用 CSS 属性和值 API(Houdini 家族的另一个成员)**来分配一些默认值,并使这些自定义属性在我们的小程序中更容易解析。但是,不幸的是,目前,属性和值 API 的浏览器支持情况并不理想。
目前,为了简单起见,我们将保留自定义属性的原样——改为在小程序中依靠一些基本的解析函数。
再回到我们的 worklet 类,让我们添加这些实用函数。
propToString(prop) {
return prop.toString().trim();
}
propToNumber(prop) {
return parseFloat(prop);
}
在没有属性和值 API 的情况下,这些简单的实用函数将帮助我们将传递给 paint()
的 properties
转换为可用的值。
使用我们新的辅助函数,我们可以解析 properties
并定义一些变量,以便在我们的 paint()
函数中使用。我们也来移除旧的“调试”代码。
paint(ctx, geometry, properties) {
const seed = this.propToNumber(properties.get("--blob-seed"));
const numPoints = this.propToNumber(properties.get("--blob-num-points"));
const variance = this.propToNumber(properties.get("--blob-variance"));
const smoothness = this.propToNumber(properties.get("--blob-smoothness"));
const fill = this.propToString(properties.get("--blob-fill"));
}
如果您记录了任何这些变量,您应该会看到 paint()
函数提供的 properties
完全对应于我们之前在 CSS 中定义的自定义属性。
如果您打开开发者工具,检查 worklet-canvas
元素,并更改任何这些自定义属性——您应该会看到日志重新运行并反映更新的值。为什么?我们的 worklet 会对输入属性的任何更改做出反应,并在检测到更改时重新运行其 paint()
函数。
好的,朋友们,现在是时候开始形成我们的 blob 形状了。为此,我们需要一种生成**随机数**的方法。毕竟,这正是使我们的 blob 具有生成性的原因!
现在,您可能在想,“嘿,我们可以使用 Math.random()
来做这个!”在许多方面,您都是正确的。但是,在 CSS Paint API worklet 中使用“常规”随机数生成器存在一个问题。让我们来看看。
Math.random()
的问题
我们之前注意到 worklet 的 paint()
函数运行频率相当高。如果我们使用 Math.random()
等方法在 paint()
中生成随机值——它们每次函数执行时都会不同。不同的随机数意味着每次 worklet 重新渲染都会产生不同的视觉效果。我们根本不希望这样。当然,我们希望我们的 blob 是随机的,但仅在创建时。它们不应该在页面上存在后发生变化,除非我们明确告诉它们这样做。
我发现这个概念一开始有点难以理解,所以我制作了几个 CodePen(最好在原生支持 CSS Paint API 的浏览器中查看)来帮助演示。在第一个示例中,我们有一个 worklet 使用 Math.random()
设置随机背景颜色。
警告:调整下面元素的大小会导致颜色闪烁。
尝试调整上面元素的大小,并注意背景颜色如何在更新时发生变化。对于某些利基应用和有趣的演示,这可能是您想要的。但是,在大多数实际使用场景中,它并不是。除了视觉上的不和谐外,这种行为 也可能对对运动敏感的用户造成无障碍问题。想象一下,您的 worklet 包含数百个点,每当页面上的某个东西大小发生变化时,这些点都会四处飞舞并闪烁!
幸运的是,这个问题很容易解决。解决方案是什么?一个伪随机数生成器!伪随机数生成器(或 PRNG)根据种子生成随机数。给定相同的种子值,PRNG 总是返回相同的随机数序列——这对我们来说非常完美,因为我们可以在每次 paint()
函数运行时重新初始化 PRNG,确保相同的随机值序列!
这是一个演示 PRNG 如何工作的 CodePen。
点击“生成”以选择一些随机数——然后,再点击几次“生成”。注意每次点击时数字序列是否相同?现在,尝试更改种子值,并重复此过程。数字将与之前的种子值不同,但在各代之间保持一致。这就是 PRNG 的魅力所在。可预测的随机性!
这是使用 PRNG 而不是 Math.random()
的 random-background-color CodePen。
啊!好多了!元素在页面加载时设置了随机颜色,但在调整大小时背景颜色不会改变。完美!您可以通过在上面的 CodePen 上点击“重新运行”并调整元素的大小来测试这一点。
向我们的 worklet 添加伪随机数
让我们继续在 Blob
类定义之上添加一个 PRNG 函数。
// source: https://github.com/bryc/code/blob/master/jshash/PRNGs.md
function mulberry32(a) {
return function () {
a |= 0;
a = (a + 0x6d2b79f5) | 0;
var t = Math.imul(a ^ (a >>> 15), 1 | a);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
现在,如果我说我完全理解这个函数在做什么,我就是在撒谎。我通过 Jake Archibald 关于使用 CSS Paint API 可预测地随机的文章 发现了这段漂亮的代码片段,并且从那以后在很多工作中都使用了它。您可以在 GitHub 上找到此函数的原始存储库——它包含了许多优秀的 PRNG,绝对值得一看。
注意:虽然我不完全理解这个函数是如何工作的,但我了解如何使用它。通常,在生成式世界中工作时(如果您和我一样,无论如何!),您会发现自己处于这种情况。当您这样做时,不要担心!使用代码片段来创建一些艺术/设计而无需完全了解它是如何工作的,这绝对是可以的。我们可以边做边学,这很棒。
好的,太好了,我们有了一个 PRNG 函数。让我们将其添加到 paint()
中。
const random = mulberry32(seed);
在此代码片段中,我们使用 --blob-seed
自定义属性作为其**种子**值调用 mulberry32()
,它返回一个全新的函数。这个新函数——random
——返回一个介于**零**和**一**之间的随机数。
很好,让我们将我们闪亮的新 PRNG 派上用场。
快速旁白:使用 CSS Paint API 绘制
在使用 CSS Paint API worklet 时,就像 HTML <canvas>
一样,我们会在 2D 上下文中绘制所有内容。此上下文具有宽度和高度。对于 worklet,此上下文的宽度和高度始终与 worklet 绘制的元素的宽度和高度匹配。
例如,如果我们想在 1920x1080px
上下文的中心添加一个点,我们可以将其可视化如下。

当我们开始编写“渲染”代码时,这一点需要注意。
我们的 blob 如何形成,概述
在我们编写任何代码之前,我想向您展示一个关于我们将如何创建 blob 形状的小 SVG 动画。如果您像我一样是视觉学习者,您可能会发现动画参考有助于理解这类事情。
将此过程分解为三个步骤。
- 在圆的半径周围绘制几个等距的点。
- 将每个点随机地拉向圆的中心。
- 通过每个点绘制一条平滑的曲线。
现在,事情将变得有点数学化,但不用担心。我们能做到!
定义 blob 的控制点
首先,让我们定义 blob 的radius
(半径)。blob 的半径决定了它的大小。
我们希望 blob 形状始终“适合”其绘制到的元素。为了确保这一点,我们检查 worklet 目标元素的宽度和高度,并相应地设置 blob 的半径。我们的 blob 本质上是一个奇怪的圆,圆的总宽度/高度始终等于其半径乘以二,因此我们将此值除以二以匹配。让我们添加一些代码来在 paint()
函数中实现这一点。
const radius = Math.min(geometry.width, geometry.height) / 2;
这是一张帮助解释这里发生了什么的图片。

酷!现在我们知道了 blob 的半径应该是什么,我们可以初始化它的点。
const points = [];
const center = {
x: geometry.width / 2,
y: geometry.height / 2,
};
const angleStep = (Math.PI * 2) / numPoints;
for (let i = 1; i <= numPoints; i++) {
const angle = i * angleStep;
const point = {
x: center.x + Math.cos(angle) * radius,
y: center.y + Math.sin(angle) * radius,
};
}
呼!在此代码片段中,我们“沿着”圆的圆周“行走”,并在我们走动时放下一些等距的点。这是如何工作的?
首先,我们定义一个 angleStep
变量。圆周上两点之间的最大角度为 Pi × 2
。通过将 Pi × 2
除以我们要创建的“点”的数量,我们得到了每个点之间所需的(等距)角度。
接下来,我们循环遍历每个点。对于每个点,我们定义一个 angle
变量。此变量是我们的 angleStep
乘以点的索引。给定圆的半径、角度和中心点,我们可以使用 Math.cos()
和 Math.sin()
来绘制每个点。
注意:如果您想了解更多关于三角函数的知识,我强烈推荐 Michelle Barker 的 精彩系列!
现在我们已经有一些完美、美丽、等距的点位于圆的圆周周围——我们应该把它们弄乱。为此,我们可以将每个点随机地“拉”向圆的中心。
我们如何做到这一点?
首先,让我们在定义 mulberry32
的下方添加一个新的 lerp
函数(线性插值的缩写)。
function lerp(position, target, amt) {
return {
x: (position.x += (target.x - position.x) * amt),
y: (position.y += (target.y - position.y) * amt),
};
}
此函数接受一个起点、一个终点和一个介于**零**和**一**之间的“amount”值。此函数的返回值是一个新点,位于起点和终点之间的某个位置。
在我们的 worklet 中,就在 for 循环中定义 point
变量的下方,我们可以使用这个 lerp
函数将点“拉向”中心位置。我们将修改后的点存储在我们的 points
数组中。
points.push(lerp(point, center, variance * random()));
对于线性插值量,我们使用 --blob-variance
属性乘以 random()
生成的随机数——由于 random()
始终返回 **零** 和 **一** 之间的值,因此此值始终介于 **零** 和 --blob-variance
数之间。
注意:较高的 --blob-variance
值会导致更疯狂的 blob 形状,因为每个点最终都可能更靠近中心。
绘制曲线
因此,我们将 blob 的点存储在一个数组中。但是,目前它们还没有被用于任何用途!在 blob 创建过程的最后一步,我们将通过它们中的每一个绘制一条平滑的曲线。
为了绘制这条曲线,我们将使用一种称为 Catmull-Rom 样条曲线 的东西。简而言之,Catmull-Rom 样条曲线 是一种通过任意数量的 { x, y }
点绘制平滑贝塞尔曲线的好方法。使用样条曲线,我们不必担心任何棘手的控制点计算。我们传入一个点的数组,并获得一条美丽的、有机的曲线。毫不费力。
让我们转到 worklet.js
文件的开头,并添加以下导入语句
import { spline } from "@georgedoescode/generative-utils";
然后像这样安装包
npm i @georgedoescode/generative-utils
这个 spline
函数相当庞大,而且有点复杂。因此,我将其打包并添加到我的 generative-utils 存储库中,这是一个包含一些便捷的生成艺术实用程序的小集合。
一旦我们导入了 spline
——我们就可以像这样在 worklet 的 paint()
函数中使用它
ctx.fillStyle = fill;
ctx.beginPath();
spline(points, smoothness, true, (CMD, data) => {
if (CMD === "MOVE") {
ctx.moveTo(...data);
} else {
ctx.bezierCurveTo(...data);
}
});
ctx.fill();
注意:将此代码段放在 for 循环之后!
我们传入我们的点、--blob-smoothness
属性和一个标志,让 spline
知道它应该返回一个闭合形状。此外,我们使用 --blob-fill
自定义属性来设置 blob 的填充颜色。现在,如果我们查看浏览器窗口,应该会看到类似这样的东西!

万岁!我们做到了!spline
函数已成功地通过每个点绘制了一条平滑的曲线,从而创建了一个华丽的(且随机的)blob 形状。如果你希望你的 blob 稍微不那么圆润,请尝试降低 --blob-smoothness
属性的值。
现在,我们只需要再添加一点随机性。
随机的随机种子值
现在,我们 blob 的 PRNG 种子是一个固定值。我们之前在 CSS 中定义了这个 --blob-seed
自定义属性,其值为 123456
——这很好,但这意味着 random()
生成的随机数,以及 blob 的核心形状,始终相同。
在某些情况下,这是理想的。你可能不希望你的 blob 是随机的!你可能希望选择一些完美的种子值,并在你的网站上使用它们,作为半生成设计系统的一部分。但在其他情况下,你可能希望你的 blob 是随机的——就像我之前向你展示的图像蒙版示例一样。
我们如何做到这一点?随机化种子!
现在,这并不像看起来那样简单。最初,当我编写本教程时,我心想,“嘿,我可以在 Blob
类的构造函数中初始化种子值!”不幸的是,我错了。
由于浏览器可能会启动多个 worklet 实例来处理对 paint()
的调用——几个 Blob
类中的一个可能会最终渲染 blob!如果我们在 worklet 类内部初始化我们的种子值,则此值在不同实例之间将不同,并可能导致我们之前讨论的视觉“故障”。
要测试这一点,请向你的 Blob
类添加一个 constructor
函数,并在其中添加以下代码
constructor() {
console.log(`My seed value is ${Math.random()}`);
}
现在,查看你的浏览器控制台,并调整窗口大小。在大多数情况下,你会得到多个具有不同随机值的日志。这种行为对我们来说并不好;我们需要我们的种子值保持不变。
为了解决这个问题,让我们在主线程上添加一些 JavaScript 代码。我将其添加到我们之前创建的 <script>
标签中
document
.querySelector(".worklet-canvas")
.style.setProperty("--blob-seed", Math.random() * 10000);
太棒了!现在,刷新浏览器窗口时,每次都应该会看到一个新的 blob 形状。
对于我们的简单演示,这很完美。在一个“真实”的应用程序中,你可能希望创建一个 .blob
类,在加载时定位所有实例,并更新每个元素的种子值。你还可以尝试将 blob 的方差、点数和圆度属性设置为随机值。
但是,对于本教程来说,就是这样!我们剩下的唯一工作就是确保我们的代码在所有浏览器中都能正常工作,或者在不正常工作时提供合适的回退方案。
加载 polyfill
通过添加 polyfill,我们的 CSS Paint API 代码将在所有主要浏览器中运行,但会增加额外的 JavaScript 体积。以下是如何更新我们的 CSS.paintWorklet.addModule
代码以向我们的示例添加一个 polyfill
(async function () {
if (CSS["paintWorklet"] === undefined) {
await import("https://unpkg.com/css-paint-polyfill");
}
CSS.paintWorklet.addModule("./worklet.bundle.js");
})();
使用此代码段,我们仅在当前浏览器不支持 CSS Paint API 时才加载 polyfill。不错!
基于 CSS 的回退方案
如果额外的 JavaScript 体积不是你的菜,没关系。我完全理解。幸运的是,使用 @supports
,我们可以为不支持 CSS Paint API 的浏览器定义一个轻量级的、仅基于 CSS 的回退方案。方法如下
.worklet-canvas {
background-color: var(--blob-fill);
border-radius: 49% 51% 70% 30% / 30% 30% 70% 70%;
}
@supports (background: paint(blob)) {
.worklet-canvas {
background-color: transparent;
border-radius: 0;
background-image: paint(blob);
}
}
在此代码段中,我们将 background-color
和 blob 样式的 border-radius
(由 fancy border radius 生成)应用于目标元素。如果支持 CSS Paint API,我们将删除这些值,并使用我们的 worklet 绘制一个生成的 blob 形状。太棒了!
旅程的终点
好吧,朋友们,我们完成了。引用 Grateful Dead 乐队的一句话——这真是一个漫长而奇怪的旅程!
我知道,这里有很多东西需要吸收。我们涵盖了生成艺术的核心概念,了解了 CSS Paint API 的所有内容,并且在此过程中创建了一些很棒的生成 blob。我不得不说,这还不错。
现在我们已经了解了基础知识,就可以开始创建各种生成魔法了。请继续关注我即将推出的更多生成 UI 设计教程,但在此期间,请尝试利用我们在本教程中学到的知识进行实验!我相信你有很多很棒的想法。
下次再见,CSS 魔法师们!
很棒的文章!非常感谢——有趣的生成设计方法。
嗨,George!
作为一名前美术画家,我出于必要转向了设计,然后出于热情转向了应用程序和 Web 开发。因此,考虑到我的背景,我真的很喜欢你的文章。我要再次感谢你,因为你让我了解了一个完全未知的 API,并教我如何使用它来创作某种艺术。我喜欢在你清晰易懂的写作过程中想到的各种可能性。
我当然不会把这些新知识扔进一个破袋子里。现在我在开发一个应用程序,但你为我提供了完美的游乐场,让我在感到不知所措时放松身心,暂时远离当前的工作。
非常感谢你!
嗨!我真的很兴奋地尝试一下,并且喜欢使用 CodePen 示例进行尝试。但我卡在了第一步,生成那个红色正方形。我得到一个空白页面,没有错误。我使用的是 Chrome,所以它应该可以工作。这些脚本的一些故障排除步骤是什么?
好的,我想我弄清楚我做错了什么。我只是下载了存储库而不是连接到它。我认为,由于我不是程序员,并且不会将内容发送回链中,因此这样做没有意义。但我想,仅仅将它放在我的本地主机上,我就失去了自动构建 worklet 包的能力。因此,我需要弄清楚如何在每次更改 worklet.js 时手动执行此操作,对吧?