Die CSS Paint API (Teil der magischen Houdini-Familie) eröffnet eine aufregende neue Welt des Designs in CSS. Mit der Paint API können wir benutzerdefinierte Formen, komplexe Muster und schöne Animationen erstellen – alles mit einem Hauch von Zufälligkeit – auf eine Weise, die portabel, schnell und reaktionsschnell ist.
Wir werden unsere Zehen in den brodelnden Kessel der generativen CSS-Magie tauchen, indem wir meine Lieblingsform erstellen: den Blob. Zufällige Blobs sind ein großartiger Ausgangspunkt für alle, die neu in generativer Kunst/Design sind, und wir werden die CSS Paint API nebenbei kennenlernen, sodass dies ein idealer Startpunkt für Leute ist, die neu in dieser Welt sind. Sie werden im Handumdrehen ein generativer CSS-Magier sein!
Steigen wir auf unsere Besen und beschwören wir ein paar Formen.
Generativ?
Für einige Leser mag generative Kunst ein unbekanntes Thema sein. Wenn Sie bereits mit generativer Kunst/Design vertraut sind, können Sie gerne zum nächsten Abschnitt springen. Wenn nicht, hier ist ein kleines Beispiel.
Stellen Sie sich für einen Moment vor, Sie sitzen an einem Schreibtisch. Sie haben drei Stempel, ein paar Würfel und ein Stück Papier. Jeder der Stempel hat eine andere Form. Es gibt ein Quadrat, eine Linie und einen Kreis. Sie würfeln. Wenn die Würfel auf eins fallen, verwenden Sie den Quadrat-Stempel auf der Seite. Wenn die Würfel auf zwei fallen, verwenden Sie den Linien-Stempel. Wenn sie auf drei fallen, verwenden Sie den Kreisstempel. Wenn die Würfel vier, fünf oder sechs zeigen, tun Sie nichts. Sie wiederholen den Würfel-und-Stempel-Prozess, bis das Papier mit Formen gefüllt ist – das ist generative Kunst!
Es mag auf den ersten Blick ein wenig beängstigend erscheinen, aber im Grunde bedeutet "generativ" nur, dass etwas mit einem Element des Zufalls/der Unvorhersehbarkeit erstellt wird. Wir definieren einige Regeln und lassen eine Zufallsquelle uns zu einem Ergebnis führen. Im oben genannten "analogen" Beispiel ist die Zufallsquelle ein paar Würfel. Wenn wir im Browser arbeiten, könnte es Math.random() oder eine ähnliche Funktion sein.
Um die Dinge für einen Moment zurück in die Welt der Einsen und Nullen zu bringen, so würde das obige Beispiel aussehen, wenn es in Code geschrieben wäre.
Ziemlich cool, oder? Indem wir einige einfache Regeln definieren und sie zufällig ausführen, haben wir ein einzigartiges Muster erstellt. In dieser Tutorial-Serie werden wir generative Techniken wie diese verwenden, um aufregende Benutzeroberflächen zu erstellen.
Was ist die CSS Paint API und was ist ein Worklet?
Die CSS Paint API ermöglicht uns einen Low-Level-Zugriff auf CSS selbst(!) über eine HTML5 <canvas>-ähnliche Zeichen-API. Wir können diese Leistung mit etwas namens Worklet nutzen.
Worklets sind, kurz gesagt, JavaScript-Klassen. Jede Worklet-Klasse muss eine paint()-Funktion haben. Eine paint()-Funktion eines Worklets kann programmatisch ein Bild für jede CSS-Eigenschaft erstellen, die eines erwartet.
Zum Beispiel:
.my-element {
background-image: paint(texture);
}
Hier haben wir ein fiktives texture-Worklet, das eine wunderschöne (ich überlasse das Ihrer Vorstellungskraft), programmatische Textur generiert. Wo wir normalerweise einen url(...)-Wert der background-image-Eigenschaft zuweisen würden, rufen wir stattdessen paint(worklet_name) auf – dies führt die paint()-Funktion des Worklets aus und rendert das Ergebnis auf das Ziel-Element.
Wir werden uns gleich im Detail damit beschäftigen, wie man Worklets schreibt, aber ich wollte Ihnen eine kurze Einführung geben, was sie sind, bevor ich anfange, darüber zu sprechen.
Was wir bauen
In diesem Tutorial werden wir also ein generatives Blob-Worklet erstellen. Unser Worklet wird einige Eingabeparameter (als CSS Custom Properties, dazu später mehr) entgegennehmen und eine schöne, zufällige Blob-Form zurückgeben.
Lassen Sie uns damit beginnen, uns einige Beispiele des fertigen Worklets in Aktion anzusehen – wenn ein Bild tausend Worte sagt, muss ein CodePen eine Million sagen, oder?
Das Blob-Worklet als Hintergrundbild
Zuerst sehen Sie hier eine Demo des Blob-Worklets, das einfach für sich allein hängt und einen Wert für die background-image-Eigenschaft eines Elements generiert.
Ich ermutige Sie, sich den CSS-Code für den obigen CodePen anzusehen, die benutzerdefinierten Eigenschaften zu ändern, das Element zu vergrößern und zu sehen, was passiert. Sehen Sie, wie die Form flüssig skaliert und sich aktualisiert, wenn sich die benutzerdefinierten Eigenschaften ändern? Machen Sie sich keine Sorgen, wenn Sie im Moment noch nicht verstehen, *wie* das funktioniert. In diesem Stadium sind wir nur daran interessiert, *was* wir bauen.
Generative Bildmasken, ein praktischer Anwendungsfall
Großartig, nachdem wir das "Standalone"-Worklet gesehen haben, schauen wir uns an, wie wir es verwenden können. In diesem Beispiel fungiert das Worklet als generative Bildmaske.
Das Ergebnis (ich denke) ist ziemlich eindrucksvoll. Das Worklet verleiht dem Design eine natürliche, auffällige Kurve. Außerdem ist die Maskenform jedes Mal anders, wenn die Seite geladen wird, was eine fantastische Möglichkeit ist, die Benutzeroberfläche frisch und aufregend zu halten – klicken Sie auf "Rerun" im obigen CodePen, um diesen Effekt zu sehen. Dieses sich ständig ändernde Verhalten ist sicherlich subtil, aber ich hoffe, es wird Leuten, die es bemerken, *ein kleines bisschen Freude* bereiten. Das Web kann ein ziemlich kalter, steriler Ort sein, und generative Berührungen wie diese können es sich viel organischer anfühlen lassen!
Hinweis: Ich schlage sicherlich nicht vor, dass wir alle unsere gesamten Schnittstellen zufällig ändern lassen. Das wäre schrecklich für die Benutzerfreundlichkeit! Diese Art von Verhalten funktioniert am besten, wenn es sparsam eingesetzt wird und nur für präsentationsbezogene Elemente Ihrer Website oder App verwendet wird. Denken Sie an Blog-Post-Header, Heldenbilder, subtile Hintergrundmuster usw.
Dies ist nur ein Beispiel (und ein einfaches noch dazu), aber ich hoffe, es gibt Ihnen einige Ideen, wie Sie das Blob-Worklet in Ihrem eigenen Design und Ihrer Entwicklung verwenden könnten. Für alle, die nach zusätzlicher Inspiration suchen, sollte eine schnelle Dribbble-Suche nach "blobs" Ihnen eine ganze Menge Ideen liefern!
Warte, brauche ich die CSS Paint API, um Blobs zu erstellen?
Kurz gesagt, nein!
Es gibt tatsächlich eine Fülle von Möglichkeiten, Blobs für Ihr UI-Design zu erstellen. Sie könnten ein Tool wie Blobmaker verwenden, etwas Magie mit border-radius betreiben, ein normales <canvas>-Element verwenden, was auch immer! Es gibt *unzählige* Wege nach Blob City.
Keine davon ist jedoch ganz dasselbe wie die Verwendung der CSS Paint API. Warum?
Nun, um ein paar Gründe zu nennen...
Es ermöglicht uns, in unserem CSS ausdrucksstark zu sein
Anstatt an Reglern zu ziehen, Radien anzupassen oder endlos auf "Neu generieren" zu klicken, in der Hoffnung, dass ein perfekter Blob zu uns kommt, können wir nur wenige menschenlesbare Werte verwenden, um das zu bekommen, was wir brauchen.
Zum Beispiel nimmt das Blob-Worklet, das wir in diesem Tutorial erstellen, die folgenden Eingabeeigenschaften entgegen.
.worklet-target {
--blob-seed: 123456;
--blob-num-points: 8;
--blob-variance: 0.375;
--blob-smoothness: 1;
--blob-fill: #000;
}
Benötigen Sie Ihre Blobs, um extrem subtil und minimalistisch zu sein? Reduzieren Sie die --blob-variance Custom Property. Benötigen Sie sie detailliert und übertrieben? Erhöhen Sie sie!
Lust auf ein Redesign Ihrer Website in einer brutalistischeren Richtung? Kein Problem! Anstatt Hunderte von Assets neu zu exportieren oder eine Reihe von border-radius-Eigenschaften manuell zu codieren, reduzieren Sie einfach die --blob-smoothness Custom Property auf Null.
Praktisch, oder? Die CSS Paint API ermöglicht uns durch Worklets die Erstellung von ständig einzigartigen UI-Elementen, die sich nahtlos in ein Designsystem einfügen.
Hinweis: Ich verwende GSAP in den obigen Beispielen, um die Eingabeeigenschaften des Paint-Worklets, das wir in diesem Tutorial erstellen, zu animieren.
Es ist super performant
Generative Arbeit kann rechnerisch *etwas* aufwendig werden. Oft müssen wir durch viele Elemente schleifen, Berechnungen durchführen und andere lustige Dinge tun. Wenn wir bedenken, dass wir möglicherweise mehrere programmatische, generative Visualisierungen auf einer Seite erstellen müssen, könnten Leistungsprobleme ein Risiko darstellen.
Glücklicherweise laufen CSS Paint API Worklets außerhalb des Haupt-Browser-Threads. Der Haupt-Browser-Thread ist der Ort, an dem der gesamte JavaScript-Code, den wir *normalerweise* schreiben, existiert und ausgeführt wird. Das Schreiben von Code auf diese Weise ist völlig in Ordnung (und im Allgemeinen vorzuziehen), **aber es kann Einschränkungen haben. Wenn wir versuchen, auf dem Haupt-Browser-Thread zu viel zu tun, kann die Benutzeroberfläche träge oder sogar blockiert werden.
Da Worklets auf einem anderen Thread als die Hauptwebsite oder -anwendung laufen, werden sie die Benutzeroberfläche nicht "blockieren" oder verlangsamen. Darüber hinaus bedeutet dies, dass der Browser viele separate Worklet-Instanzen starten kann, die bei Bedarf aufgerufen werden können – dies ähnelt der Containerisierung und führt zu *blitzschneller* Leistung!
Es vermüllt nicht das DOM
Da die CSS Paint API im Wesentlichen ein Bild zu einer CSS-Eigenschaft hinzufügt, werden keine zusätzlichen Elemente zum DOM hinzugefügt. Für mich fühlt sich das wie ein super sauberer Ansatz zur Erstellung von generativen visuellen Elementen an. Ihre HTML-Struktur bleibt klar, semantisch und unverunreinigt, während Ihre CSS sich um das Aussehen kümmert.
Browser-Unterstützung
Es ist erwähnenswert, dass die CSS Paint API eine relativ neue Technologie ist und obwohl die Unterstützung wächst, ist sie in einigen wichtigen Browsern immer noch nicht verfügbar. Hier ist eine Tabelle zur Browserunterstützung.
Diese Browser-Supportdaten stammen von Caniuse, wo Sie weitere Details finden. Eine Zahl gibt an, dass der Browser die Funktion ab dieser Version und aufwärts unterstützt.
Desktop
| Chrome | Firefox | IE | Edge | Safari |
|---|---|---|---|---|
| 65 | Nein | Nein | 79 | Nein |
Mobil / Tablet
| Android Chrome | Android Firefox | Android | iOS Safari |
|---|---|---|---|
| 127 | Nein | 127 | Nein |
Obwohl die Browserunterstützung noch etwas lückenhaft ist – in diesem Tutorial werden wir uns ansehen, wie wir das css-paint-polyfill verwenden, das von GoogleChromeLabs gepflegt wird, um sicherzustellen, dass Benutzer in allen Browsern unsere Kreationen genießen können.
Wir werden uns zusätzlich damit beschäftigen, wie man "graceful fallback" macht, wenn die CSS Paint API nicht unterstützt wird. Ein Polyfill bedeutet zusätzliches JavaScript-Gewicht, daher ist es für einige Leute keine praktikable Lösung. Wenn Sie dazugehören, machen Sie sich keine Sorgen. Wir werden Browser-Support-Optionen für alle untersuchen.
Lass uns coden!
Okay, okay! Wir wissen, was wir bauen und warum die CSS Paint API rockt – jetzt lass uns loslegen! Zuerst richten wir eine Entwicklungsumgebung ein.
Hinweis: Wenn Sie während dieses Tutorials an irgendeinem Punkt nicht weiterkommen, können Sie eine fertige Version des Worklets einsehen.
Eine einfache Entwicklungsumgebung
Um zu beginnen, habe ich ein Worklet-Starter-Kit-Repository erstellt. Als ersten Schritt besuchen Sie GitHub und klonen Sie es. Sobald Sie das Repository geklont und sich darin navigiert haben, führen Sie
npm install
Gefolgt von
npm run start
Nachdem Sie die obigen Befehle ausgeführt haben, startet ein einfacher Entwicklungsserver im aktuellen Verzeichnis und Ihr Standardbrowser öffnet sich. Da Worklets entweder über HTTPS oder von localhost geladen werden müssen – diese Einrichtung stellt sicher, dass wir unser Worklet ohne CORS-Probleme verwenden können. Das Starter-Kit kümmert sich auch um das automatische Aktualisieren des Browsers, wenn wir Änderungen vornehmen.
Neben der Bereitstellung unserer Inhalte und der Bereitstellung eines grundlegenden Live-Reloads verfügt dieses Repository über einen einfachen Build-Schritt. Angetrieben von esbuild, bündelt dieser Prozess alle JavaScript-imports in unserem Worklet und gibt das Ergebnis in eine Datei namens worklet.bundle.js aus. Alle Änderungen in worklet.js spiegeln sich automatisch in worklet.bundle.js wider.
Wenn Sie sich das Repository ansehen, bemerken Sie vielleicht, dass bereits HTML und CSS vorhanden sind. Wir haben eine einfache index.html-Datei mit einem einzigen worklet-canvas-Div und etwas CSS, um es auf der Seite zu zentrieren und es an den Viewport anzupassen. Betrachten Sie dies als eine leere Leinwand für all Ihre Worklet-Experimente!
Initialisierung unseres Worklets
Okay, jetzt wo wir unsere Entwicklungsumgebung am Laufen haben, ist es Zeit, unser Worklet zu erstellen. Beginnen wir mit der Navigation zur Datei worklet.js.
Hinweis: Denken Sie daran, dass worklet.bundle.js automatisch von unserem Build-Schritt generiert wird. Wir sollten diese Datei niemals direkt bearbeiten.
In unserer worklet.js-Datei können wir unsere Blob-Klasse definieren und sie mit der Funktion registerPaint registrieren. Wir übergeben zwei Werte an registerPaint – den Namen, den unser Worklet haben soll (in unserem Fall blob) und die Klasse, die ihn definiert.
class Blob {}
registerPaint("blob", Blob);
Ausgezeichnet! Wir haben den ersten Schritt zur Erstellung unserer Blobs gemacht!
Hinzufügen einer paint()-Funktion
Nun passiert noch nicht viel, also fügen wir unserer Blob-Klasse eine einfache paint()-Funktion hinzu, um zu prüfen, ob alles in Ordnung ist.
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);
}
Wir können uns diese paint()-Funktion wie einen Callback vorstellen. Sie wird anfangs ausgeführt, wenn das Ziel-Element des Worklets zum ersten Mal gerendert wird. Danach wird sie jedes Mal wieder ausgeführt, wenn sich die Abmessungen des Elements ändern oder die Eingabeeigenschaften des Worklets aktualisiert werden.
Wenn die paint()-Funktion aufgerufen wird, werden automatisch einige Werte übergeben. In diesem Tutorial nutzen wir die ersten drei:
context– ein 2D-Zeichenkontext, ähnlich dem eines<canvas>-Elements, den wir zum Zeichnen von Dingen verwenden.geometry– ein Objekt, das die Breite und Höhe des Zielelements enthält.properties– ein Array von benutzerdefinierten Eigenschaften.
Nachdem wir nun eine einfache paint()-Funktion definiert haben, wechseln wir zur Datei index.html und laden unser Worklet. Dazu fügen wir ein neues <script>-Tag direkt vor unserem schließenden </body>-Tag ein.
<script>
if (CSS["paintWorklet"] !== undefined) {
CSS.paintWorklet.addModule("./worklet.bundle.js");
}
</script>
Hinweis: Wir registrieren die *gebündelte* Version unseres Worklets!
Ausgezeichnet. Unser blob-Worklet ist jetzt geladen und einsatzbereit in unserem CSS. Lassen Sie es uns verwenden, um ein background-image für unsere worklet-canvas-Klasse zu generieren.
.worklet-canvas {
background-image: paint(blob);
}
Sobald Sie den obigen Ausschnitt hinzugefügt haben, sollten Sie ein rotes Quadrat sehen. Unser Worklet lebt! Gut gemacht. Wenn Sie das Browserfenster vergrößern, sollten Sie die Abmessungen des worklet-canvas-Elements in der Browserkonsole sehen. Denken Sie daran, dass die paint()-Funktion immer dann ausgeführt wird, wenn sich die Abmessungen des Worklet-Ziels ändern.
Definieren der Eingabeeigenschaften des Worklets
Damit unser Worklet schöne Blobs generieren kann, müssen wir ihm helfen und ihm einige Eigenschaften übergeben. Die benötigten Eigenschaften sind:
--blob-seed– ein "Seed"-Wert für einen Pseudozufallszahlengenerator; mehr dazu gleich.--blob-num-points– wie detailliert der Blob basierend auf der Anzahl der Punkte entlang der Form ist.--blob-variance– wie variiert die Kontrollpunkte des Blobs sind.--blob-smoothness– die Glätte/Schärfe der Ränder des Blobs.--blob-fill– die Füllfarbe des Blobs.
Teilen wir unserem Worklet mit, dass es diese Eigenschaften empfangen und auf Änderungen überwachen muss. Dazu gehen wir zurück zu unserer Blob-Klasse und fügen einen inputProperties-Getter hinzu.
static get inputProperties() {
return [
"--blob-seed",
"--blob-num-points",
"--blob-variance",
"--blob-smoothness",
"--blob-fill",
];
}
Prima. Jetzt, da unser Worklet weiß, welche Eingabeeigenschaften es erwarten soll, sollten wir sie unserem CSS hinzufügen.
.worklet-canvas {
--blob-seed: 123456;
--blob-num-points: 8;
--blob-variance: 0.375;
--blob-smoothness: 1;
--blob-fill: #000;
}
An diesem Punkt könnten wir die CSS Properties and Values API (ein weiteres Mitglied der Houdini-Familie) verwenden, **um einige Standardwerte zuzuweisen und diese benutzerdefinierten Eigenschaften in unserem Worklet etwas einfacher zu parsen. Leider ist die Properties and Values API derzeit noch nicht optimal im Browser unterstützt.
Fürs Erste, um die Dinge einfach zu halten, lassen wir unsere benutzerdefinierten Eigenschaften so, wie sie sind – und verlassen uns stattdessen auf einige einfache Parsing-Funktionen in unserem Worklet.
Zurück zu unserer Worklet-Klasse, fügen wir diese Hilfsfunktionen hinzu.
propToString(prop) {
return prop.toString().trim();
}
propToNumber(prop) {
return parseFloat(prop);
}
In Abwesenheit der Properties and Values API helfen uns diese einfachen Hilfsfunktionen, die an paint() übergebenen properties in nutzbare Werte umzuwandeln.
Mit unseren neuen Hilfsfunktionen können wir properties parsen und einige Variablen definieren, die wir in unserer paint()-Funktion verwenden können. Entfernen wir auch den alten "Debug"-Code.
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"));
}
Wenn Sie eine dieser Variablen loggen, sollten Sie sehen, dass die properties, die von der paint()-Funktion bereitgestellt werden, exakt den benutzerdefinierten Eigenschaften entsprechen, die wir vorhin in unserem CSS definiert haben.
Wenn Sie die Entwicklertools öffnen, das worklet-canvas-Element inspizieren und eine dieser benutzerdefinierten Eigenschaften ändern – sollten Sie sehen, dass die Logs neu ausgeführt werden und den aktualisierten Wert widerspiegeln. Warum? Unser Worklet reagiert auf jede Änderung seiner Eingabeeigenschaften und führt seine paint()-Funktion erneut aus, wenn es diese erkennt.
Okay, Leute, es ist Zeit, die Form unserer Blobs zu gestalten. Dazu brauchen wir eine Möglichkeit, Zufallszahlen zu generieren. Das wird schließlich dafür sorgen, dass unsere Blobs generativ sind!
Nun denken Sie vielleicht: „Hey, wir können dafür Math.random() verwenden!“ und in vielerlei Hinsicht hätten Sie Recht. Es gibt jedoch ein Problem mit der Verwendung eines "normalen" Zufallszahlengenerators in CSS Paint API Worklets. Schauen wir es uns an.
Das Problem mit Math.random()
Wir haben vorhin bemerkt, wie oft die paint()-Funktion eines Worklets ausgeführt wird. Wenn wir eine Methode wie Math.random() verwenden, um Zufallswerte innerhalb von paint() zu generieren – werden diese bei jeder Ausführung der Funktion unterschiedlich sein. Unterschiedliche Zufallszahlen bedeuten bei jeder Neudarstellung des Worklets ein anderes visuelles Ergebnis. Das wollen wir überhaupt nicht. Sicher, wir wollen, dass unsere Blobs *zufällig* sind, aber nur zum Zeitpunkt der Konzeption. Sie sollten sich nicht ändern, sobald sie auf der Seite existieren, es sei denn, wir sagen ihnen ausdrücklich, dies zu tun.
Ich fand dieses Konzept anfangs etwas schwierig zu begreifen, daher habe ich ein paar CodePens erstellt (am besten in einem Browser angezeigt, der die CSS Paint API nativ unterstützt), um zu helfen. Im ersten Beispiel haben wir ein Worklet, das eine zufällige Hintergrundfarbe setzt, unter Verwendung von Math.random().
Warnung: Das Vergrößern/Verkleinern des Elements unten führt zu einem Farbblitz.
Versuchen Sie, das Element oben zu vergrößern/verkleinern und bemerken Sie, wie sich die Hintergrundfarbe ändert, wenn es aktualisiert wird. Für einige Nischenanwendungen und unterhaltsame Demos ist dies *vielleicht* das, was Sie wollen. In den meisten praktischen Anwendungsfällen ist es das jedoch nicht. Abgesehen davon, dass es visuell störend ist, könnte ein solches Verhalten ein Zugänglichkeitsproblem für Benutzer sein, die empfindlich auf Bewegung reagieren. Stellen Sie sich vor, Ihr Worklet enthielte Hunderte von Punkten, die alle zu fliegen begännen und aufleuchten würden, wann immer sich etwas auf der Seite in der Größe ändert!
Glücklicherweise ist dieses Problem recht einfach zu beheben. Die Lösung? Ein pseudozufälliger Zahlen-Generator! Pseudozufallszahlengeneratoren (oder PRNGs) generieren Zufallszahlen basierend auf einem Seed. Bei gleichem Seed-Wert gibt ein PRNG immer die gleiche Sequenz von Zufallszahlen zurück – das ist perfekt für uns, da wir den PRNG jedes Mal neu initialisieren können, wenn die paint()-Funktion ausgeführt wird, und so die gleiche Sequenz von Zufallswerten sicherstellen!
Hier ist ein CodePen, der zeigt, wie ein PRNG funktioniert.
Klicken Sie auf "Generieren", um einige Zufallszahlen auszuwählen – klicken Sie dann ein paar Mal mehr auf "Generieren". Beachten Sie, wie die Zahlenfolge jedes Mal gleich ist, wenn Sie klicken? Ändern Sie nun den Seed-Wert und wiederholen Sie diesen Vorgang. Die Zahlen werden von dem vorherigen Seed-Wert abweichen, aber über die Generationen hinweg konsistent sein. Das ist die Schönheit eines PRNG. Vorhersehbare Zufälligkeit!
Hier ist der CodePen mit zufälliger Hintergrundfarbe erneut, der einen PRNG anstelle von Math.random() verwendet.
Ah! Viel besser! Das Element hat beim Laden der Seite eine zufällige Farbe erhalten, aber die Hintergrundfarbe ändert sich nicht, wenn es vergrößert/verkleinert wird. Perfekt! Sie können dies testen, indem Sie im obigen CodePen auf "Rerun" klicken und das Element vergrößern/verkleinern.
Hinzufügen von Pseudozufallszahlen zu unserem Worklet
Fügen wir nun eine PRNG-Funktion oberhalb unserer Blob-Klassendefinition hinzu.
// 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;
};
}
Nun, ich würde lügen, wenn ich sagen würde, dass ich buchstäblich alles verstehe, was diese Funktion tut. Ich habe diesen schönen kleinen Code-Schnipsel durch Jakes Archibalds hervorragenden Artikel über vorhersehbare Zufälligkeit mit der CSS Paint API entdeckt und ihn seitdem in vielen Arbeiten verwendet. Sie können das Original-Repository für diese Funktion auf GitHub finden – es enthält eine ganze Reihe ausgezeichneter PRNGs und ist einen Blick wert.
Hinweis: Auch wenn ich nicht vollständig verstehe, wie diese Funktion funktioniert, weiß ich, wie man sie benutzt. Oftmals, wenn Sie in der Welt der Generierung arbeiten (wenn Sie überhaupt wie ich sind!), werden Sie sich in dieser Situation wiederfinden. Machen Sie sich dann keine Sorgen! Es ist absolut in Ordnung, einen Code-Schnipsel zu verwenden, um etwas Kunst/Design zu erstellen, ohne genau zu wissen, wie es funktioniert. Wir können durch Tun lernen, und das ist großartig.
Okay, großartig, wir haben eine PRNG-Funktion. Fügen wir sie zu paint() hinzu.
const random = mulberry32(seed);
In diesem Snippet rufen wir mulberry32() mit unserer --blob-seed Custom Property als Seed-Wert auf, und es gibt eine brandneue Funktion zurück. Diese neue Funktion – random – gibt eine Zufallszahl zwischen null und eins zurück.
Prima, lassen Sie uns unseren glänzenden neuen PRNG nutzen.
Ein kurzer Exkurs: Zeichnen mit der CSS Paint API
Bei der Arbeit mit CSS Paint API Worklets zeichnen wir, genau wie bei HTML <canvas>, alles innerhalb eines 2D-Kontextes. Dieser Kontext hat eine Breite und eine Höhe. Für Worklets entsprechen die Breite und Höhe dieses Kontexts immer der des Elements, auf das das Worklet zeichnet.
Sagen wir zum Beispiel, wir wollten einen Punkt in der Mitte eines 1920x1080px-Kontextes hinzufügen, wir könnten ihn so visualisieren.

