This tutorial is perfect for folks familiar with generative art and comfortable working with JavaScript/SVG.
Voronoi Grids
Randomness in generative art is a double-edged sword. It is a wildly powerful tool for us artists, but can be difficult to tame and sculpt into something that feels organic/balanced.
When composing generative patterns, placing objects on a canvas purely at random can feel chaotic, while aligning them to a traditional grid can feel rigid/predictable. While both chaos and exacting precision can both be beautiful qualities in generative art, we rarely — if ever — find examples of either extreme in the natural world.
In this tutorial, we will be learning how to form aesthetically pleasing patterns inspired by nature. Random and unpredictable, yet efficient and harmonious. To do so, we will be using a classic generative tool, the Voronoi tessellation. Let’s get making!
A visual overview
Before we get started, I would like to show you what Voronoi tessellations are, how they work, and how they can help form the basis of gorgeous generative patterns. Here’s a simple animated example to get us started:
To break this process down into steps:
- Add some
{ x, y }
points to a 2D space. - Partition the space into a collection of polygons. Each polygon should contain just one of the points added in the first step, and each of its vertices should be closer to its generating point than any other.
- Balance out the size of each polygon by moving its generating point towards its center point.
- For each polygon, compute the largest possible circle that will fit at its center point without touching any edges.
- Using the radius of the center circle as a guide, add a random shape to each cell.
Lovely. The shapes on the canvas exist in perfect harmony with each other and do not overlap, but they all have a random position. The resulting pattern has a sense of order while remaining unpredictable and, well, generative!
To demonstrate how Voronoi tessellations can function almost as “natural grids”, I have put together some examples. The first contains shapes placed randomly on a canvas, the second on a grid, the third on a Voronoi tessellation:
All three approaches here absolutely have their place in generative art/design, but to me, the Voronoi-based pattern looks the most organic.
Examples in nature
So, why do Voronoi tessellations appear so organic/natural to us humans? Well, from the pattern on the back of a giraffe, to cracks on the muddy ground, to the growth of a leaf’s veins — Voronoi tessellations appear everywhere in nature.
Let me show you a few examples:
In this tutorial (aside from debugging) we aren’t focused on rendering Voronoi tessellations themselves. Layouts created with them, however, can still remind us of natural phenomena — the way they are “packed” so efficiently, with purpose and meaning to their form, is reminiscent of patterns we find in the physical world.
The magic of this technique
In the “visual overview” example above, we placed our tessellations’ generating points randomly on the canvas. Random placement is neat, but the real magic here happens when we experiment with creative ways to initialize these points. Imagine, for example, that we created them based on the colors of an image, the values in a 2D noise grid, or used a Gaussian random distribution?
As a more developed example, here’s a pattern with generating points focused around the brightness values of an image.
Here is the original:
And here is the resulting pattern:
Notice how the output becomes more detailed near the darker pixels? Using a Voronoi tessellation, we can create patterns that are subtly — or not so subtly — influenced by an external force.
A (positive) note on performance
In addition to looking awesome, Voronoi-based patterns are also very fast to generate — creating an outcome similar to the above through “brute force” would be extremely slow in comparison.
An interactive example
All of this tessellation business can be tricky to understand at first (honestly, I still find it a little confusing sometimes!) — with this in mind, I have created an interactive example that we can use to get our bearings before writing any code. Sometimes it helps to “feel” algorithms like this work, reacting to your input rather than some abstract data.
Try clicking on the canvas above to add new points. Notice how the tessellation changes?
When you are happy with the generating points on the canvas, try dragging the “relaxation” slider. Notice how the cells all become more even in size? When we relax a Voronoi tessellation, we move each cell’s control point (a green dot) towards its centroid (a red dot) — relaxation is an entirely optional step but can go a long way towards creating balanced layouts.
For this tutorial, this is everything we need to know about Voronoi tessellations. If you would like to dive a little deeper, however, I recommend checking out Wolfram Mathworld.
Let’s code!
To let us focus on the creative application of Voronoi tessellations, rather than the math/logic behind them, I have added a createVoronoiTessellation
function to my generative-utils repository. We will be using this function to produce a simple, organic pattern that will serve as an excellent springboard for further experimentation.
The following example/other demos in this article are all SVG-based, but this technique works great with any medium.
This CodePen has all of the boilerplate HTML/JS/CSS needed to follow along with this tutorial.
Creating a blank SVG canvas
Before we start forming our pattern, we need a blank canvas to draw on. Our canvas in this tutorial is an <svg>
element, rendered using svg.js — a fantastic JavaScript library that greatly simplifies SVG scripting.
To create our canvas, we can add the following code to our JavaScript:
const width = 196;
const height = 196;
const svg = SVG().viewbox(0, 0, width, height);
svg.addTo('body');
In this snippet, we are:
- Defining a
width
andheight
property for our canvas/drawing space. - Creating a new svg.js instance, passing in the
width
+height
variables defined previously for its viewBox. - Adding the svg.js instance’s
<svg>
element to the DOM.
Initialising the generating points
As the first step towards creating our pattern, we need to define some points for our Voronoi tessellation. Let’s make that happen:
const points = [...Array(1024)].map(() => {
return {
x: random(0, width),
y: random(0, height),
};
});
Here, we generate 1024 points (an arbitrary choice) positioned randomly within the space defined by our width
and height
variables. If we were to visualize these points, they would look something like this:
Perfect! These will do nicely — onto the next step.
Creating the Voronoi tessellation
Now that we have some points, we can pass them, along with a few other configuration options, to the createVoronoiTessellation
function:
const tessellation = createVoronoiTessellation({
// The width of our canvas/drawing space
width,
// The height of our canvas/drawing space
height,
// The generating points we just created
points,
// How much we should "even out" our cell dimensions
relaxIterations: 6,
});
From here, we can “inspect” our tessellation by rendering the outline of each of its cells. To do so, we can iterate over each cell
and use svg.polygon
to turn its vertices into a shape. If you like, you can add a debug
variable here to toggle the tessellation outline on/off:
const debug = true;
tessellation.cells.forEach((cell) => {
if (debug) {
svg.polygon(cell.points).fill('none').stroke('#000');
}
});
Once you have added the above code, you should see something like this:
Rendering the cells of our base tessellation can be a helpful when debugging.
Adding shapes to our pattern
So, we have an excellent Voronoi tessellation but no actual pattern! Let’s fix that by adding some colorful circles to our canvas:
tessellation.cells.forEach((cell) => {
if (debug) {
svg.polygon(cell.points).fill('none').stroke('#000');
}
svg
.circle(cell.innerCircleRadius * 2)
.cx(cell.centroid.x)
.cy(cell.centroid.y)
.fill(random(['#7257FA', '#FFD53D', '#1D1934', '#F25C54']))
// Reduce each circle's size a little, to give the pattern some room
.scale(0.75);
});
Our pattern should now look something like this:
In the above code snippet, we use cell.centroid
to position the circles within their “parent” cell and cell.innerCircleRadius
to help determine their size.
The centroid
property of each cell
is an { x, y }
point located at its center.
The innerCircleRadius
property of each cell
is the radius of the largest possible circle that can sit at its center and not touch any of its edges — think of it as a rough guide for when you want to avoid overlapping objects.
I often use innerCircleRadius
to find the maximum possible width/height for
an object, then scale
it down a little to give my patterns some breathing
room.
If we wanted to expand our pattern a little and draw a mixture of lines and circles,
we could use the innerCircleRadius
property once again to ensure each line stays
within its parent cell’s edges:
tessellation.cells.forEach((cell) => {
if (debug) {
svg.polygon(cell.points).fill('none').stroke('#000');
}
// Choose either a circle or a line
if (random(0, 1) > 0.5) {
svg
.circle(cell.innerCircleRadius * 2)
.cx(cell.centroid.x)
.cy(cell.centroid.y)
.fill(random(['#7257FA', '#FFD53D', '#1D1934', '#F25C54']))
// Reduce each circle's size a little, to give the pattern some breathing room
.scale(0.75);
} else {
svg
.line(
cell.centroid.x - cell.innerCircleRadius / 2,
cell.centroid.y - cell.innerCircleRadius / 2,
cell.centroid.x + cell.innerCircleRadius / 2,
cell.centroid.y + cell.innerCircleRadius / 2
)
.stroke({
width: cell.innerCircleRadius / 2,
color: random(['#7257FA', '#FFD53D', '#1D1934', '#F25C54']),
})
.rotate(random(0, 360));
}
});
Awesome. For our first foray into Voronoi-based patterns, I think this is all we need. Our final piece should look something like this:
Beautiful! This example is quite simple, but in creating it we have covered everything we need to create endless organic patterns. From here, we can play!
Further experimentation
Before we part ways and you leave to create your very own generative patterns, I would like to show you a few more demos created using this technique, as — well, hopefully — a little inspiration.
I won’t be diving into the code for the following examples, as they all follow a very similar format to the pattern we just worked on together. The key difference in each of these demos is how the generating points are initialized.
Weighted random circles:
2D noise lines:
Voronoi blobs:
I hope these examples show just how versatile this technique is, there truly are endless applications for Voronoi-based patterns!