Der Browser ist seit langem ein Medium für Kunst und Design. Von Lynn Fishers fröhlichen Kreationen A Single Div bis zu den erstaunlich detaillierten CSS-Gemälden von Diana Smith haben höchst talentierte und fähige Entwickler im Laufe der Jahre die Webtechnologien kontinuierlich an ihre Grenzen gebracht und innovative, inspirierende Grafiken geschaffen.
CSS hatte jedoch noch nie eine API, die sich… nun ja, nur dem Zeichnen von Dingen widmet! Wie die oben genannten talentierten Leute gezeigt haben, kann sie zwar die meisten Dinge rendern, aber es ist nicht immer einfach und für Produktionsseiten/-anwendungen nicht immer praktikabel.
Kürzlich wurde CSS jedoch um eine aufregende neue Reihe von APIs erweitert, bekannt als Houdini, und eine davon – die Paint API – ist speziell für das Rendern von 2D-Grafiken konzipiert. Für uns Web-Entwickler ist das unglaublich aufregend. Zum ersten Mal haben wir einen Teil von CSS, der dem alleinigen Zweck der programmatischen Erstellung von Bildern dient. Die Türen zu einer mystischen neuen Welt sind weit und wahrhaftig offen!
In diesem Tutorial werden wir die Paint API verwenden, um drei (hoffentlich!) schöne, generative Muster zu erstellen, die verwendet werden können, um einer Reihe von Websites/Anwendungen einen köstlichen Löffel Charakter zu verleihen.
Zauberbücher/Texteditoren bereit, Freunde, lasst uns etwas Magie wirken!
Zielgruppe
Dieses Tutorial ist perfekt für Leute, die sich mit HTML, CSS und JavaScript auskennen. Ein wenig Vertrautheit mit generativer Kunst und Kenntnisse der Paint API/des HTML Canvas sind hilfreich, aber nicht zwingend erforderlich. Wir werden eine kurze Übersicht geben, bevor wir beginnen. Wo wir gerade dabei sind...
Bevor wir beginnen
Für eine umfassende Einführung in die Paint API und generative Kunst/Design empfehle ich Ihnen, den ersten Beitrag dieser Serie zu lesen. Wenn Sie mit beiden Themen neu sind, ist dies ein großartiger Anfang. Wenn Sie jedoch keine Lust haben, einen weiteren Artikel zu durchsuchen, sind hier einige wichtige Konzepte, mit denen Sie vertraut sein sollten, bevor Sie fortfahren.
Wenn Sie bereits mit der CSS Paint API und generativer Kunst/Design vertraut sind, können Sie gerne zum nächsten Abschnitt springen.
Was ist generative Kunst/Design?
Generative Kunst/Design ist jedes Werk, das mit einem Zufallselement erstellt wird. Wir legen einige Regeln fest und lassen eine Zufallsquelle uns zu einem Ergebnis führen. Zum Beispiel könnte eine Regel lauten: „Wenn eine Zufallszahl größer als 50 ist, rendern Sie ein rotes Quadrat, wenn sie kleiner als 50 ist, rendern Sie ein blaues Quadrat*“*, und im Browser könnte eine Zufallsquelle Math.random() sein.
Durch die Anwendung eines generativen Ansatzes zur Erstellung von Mustern können wir nahezu unendliche Variationen einer einzigen Idee erzeugen – dies ist sowohl eine inspirierende Ergänzung des kreativen Prozesses als auch eine fantastische Gelegenheit, unsere Benutzer zu begeistern. Anstatt den Leuten jedes Mal die gleiche Grafik zu zeigen, wenn sie eine Seite besuchen, können wir ihnen etwas Besonderes und Einzigartiges präsentieren!
Was ist die CSS Paint API?
Die Paint API ermöglicht uns einen Low-Level-Zugriff auf das CSS-Rendering. Über „Paint Worklets“ (JavaScript-Klassen mit einer speziellen paint()-Funktion) können wir dynamisch Bilder mit einer Syntax erstellen, die fast identisch mit dem HTML Canvas ist. Worklets können überall dort ein Bild rendern, wo CSS eines erwartet. Zum Beispiel
.worklet-canvas {
background-image: paint(workletName);
}
Paint API Worklets sind schnell, reaktionsschnell und passen ganz wunderbar zu bestehenden CSS-basierten Designsystemen. Kurz gesagt, sie sind das Coolste überhaupt. Das Einzige, was ihnen derzeit fehlt, ist eine breite Browserunterstützung. Hier ist eine Tabelle
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 |
Etwas dünn gesät! Aber das ist in Ordnung. Da die Paint API fast von Natur aus dekorativ ist, können wir sie als progressive Verbesserung nutzen, wenn sie verfügbar ist, und eine einfache, zuverlässige Fallback-Lösung bereitstellen, wenn nicht.
Was wir machen werden
In diesem Tutorial lernen wir, wie wir drei einzigartige generative Muster erstellen können. Diese Muster sind recht einfach, werden aber als wunderbarer Sprungbrett für weitere Experimente dienen. Hier sind sie in ihrer ganzen Pracht!
Die Demos in diesem Tutorial funktionieren derzeit nur in Chrome und Edge.
„Winzige Sprenkel“
„Bauhaus“
„Voronoi Bögen“
Nehmen Sie sich einen Moment Zeit, um die obigen Beispiele zu erkunden, bevor Sie fortfahren. Versuchen Sie, die benutzerdefinierten Eigenschaften zu ändern und das Browserfenster zu vergrößern/verkleinern – beobachten Sie, wie die Muster reagieren. Können Sie erraten, wie sie funktionieren könnten, ohne einen Blick auf den JavaScript-Code zu werfen?
Einrichtung
Um Zeit zu sparen und den Bedarf an benutzerdefinierten Build-Prozessen zu eliminieren, werden wir während dieses gesamten Tutorials ausschließlich in CodePen arbeiten. Ich habe sogar einen „Starter-Pen“ erstellt, den wir als Basis für jedes Muster verwenden können!
Ich weiß, es sieht noch nicht viel aus… aber das wird sich noch ändern.
Im Starter-Pen verwenden wir den JavaScript-Abschnitt, um das Worklet selbst zu schreiben. Dann laden wir im HTML-Abschnitt das JavaScript direkt über ein internes <script>-Tag. Da Paint API Worklets spezielle Worker sind (Code, der in einem separaten Browser-Thread läuft), muss ihr Ursprung1 in einer eigenständigen .js-Datei liegen.
Lassen Sie uns die wichtigsten Codeabschnitte hier aufschlüsseln.
Wenn Sie bereits Paint API Worklets geschrieben haben und mit CodePen vertraut sind, können Sie zum nächsten Abschnitt springen.
Definition der Worklet-Klasse
Zuerst einmal: Schauen wir uns den JavaScript-Tab an. Hier definieren wir eine Worklet-Klasse mit einer einfachen paint()-Funktion
class Worklet {
paint(ctx, geometry, props) {
const { width, height } = geometry;
ctx.fillStyle = "#000";
ctx.fillRect(0, 0, width, height);
}
}
Ich betrachte die paint()-Funktion eines Worklets als Callback. Wenn sich das Ziel-Element des Worklets aktualisiert (Abmessungen ändert, benutzerdefinierte Eigenschaften modifiziert), wird es erneut ausgeführt. Die paint()-Funktion eines Worklets erhält beim Ausführen automatisch einige Parameter übergeben. In diesem Tutorial interessieren uns die ersten drei
ctx– ein 2D-Zeichenkontext, der dem des HTML Canvas sehr ähnlich istgeometry– ein Objekt, das die Breiten-/Höhenabmessungen des Ziel-Elements des Worklets enthältprops– ein Array von CSS-benutzerdefinierten Eigenschaften, die wir auf Änderungen „beobachten“ und neu rendern können, wenn sie auftreten. Diese sind eine großartige Möglichkeit, Werte an Paint Worklets zu übergeben.
Unser Starter-Worklet rendert ein schwarzes Quadrat, das die gesamte Breite/Höhe seines Ziel-Elements abdeckt. Wir werden diese paint()-Funktion für jedes Beispiel komplett neu schreiben, aber es ist schön, etwas Definiertes zu haben, um zu überprüfen, ob alles funktioniert.
Registrierung des Worklets
Sobald eine Worklet-Klasse definiert ist, muss sie registriert werden, bevor wir sie verwenden können. Dazu rufen wir registerPaint in der Worklet-Datei selbst auf
if (typeof registerPaint !== "undefined") {
registerPaint("workletName", Worklet);
}
Gefolgt von CSS.paintWorklet.addModule() in unserem „Haupt“-JavaScript/HTML
<script id="register-worklet">
if (CSS.paintWorklet) {
CSS.paintWorklet.addModule('https://codepen.io/georgedoescode/pen/bGrMXxm.js');
}
</script>
Wir prüfen hier, ob registerPaint definiert ist, bevor wir es ausführen, da das JavaScript unseres Pens immer einmal im Haupt-Browser-Thread ausgeführt wird – registerPaint wird erst verfügbar, wenn die JavaScript-Datei mit CSS.paintWorklet.addModule(...) in ein Worklet geladen wurde.
Anwendung des Worklets
Nach der Registrierung können wir unser Worklet verwenden, um ein Bild für jede CSS-Eigenschaft zu generieren, die eines erwartet. In diesem Tutorial konzentrieren wir uns auf background-image
.worklet-canvas {
background-image: paint(workletName);
}
Paketimporte
Sie werden vielleicht ein paar Paketimporte am Anfang der Worklet-Datei des Starter-Pens bemerken
import random from "https://cdn.skypack.dev/random";
import seedrandom from "https://cdn.skypack.dev/seedrandom";
Können Sie erraten, was sie sind?
Zufallszahlengeneratoren!
Alle drei Muster, die wir in diesem Tutorial erstellen, basieren stark auf Zufälligkeit. Paint API Worklets sollten jedoch (fast) immer deterministisch sein. Bei gleichen Eingabeeigenschaften und Dimensionen sollte die paint()-Funktion eines Worklets immer dasselbe rendern.
Warum?
- Die Paint API möchte möglicherweise eine zwischengespeicherte Version der
paint()-Ausgabe eines Worklets für bessere Leistung verwenden. Die Einführung eines unvorhersehbaren Elements in einem Worklet macht dies unmöglich! - Die
paint()-Funktion eines Worklets wird jedes Mal neu ausgeführt, wenn sich das Element, auf das sie angewendet wird, in den Abmessungen ändert. In Verbindung mit „reiner“ Zufälligkeit kann dies zu erheblichen Bildflackern führen – ein potenzielles Barrierefreiheitsproblem für einige Leute.
Für uns macht das alles Math.random() etwas nutzlos, da es völlig unvorhersehbar ist. Als Alternative ziehen wir random (eine ausgezeichnete Bibliothek für die Arbeit mit Zufallszahlen) und seedrandom (einen Pseudozufallszahlengenerator als Basisalgorithmus) heran.
Hier ist als schnelles Beispiel ein „zufällige Kreise“-Worklet, das einen Pseudozufallszahlengenerator verwendet
Und hier ist ein ähnliches Worklet, das Math.random() verwendet. Warnung: Das Ändern der Größe des Elements führt zu flackernden Bildern.
Es gibt einen kleinen resize-Griff unten rechts in beiden obigen Mustern. Versuchen Sie, beide Elemente in der Größe zu ändern. Beachten Sie den Unterschied?
Einrichtung jedes Musters
Bevor Sie mit jedem der folgenden Muster beginnen, navigieren Sie zum Starter-Pen und klicken Sie in der Fußzeile auf die Schaltfläche „Fork“. Das Forken eines Pens erstellt eine Kopie des Originals in dem Moment, in dem Sie auf die Schaltfläche klicken. Von diesem Punkt an gehört er Ihnen und Sie können damit machen, was Sie wollen.
Nachdem Sie den Starter-Pen geforkt haben, müssen Sie noch einen kritischen zusätzlichen Schritt ausführen. Die an CSS.paintWorklet.addModule übergebene URL muss aktualisiert werden, um auf die JavaScript-Datei Ihres neuen Forks zu verweisen. Um den Pfad für das JavaScript Ihres Forks zu finden, werfen Sie einen Blick auf die URL in Ihrem Browser. Sie möchten die URL Ihres Forks ohne alle Abfrageparameter abrufen und .js anhängen – etwas wie dieses

