CSS Paint API(神奇的Houdini家族的一部分)为CSS的设计打开了一扇令人兴奋的新世界之门。使用Paint API,我们可以创建自定义的形状、复杂的图案和美丽的动画--所有这些都带有随机性--而且是以一种可移植、快速和响应的方式。
我们将通过创建我最喜欢的形状--blob,来尝试一下生成性CSS魔法的大锅。对于任何一个刚接触生成艺术/设计的人来说,随机的小球都是一个很好的起点,我们将在学习过程中学习CSS绘画API,所以对于那些刚接触这个世界的人来说,这是一个理想的起点。你很快就会成为一个生成性CSS的魔术师。
让我们跳上我们的扫帚,变出一些形状。
生成式?
对于一些读者来说,生成艺术可能是一个陌生的话题。如果你对生成艺术/设计已经很熟悉了,请随意跳到下一节。如果没有,这里有一个小例子。
想象一下,你正坐在一张桌子前。你有三个邮票,一些骰子和一张纸。每张邮票上都有一个不同的形状。有一个正方形,一条直线,和一个圆形。你掷骰子。如果骰子落在一个上,你就用这页纸上的方形邮票。如果骰子落在二上,你就用线的印章。如果它落在三上,你就用圆圈印章。如果骰子的结果是四、五或六,你什么都不做。你重复掷骰子和盖章的过程,直到页面上布满了各种形状--这就是生成艺术!这就是生成艺术。
一开始可能看起来有点吓人,但实际上,这就是 "生成 "的意思--用机会/不可预测的元素创造的东西。我们定义一些规则,让随机性的源头引导我们得出结果。在上面的 "模拟 "例子中,随机性来源是一些骰子。当我们在浏览器中工作时,它可能是Math.random() 或其他类似的功能。
通过定义一些简单的规则并随机采取行动,我们已经创建了一个独特的模式。在这个教程系列中,我们将使用像这样的生成技术来创建令人兴奋的用户界面。
什么是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将接受一些输入参数(如CSS自定义属性,稍后会详细介绍),并返回一个漂亮的、随机的blob形状。
让我们先来看看这个已经完成的worklet的一些实例--如果一幅画能描绘出千言万语,那么CodePen一定能描绘出一百万,对吗?
作为背景图片的blob工作单元
首先,这里有一个blob worklet的演示,它只是自己挂着,为一个元素的background-image 属性生成一个值。
我鼓励你看一下上述CodePen的CSS,改变自定义属性,调整元素的大小,看看会发生什么。看到当自定义属性改变时,形状是如何流畅地调整和更新的吗?不要担心现在就能理解这是如何工作的。在这个阶段,我们只关心我们正在构建的_东西_。
生成性图像遮罩,一个实际的用例
很好,现在我们已经看到了 "独立 "的worklet,让我们看看我们如何使用它。 结果_(我认为_)是相当引人注目的。Worklet为设计添加了一个自然、醒目的曲线。此外,每次加载页面时,遮罩的形状都是不同的,这是保持用户界面新鲜感和刺激性的绝妙方法--点击上面CodePen的 "重新运行",可以看到这个效果。这种不断变化的行为肯定是微妙的,但我希望它能给注意到它的人带来_一点快乐_。网络可能是一个相当冷酷无情的地方,而像这样的生成性触摸可以使它感觉更有机。
注意: 我当然不是建议我们都开始让我们的整个界面随机变化。那对可用性来说是_很糟糕的_。这类行为最好只应用于你的网站或应用程序的展示性元素。想想看,博客文章的标题,英雄图像,微妙的背景图案,等等。
现在,这只是一个例子(而且是简单的例子),但我希望它能给你一些想法,让你知道如何在自己的设计和开发中使用blob小程序。如果有人想寻找一些额外的灵感,可以在Dribbble上快速搜索 "blob",应该会有一大堆的想法。
等等,我需要CSS Paint API来制作blobs吗?
简而言之,不需要。
事实上,有大量的方法来制作Blobs,用于你的UI设计。你可以使用Blobmaker这样的工具,使用border-radius ,使用普通的<canvas> ,等等。通往blob城市的道路有_很多_。
然而,这些都与使用CSS绘画API不尽相同。为什么呢?
好吧,举几个原因...
它允许我们在CSS中表达自己的观点
我们不需要拖动滑块,调整半径,或者无休止地点击 "重新生成 "来希望得到一个完美的blob,我们只需要使用一些人类可读的值就可以得到我们需要的东西。
例如,我们将在本教程中建立的blob工作单元需要以下输入属性:
.worklet-target {
--blob-seed: 123456;
--blob-num-points: 8;
--blob-variance: 0.375;
--blob-smoothness: 1;
--blob-fill: #000;
}
它具有超强的性能
碰巧的是,生成性工作在计算上可能会_有点_繁重。我们经常发现自己在很多元素中循环,进行计算,以及其他有趣的事情。当我们考虑到我们可能需要在一个页面上创建多个程序化的、生成性的视觉效果时,性能问题可能会成为一种风险。
幸运的是,CSS Paint API工作小程序在浏览器主线程之外完成了所有的魔法。浏览器主线程是我们_通常_编写的所有JavaScript存在和执行的地方。这样写代码是完全可以的(而且通常是最好的),**但它也有局限性。当我们试图在浏览器主线程上做太多的事情时,用户界面就会变得迟缓,甚至被阻塞。
由于worklets运行在与主网站或应用程序不同的线程上,它们不会 "阻塞 "或减慢界面。此外,这意味着浏览器可以旋转出很多独立的worklet实例,在需要的时候可以调用--这类似于容器化,并能带来_极快_的性能
它不会使DOM杂乱无章
因为CSS Paint API本质上是将图像添加到一个CSS属性中,它不会向DOM添加任何额外的元素。对我来说,这感觉是一种创建生成性视觉元素的超级干净的方法。你的HTML结构保持清晰、语义和无污染,而你的CSS则处理事物的外观。
浏览器支持
值得注意的是,CSS绘画API是一项相对较新的技术,虽然支持度在不断提高,但它在一些主要的浏览器中仍然不可用。这里有一个浏览器支持表。
这个浏览器支持数据来自Caniuse,它有更多的细节。数字表示该浏览器在该版本及以上支持该功能。
桌面浏览器
| 浏览器 | 火狐 | IE | 边缘浏览器 | Safari |
|---|---|---|---|---|
| 65 | 没有 | 没有 | 79 | 没有 |
手机/平板电脑
| 安卓浏览器 | 安卓火狐 | 浏览器 | iOS Safari |
|---|---|---|---|
| 92 | 没有 | 92 | 没有 |
尽管浏览器的支持仍然有点薄弱--在本教程中,我们将研究如何使用由GoogleChromeLabs维护的css-paint-polyfill,以确保所有浏览器的用户都能享受我们的创作。
此外,我们还将研究如何在不支持CSS Paint API的情况下 "优雅地失败"。Polyfill意味着额外的JavaScript重量,所以对于一些人来说,这不是一个可行的解决方案。如果你是这样的人,不要担心。我们将为每个人探索浏览器支持选项。
让我们来写代码吧!
好了,好了!我们知道了我们正在建造的东西,以及为什么CSS Paint API会如此受欢迎--现在让我们开始编码吧!首先,让我们建立一个开发环境。
注意: 如果你在本教程中的任何时候迷失了方向,你可以查看工作单元的完成版本。
一个简单的开发环境
为了让我们开始工作,我创建了一个worklet-starter-kit资源库。作为第一步,请到GitHub上克隆它。一旦你克隆了并浏览了该仓库,就运行:
npm install
接着是:
npm run start
一旦你运行了上述命令,一个简单的开发服务器就会在当前目录下启动,并打开你的默认浏览器。由于worklet必须通过HTTPS或从localhost 加载--这种设置确保我们可以使用我们的worklet而不存在任何CORS问题。当我们做任何改变时,启动套件还能处理自动刷新浏览器的问题。
除了为我们的内容提供服务和提供基本的实时重载,这个资源库还具有一个简单的构建步骤。在esbuild的支持下,这个过程将我们的worklet内的任何JavaScriptimports ,并将结果输出到一个worklet.bundle.js 文件。worklet.js 中的任何变化都会自动反映在worklet.bundle.js 中。
如果你在资源库里找找看,你可能会注意到已经有一些HTML和CSS在运行了。我们有一个简单的index.html 文件,其中有一个单一的worklet-canvas div,以及一些CSS,使其在页面上居中,并根据视口进行缩放。把它看作是一块空白的画布,供你进行所有的worklet实验
初始化我们的worklet
好了,现在我们的开发环境已经启动并运行,是时候创建我们的worklet了。让我们从导航到worklet.js 文件开始。
注意: 记住,worklet.bundle.js 是由我们的构建步骤自动生成的。我们不希望直接编辑这个文件。
在我们的worklet.js 文件中,我们可以定义我们的Blob 类,并将其与registerPaint 函数注册。我们向registerPaint 传递两个值--我们希望我们的worklet拥有的名称(在我们的例子中是blob)和定义它的类。
class Blob {}
registerPaint("blob", Blob);
很好!我们刚刚迈出了创建Worklet的第一步。我们刚刚迈出了创建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() 函数看成是一个回调函数。它最初是在worklet的目标元素第一次渲染时运行。在这之后,当元素的尺寸发生变化或worklet的输入属性更新时,它会再次运行。
当paint() 函数被调用时,它自动有几个值通过它。在本教程中,我们要利用前三个:
context- 一个类似于<canvas>元素的二维绘图环境,我们用它来画东西。geometry- 一个包含目标元素的宽度和高度的对象properties- 一个自定义属性的数组
现在我们已经定义了一个简单的paint() 函数,让我们跳到index.html 文件并加载我们的worklet。要做到这一点,我们将在结束的</body> 标签之前添加一个新的<script> 。
<script>
if (CSS["paintWorklet"] !== undefined) {
CSS.paintWorklet.addModule("./worklet.bundle.js");
}
</script>
注意: 我们正在注册我们的worklet的_捆绑_版本
好极了。我们的blob Worklet现在已经加载,并准备好在我们的CSS中使用。让我们用它来为我们的worklet-canvas 类生成一个background-image 。
.worklet-canvas {
background-image: paint(blob);
}
一旦你添加了上述片段,你应该看到一个红色的方块。我们的worklet是活的干得好。如果你调整浏览器窗口的大小,你应该看到worklet-canvas 元素的尺寸打印在浏览器控制台中。记住,只要worklet目标的尺寸发生变化,paint() 函数就会运行。
定义worklet的输入属性
为了让我们的worklet能够生成漂亮的blobs,我们需要帮助它,给它传递一些属性。我们需要的属性是:
--blob-seed- 一个伪随机数生成器的 "种子 "值;稍后会有更多关于这个的内容--blob-num-points- 根据形状上使用的点的数量来确定blob的详细程度--blob-variance- Blob的控制点的变化程度--blob-smoothness- Blob边缘的平滑度/锐利度--blob-fill--blob的填充颜色
让我们告诉我们的worklet,它将收到这些属性,并需要观察它们的变化。要做到这一点,我们可以回到我们的Blob 类,并添加一个inputProperties getter。
static get inputProperties() {
return [
"--blob-seed",
"--blob-num-points",
"--blob-variance",
"--blob-smoothness",
"--blob-fill",
];
}
酷。现在,我们的worklet知道应该期待哪些输入属性,我们应该把它们添加到我们的CSS中。
.worklet-canvas {
--blob-seed: 123456;
--blob-num-points: 8;
--blob-variance: 0.375;
--blob-smoothness: 1;
--blob-fill: #000;
}
现在,在这一点上,我们可以使用CSS属性和值API(Houdini家族的另一个成员)**来指定一些默认值,使这些自定义属性在我们的worklet中更容易解析。然而,不幸的是,此时此刻,属性和值API并没有最好的浏览器支持。
现在,为了保持简单,我们要让我们的自定义属性保持原样--在我们的worklet中依靠一些基本的解析功能。
先回到我们的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中定义的自定义属性完全对应。
如果你打开dev-tools,检查worklet-canvas 元素,并改变这些自定义属性中的任何一个--你应该看到日志重新运行并反映出更新的值。为什么?我们的worklet会对其输入属性的任何变化做出反应,并在检测到这些变化时重新运行其paint() 函数。
好了,伙计们,现在是时候开始形成我们的blob形状了。要做到这一点,我们需要一种生成随机数的方法。毕竟,这将使我们的Blobs具有生成性!
现在,你可能会想,_"嘿,我们可以使用Math.random() !"_在许多方面,你会是正确的。然而,在CSS Paint API工作单元中使用 "常规 "的随机数生成器有一个问题。我们来看看这个问题。
问题在于Math.random()
我们在前面注意到一个worklet的paint() 函数是如何运行得相当频繁的。如果我们使用一个诸如Math.random() 的方法来在paint() 内生成随机值--每次函数执行时它们都会不同。不同的随机数意味着每次worklet重新渲染时都有不同的视觉结果。我们根本不希望这样。当然,我们希望我们的Blobs是_随机的_,但只是在构思的时候。一旦它们存在于页面上,它们就不应该改变,除非我们明确告诉它们这样做。
我发现这个概念一开始有点难以理解,所以我做了几个CodePens(最好在原生支持CSS Paint API的浏览器中查看)来帮助演示。在第一个例子中,我们有一个设置随机背景颜色的小工作,使用Math.random() 。
警告: 调整下面这个元素的大小将导致颜色的闪现。
试着调整上面的元素的大小,注意背景颜色是如何更新变化的。对于一些小众的应用和有趣的演示,这_可能_是你想要的。但在大多数实际的使用情况下,它不是。除了在视觉上令人不快之外,对于那些对运动敏感的用户来说,这样的行为可能是一个无障碍问题。想象一下,你的worklet包含了数百个点,每当页面上的东西改变大小时,这些点都开始飞来飞去,闪闪发光
幸运的是,这个问题很容易解决。解决办法是什么?一个_伪随机数_生成器!伪随机数生成器 _(或PRNGs)_基于_种子_生成随机数。给定相同的种子值,PRNG总是返回相同的随机数序列--这对我们来说是完美的,因为我们可以在每次运行paint() 函数时重新初始化PRNG,确保相同的随机值序列。
点击 "生成 "来选择一些随机数 - 然后,再点击 "生成 "几次。注意到你每次点击的数字序列是怎样的吗?现在,试着改变种子值,并重复这个过程。这些数字将与之前的种子值不同,但各代之间是一致的。这就是PRNG的魅力所在。可预测的随机性!
这里是随机背景颜色的CodePen,使用PRNG而不是Math.random() 。
在我们的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 - 返回一个介于0和1之间的随机数。
可爱的,让我们把我们闪亮的新PRNG投入使用。
顺便提一下:使用CSS Paint API绘图
在使用CSS Paint API工作小程序时,就像HTML<canvas> 一样,我们在一个2D上下文中绘制所有的东西。这个上下文有一个宽度和一个高度。对于worklet来说,这个上下文的宽度和高度总是与worklet所画的元素相匹配的。
比如说,我们想在一个1920x1080px 上下文的中心添加一个点,我们可以这样来想象:
当我们开始写我们的 "渲染 "代码时,最好记住这一点。
我们的blob是如何形成的,一个概述
在我们写任何代码之前,我想给你看一个小的SVG动画,说明我们将如何创建我们的blob形状。如果你像我一样是一个视觉学习者,你可能会发现一个动画参考对理解这种事情有帮助。
将这一过程分解为三个步骤:
- 在一个圆的半径上绘制几个等距的点。
- 将每个点随机拉向圆心。
- 绘制一条通过每个点的平滑曲线。
现在,事情变得有点像数学_,_但不要担心。我们已经得到了这个!
定义blob的控制点
首先,让我们定义我们的blob的radius 。Blob的半径决定了它的大小。
我们希望我们的blob形状总是 "适合 "它所画的元素。为了确保这一点,我们检查worklet的目标元素的宽度和高度,并相应地设置blob的半径。我们的blob本质上是一个奇怪的圆,而一个圆的总宽度/高度总是等于它的半径乘以2,所以我们要把这个值除掉。让我们在我们的paint() 函数中添加一些代码来实现这一点。
const radius = Math.min(geometry.width, geometry.height) / 2;
这里有一张图片来帮助解释这里发生的事情。
酷!"。现在我们知道了我们的圆球的半径应该是多少,我们可以初始化它的点。
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),
};
}
这个函数接收一个起始点、一个终点和一个介于0和1之间的 "量 "值。这个函数的返回值是一个新的点,位于起点和终点之间。
在我们的worklet中,就在我们的for-loop中定义point 变量的下方,我们可以使用这个lerp 函数将点 "拉 "向中心位置。我们将修改后的点存储在我们的points 数组中。
points.push(lerp(point, center, variance * random()));
对于线性插值量,我们使用我们的--blob-variance 属性乘以一个由random() 生成的随机数--因为random() 总是返回一个介于0和1之间的值,这个量将总是介于0和我们的--blob-variance 数字之间。
注意: 一个较高的--blob-variance ,会导致更疯狂的blobs,因为每个点最终都会更接近中心。
绘制曲线
所以,我们已经将我们的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-loop之后!
我们传入我们的点,--blob-smoothness 属性,以及一个标志,让spline 知道它应该返回一个封闭的形状。此外,我们使用我们的--blob-fill 自定义属性来设置blob的填充颜色。现在,如果我们看一下我们的浏览器窗口,我们应该看到像这样的东西!
欢呼吧!我们成功了!spline 函数已经成功地在我们的每个点上画出了一条平滑的曲线,从而形成了一个华丽的(而且是_随机的_)blob形状。如果你想让你的圆球不那么圆滑,可以尝试减少--blob-smoothness 属性。
现在,我们所要做的就是增加_一丝_随机性。
一个随机的随机种子值
现在,我们的blob的PRNG种子是一个固定值。我们之前在CSS中定义了这个--blob-seed 自定义属性,其值为123456 - 这很好,但这意味着由random() 生成的随机数,因此,blob的核心形状总是相同的。
在某些情况下,这是很理想的。你可能不希望你的Blobs是随机的!你可能想选择一些_完美的_种子值,并在你的网站上使用它们作为半生成设计系统的一部分。但在其他情况下,你可能希望你的Blobs是随机的--就像我之前给你看的图像掩码的例子。
我们怎样才能做到这一点呢?将种子随机化!
现在,这并不像它看起来那么简单。最初,当我在编写本教程时,我想,_"嘿,我可以在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 代码,以便在我们的例子中添加一个:
(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 (由花哨的边框半径生成)。如果支持CSS Paint API,我们就删除这些值,并使用我们的worklet来绘制一个生成的blob形状。真棒!真棒
路的尽头
好了,伙计们,我们都完成了。引用 "感恩而死 "的说法--这是一次多么漫长而奇怪的旅行啊
我知道,这里有很多东西需要接受。我们已经涵盖了核心的生成艺术概念,了解了所有关于CSS Paint API的知识,_并且_在我们进行的时候制作了一些令人敬畏的生成性斑块。我想说的是,进展还不错。
现在我们已经学会了基础知识,但我们已经准备好开始创造各种生成性的魔法。请注意,我很快就会提供更多的生成性UI设计教程,但与此同时,请尝试利用我们在本教程中所学到的知识进行实验我相信你一定会有很多奇妙的想法。
直到下一次,各位CSS魔术师们