Wenn wir beginnen, unseren "Rendering"-Code zu schreiben, ist es gut, dies im Hinterkopf zu behalten.
Wie unsere Blob-Form entsteht, ein Überblick
Bevor wir Code schreiben, möchte ich Ihnen eine kleine SVG-Animation zeigen, wie wir unsere Blob-Form erstellen werden. Wenn Sie ein visueller Lerner sind wie ich, finden Sie eine animierte Referenz möglicherweise hilfreich, um diese Art von Dingen zu verstehen.
Um diesen Prozess in drei Schritte zu unterteilen
- Mehrere gleichmäßig verteilte Punkte um den Radius eines Kreises plotten.
- Jeden Punkt zufällig zum Zentrum des Kreises ziehen.
- Eine glatte Kurve durch jeden der Punkte zeichnen.
Nun wird es ein klein wenig mathematisch, aber keine Sorge. Wir kriegen das hin!
Definieren der Kontrollpunkte des Blobs
Zuerst definieren wir den radius unseres Blobs. Der Radius des Blobs bestimmt, wie groß oder klein er ist.
Wir möchten, dass unsere Blob-Form immer in das Element passt, auf dem sie gezeichnet wird. Um sicherzustellen, dass dies der Fall ist, prüfen wir die Breite und Höhe des Zielelements des Worklets und legen den Radius des Blobs entsprechend fest. Unser Blob ist im Wesentlichen ein seltsamer Kreis, und die Gesamtbreite/Höhe eines Kreises ist immer gleich seinem Radius multipliziert mit zwei, daher teilen wir diesen Wert, um ihn anzupassen. Fügen wir etwas Code hinzu, um dies in unserer paint()-Funktion zu erreichen.
const radius = Math.min(geometry.width, geometry.height) / 2;
Hier ist ein Bild, das erklärt, was hier vor sich geht