Schön. Das ist der richtige Weg! Sobald Sie die URL für Ihr JavaScript haben, stellen Sie sicher, dass Sie sie hier aktualisieren
<script id="register-worklet">
if (CSS.paintWorklet) {
// ⚠️ hey friend! update the URL below each time you fork this pen! ⚠️
CSS.paintWorklet.addModule('https://codepen.io/georgedoescode/pen/QWMVdPG.js');
}
</script>
Wenn Sie mit dieser Einrichtung arbeiten, müssen Sie den Pen möglicherweise gelegentlich manuell neu laden, um Ihre Änderungen zu sehen. Drücken Sie dazu CMD/CTRL + Shift + 7.
Muster #1 (Winzige Sprenkel)
OK, wir sind bereit, unser erstes Muster zu erstellen. Forken Sie den Starter-Pen, aktualisieren Sie die Referenz zur .js-Datei und machen Sie sich bereit für etwas generativen Spaß!
Zur schnellen Erinnerung: Hier ist das fertige Muster
Aktualisierung des Namens des Worklets
Noch einmal, zuerst die Dinge: Aktualisieren wir den Namen des Starter-Worklets und die entsprechenden Referenzen
class TinySpecksPattern {
// ...
}
if (typeof registerPaint !== "undefined") {
registerPaint("tinySpecksPattern", TinySpecksPattern);
}
.worklet-canvas {
/* ... */
background-image: paint(tinySpecksPattern);
}
Definition der Eingabeeigenschaften des Worklets
Unser „Tiny Specks“-Worklet akzeptiert die folgenden Eingabeeigenschaften
--pattern-seed– ein Seed-Wert für den Pseudozufallszahlengenerator--pattern-colors– die verfügbaren Farben für jeden Sprenkel--pattern-speck-count– wie viele einzelne Sprenkel das Worklet rendern soll--pattern-speck-min-size– die Mindestgröße für jeden Sprenkel--pattern-speck-max-size– die Maximalgröße für jeden Sprenkel
Als nächsten Schritt definieren wir die inputProperties, die unser Worklet empfangen kann. Dazu können wir einen Getter zu unserer TinySpecksPattern-Klasse hinzufügen
class TinySpecksPattern {
static get inputProperties() {
return [
"--pattern-seed",
"--pattern-colors",
"--pattern-speck-count",
"--pattern-speck-min-size",
"--pattern-speck-max-size"
];
}
// ...
}
Neben einigen benutzerdefinierten Eigenschaftsdefinitionen in unserem CSS
@property --pattern-seed {
syntax: "<number>";
initial-value: 1000;
inherits: true;
}
@property --pattern-colors {
syntax: "<color>#";
initial-value: #161511, #dd6d45, #f2f2f2;
inherits: true;
}
@property --pattern-speck-count {
syntax: "<number>";
initial-value: 3000;
inherits: true;
}
@property --pattern-speck-min-size {
syntax: "<number>";
initial-value: 0;
inherits: true;
}
@property --pattern-speck-max-size {
syntax: "<number>";
initial-value: 3;
inherits: true;
}
Wir verwenden hier die Properties and Values API (ein weiteres Mitglied der Houdini-Familie), um unsere benutzerdefinierten Eigenschaften zu definieren. Dies bietet uns zwei wertvolle Vorteile. Erstens können wir sinnvolle Standardwerte für die Eingabeeigenschaften definieren, die unser Worklet erwartet. Ein köstlicher Spritzer Entwicklererfahrung! Zweitens kann unser Worklet durch die Einbeziehung einer syntax-Definition für jede benutzerdefinierte Eigenschaft diese intelligent interpretieren.
Zum Beispiel definieren wir die Syntax <color># für --pattern-colors. Dies wiederum ermöglicht es uns, ein Array von kommagetrennten Farben an das Worklet in jedem gültigen CSS-Farbformat zu übergeben. Wenn unser Worklet diese Werte empfängt, wurden sie in RGB konvertiert und in einem ordentlichen kleinen Array platziert. Ohne eine syntax-Definition interpretiert ein Worklet alle props als einfache Strings.
Wie die Paint API hat auch die Properties and Values API eine begrenzte Browserunterstützung.
Die paint()-Funktion
Großartig! Das ist der spaßige Teil. Wir haben unsere „Tiny Speck“-Worklet-Klasse erstellt, sie registriert und definiert, welche Eingabeeigenschaften sie empfangen kann. Nun lassen wir sie etwas tun!
Als ersten Schritt löschen wir die paint()-Funktion des Starter-Pens und behalten nur die width- und height-Definitionen bei
paint(ctx, geometry, props) {
const { width, height } = geometry;
}
Als Nächstes speichern wir unsere Eingabeeigenschaften in einigen Variablen
const seed = props.get("--pattern-seed").value;
const colors = props.getAll("--pattern-colors").map((c) => c.toString());
const count = props.get("--pattern-speck-count").value;
const minSize = props.get("--pattern-speck-min-size").value;
const maxSize = props.get("--pattern-speck-max-size").value;
Als Nächstes initialisieren wir unseren Pseudozufallszahlengenerator
random.use(seedrandom(seed));
Ahhh, vorhersagbare Zufälligkeit! Wir füllen seedrandom bei jeder Ausführung von paint() mit demselben seed-Wert neu, was zu einem konsistenten Strom von Zufallszahlen über die Renderings hinweg führt.
Zum Schluss malen wir unsere Sprenkel!
Zuerst erstellen wir eine for-Schleife, die count-mal iteriert. In jeder Iteration dieser Schleife erstellen wir einen einzelnen Sprenkel
for (let i = 0; i < count; i++) {
}
Als erste Aktion in unserer for-Schleife definieren wir eine x- und y-Position für den Sprenkel. Irgendwo zwischen 0 und der Breite/Höhe des Ziel-Elements des Worklets ist perfekt
const x = random.float(0, width);
const y = random.float(0, height);
Als Nächstes wählen wir eine zufällige Größe (für den radius)
const radius = random.float(minSize, maxSize);
Also, wir haben eine Position und eine Größe für den Sprenkel definiert. Wählen wir eine zufällige Farbe aus unseren colors, um ihn zu füllen
ctx.fillStyle = colors[random.int(0, colors.length - 1)];
Alles klar. Wir sind bereit. Lassen Sie uns ctx verwenden, um etwas zu rendern!
Als Erstes müssen wir den Zustand unseres Zeichenkontexts save(). Warum? Wir wollen jeden Sprenkel drehen, aber beim Arbeiten mit einem 2D-Zeichenkontext wie diesem können wir keine einzelnen Elemente drehen. Um ein Objekt zu drehen, müssen wir den gesamten Zeichenbereich drehen. Wenn wir den Kontext nicht save() und restore(), stapelt sich die Drehung/Translation in jeder Iteration, was zu einem sehr unordentlichen (oder leeren) Canvas führt!
ctx.save();
Nachdem wir den Zustand des Zeichenkontexts gespeichert haben, können wir zum Mittelpunkt des Sprenkels (definiert durch unsere x/y-Variablen) translate und eine Drehung anwenden. Die Übersetzung zum Mittelpunkt eines Objekts vor der Drehung stellt sicher, dass sich das Objekt um seine Mittelachse dreht
ctx.translate(x, y);
ctx.rotate(((random.float(0, 360) * 180) / Math.PI) * 2);
ctx.translate(-x, -y);
Nachdem wir unsere Drehung angewendet haben, übersetzen wir zurück zur oberen linken Ecke des Zeichenbereichs.
Wir wählen hier einen zufälligen Wert zwischen 0 und 360 (Grad) und konvertieren ihn dann in Radiant (das Rotationsformat, das ctx versteht).
Großartig! Schließlich rendern wir eine Ellipse – das ist die Form, die unsere Sprenkel definiert
ctx.beginPath();
ctx.ellipse(x, y, radius, radius / 2, 0, Math.PI * 2, 0);
ctx.fill();
Hier ist ein einfacher Pen, der die Form unserer zufälligen Sprenkel etwas genauer zeigt
Perfekt. Jetzt müssen wir nur noch den Zeichenkontext wiederherstellen
ctx.restore();
Das war's! Unser erstes Muster ist fertig. Lassen Sie uns auch eine background-color auf unseren Worklet-Canvas anwenden, um den Effekt zu vervollständigen
.worklet-canvas {
background-color: #90c3a5;
background-image: paint(tinySpecksPattern);
}
Nächste Schritte
Von hier aus versuchen Sie, die Farben, Formen und die Verteilung der Sprenkel zu ändern. Es gibt Hunderte von Richtungen, in die Sie dieses Muster weiterentwickeln könnten! Hier ist ein Beispiel, das kleine Dreiecke anstelle von Ellipsen verwendet
Weiter geht's!
Muster #2 (Bauhaus)
Gute Arbeit! Ein Muster ist geschafft. Weiter zum nächsten. Forken Sie erneut den Starter-Pen und aktualisieren Sie die JavaScript-Referenz des Worklets, um zu beginnen.
Zur schnellen Auffrischung: Hier ist das fertige Muster, auf das wir hinarbeiten
Aktualisierung des Namens des Worklets
Genau wie letztes Mal beginnen wir damit, den Namen des Worklets und die entsprechenden Referenzen zu aktualisieren
class BauhausPattern {
// ...
}
if (typeof registerPaint !== "undefined") {
registerPaint("bauhausPattern", BauhausPattern);
}
.worklet-canvas {
/* ... */
background-image: paint(bauhausPattern);
}
Sehr gut.
Definition der Eingabeeigenschaften des Worklets
Unser „Bauhaus Pattern“-Worklet erwartet die folgenden Eingabeeigenschaften
--pattern-seed– ein Seed-Wert für den Pseudozufallszahlengenerator--pattern-colors– die verfügbaren Farben für jede Form im Muster--pattern-size– der Wert, der sowohl für die Breite als auch für die Höhe eines quadratischen Musterbereichs verwendet wird--pattern-detail– die Anzahl der Spalten/Reihen, in die das quadratische Muster unterteilt werden soll
Fügen wir diese Eingabeeigenschaften unserem Worklet hinzu
class BahausPattern {
static get inputProperties() {
return [
"--pattern-seed",
"--pattern-colors",
"--pattern-size",
"--pattern-detail"
];
}
// ...
}
…und definieren sie in unserem CSS, wiederum unter Verwendung der Properties and Values API
@property --pattern-seed {
syntax: "<number>";
initial-value: 1000;
inherits: true;
}
@property --pattern-colors {
syntax: "<color>#";
initial-value: #2d58b5, #f43914, #f9c50e, #ffecdc;
inherits: true;
}
@property --pattern-size {
syntax: "<number>";
initial-value: 1024;
inherits: true;
}
@property --pattern-detail {
syntax: "<number>";
initial-value: 12;
inherits: true;
}
Ausgezeichnet. Malen wir!
Die paint()-Funktion
Löschen wir wieder die paint-Funktion des Starter-Worklets und lassen nur die width- und height-Definition stehen
paint(ctx, geometry, props) {
const { width, height } = geometry;
}
Als Nächstes speichern wir unsere Eingabeeigenschaften in einigen Variablen
const patternSize = props.get("--pattern-size").value;
const patternDetail = props.get("--pattern-detail").value;
const seed = props.get("--pattern-seed").value;
const colors = props.getAll("--pattern-colors").map((c) => c.toString());
Nun können wir unseren Pseudozufallszahlengenerator wie zuvor starten
random.use(seedrandom(seed));
Großartig! Wie Sie vielleicht bemerkt haben, ist die Einrichtung von Paint API Worklets immer recht ähnlich. Es ist nicht der aufregendste Prozess, aber er bietet eine hervorragende Gelegenheit, über die Architektur Ihres Worklets und wie andere Entwickler es nutzen könnten, nachzudenken.
Bei diesem Worklet erstellen wir also ein Muster mit festen Abmessungen, das mit Formen gefüllt ist. Dieses Muster mit festen Abmessungen wird dann skaliert, um das Ziel-Element des Worklets zu bedecken. Denken Sie an dieses Verhalten ein wenig wie an background-size: cover in CSS!
Hier ist eine Grafik

