Creating Generative Patterns with The CSS Paint API


The browser has long been a medium for artwork and design. From Lynn Fisher’s joyful A Single Div creations to Diana Smith’s staggeringly detailed CSS work, wildly inventive, extremely expert builders have — over time — constantly pushed internet applied sciences to their limits and crafted modern, inspiring visuals.

CSS, nonetheless, has by no means actually had an API devoted to… properly, simply drawing stuff! As demonstrated by the gifted people above, it definitely can render most issues, nevertheless it’s not all the time straightforward, and it’s not all the time sensible for manufacturing websites/functions.

Recently, although, CSS was gifted an thrilling new set of APIs referred to as Houdini, and one in all them — the Paint API — is particularly designed for rendering 2D graphics. For us internet people, that is extremely thrilling. For the primary time, we now have a bit of CSS that exists for the only real objective of programmatically creating pictures. The doorways to a mystical new world are properly and really open!

In this tutorial, we will likely be utilizing the Paint API to create three (hopefully!) lovely, generative patterns that might be used so as to add a scrumptious spoonful of character to a variety of internet sites/functions.

Spellbooks/textual content editors on the prepared, mates, let’s do some magic!

Intended viewers

This tutorial is ideal for folk who’re comfy writing HTML, CSS, and JavaScript. Somewhat familiarity with generative artwork and a few data of the Paint API/HTML canvas will likely be useful however not important. We will do a fast overview earlier than we get began. Speaking of which…

Before we begin

For a complete introduction to each the Paint API and generative artwork/design, I like to recommend popping over to the primary entry on this collection. If you might be new to both topic, this will likely be an important place to start out. If you don’t really feel like navigating one other article, nonetheless, listed here are a few key ideas to be accustomed to earlier than shifting on.

If you might be already accustomed to the CSS Paint API and generative artwork/design, be at liberty to skip forward to the subsequent part.

What is generative artwork/design?

Generative artwork/design is any work created with a component of likelihood. We outline some guidelines and permit a supply of randomness to information us to an end result. For instance, a rule might be “if a random number is greater than 50, render a red square, if it is less than 50, render a blue square*,”* and, within the browser, a supply of randomness might be Math.random().

By taking a generative method to creating patterns, we are able to generate near-infinite variations of a single concept — that is each an inspiring addition to the inventive course of and a unbelievable alternative to thrill our customers. Instead of displaying folks the identical imagery each time they go to a web page, we are able to show one thing particular and distinctive for them!

What is the CSS Paint API?

The Paint API offers us low-level entry to CSS rendering. Through “paint worklets” (JavaScript courses with a particular paint() operate), it permits us to dynamically create pictures utilizing a syntax virtually similar to HTML canvas. Worklets can render a picture wherever CSS expects one. For instance:

.worklet-canvas {
  background-image: paint(workletName);
}

Paint API worklets are quick, responsive, and play ever so properly with present CSS-based design methods. In short, they’re the good factor ever. The solely factor they’re missing proper now could be widespread browser help. Here’s a desk:

Desktop

Chrome Firefox IE Edge Safari
65 No No 79 No

Mobile / Tablet

Android Chrome Android Firefox Android iOS Safari
96 No 96 No

Somewhat skinny on the bottom! That’s OK, although. As the Paint API is sort of inherently ornamental, we are able to use it as a progressive enhancement if it’s out there and supply a easy, reliable fallback if not.

What we’re making

In this tutorial, we will likely be studying methods to create three distinctive generative patterns. These patterns are fairly easy, however will act as a beautiful springboard for additional experimentation. Here they’re in all their glory!

The demos on this tutorial at the moment solely work in Chrome and Edge.

“Tiny Specks”

“Bauhaus”

“Voronoi Arcs”

Before shifting on, take a second to discover the examples above. Try altering the customized properties and resizing the browser window — watch how the patterns react. Can you guess how they may work with out peeking on the JavaScript?

Getting arrange

To save time and remove the necessity for any customized construct processes, we will likely be working completely in CodePen all through this tutorial. I’ve even created a “starter Pen” that we are able to use as a base for every sample!