Cool! Jetzt, da wir wissen, wie groß der Radius unseres Blobs sein soll, können wir seine Punkte initialisieren.
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,
};
}
Puh! In diesem Ausschnitt „gehen“ wir den Umfang eines Kreises entlang und setzen dabei einige gleichmäßig verteilte Punkte. Wie funktioniert das?
Zuerst definieren wir eine Variable angleStep. Der maximale Winkel zwischen zwei Punkten am Umfang eines Kreises beträgt Pi × 2. Indem wir Pi × 2 durch die Anzahl der zu erstellenden „Punkte“ teilen, erhalten wir den gewünschten (gleichmäßig verteilten) Winkel zwischen jedem Punkt.
Als Nächstes durchlaufen wir jeden Punkt. Für jeden dieser Punkte definieren wir eine Variable angle. Diese Variable ist unser angleStep multipliziert mit dem Index des Punkts. Gegeben einen Radius, einen Winkel und einen Mittelpunkt für einen Kreis, können wir Math.cos() und Math.sin() verwenden, um jeden Punkt zu plotten.
Hinweis: Wenn Sie mehr über trigonometrische Funktionen erfahren möchten, kann ich Michelle Barkers ausgezeichnete Serie wärmstens empfehlen!
Jetzt, da wir einige perfekte, schöne, gleichmäßig verteilte Punkte haben, die um den Umfang eines Kreises positioniert sind – sollten wir sie durcheinanderbringen. Dazu können wir jeden Punkt mit einem zufälligen Betrag zum Zentrum des Kreises „ziehen“.
Wie können wir das tun?
Fügen wir zuerst eine neue Funktion lerp (kurz für lineare Interpolation) direkt unter der Stelle hinzu, an der wir mulberry32 definiert haben.
function lerp(position, target, amt) {
return {
x: (position.x += (target.x - position.x) * amt),
y: (position.y += (target.y - position.y) * amt),
};
}
Diese Funktion nimmt einen Startpunkt, einen Endpunkt und einen „Betrag“-Wert zwischen null und eins entgegen. Der Rückgabewert dieser Funktion ist ein neuer Punkt, der irgendwo zwischen dem Start- und dem Endpunkt platziert ist.
In unserem Worklet können wir direkt unter der Stelle, an der wir die Variable point in unserer for-Schleife definieren, diese Funktion lerp verwenden, um den Punkt zum Zentrum hin zu „ziehen“. Wir speichern den modifizierten Punkt in unserem Array points.
points.push(lerp(point, center, variance * random()));
Für den Betrag der linearen Interpolation verwenden wir unsere Eigenschaft --blob-variance multipliziert mit einer Zufallszahl, die von random() generiert wurde – da random() immer einen Wert zwischen null und eins zurückgibt, liegt dieser Betrag immer irgendwo zwischen null und unserer Zahl --blob-variance.
Hinweis: Ein höherer --blob-variance führt zu wilderen Blobs, da jeder Punkt näher am Zentrum landen kann.
Die Kurve zeichnen
Wir haben also die Punkte unseres Blobs in einem Array gespeichert. Im Moment werden sie jedoch für nichts verwendet! Im letzten Schritt unseres Blob-Erstellungsprozesses zeichnen wir eine glatte Kurve durch jeden von ihnen.
Um diese Kurve zu zeichnen, verwenden wir etwas namens Catmull-Rom-Spline. Ein Catmull-Rom-Spline ist kurz gesagt eine großartige Möglichkeit, eine glatte Bézier-Kurve durch eine beliebige Anzahl von { x, y }-Punkten zu zeichnen. Mit einer Spline müssen wir uns keine Gedanken über komplizierte Kontrollpunktberechnungen machen. Wir übergeben ein Array von Punkten und erhalten eine schöne, organische Kurve zurück. Kein Problem.
Gehen wir zum Anfang unserer Datei worklet.js und fügen wir den folgenden Import hinzu.
import { spline } from "@georgedoescode/generative-utils";
Installieren Sie dann das Paket wie folgt.
npm i @georgedoescode/generative-utils
Diese Funktion spline ist ziemlich umfangreich und etwas komplex. Aus diesem Grund habe ich sie verpackt und zu meinem generative-utils-Repository hinzugefügt, einer kleinen Sammlung nützlicher Werkzeuge für generative Kunst.
Sobald wir spline importiert haben – können wir es in der paint()-Funktion unseres Worklets wie folgt verwenden:
ctx.fillStyle = fill;
ctx.beginPath();
spline(points, smoothness, true, (CMD, data) => {
if (CMD === "MOVE") {
ctx.moveTo(...data);
} else {
ctx.bezierCurveTo(...data);
}
});
ctx.fill();
Hinweis: Platzieren Sie diesen Ausschnitt direkt nach Ihrer for-Schleife!
Wir übergeben unsere Punkte, die Eigenschaft --blob-smoothness und ein Flag, damit spline weiß, dass es eine geschlossene Form zurückgeben soll. Zusätzlich verwenden wir unsere benutzerdefinierte Eigenschaft --blob-fill, um die Füllfarbe des Blobs festzulegen. Wenn wir uns jetzt unser Browserfenster ansehen, sollten wir etwas Ähnliches sehen!