Um dieses Verhalten in unserem Code zu erreichen, fügen wir eine scaleContext-Funktion zu unserer Worklet-Klasse hinzu
scaleCtx(ctx, width, height, elementWidth, elementHeight) {
const ratio = Math.max(elementWidth / width, elementHeight / height);
const centerShiftX = (elementWidth - width * ratio) / 2;
const centerShiftY = (elementHeight - height * ratio) / 2;
ctx.setTransform(ratio, 0, 0, ratio, centerShiftX, centerShiftY);
}
Und rufen sie in unserer paint()-Funktion auf
this.scaleCtx(ctx, patternSize, patternSize, width, height);
Jetzt können wir mit festen Abmessungen arbeiten und unser Zeichenkontext wird automatisch alles für uns skalieren – eine praktische Funktion für viele Anwendungsfälle.
Als Nächstes erstellen wir ein 2D-Raster von Zellen. Dazu definieren wir eine Variable cellSize (die Größe des Musterbereichs geteilt durch die Anzahl der gewünschten Spalten/Reihen)
const cellSize = patternSize / patternDetail;
Dann können wir die Variable cellSize verwenden, um das Raster „durchzuschreiten“ und gleichmäßig beabstandete, gleich große Zellen zu erstellen, zu denen wir zufällige Formen hinzufügen können
for (let x = 0; x < patternSize; x += cellSize) {
for (let y = 0; y < patternSize; y += cellSize) {
}
}
Innerhalb der zweiten verschachtelten Schleife können wir mit dem Rendern beginnen!
Zuerst wählen wir eine zufällige Farbe für die aktuelle Form
const color = colors[random.int(0, colors.length - 1)];
ctx.fillStyle = color;
Als Nächstes speichern wir eine Referenz auf die Mittelpunkt-x- und y-Position der aktuellen Zelle
const cx = x + cellSize / 2;
const cy = y + cellSize / 2;
In diesem Worklet positionieren wir alle unsere Formen relativ zu ihrem Mittelpunkt. Solange wir hier sind, fügen wir unserem Worklet-File einige Hilfsfunktionen hinzu, die uns helfen, schnell zentrierte Formen-Objekte zu rendern. Diese können außerhalb der Worklet-Klasse leben
function circle(ctx, cx, cy, radius) {
ctx.beginPath();
ctx.arc(cx, cy, radius, 0, Math.PI * 2);
ctx.closePath();
}
function arc(ctx, cx, cy, radius) {
ctx.beginPath();
ctx.arc(cx, cy, radius, 0, Math.PI * 1);
ctx.closePath();
}
function rectangle(ctx, cx, cy, size) {
ctx.beginPath();
ctx.rect(cx - size / 2, cy - size / 2, size, size);
ctx.closePath();
}
function triangle(ctx, cx, cy, size) {
const originX = cx - size / 2;
const originY = cy - size / 2;
ctx.beginPath();
ctx.moveTo(originX, originY);
ctx.lineTo(originX + size, originY + size);
ctx.lineTo(originX, originY + size);
ctx.closePath();
}
Ich werde hier nicht zu sehr ins Detail gehen, aber hier ist eine Grafik, die visualisiert, wie jede dieser Funktionen funktioniert