I do know, it’s not a lot to take a look at… but.

In the starter Pen, we’re utilizing the JavaScript part to put in writing the worklet itself. Then, within the HTML part, we load the JavaScript instantly utilizing an inner <script> tag. As Paint API worklets are particular employees (code that runs on a separate browser thread), their origin should1 exist in a standalone .js file.

Let’s break down the important thing items of code right here.

If you may have written Paint API worklets earlier than, and are accustomed to CodePen, you possibly can skip forward to the subsequent part.

Defining the worklet class

First issues first: Let’s try the JavaScript tab. Here we outline a worklet class with a easy paint() operate:

class Worklet {
  paint(ctx, geometry, props) {
    const { width, top } = geometry;
    ctx.fillStyle = "#000";
    ctx.fillRect(0, 0, width, top);
  }
}

I like to think about a worklet’s paint() operate as a callback. When the worklet’s goal aspect updates (modifications dimensions, modifies customized properties), it re-runs. A worklet’s paint() operate robotically has a number of parameters handed when it executes. In this tutorial, we have an interest within the first three:

  • ctx — a 2D drawing context similar to that of HTML canvas
  • geometry — an object containing the width/top dimensions of the worklet’s goal aspect
  • props — an array of CSS customized properties that we are able to “watch” for modifications and re-render after they do. These are an effective way of passing values to color worklets.

Our starter worklet renders a black sq. that covers the whole width/top of its goal aspect. We will utterly rewrite this paint() operate for every instance, nevertheless it’s good to have one thing outlined to examine issues are working.

Registering the worklet

Once a worklet class is outlined, it must be registered earlier than we are able to use it. To accomplish that, we name registerPaint within the worklet file itself:

if (typeof registerPaint !== "undefined") {
  registerPaint("workletName", Worklet);
}

Followed by CSS.paintWorklet.addModule() in our “main” JavaScript/HTML:

<script id="register-worklet">
  if (CSS.paintWorklet) {
    CSS.paintWorklet.addModule('https://codepen.io/georgedoescode/pen/bGrMXxm.js');
  }
</script>

We are checking registerPaint is outlined earlier than working it right here, as our pen’s JavaScript will all the time run as soon as on the primary browser thread — registerPaint solely turns into out there as soon as the JavaScript file is loaded right into a worklet utilizing CSS.paintWorklet.addModule(...).

Applying the worklet

Once registered, we are able to use our worklet to generate a picture for any CSS property that expects one. In this tutorial, we are going to concentrate on background-image:

.worklet-canvas {
  background-image: paint(workletName);
}

Package imports

You could discover a few package deal imports dangling on the high of the starter pen’s worklet file:

import random from "https://cdn.skypack.dev/random";
import seedrandom from "https://cdn.skypack.dev/seedrandom";
Can you guess what they’re?

Random quantity turbines!

All three of the patterns we’re creating on this tutorial rely closely on randomness. Paint API worklets ought to, nonetheless, (virtually) all the time be deterministic. Given the identical enter properties and dimensions, a worklet’s paint() operate ought to all the time render the identical factor.

Why?

  1. The Paint API could wish to use a cached model of a worklet’s paint() output for higher efficiency. Introducing an unpredictable aspect to a worklet renders this unattainable!
  2. A worklet’s paint() operate re-runs each time the aspect it applies to modifications dimensions. When coupled with “pure” randomness, this can lead to vital flashes of content material — a possible accessibility situation for some people.

For us, all this renders Math.random() a little bit ineffective, as it’s completely unpredictable. As another, we’re pulling in random (a superb library for working with random numbers) and seedrandom (a pseudo-random quantity generator to make use of as its base algorithm).

As a fast instance, right here’s a “random circles” worklet utilizing a pseudo-random quantity generator:

And right here’s an analogous worklet utilizing Math.random(). Warning: Resizing the aspect leads to flashing imagery.

There’s a little bit resize deal with within the bottom-right of each of the above patterns. Try resizing each parts. Notice the distinction?

Setting up every sample

