Creating Generative Patterns with The CSS Paint API
Publikováno: 24.11.2021
The browser has long been a medium for art and design. From Lynn Fisher’s joyful A Single Div creations to Diana Smith’s staggeringly detailed CSS paintings, wildly creative, highly skilled developers have — over the years — continuously pushed …
The post Creating Generative Patterns with The CSS Paint API appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.
The browser has long been a medium for art and design. From Lynn Fisher’s joyful A Single Div creations to Diana Smith’s staggeringly detailed CSS paintings, wildly creative, highly skilled developers have — over the years — continuously pushed web technologies to their limits and crafted innovative, inspiring visuals.
CSS, however, has never really had an API dedicated to… well, just drawing stuff! As demonstrated by the talented folks above, it certainly can render most things, but it’s not always easy, and it’s not always practical for production sites/applications.
Recently, though, CSS was gifted an exciting new set of APIs known as Houdini, and one of them — the Paint API — is specifically designed for rendering 2D graphics. For us web folk, this is incredibly exciting. For the first time, we have a section of CSS that exists for the sole purpose of programmatically creating images. The doors to a mystical new world are well and truly open!
In this tutorial, we will be using the Paint API to create three (hopefully!) beautiful, generative patterns that could be used to add a delicious spoonful of character to a range of websites/applications.
Spellbooks/text editors at the ready, friends, let’s do some magic!
Intended audience
This tutorial is perfect for folks who are comfortable writing HTML, CSS, and JavaScript. A little familiarity with generative art and some knowledge of the Paint API/HTML canvas will be handy but not essential. We will do a quick overview before we get started. Speaking of which…
Before we start
For a comprehensive introduction to both the Paint API and generative art/design, I recommend popping over to the first entry in this series. If you are new to either subject, this will be a great place to start. If you don’t feel like navigating another article, however, here are a couple of key concepts to be familiar with before moving on.
If you are already familiar with the CSS Paint API and generative art/design, feel free to skip ahead to the next section.
What is generative art/design?
Generative art/design is any work created with an element of chance. We define some rules and allow a source of randomness to guide us to an outcome. For example, a rule could be “if a random number is greater than 50, render a red square, if it is less than 50, render a blue square*,”* and, in the browser, a source of randomness could be Math.random()
.
By taking a generative approach to creating patterns, we can generate near-infinite variations of a single idea — this is both an inspiring addition to the creative process and a fantastic opportunity to delight our users. Instead of showing people the same imagery every time they visit a page, we can display something special and unique for them!
What is the CSS Paint API?
The Paint API gives us low-level access to CSS rendering. Through “paint worklets” (JavaScript classes with a special paint()
function), it allows us to dynamically create images using a syntax almost identical to HTML canvas. Worklets can render an image wherever CSS expects one. For example:
.worklet-canvas {
background-image: paint(workletName);
}
Paint API worklets are fast, responsive, and play ever so well with existing CSS-based design systems. In short, they are the coolest thing ever. The only thing they are lacking right now is widespread browser support. Here’s a table:
This browser support data is from Caniuse, which has more detail. A number indicates that browser supports the feature at that version and up.
Desktop
Chrome | Firefox | IE | Edge | Safari |
---|---|---|---|---|
65 | No | No | 79 | No |
Mobile / Tablet
Android Chrome | Android Firefox | Android | iOS Safari |
---|---|---|---|
95 | No | 95 | No |
A little thin on the ground! That’s OK, though. As the Paint API is almost inherently decorative, we can use it as a progressive enhancement if it’s available and provide a simple, dependable fallback if not.
What we are making
In this tutorial, we will be learning how to create three unique generative patterns. These patterns are quite simple, but will act as a wonderful springboard for further experimentation. Here they are in all their glory!
The demos in this tutorial currently only work in Chrome and Edge.
“Tiny Specks”
“Bauhaus”
“Voronoi Arcs”
Before moving on, take a moment to explore the examples above. Try changing the custom properties and resizing the browser window — watch how the patterns react. Can you guess how they might work without peeking at the JavaScript?
Getting set up
To save time and eliminate the need for any custom build processes, we will be working entirely in CodePen throughout this tutorial. I have even created a “starter Pen” that we can use as a base for each pattern!
I know, it’s not much to look at… yet.
In the starter Pen, we are using the JavaScript section to write the worklet itself. Then, in the HTML section, we load the JavaScript directly using an internal <script>
tag. As Paint API worklets are special workers (code that runs on a separate browser thread), their origin must1 exist in a standalone .js
file.
Let’s break down the key pieces of code here.
If you have written Paint API worklets before, and are familiar with CodePen, you can skip ahead to the next section.
Defining the worklet class
First things first: Let’s check out the JavaScript tab. Here we define a worklet class with a simple paint()
function:
class Worklet {
paint(ctx, geometry, props) {
const { width, height } = geometry;
ctx.fillStyle = "#000";
ctx.fillRect(0, 0, width, height);
}
}
I like to think of a worklet’s paint()
function as a callback. When the worklet’s target element updates (changes dimensions, modifies custom properties), it re-runs. A worklet’s paint()
function automatically has a few parameters passed when it executes. In this tutorial, we are interested in the first three:
ctx
— a 2D drawing context very similar to that of HTML canvasgeometry
— an object containing the width/height dimensions of the worklet’s target elementprops
— an array of CSS custom properties that we can “watch” for changes and re-render when they do. These are a great way of passing values to paint worklets.
Our starter worklet renders a black square that covers the entire width/height of its target element. We will completely rewrite this paint()
function for each example, but it’s nice to have something defined to check things are working.
Registering the worklet
Once a worklet class is defined, it needs to be registered before we can use it. To do so, we call registerPaint
in 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 defined before running it here, as our pen’s JavaScript will always run once on the main browser thread — registerPaint
only becomes available once the JavaScript file is loaded into a worklet using CSS.paintWorklet.addModule(...)
.
Applying the worklet
Once registered, we can use our worklet to generate an image for any CSS property that expects one. In this tutorial, we will focus on background-image
:
.worklet-canvas {
background-image: paint(workletName);
}
Package imports
You may notice a couple of package imports dangling at the top 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 are?
Random number generators!
All three of the patterns we are creating in this tutorial rely heavily on randomness. Paint API worklets should, however, (almost) always be deterministic. Given the same input properties and dimensions, a worklet’s paint()
function should always render the same thing.
Why?
- The Paint API may want to use a cached version of a worklet’s
paint()
output for better performance. Introducing an unpredictable element to a worklet renders this impossible! - A worklet’s
paint()
function re-runs whenever the element it applies to changes dimensions. When coupled with “pure” randomness, this can result in significant flashes of content — a potential accessibility issue for some folks.
For us, all this renders Math.random()
a little useless, as it is entirely unpredictable. As an alternative, we are pulling in random (an excellent library for working with random numbers) and seedrandom (a pseudo-random number generator to use as its base algorithm).
As a quick example, here’s a “random circles” worklet using a pseudo-random number generator:
And here’s a similar worklet using Math.random()
. Warning: Resizing the element results in flashing imagery.
There’s a little resize
handle in the bottom-right of both of the above patterns. Try resizing both elements. Notice the difference?
Setting up each pattern
Before beginning each of the following patterns, navigate to the starter Pen and click the “Fork” button in the footer. Forking a Pen creates a copy of the original the moment you click the button. From this point, it is yours to do whatever you like.
Once you have forked the starter Pen, there is a critical extra step to complete. The URL passed to CSS.paintWorklet.addModule
must be updated to point to the new fork’s JavaScript file. To find the path for your fork’s JavaScript, take a peek at the URL shown in your browser. You want to grab your fork’s URL with all query parameters removed, and append .js
— something like this:
Lovely. That’s the ticket! Once you have the URL for your JavaScript, make sure you update it here:
<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>
When working with this setup, you may occasionally need to manually refresh the Pen in order to see your changes. To do so, hit CMD/CTRL + Shift + 7.
Pattern #1 (Tiny Specks)
OK, we are ready to make our first pattern. Fork the starter Pen, update the .js
file reference, and settle in for some generative fun!
As a quick reminder, here’s the finished pattern:
Updating the worklet’s name
Once again, first things first: Let’s update the starter worklet’s name and relevant references:
class TinySpecksPattern {
// ...
}
if (typeof registerPaint !== "undefined") {
registerPaint("tinySpecksPattern", TinySpecksPattern);
}
.worklet-canvas {
/* ... */
background-image: paint(tinySpecksPattern);
}
Defining the worklet’s input properties
Our “Tiny Specks” worklet will accept the following input properties:
--pattern-seed
— a seed value for the pseudo-random number generator--pattern-colors
— the available colors for each speck--pattern-speck-count
— how many individual specks the worklet should render--pattern-speck-min-size
— the minimum size for each speck--pattern-speck-max-size
— the maximum size for each speck
As our next step, let’s define the inputProperties
our worklet can receive. To do so, we can add a getter to our TinySpecksPattern
class:
class TinySpecksPattern {
static get inputProperties() {
return [
"--pattern-seed",
"--pattern-colors",
"--pattern-speck-count",
"--pattern-speck-min-size",
"--pattern-speck-max-size"
];
}
// ...
}
Alongside some custom 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 using the Properties and Values API here (another member of the Houdini family) to define our custom properties. Doing so affords us two valuable benefits. First, we can define sensible defaults for the input properties our worklet expects. A tasty sprinkle of developer experience! Second, by including a syntax
definition for each custom property, our worklet can interpret them intelligently.
For example, we define the syntax <color>#
for --pattern-colors
. In turn, this allows us to pass an array of comma-separated colors to the worklet in any valid CSS color format. When our worklet receives these values, they have been converted to RGB and placed in a neat little array. Without a syntax
definition, worklets interpret all props
as simple strings.
Like the Paint API, the Properties and Values API also has limited browser support.
The paint()
function
Awesome! Here’s the fun bit. We have created our “Tiny Speck” worklet class, registered it, and defined what input properties it can expect to receive. Now, let’s make it do something!
As a first step, let’s clear out the starter Pen’s paint()
function, keeping only the width
and height
definitions:
paint(ctx, geometry, props) {
const { width, height } = geometry;
}
Next, let’s store our input properties in some variables:
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;
Next, we should initialize our pseudo-random number generator:
random.use(seedrandom(seed));
Ahhh, predictable randomness! We are re-seeding seedrandom
with the same seed
value every time paint()
runs, resulting in a consistent stream of random numbers across renders.
Finally, let’s paint our specks!
First off, we create a for-loop that iterates count
times. In every iteration of this loop, we are creating one individual speck:
for (let i = 0; i < count; i++) {
}
As the first action in our for-loop, we define an x
and y
position for the speck. Somewhere between 0 and the width/height of the worklet’s target element is perfect:
const x = random.float(0, width);
const y = random.float(0, height);
Next, we choose a random size (for the radius
):
const radius = random.float(minSize, maxSize);
So, we have a position and a size defined for the speck. Let’s choose a random color from our colors
to fill it with:
ctx.fillStyle = colors[random.int(0, colors.length - 1)];
Alright. We are all set. Let’s use ctx
to render something!
The first thing we need to do at this point is save()
the state of our drawing context. Why? We want to rotate each speck, but when working with a 2D drawing context like this, we cannot rotate individual items. To rotate an object, we have to spin the entire drawing space. If we don’t save()
and restore()
the context, the rotation/translation in every iteration will stack, leaving us with a very messy (or empty) canvas!
ctx.save();
Now that we have saved the drawing context’s state, we can translate
to the speck’s center point (defined by our x
/y
variables) and apply a rotation. Translating to the center point of an object before rotating ensures the object rotates around its center axis:
ctx.translate(x, y);
ctx.rotate(((random.float(0, 360) * 180) / Math.PI) * 2);
ctx.translate(-x, -y);
After applying our rotation, we translate back to the top-left corner of the drawing space.
We choose a random value between 0 and 360 (degrees) here, then convert it into radians (the rotation format ctx
understands).
Awesome! Finally, let’s render an ellipse — this is the shape that defines our specks:
ctx.beginPath();
ctx.ellipse(x, y, radius, radius / 2, 0, Math.PI * 2, 0);
ctx.fill();
Here’s a simple pen showing the form of our random specks, a little closer up:
Perfect. Now, all we need to do is restore the drawing context:
ctx.restore();
That’s it! Our first pattern is complete. Let’s also apply a background-color
to our worklet canvas to finish off the effect:
.worklet-canvas {
background-color: #90c3a5;
background-image: paint(tinySpecksPattern);
}
Next steps
From here, try changing the colors, shapes, and distribution of the specks. There are hundreds of directions you could take this pattern! Here’s an example using little triangles rather than ellipses:
Onwards!
Pattern #2 (Bauhaus)
Nice work! That’s one pattern down. Onto the next one. Once again, fork the starter Pen and update the worklet’s JavaScript reference to get started.
As a quick refresher, here’s the finished pattern we are working toward:
Updating the worklet’s name
Just like we did last time, let’s kick things off by updating the worklet’s name and relevant references:
class BauhausPattern {
// ...
}
if (typeof registerPaint !== "undefined") {
registerPaint("bauhausPattern", BauhausPattern);
}
.worklet-canvas {
/* ... */
background-image: paint(bauhausPattern);
}
Lovely.
Defining the worklet’s input properties
Our “Bauhaus Pattern” worklet expects the following input properties:
--pattern-seed
— a seed value for the pseudo-random number generator--pattern-colors
— the available colors for each shape in the pattern--pattern-size
— the value used to define both the width and height of a square pattern area--pattern-detail
— the number of columns/rows to divide the square pattern into
Let’s add these input properties to our worklet:
class BahausPattern {
static get inputProperties() {
return [
"--pattern-seed",
"--pattern-colors",
"--pattern-size",
"--pattern-detail"
];
}
// ...
}
…and define them in our CSS, again, using 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()
function
Again, let’s clear out the starter worklet’s paint function, leaving only the width
and height
definition:
paint(ctx, geometry, props) {
const { width, height } = geometry;
}
Next, let’s store our input properties in some variables:
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());
Now, we can seed our pseudo-random number generator just like before:
random.use(seedrandom(seed));
Awesome! As you might have noticed, the setup for Paint API worklets is always somewhat similar. It’s not the most exciting process, but it’s an excellent opportunity to reflect on the architecture of your worklet and how other developers may use it.
So, with this worklet, we create a fixed-dimension square pattern filled with shapes. This fixed-dimension pattern is then scaled up or down to cover the worklet’s target element. Think of this behavior a bit like background-size: cover
in CSS!
Here’s a diagram:
To achieve this behavior in our code, let’s add a scaleContext
function to our worklet class:
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);
}
And call it in our paint()
function:
this.scaleCtx(ctx, patternSize, patternSize, width, height);
Now, we can work to a set of fixed dimensions and have our worklet’s drawing context automatically scale everything for us — a handy function for lots of use cases.
Next up, we are going to create a 2D grid of cells. To do so, we define a cellSize
variable (the size of the pattern area divided by the number of columns/rows we would like):
const cellSize = patternSize / patternDetail;
Then, we can use the cellSize
variable to “step-through” the grid, creating equally-spaced, equally-sized cells 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 can begin to render stuff!
First off, let’s choose a random color for the current shape:
const color = colors[random.int(0, colors.length - 1)];
ctx.fillStyle = color;
Next, let’s store a reference to the current cell’s center x
and y
position:
const cx = x + cellSize / 2;
const cy = y + cellSize / 2;
In this worklet, we are positioning all of our shapes relative to their center point. While we are here, let’s add some utility functions to our worklet file to help us quickly render center-aligned shape objects. These can live outside of the Worklet
class:
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();
}
I won’t go into too much detail here, but here’s a diagram visualizing how each of these functions work:
If you get stuck on the graphics rendering part of any of the worklets in this tutorial, look at the MDN docs on HTML canvas. The syntax/usage is almost identical to the 2D graphics context available in Paint API worklets.
Cool! Let’s head back over to our paint()
function’s nested loop. The next thing we need to do is choose what shape to render. To do so, we can pick a random string from an array of possibilities:
const shapeChoice = ["circle", "arc", "rectangle", "triangle"][
random.int(0, 3)
];
We can also pick a random rotation amount in a very similar way:
const rotationDegrees = [0, 90, 180][random.int(0, 2)];
Perfect. We are ready to render!
To start, let’s save our drawing context’s state, just like in the previous worklet:
ctx.save();
Next, we can translate
to the center point of the current cell and rotate the canvas using the random value we just chose:
ctx.translate(cx, cy);
ctx.rotate((rotationDegrees * Math.PI) / 180);
ctx.translate(-cx, -cy);
Now we can render the shape itself! Let’s pass our shapeChoice
variable to a switch
statement and use it to decide which shape rendering function to run:
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();
Finally, all we need to do is restore()
our drawing context ready for the next shape:
ctx.restore();
With that, our Bauhaus Grids worklet is complete!
Next steps
There are so many directions you could take this worklet. How could you parameterize it further? Could you add a “bias” for specific shapes/colors? Could you add more shape types?
Always experiment — following along with the examples we are creating together is an excellent start, but the best way to learn is to make your own stuff! If you are stuck for inspiration, take a peek at some patterns on Dribbble, look to your favorite artists, the architecture around you, nature, you name it!
As a simple example, here’s the same worklet, in an entirely different color scheme:
Pattern #3 (Voronoi Arcs)
So far, we have created both a chaotic pattern and one that aligns strictly to a grid. For our last example, let’s build one that sits somewhere between the two.
As one last reminder, here’s the finished pattern:
Before we jump in and write any code, let’s take a look at how this worklet… works.
A brief introduction to Voronoi tessellations
As suggested by the name, this worklet uses something called a Voronoi tessellation to calculate its layout. A Voronoi tessellation (or diagram) is, in short, a way to partition a space into non-overlapping polygons.
We add a collection of points to a 2D space. Then for each point, calculate a polygon that contains only it and no other points. Once calculated, the polygons can be used as a kind of “grid” to position anything.
Here’s an animated example:
The fascinating thing about Voronoi-based layouts is that they are responsive in a rather unusual way. As the points in a Voronoi tessellation move around, the polygons automatically re-arrange themselves to fill the space!
Try resizing the element below and watch what happens!
Cool, right?
If you would like to learn more about all things Voronoi, I have an article that goes in-depth. For now, though, this is all we need.
Updating the worklet’s name
Alright, folks, we know the deal here. Let’s fork the starter Pen, update the JavaScript import, and change the worklet’s name and references:
class VoronoiPattern {
// ...
}
if (typeof registerPaint !== "undefined") {
registerPaint("voronoiPattern", VoronoiPattern);
}
.worklet-canvas {
/* ... */
background-image: paint(voronoiPattern);
}
Defining the worklet’s input properties
Our VoronoiPattern
worklet expects the following input properties:
--pattern-seed
— a seed value for the pseudo-random number generator--pattern-colors
— the available colors for each arc/circle in the pattern--pattern-background
— the pattern’s background color
Let’s add these input properties to our worklet:
class VoronoiPattern {
static get inputProperties() {
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, friends — let us paint.
The paint()
function
First, let’s clear out the starter worklet’s paint()
function, retaining only the width
and height
definitions. We can then create some variables using our input properties, and seed our pseudo-random number generator, too. Just like in our previous examples:
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));
}
Before we do anything else, let’s paint a quick background color:
ctx.fillStyle = background;
ctx.fillRect(0, 0, width, height);
Next, let’s import a helper function that will allow us to quickly cook up a Voronoi tessellation:
import { createVoronoiTessellation } from "https://cdn.skypack.dev/@georgedoescode/generative-utils";
This function is essentially a wrapper around d3-delaunay and is part of my generative-utils repository. You can view the source code on GitHub. With “classic” data structures/algorithms such as Voronoi tessellations, there is no need to reinvent the wheel — unless you want to, of course!
Now that we have our createVoronoiTessellation
function available, let’s add it to paint()
:
const { cells } = createVoronoiTessellation({
width,
height,
points: [...Array(24)].map(() => ({
x: random.float(0, width),
y: random.float(0, height)
}))
});
Here, we create a Voronoi Tessellation at the width and height of the worklet’s target element, with 24 controlling points.
Awesome. Time to render our shapes! Lots of this code should be familiar to us, thanks to the previous two examples.
First, we loop through each cell in the tessellation:
cells.forEach((cell) => {
});
For each cell, the first thing we do is choose a color:
ctx.fillStyle = colors[random.int(0, colors.length - 1)];
Next, we store a reference to the center x and y values of the cell:
const cx = cell.centroid.x;
const cy = cell.centroid.y;
Next, we save
the context’s current state and rotate the canvas around the cell’s center point:
ctx.save();
ctx.translate(cx, cy);
ctx.rotate((random.float(0, 360) / 180) * Math.PI);
ctx.translate(-cx, -cy);
Cool! Now, we can render something. Let’s draw an arc with an end angle of either PI
or PI * 2
. To me and 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
function attaches a special innerCircleRadius
to each cell
— this is the largest possible circle that can fit at its center without touching any edges. Think of it as a handy guide for scaling objects to the bounds of a cell. In the snippet above, we are using innerCircleRadius
to determine the size of our arcs.
Here’s a simple pen highlighting what’s happening here:
Now that we have added a “primary” arc to each cell, let’s add another one, 25% of the time. This time, however, we can set the arc’s fill color to our worklets background color. Doing so gives us the effect of a little hole in the middle of some 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 need to do now is restore the drawing context:
ctx.restore();
And, that’s it!
Next steps
The beautiful thing about Voronoi tessellations is that you can use them to position anything at all. In our example, we used arcs, but you could render rectangles, lines, triangles, whatever! Perhaps you could even render the outlines of the cells themselves?
Here’s a version of our VoronoiPattern
worklet that renders lots of small lines, rather than circles and semicircles:
Randomizing patterns
You may have noticed that up until this point, all of our patterns have received a static --pattern-seed
value. This is fine, but what if we would like our patterns to be random each time they display? Well, lucky for us, all we need to do is set the --pattern-seed
variable when the page loads to be a random number. Something like this:
document.documentElement.style.setProperty('--pattern-seed', Math.random() * 10000);
We touched on this briefly earlier, but this is a lovely way to make sure a webpage is a tiny bit different for everyone that sees it.
Until next time
Well, friends, what a trip!
We have created three beautiful patterns together, learned lots of handy Paint API tricks, and (hopefully!) had some fun, too. From here, I hope you feel inspired to make some more generative art/design with CSS Houdini! I’m not sure about you, but I feel like my portfolio site needs a new coat of paint…
Until next time, fellow CSS magicians!
Oh! Before you go, I have a challenge for you. There is a generative Paint API worklet running on this very page! Can you spot it?
- There are certainly ways around this rule, but they can be complex and not entirely suitable for this tutorial. ⮑
The post Creating Generative Patterns with The CSS Paint API appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.