Wenn Sie beim Rendern von Grafiken in einem der Worklets dieses Tutorials stecken bleiben, schauen Sie in die MDN-Dokumentation zu HTML Canvas. Die Syntax/Verwendung ist fast identisch mit dem 2D-Grafikkontext, der in Paint API Worklets verfügbar ist.
Gut! Kehren wir zur verschachtelten Schleife unserer paint()-Funktion zurück. Als Nächstes müssen wir auswählen, welche Form gerendert werden soll. Dazu können wir einen zufälligen String aus einem Array von Möglichkeiten auswählen
const shapeChoice = ["circle", "arc", "rectangle", "triangle"][
random.int(0, 3)
];
Ebenso können wir einen zufälligen Rotationsbetrag auswählen
const rotationDegrees = [0, 90, 180][random.int(0, 2)];
Perfekt. Wir sind bereit zu rendern!
Zuerst speichern wir den Zustand unseres Zeichenkontexts, genau wie im vorherigen Worklet
ctx.save();
Als Nächstes können wir zum Mittelpunkt der aktuellen Zelle translate und die Leinwand mit dem gerade gewählten Zufallswert drehen
ctx.translate(cx, cy);
ctx.rotate((rotationDegrees * Math.PI) / 180);
ctx.translate(-cx, -cy);
Nun können wir die Form selbst rendern! Lassen Sie uns unsere shapeChoice-Variable an eine switch-Anweisung übergeben und sie verwenden, um zu entscheiden, welche Form-Rendering-Funktion ausgeführt werden soll
switch (shapeChoice) {
case "circle":
circle(ctx, cx, cy, cellSize / 2);
break;
case "arc":
arc(ctx, cx, cy, cellSize / 2);
break;
case "rectangle":
rectangle(ctx, cx, cy, cellSize);
break;
case "triangle":
triangle(ctx, cx, cy, cellSize);
break;
}
ctx.fill();
Schließlich müssen wir nur noch unseren Zeichenkontext für die nächste Form restore()
ctx.restore();
Damit ist unser Bauhaus Grids Worklet fertig!
Nächste Schritte
Es gibt so viele Richtungen, in die Sie dieses Worklet entwickeln könnten. Wie könnten Sie es weiter parametrisieren? Könnten Sie eine „Tendenz“ für bestimmte Formen/Farben hinzufügen? Könnten Sie mehr Formtypen hinzufügen?
Experimentieren Sie immer – das Mitverfolgen der Beispiele, die wir gemeinsam erstellen, ist ein ausgezeichneter Anfang, aber der beste Weg zu lernen ist, Ihre eigenen Dinge zu machen! Wenn Sie Inspiration benötigen, schauen Sie sich einige Muster auf Dribbble an, schauen Sie zu Ihren Lieblingskünstlern, der Architektur um Sie herum, der Natur, was auch immer!
Als einfaches Beispiel, hier ist dasselbe Worklet in einem völlig anderen Farbschema
Muster #3 (Voronoi Bögen)
Bisher haben wir sowohl ein chaotisches Muster als auch eines, das sich strikt an einem Raster orientiert, erstellt. Für unser letztes Beispiel bauen wir eines, das irgendwo dazwischen liegt.
Als letzte Erinnerung: Hier ist das fertige Muster
Bevor wir anfangen und Code schreiben, werfen wir einen Blick darauf, wie dieses Worklet… funktioniert.
Eine kurze Einführung in Voronoi-Tessellationen
Wie der Name schon sagt, verwendet dieses Worklet etwas namens Voronoi-Tessellation, um sein Layout zu berechnen. Eine Voronoi-Tessellation (oder ein Voronoi-Diagramm) ist, kurz gesagt, eine Methode, um einen Raum in nicht überlappende Polygone zu unterteilen.
Wir fügen eine Sammlung von Punkten zu einem 2D-Raum hinzu. Dann berechnen wir für jeden Punkt ein Polygon, das nur diesen Punkt und keine anderen Punkte enthält. Sobald die Polygone berechnet sind, können sie als eine Art "Raster" verwendet werden, um alles zu positionieren.
Hier ist ein animiertes Beispiel
Das Faszinierende an Voronoi-basierten Layouts ist, dass sie auf eine eher ungewöhnliche Weise reaktionsfähig sind. Wenn sich die Punkte in einer Voronoi-Tessellation bewegen, ordnen sich die Polygone automatisch neu an, um den Raum zu füllen!
Versuchen Sie, das Element unten in der Größe zu ändern und beobachten Sie, was passiert!
Cool, oder?
Wenn Sie mehr über alles rund um Voronoi erfahren möchten, habe ich einen Artikel, der ins Detail geht. Vorerst ist das jedoch alles, was wir brauchen.
Aktualisierung des Namens des Worklets
Okay, Leute, wir kennen das hier. Erstellen wir ein Fork des Starter-Pens, aktualisieren wir den JavaScript-Import und ändern den Namen und die Referenzen des Worklets
class VoronoiPattern {
// ...
}
if (typeof registerPaint !== "undefined") {
registerPaint("voronoiPattern", VoronoiPattern);
}
.worklet-canvas {
/* ... */
background-image: paint(voronoiPattern);
}
Definition der Eingabeeigenschaften des Worklets
Unser VoronoiPattern-Worklet erwartet die folgenden Eingabeeigenschaften
--pattern-seed– ein Seed-Wert für den Pseudozufallszahlengenerator--pattern-colors— die verfügbaren Farben für jeden Bogen/Kreis im Muster--pattern-background— die Hintergrundfarbe des Musters
Fügen wir diese Eingabeeigenschaften unserem Worklet hinzu
class VoronoiPattern {
static get inputProperties() {
return ["--pattern-seed", "--pattern-colors", "--pattern-background"];
}
// ...
}
…und registrieren wir sie in unserem CSS
@property --pattern-seed {
syntax: "<number>";
initial-value: 123456;
inherits: true;
}
@property --pattern-background {
syntax: "<color>";
inherits: false;
initial-value: #141b3d;
}
@property --pattern-colors {
syntax: "<color>#";
initial-value: #e9edeb, #66aac6, #e63890;
inherits: true;
}
Schön! Wir sind bereit. Overall anziehen, Freunde — lasst uns malen.
Die paint()-Funktion
Zuerst räumen wir die paint()-Funktion des Starter-Worklets auf und behalten nur die Definitionen für width und height bei. Dann können wir einige Variablen mithilfe unserer Eingabeeigenschaften erstellen und auch unseren Pseudo-Zufallszahlengenerator initialisieren. Genau wie in unseren vorherigen Beispielen
paint(ctx, geometry, props) {
const { width, height } = geometry;
const seed = props.get("--pattern-seed").value;
const background = props.get("--pattern-background").toString();
const colors = props.getAll("--pattern-colors").map((c) => c.toString());
random.use(seedrandom(seed));
}
Bevor wir etwas anderes tun, malen wir schnell eine Hintergrundfarbe
ctx.fillStyle = background;
ctx.fillRect(0, 0, width, height);
Als nächstes importieren wir eine Hilfsfunktion, mit der wir schnell eine Voronoi-Tessellation erstellen können
import { createVoronoiTessellation } from "https://cdn.skypack.dev/@georgedoescode/generative-utils";
Diese Funktion ist im Wesentlichen ein Wrapper um d3-delaunay und Teil meines generative-utils-Repositorys. Sie können den Quellcode auf GitHub einsehen. Bei „klassischen“ Datenstrukturen/Algorithmen wie Voronoi-Tessellationen gibt es keinen Grund, das Rad neu zu erfinden — es sei denn, Sie möchten das natürlich!
Jetzt, da wir unsere createVoronoiTessellation-Funktion verfügbar haben, fügen wir sie zu paint() hinzu
const { cells } = createVoronoiTessellation({
width,
height,
points: [...Array(24)].map(() => ({
x: random.float(0, width),
y: random.float(0, height)
}))
});
Hier erstellen wir eine Voronoi-Tessellation bei der Breite und Höhe des Ziel-Elements des Worklets, mit 24 Steuerpunkten.
Großartig. Zeit, unsere Formen zu rendern! Vieles dieses Codes sollte uns dank der beiden vorherigen Beispiele vertraut sein.
Zuerst durchlaufen wir jede Zelle der Tessellation
cells.forEach((cell) => {
});
Für jede Zelle ist das Erste, was wir tun, eine Farbe auswählen
ctx.fillStyle = colors[random.int(0, colors.length - 1)];
Als Nächstes speichern wir eine Referenz auf die x- und y-Koordinaten des Zentrums der Zelle
const cx = cell.centroid.x;
const cy = cell.centroid.y;
Als Nächstes **sichern** wir den aktuellen Zustand des Kontexts und drehen die Leinwand um den Mittelpunkt der Zelle
ctx.save();
ctx.translate(cx, cy);
ctx.rotate((random.float(0, 360) / 180) * Math.PI);
ctx.translate(-cx, -cy);
Cool! Jetzt können wir etwas rendern. Lassen Sie uns einen Bogen mit einem Endwinkel von entweder PI oder PI * 2 zeichnen. Für Sie und mich, einen Halbkreis oder einen Kreis
ctx.beginPath();
ctx.arc(
cell.centroid.x,
cell.centroid.y,
cell.innerCircleRadius * 0.75,
0,
Math.PI * random.int(1, 2)
);
ctx.fill();
Unsere Funktion createVoronoiTessellation weist jeder cell einen speziellen innerCircleRadius zu — dies ist der größte Kreis, der in seine Mitte passt, ohne Kanten zu berühren. Denken Sie daran als praktische Hilfe zum Skalieren von Objekten innerhalb der Grenzen einer Zelle. Im obigen Ausschnitt verwenden wir innerCircleRadius, um die Größe unserer Bögen zu bestimmen.
Hier ist ein einfacher Pen, der hervorhebt, was hier passiert
Nachdem wir jeder Zelle einen „primären“ Bogen hinzugefügt haben, fügen wir 25 % der Zeit einen weiteren hinzu. Dieses Mal können wir jedoch die Füllfarbe des Bogens auf die Hintergrundfarbe unseres Worklets setzen. Dadurch erhalten wir den Effekt eines kleinen Lochs in der Mitte einiger Formen!
if (random.float(0, 1) > 0.25) {
ctx.fillStyle = background;
ctx.beginPath();
ctx.arc(
cell.centroid.x,
cell.centroid.y,
(cell.innerCircleRadius * 0.75) / 2,
0,
Math.PI * 2
);
ctx.fill();
}
Prima! Alles, was wir jetzt noch tun müssen, ist, den Zeichenkontext wiederherzustellen
ctx.restore();
Und das war's!
Nächste Schritte
Das Schöne an Voronoi-Tessellationen ist, dass man sie verwenden kann, um alles Mögliche zu positionieren. In unserem Beispiel haben wir Bögen verwendet, aber Sie könnten Rechtecke, Linien, Dreiecke, was auch immer rendern! Vielleicht könnten Sie sogar die Umrisse der Zellen selbst rendern?
Hier ist eine Version unseres VoronoiPattern-Worklets, das viele kleine Linien anstelle von Kreisen und Halbkreisen rendert
Muster randomisieren
Sie haben vielleicht bemerkt, dass bis jetzt alle unsere Muster einen statischen --pattern-seed-Wert erhalten haben. Das ist in Ordnung, aber was ist, wenn wir möchten, dass unsere Muster jedes Mal zufällig angezeigt werden? Nun, glücklicherweise müssen wir nur die Variable --pattern-seed setzen, wenn die Seite geladen wird, und ihr eine zufällige Zahl zuweisen. So etwas wie hier
document.documentElement.style.setProperty('--pattern-seed', Math.random() * 10000);
Wir haben das kurz früher angesprochen, aber das ist eine schöne Möglichkeit, sicherzustellen, dass eine Webseite für jeden, der sie sieht, **ein klein wenig anders** ist.
Bis zum nächsten Mal
Nun, Freunde, was für eine Reise!
Wir haben zusammen drei wunderschöne Muster erstellt, viele nützliche Paint API-Tricks gelernt und (hoffentlich!) auch etwas Spaß gehabt. Von hier aus hoffe ich, dass Sie sich inspiriert fühlen, mehr generative Kunst/Design mit CSS Houdini zu erstellen! Ich bin mir bei Ihnen nicht sicher, aber mein Portfolio-Website braucht meiner Meinung nach einen neuen Anstrich...
Bis zum nächsten Mal, liebe CSS-Magier!
Oh! Bevor Sie gehen, habe ich eine Herausforderung für Sie. Auf dieser Seite läuft ein generatives Paint API-Worklet! Können Sie es entdecken?
- Es gibt sicherlich Wege, diese Regel zu umgehen, aber sie können komplex und für dieses Tutorial nicht ganz geeignet sein. ⮑
Toller Artikel, George! Und perfekte Zeit; ich fange gerade ein neues Projekt an und Sie haben mich vielleicht gerade für das Design inspiriert. Vielen Dank für all die Erklärungen und Beispiele.
Vielen Dank! Ich freue mich zu hören, dass es Ihnen gefallen hat. Viel Erfolg bei dem neuen Projekt!
Oh ja, natürlich kann ich es erkennen. Es ist auf der Footer-Seite. Danke für die umfassende Einführung in die Paint API. Die Ergebnisse der zufällig generierten Muster sind einfach erstaunlich. Es wird einige Zeit dauern, bis ich dieses Tutorial durchgearbeitet habe. Aber es lohnt sich, die Muster für meine Websites hinzuzufügen und anzupassen. Vielen Dank für diese Ressource! Ich schätze es sehr!
Danke *Ihnen*! Ich bin so froh, dass das Tutorial hilfreich war!
Oh mein Gott. DAS IST GENIAL. Aber frisst es genauso viel CPU wie SVG?
Danke! Paint API Worklets sollten *im Allgemeinen* recht ressourcenschonend sein. Da sie die meiste Arbeit außerhalb des Hauptthreads erledigen und keinen DOM haben, den sie (ähnlich wie
<canvas>-Elemente) verfolgen müssen, können wir es uns leisten, komplexere, mehrschichtige Bilder ohne allzu große Leistungsprobleme zu erstellen. Natürlich gibt es immer einen Preis für diese Art von Dingen, aber solange wir keine *super* komplexen Bilder mit zig/hunderttausenden von Elementen erstellen, ist es cool!Phänomenaler Artikel, George! Ich liebe es, wie Sie immer wieder neue kreative Techniken einbringen.
Vielen Dank!
Generative Kunst muss sich nicht auf Zufälligkeit verlassen. Es ist Kunst, die „mit Hilfe eines autonomen Systems erstellt wird“, wie Wikipedia es definiert, was bedeutet, dass Sie die Regeln schreiben, die das Rendering erzeugen, anstatt das Rendering selbst. Wenn ich zum Beispiel über 10 Elemente loopen und etwas basierend auf ihrem Index tun, ist das generativ, aber es ist keine Zufälligkeit beteiligt...
Das ist ein wichtiger Punkt. Ich denke, *im Allgemeinen* hat die meiste generative Kunst ein Element der Zufälligkeit, aber wie Sie sagen, muss sie das sicherlich nicht. Ich muss die Terminologie dort ein wenig aktualisieren. Vielen Dank, dass Sie mich darauf aufmerksam gemacht haben und mir geholfen haben, meine eigene Definition von generativer Kunst zu überdenken!