Before starting every of the next patterns, navigate to the starter Pen and click on the “Fork” button within the footer. Forking a Pen creates a replica of the unique the second you click on the button. From this level, it’s yours to do no matter you want.

Once you may have forked the starter Pen, there’s a crucial additional step to finish. The URL handed to CSS.paintWorklet.addModule have to be up to date to level to the brand new fork’s JavaScript file. To discover the trail to your fork’s JavaScript, take a peek on the URL proven in your browser. You wish to seize your fork’s URL with all question parameters eliminated, and append .js — one thing like this:

Lovely. That’s the ticket! Once you may have the URL to your JavaScript, be sure you replace it right here:

<script id="register-worklet">
  if (CSS.paintWorklet) {
    // ⚠️ hey pal! replace the URL under every time you fork this pen! ⚠️
    CSS.paintWorklet.addModule('https://codepen.io/georgedoescode/pen/QWMVdPG.js');
  }
</script>

When working with this setup, chances are you’ll often must manually refresh the Pen with a view to see your modifications. To accomplish that, hit CMD/CTRL + Shift + 7.

Pattern #1 (Tiny Specks)

OK, we’re able to make our first sample. Fork the starter Pen, replace the .js file reference, and settle in for some generative enjoyable!

As a fast reminder, right here’s the completed sample:

Updating the worklet’s title

Once once more, first issues first: Let’s replace the starter worklet’s title and related references:

class TinySpecksPattern {
  // ...
}
if (typeof registerPaint !== "undefined") {
  registerPaint("tinySpecksPattern", TinySpecksPattern);
}
.worklet-canvas {
  /* ... */
  background-image: paint(tinySpecksPattern);
}

Defining the worklet’s enter properties

Our “Tiny Specks” worklet will settle for the next enter properties:

  • --pattern-seed — a seed worth for the pseudo-random quantity generator
  • --pattern-colors — the out there colours for every speck
  • --pattern-speck-count — what number of particular person specks the worklet ought to render
  • --pattern-speck-min-size — the minimal measurement for every speck
  • --pattern-speck-max-size — the utmost measurement for every speck

As our subsequent step, let’s outline the enterProperties our worklet can obtain. To accomplish that, we are able to add a getter to our TinySpecksPattern class:

class TinySpecksPattern {
  static get enterProperties() {
    return [
      "--pattern-seed",
      "--pattern-colors",
      "--pattern-speck-count",
      "--pattern-speck-min-size",
      "--pattern-speck-max-size"
    ];
  }
  // ...
}

Alongside some customized property definitions in our 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;
}

We are utilizing the Properties and Values API right here (one other member of the Houdini household) to outline our customized properties. Doing so affords us two priceless advantages. First, we are able to outline wise defaults for the enter properties our worklet expects. A tasty sprinkle of developer expertise! Second, by together with a syntax definition for every customized property, our worklet can interpret them intelligently.

For instance, we outline the syntax <coloration># for --pattern-colors. In flip, this permits us to go an array of comma-separated colours to the worklet in any legitimate CSS coloration format. When our worklet receives these values, they’ve been transformed to RGB and positioned in a neat little array. Without a syntax definition, worklets interpret all props as easy strings.

Like the Paint API, the Properties and Values API additionally has restricted browser help.

The paint() operate

Awesome! Here’s the enjoyable bit. We have created our “Tiny Speck” worklet class, registered it, and outlined what enter properties it could actually anticipate to obtain. Now, let’s make it do one thing!

As a primary step, let’s filter out the starter Pen’s paint() operate, conserving solely the width and top definitions:

paint(ctx, geometry, props) {
  const { width, top } = geometry;
}

Next, let’s retailer our enter properties in some variables:

const seed = props.get("--pattern-seed").worth;
const colours = props.getAll("--pattern-colors").map((c) => c.toString());
const depend = props.get("--pattern-speck-count").worth;
const minSize = props.get("--pattern-speck-min-size").worth;
const maxSize = props.get("--pattern-speck-max-size").worth;

Next, we should always initialize our pseudo-random quantity generator:

random.use(seedrandom(seed));