Hurra! Wir haben es geschafft! Die Funktion spline hat erfolgreich eine glatte Kurve durch jeden unserer Punkte gezeichnet und somit eine wunderschöne (und zufällige) Blob-Form erstellt. Wenn Sie möchten, dass Ihr Blob etwas weniger rund ist, versuchen Sie, die Eigenschaft --blob-smoothness zu reduzieren.
Nun müssen wir nur noch ein klein wenig mehr Zufälligkeit hinzufügen.
Ein zufälliger Zufalls-Seed-Wert
Derzeit ist der PRNG-Seed unseres Blobs ein fester Wert. Wir haben diese benutzerdefinierte Eigenschaft --blob-seed in unserem CSS zuvor mit dem Wert 123456 definiert – das ist großartig, bedeutet aber, dass die von random() generierten Zufallszahlen und damit die Kernform des Blobs immer gleich sind.
Für einige Fälle ist dies ideal. Sie möchten Ihre Blobs vielleicht nicht zufällig haben! Sie möchten vielleicht einige perfekte Seed-Werte auswählen und sie als Teil eines semi-generativen Designsystems auf Ihrer Website verwenden. Für andere Fälle möchten Sie Ihre Blobs jedoch vielleicht zufällig haben – genau wie im Bildmaskenbeispiel, das ich Ihnen zuvor gezeigt habe.
Wie können wir das tun? Den Seed randomisieren!
Nun, das ist nicht ganz so einfach, wie es scheint. Anfangs, als ich an diesem Tutorial arbeitete, dachte ich: „Hey, ich kann den Seed-Wert im Konstruktor der Blob-Klasse initialisieren!“ Leider lag ich falsch.
Da der Browser mehrere Instanzen eines Worklets hochfahren kann, um Aufrufe an paint() zu verarbeiten – könnte eine von mehreren Blob-Klassen den Blob rendern! Wenn wir unseren Seed-Wert innerhalb der Worklet-Klasse initialisieren, ist dieser Wert über Instanzen hinweg unterschiedlich und könnte zu den visuellen „Glitches“ führen, die wir zuvor besprochen haben.
Um dies zu testen, fügen Sie Ihrer Blob-Klasse eine constructor-Funktion mit dem folgenden Code hinzu.
constructor() {
console.log(`My seed value is ${Math.random()}`);
}
Überprüfen Sie nun Ihre Browserkonsole und ändern Sie die Fenstergröße. In den meisten Fällen erhalten Sie mehrere Protokolle mit unterschiedlichen Zufallswerten. Dieses Verhalten ist für uns nicht gut; wir brauchen einen konstanten Seed-Wert.
Um dieses Problem zu lösen, fügen wir ein kleines JavaScript im Hauptthread hinzu. Ich füge dies in das <script>-Tag ein, das wir zuvor erstellt haben.
document
.querySelector(".worklet-canvas")
.style.setProperty("--blob-seed", Math.random() * 10000);
Ausgezeichnet! Wenn wir nun das Browserfenster neu laden, sollten wir jedes Mal eine neue Blob-Form sehen.
Für unsere einfache Demo ist das perfekt. In einer „echten“ Anwendung möchten Sie vielleicht eine Klasse .blob erstellen, alle Instanzen davon beim Laden ansprechen und den Seed-Wert jedes Elements aktualisieren. Sie könnten auch experimentieren, indem Sie die Varianz des Blobs, die Anzahl der Punkte und die Rundungseigenschaften auf zufällige Werte setzen.
Für dieses Tutorial ist das jedoch alles! Alles, was wir noch tun müssen, ist sicherzustellen, dass unser Code für Benutzer in allen Browsern einwandfrei funktioniert oder eine geeignete Fallback-Lösung bereitzustellen, wenn dies nicht der Fall ist.
Laden eines Polyfills
Durch das Hinzufügen eines Polyfills funktioniert unser CSS Paint API-Code in allen gängigen Browsern, allerdings auf Kosten von zusätzlichem JavaScript-Gewicht. Hier ist, wie wir unseren Code CSS.paintWorklet.addModule aktualisieren können, um einen zu unserem Beispiel hinzuzufügen.
(async function () {
if (CSS["paintWorklet"] === undefined) {
await import("https://unpkg.com/css-paint-polyfill");
}
CSS.paintWorklet.addModule("./worklet.bundle.js");
})();
Mit diesem Ausschnitt laden wir den Polyfill nur, wenn der aktuelle Browser die CSS Paint API nicht unterstützt. Gut so!
Eine CSS-basierte Fallback-Lösung
Wenn Ihnen zusätzliches JavaScript-Gewicht nicht gefällt, ist das cool. Das verstehe ich vollkommen. Glücklicherweise können wir mit @supports eine leichte, nur CSS-basierte Fallback-Lösung für Browser definieren, die die CSS Paint API nicht unterstützen. Hier ist, wie:
.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);
}
}
In diesem Ausschnitt wenden wir eine background-color und einen Blob-ähnlichen border-radius (generiert von fancy border radius) auf das Ziel-Element an. Wenn die CSS Paint API unterstützt wird, entfernen wir diese Werte und verwenden unser Worklet, um eine generative Blob-Form zu malen. Genial!
Das Ende der Straße
Nun, Leute, wir sind fertig. Um die Grateful Dead zu zitieren – was für eine lange, seltsame Reise das war!
Ich weiß, hier gibt es viel zu verdauen. Wir haben Kernkonzepte der generativen Kunst behandelt, alles über die CSS Paint API gelernt und dabei auch noch einige tolle generative Blobs erstellt. Nicht schlecht, sage ich.
Nachdem wir nun die Grundlagen gelernt haben, sind wir bereit, allerhand generative Magie zu kreieren. Halten Sie bald Ausschau nach weiteren Tutorials zu generativem UI-Design von mir, aber bis dahin versuchen Sie, das Gelernte aus diesem Tutorial aufzugreifen und zu experimentieren! Ich bin sicher, Sie haben eine Menge fantastischer Ideen.
Bis zum nächsten Mal, liebe CSS-Magier!
Toller Artikel! Vielen Dank – interessante Ansätze für generatives Design.
Hallo, George!
Als ehemalige bildende Künstlerin bin ich aus Notwendigkeit ins Design eingestiegen und dann aus Leidenschaft zur App- und Webentwicklung. Mit diesem Hintergrund im Hinterkopf habe ich Ihren Artikel wirklich genossen. Ich sage zweimal Danke, dafür, dass Sie mich auf eine völlig unbekannte API aufmerksam gemacht haben, und dafür, dass Sie mir beigebracht haben, wie ich sie für die Erstellung einer Art von Kunst nutzen kann. Ich liebe die breite Palette von Möglichkeiten, die ich mir vorgestellt habe, während ich Ihren klaren und selbsterklärenden Text durchgearbeitet habe.
Dieses neue Wissen werde ich sicher nicht in einen kaputten Sack werfen. Im Moment bin ich mit der Entwicklung einer App beschäftigt, aber Sie haben mir das perfekte Spielfeld gegeben, um mich zu entspannen und mich von meiner aktuellen Arbeit zu distanzieren, wenn ich mich überfordert fühle.
Vielen Dank dafür!
Hallo! Ich war wirklich aufgeregt, das auszuprobieren & hatte Spaß beim Herumspielen mit dem CodePen-Beispiel. Aber ich stecke beim allerersten Schritt fest, dem Generieren dieses roten Quadrats. Ich bekomme eine leere Seite ohne Fehler. Ich benutze Chrome, also sollte es funktionieren. Was sind einige Schritte zur Fehlerbehebung für diese Skripte?
Okay, ich glaube, ich habe herausgefunden, was ich falsch gemacht habe. Ich habe das Repository nur heruntergeladen, anstatt mich damit zu verbinden. Ich dachte, da ich kein Programmierer bin und nichts zurückmelden würde, gäbe es keinen Sinn, das zu tun. Aber ich glaube, indem ich es einfach auf meinem Localhost platziert habe, habe ich die Fähigkeit verloren, den Worklet-Bundle automatisch zu erstellen. Ich muss also herausfinden, wie ich das manuell mache, wann immer ich Änderungen an worklet.js vornehme, richtig?