Ahhh, predictable randomness! We are re-seeding seedrandom with the identical seed worth each time paint() runs, leading to a constant stream of random numbers throughout renders.

Finally, let’s paint our specks!

First off, we create a for-loop that iterates depend occasions. In each iteration of this loop, we’re creating one particular person speck:

for (let i = 0; i < depend; i++) {
}

As the primary motion in our for-loop, we outline an x and y place for the speck. Somewhere between 0 and the width/top of the worklet’s goal aspect is ideal:

const x = random.float(0, width);
const y = random.float(0, top);

Next, we select a random measurement (for the radius):

const radius = random.float(minSize, maxSize);

So, we now have a place and a measurement outlined for the speck. Let’s select a random coloration from our colours to fill it with:

ctx.fillStyle = colours[random.int(0, colors.length - 1)];

Alright. We are all set. Let’s use ctx to render one thing!

The very first thing we have to do at this level is save() the state of our drawing context. Why? We wish to rotate every speck, however when working with a 2D drawing context like this, we can not rotate particular person objects. To rotate an object, we now have to spin the whole drawing area. If we don’t save() and restore() the context, the rotation/translation in each iteration will stack, leaving us with a really messy (or empty) canvas!

ctx.save();

Now that we now have saved the drawing context’s state, we are able to translate to the speck’s heart level (outlined by our x/y variables) and apply a rotation. Translating to the middle level of an object earlier than rotating ensures the item rotates round its heart axis:

ctx.translate(x, y);
ctx.rotate(((random.float(0, 360) * 180) / Math.PI) * 2);
ctx.translate(-x, -y);

After making use of our rotation, we translate again to the top-left nook of the drawing area.

We select a random worth between 0 and 360 (levels) right here, then convert it into radians (the rotation format ctx understands).

Awesome! Finally, let’s render an ellipse — that is the form that defines our specks:

ctx.beginPath();
ctx.ellipse(x, y, radius, radius / 2, 0, Math.PI * 2, 0);
ctx.fill();

Here’s a easy pen displaying the type of our random specks, a little bit nearer up:

Perfect. Now, all we have to do is restore the drawing context:

ctx.restore();

That’s it! Our first sample is full. Let’s additionally apply a background-color to our worklet canvas to complete off the impact:

.worklet-canvas {
  background-color: #90c3a5;
  background-image: paint(tinySpecksPattern);
}

Next steps

From right here, strive altering the colours, shapes, and distribution of the specks. There are tons of of instructions you could possibly take this sample! Here’s an instance utilizing little triangles reasonably than ellipses:

Onwards!

Pattern #2 (Bauhaus)

Nice work! That’s one sample down. Onto the subsequent one. Once once more, fork the starter Pen and replace the worklet’s JavaScript reference to get began.

As a fast refresher, right here’s the completed sample we’re working towards:

Updating the worklet’s title

Just like we did final time, let’s kick issues off by updating the worklet’s title and related references:

class BauhausPattern {
  // ...
}

if (typeof registerPaint !== "undefined") {
  registerPaint("bauhausPattern", BauhausPattern);
}
.worklet-canvas {
  /* ... */
  background-image: paint(bauhausPattern);
}

Lovely.

Defining the worklet’s enter properties

Our “Bauhaus Pattern” worklet expects the next enter properties:

  • --pattern-seed — a seed worth for the pseudo-random quantity generator
  • --pattern-colors — the out there colours for every form within the sample
  • --pattern-size — the worth used to outline each the width and top of a sq. sample space
  • --pattern-detail — the variety of columns/rows to divide the sq. sample into

Let’s add these enter properties to our worklet:

class BahausPattern {
  static get enterProperties() {
    return [
      "--pattern-seed",
      "--pattern-colors",
      "--pattern-size",
      "--pattern-detail"
    ];
  }
  // ...
}

…and outline them in our CSS, once more, utilizing the 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;
}

Excellent. Let’s paint!

The paint() operate

Again, let’s filter out the starter worklet’s paint operate, leaving solely the width and top definition:

paint(ctx, geometry, props) {
  const { width, top } = geometry;
}

Next, let’s retailer our enter properties in some variables:

const patternSize = props.get("--pattern-size").worth;
const patternDetail = props.get("--pattern-detail").worth;
const seed = props.get("--pattern-seed").worth;
const colours = props.getAll("--pattern-colors").map((c) => c.toString());

Now, we are able to seed our pseudo-random quantity generator identical to earlier than:

random.use(seedrandom(seed));

Awesome! As you might need observed, the setup for Paint API worklets is all the time considerably related. It’s not essentially the most thrilling course of, nevertheless it’s a superb alternative to replicate on the structure of your worklet and the way different builders could use it.

So, with this worklet, we create a fixed-dimension sq. sample full of shapes. This fixed-dimension sample is then scaled up or right down to cowl the worklet’s goal aspect. Think of this conduct a bit like background-size: cowl in CSS!

Here’s a diagram:

To obtain this conduct in our code, let’s add a scaleContext operate to our worklet class:

scaleCtx(ctx, width, top, elementWidth, elementHeight) {
  const ratio = Math.max(elementWidth / width, elementHeight / top);
  const heartShiftX = (elementWidth - width * ratio) / 2;
  const heartShiftY = (elementHeight - top * ratio) / 2;
  ctx.setTransform(ratio, 0, 0, ratio, heartShiftX, heartShiftY);
}

And name it in our paint() operate:

this.scaleCtx(ctx, patternSize, patternSize, width, top);

Now, we are able to work to a set of fastened dimensions and have our worklet’s drawing context robotically scale every thing for us — a useful operate for plenty of use circumstances.

Next up, we’re going to create a 2D grid of cells. To accomplish that, we outline a cellSize variable (the scale of the sample space divided by the variety of columns/rows we want):

const cellSize = patternSize / patternDetail;

Then, we are able to use the cellSize variable to “step-through” the grid, creating equally-spaced, equally-sized cells so as to add random shapes to:

for (let x = 0; x < patternSize; x += cellSize) {
  for (let y = 0; y < patternSize; y += cellSize) {
  }
}

Within the second nested loop, we are able to start to render stuff!

First off, let’s select a random coloration for the present form:

const coloration = colours[random.int(0, colors.length - 1)];

ctx.fillStyle = coloration;

Next, let’s retailer a reference to the present cell’s heart x and y place:

const cx = x + cellSize / 2;
const cy = y + cellSize / 2;

In this worklet, we’re positioning all of our shapes relative to their heart level. While we’re right here, let’s add some utility features to our worklet file to assist us rapidly render center-aligned form objects. These can reside exterior of the Worklet class:

operate circle(ctx, cx, cy, radius) {
  ctx.beginPath();
  ctx.arc(cx, cy, radius, 0, Math.PI * 2);
  ctx.closePath();
}

operate arc(ctx, cx, cy, radius) {
  ctx.beginPath();
  ctx.arc(cx, cy, radius, 0, Math.PI * 1);
  ctx.closePath();
}

operate rectangle(ctx, cx, cy, measurement) {
  ctx.beginPath();
  ctx.rect(cx - measurement / 2, cy - measurement / 2, measurement, measurement);
  ctx.closePath();
}

operate triangle(ctx, cx, cy, measurement) {
  const originX = cx - measurement / 2;
  const originY = cy - measurement / 2;
  ctx.beginPath();
  ctx.transferTo(originX, originY);
  ctx.lineTo(originX + measurement, originY + measurement);
  ctx.lineTo(originX, originY + measurement);
  ctx.closePath();
}

I gained’t go into an excessive amount of element right here, however right here’s a diagram visualizing how every of those features work:

If you get caught on the graphics rendering a part of any of the worklets on this tutorial, take a look at the MDN docs on HTML canvas. The syntax/utilization is sort of similar to the 2D graphics context out there in Paint API worklets.

Cool! Let’s head again over to our paint() operate’s nested loop. The subsequent factor we have to do is select what form to render. To accomplish that, we are able to choose a random string from an array of potentialities:

const shapeChoice = ["circle", "arc", "rectangle", "triangle"][
  random.int(0, 3)
];

We may also choose a random rotation quantity in a really related manner:

const rotationDegrees = [0, 90, 180][random.int(0, 2)];

Perfect. We are able to render!

To begin, let’s save our drawing context’s state, identical to within the earlier worklet:

ctx.save();

Next, we are able to translate to the middle level of the present cell and rotate the canvas utilizing the random worth we simply selected:

ctx.translate(cx, cy);
ctx.rotate((rotationDegrees * Math.PI) / 180);
ctx.translate(-cx, -cy);

Now we are able to render the form itself! Let’s go our shapeChoice variable to a change assertion and use it to determine which form rendering operate to run:

change (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();

Finally, all we have to do is restore() our drawing context prepared for the subsequent form:

ctx.restore();

With that, our Bauhaus Grids worklet is full!

Next steps

There are so many instructions you could possibly take this worklet. How may you parameterize it additional? Could you add a “bias” for particular shapes/colours? Could you add extra form sorts?

Always experiment — following together with the examples we’re creating collectively is a wonderful begin, however one of the simplest ways to study is to make your personal stuff! If you might be caught for inspiration, take a peek at some patterns on Dribbble, look to your favourite artists, the structure round you, nature, you title it!

As a easy instance, right here’s the identical worklet, in a wholly completely different coloration scheme:

Pattern #3 (Voronoi Arcs)

So far, we now have created each a chaotic sample and one which aligns strictly to a grid. For our final instance, let’s construct one which sits someplace between the 2.

As one final reminder, right here’s the completed sample:

Before we leap in and write any code, let’s check out how this worklet… works.

A short introduction to Voronoi tessellations

As instructed by the title, this worklet makes use of one thing known as a Voronoi tessellation to calculate its structure. A Voronoi tessellation (or diagram) is, in short, a technique to partition an area into non-overlapping polygons.

We add a group of factors to a 2D area. Then for every level, calculate a polygon that comprises solely it and no different factors. Once calculated, the polygons can be utilized as a type of “grid” to place something.

Here’s an animated instance:

The fascinating factor about Voronoi-based layouts is that they’re responsive in a reasonably uncommon manner. As the factors in a Voronoi tessellation transfer round, the polygons robotically re-arrange themselves to fill the area!

Try resizing the aspect under and watch what occurs!

Cool, proper?

If you want to study extra about all issues Voronoi, I’ve an article that goes in-depth. For now, although, that is all we want.

Updating the worklet’s title

Alright, people, we all know the deal right here. Let’s fork the starter Pen, replace the JavaScript import, and alter the worklet’s title and references:

class VoronoiPattern {
  // ...
}

if (typeof registerPaint !== "undefined") {
  registerPaint("voronoiPattern", VoronoiPattern);
}
.worklet-canvas {
  /* ... */
  background-image: paint(voronoiPattern);
}

Defining the worklet’s enter properties

Our VoronoiPattern worklet expects the next enter properties:

  • --pattern-seed — a seed worth for the pseudo-random quantity generator
  • --pattern-colors — the out there colours for every arc/circle within the sample
  • --pattern-background — the sample’s background coloration

Let’s add these enter properties to our worklet:

class VoronoiPattern {
  static get enterProperties() {
    return ["--pattern-seed", "--pattern-colors", "--pattern-background"];
  }
  // ...
}

…and register them in our 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;
}

Nice! We are all set. Overalls on, mates — allow us to paint.

The paint() operate

First, let’s filter out the starter worklet’s paint() operate, retaining solely the width and top definitions. We can then create some variables utilizing our enter properties, and seed our pseudo-random quantity generator, too. Just like in our earlier examples:

paint(ctx, geometry, props) {
  const { width, top } = geometry;

  const seed = props.get("--pattern-seed").worth;
  const background = props.get("--pattern-background").toString();
  const colours = props.getAll("--pattern-colors").map((c) => c.toString());

  random.use(seedrandom(seed));
}

Before we do the rest, let’s paint a fast background coloration:

ctx.fillStyle = background;
ctx.fillRect(0, 0, width, top);

Next, let’s import a helper operate that may enable us to rapidly cook dinner up a Voronoi tessellation:

import { createVoronoiTessellation } from "https://cdn.skypack.dev/@georgedoescode/generative-utils";

This operate is basically a wrapper round d3-delaunay and is a part of my generative-utils repository. You can view the supply code on GitHub. With “classic” information buildings/algorithms equivalent to Voronoi tessellations, there isn’t a must reinvent the wheel — until you wish to, after all!

Now that we now have our createVoronoiTessellation operate out there, let’s add it to paint():

const { cells } = createVoronoiTessellation({
  width,
  top,
  factors: [...Array(24)].map(() => ({
    x: random.float(0, width),
    y: random.float(0, top)
  }))
});

Here, we create a Voronoi Tessellation on the width and top of the worklet’s goal aspect, with 24 controlling factors.

Awesome. Time to render our shapes! Lots of this code ought to be acquainted to us, because of the earlier two examples.

First, we loop by every cell within the tessellation:

cells.forEach((cell) => {
});

For every cell, the very first thing we do is select a coloration:

ctx.fillStyle = colours[random.int(0, colors.length - 1)];

Next, we retailer a reference to the middle x and y values of the cell:

const cx = cell.centroid.x;
const cy = cell.centroid.y;

Next, we save the context’s present state and rotate the canvas across the cell’s heart level:

ctx.save();

ctx.translate(cx, cy);
ctx.rotate((random.float(0, 360) / 180) * Math.PI);
ctx.translate(-cx, -cy);

Cool! Now, we are able to render one thing. Let’s draw an arc with an finish angle of both PI or PI * 2. To me and also you, a semi-circle or a circle:

ctx.beginPath();
ctx.arc(
  cell.centroid.x,
  cell.centroid.y,
  cell.innerCircleRadius * 0.75,
  0,
  Math.PI * random.int(1, 2)
);
ctx.fill();

Our createVoronoiTessellation operate attaches a particular innerCircleRadius to every cell — that is the biggest doable circle that may match at its heart with out touching any edges. Think of it as a useful information for scaling objects to the bounds of a cell. In the snippet above, we’re utilizing innerCircleRadius to find out the scale of our arcs.

Here’s a easy pen highlighting what’s taking place right here:

Now that we now have added a “primary” arc to every cell, let’s add one other one, 25% of the time. This time, nonetheless, we are able to set the arc’s fill coloration to our worklets background coloration. Doing so offers us the impact of a little bit gap in the course of a few of the shapes!

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();
}

Great! All we have to do now could be restore the drawing context:

ctx.restore();

And, that’s it!

Next steps

The lovely factor about Voronoi tessellations is that you need to use them to place something in any respect. In our instance, we used arcs, however you could possibly render rectangles, traces, triangles, no matter! Perhaps you could possibly even render the outlines of the cells themselves?

Here’s a model of our VoronoiPattern worklet that renders numerous small traces, reasonably than circles and semicircles:

Randomizing patterns

You could have observed that up till this level, all of our patterns have obtained a static --pattern-seed worth. This is okay, however what if we want our patterns to be random every time they show? Well, fortunate for us, all we have to do is ready the --pattern-seed variable when the web page hundreds to be a random quantity. Something like this:

doc.documentElement.fashion.setProperty('--pattern-seed', Math.random() * 10000);

We touched on this briefly earlier, however this can be a pretty manner to ensure a webpage is a tiny bit completely different for everybody that sees it.

Until subsequent time

Well, mates, what a visit!

We have created three lovely patterns collectively, discovered numerous useful Paint API methods, and (hopefully!) had some enjoyable, too. From right here, I hope you are feeling impressed to make some extra generative artwork/design with CSS Houdini! I’m undecided about you, however I really feel like my portfolio web site wants a brand new coat of paint…

Until subsequent time, fellow CSS magicians!

Oh! Before you go, I’ve a problem for you. There is a generative Paint API worklet working on this very web page! Can you see it?

Synesy.org