guide 021

The Generative Part of Generative Art

Part of the creation of the Craft Lab brand has involved generative elements. I have always been interested in them, and a community platform built for design engineers felt like the perfect opportunity to explore and enforce my experience with generative art.

After building multiple pieces of generative art I want to document what I have learned so that if this is something you are interested in exploring maybe this will expedite your understanding.

Random Values

What makes generative art…well generative? The idea is that by using some source of randomized data, usually in the form of numbers, and applying it to a visual algorithm you can create unique and interesting art that changes every time the underlying data changes.

A Visual Algorithm is just an easier way to refer to the function (or functions) that you use to take in randomized data, apply some rules to that data, and output a designed visual that reacts to the data.

There is a lot to unpack in what I just said, and that is what we are here to do. We are going to break down what it means to build a visual algorithm down the most basic example I can think of. Then build on top of that to slowly introduce more concepts that make a complete visual algorithm.

Applying Values to Visuals

Let's start by applying a random value to a single visual output.

We will do this using JavaScript’s built-in random number function Math.random() to pick a single color.

/**
* This function is used to return a random positive integer (whole number)
* that will never be any larger than the max integer you pass in.
* This is extremely useful when trying to get a randomized value from
* an array.
**/
function getRandomPositiveIntWithin(max: number) {
  return Math.floor(Math.random() * max)
}

/**
* This function will give us a random color from the array of colors
* we have defined using the `getRandomPositiveIntWithin`. We use the
* length of the colors array to make sure the index lookup will be
* guaranteed to find a match.
**/
function generateColor() {
  const colors = []
  return colors[getRandomPositiveIntWithin(colors.length)]
}

function ColorBox() {
  const color = generateColor()
  return 
}

BOOM! Generative art.

Let’s take it a little further

We implemented randomized color selection, but what if we want to do more than one color? What if we want a grid of colors where the size of that grid is also generative?

/**
 * I am using tailwind classes for existing color variables on my site, but
 * you can use hex codes, hsl values, or any supported color property
 **/
const colors = [
	'fill-pink',
	'fill-orange',
	'fill-yellow',
	'fill-lime',
	'fill-green',
	'fill-blue',
	'fill-purple',
]

/**
* This function is used to return a random positive integer (whole number)
* that will never be any larger than the max integer you pass in.
* This is extremely useful when trying to get a randomized value from
* an array.
**/
function getRandomPositiveIntWithin(max: number) {
  return Math.floor(Math.random() * max)
}

/**
* This function will give us a random color from the array of colors
* we have defined using the `getRandomPositiveIntWithin`. We use the
* length of the colors array to make sure the index lookup will be
* guaranteed to find a match.
**/
function generateColor() {
  return colors[getRandomPositiveIntWithin(colors.length)]
}

/**
* This function will allow us to pass a length we need and array to be
* and will return an array where each value in the array is its index.
* Useful for creating rows and columns as seen below.
**/
function createArrayOfLength(length: number): number[] {
	return Array.from(Array(length), (_, i) => i)
}

function ColorBox(props: ComponentProps<'rect'>) {
  const color = generateColor()
  return 
}

function ColorGrid() {
  const rows = createArrayOfLength(getRandomPositiveIntWithin(100))
  const columns = createArrayOfLength(getRandomPositiveIntWithin(40))
  const boxSize = 6
  return (
    
      {rows.map((x => columns.map(y => {
        
      ))
    
  )
}

Okay, now we're cooking! We have color generation and size generation. However, just because we are generating randomized values that are influencing our output the question is:

Is this good?

No. The answer is no.

Constraints Breed Craft

Objectively speaking it isn't anything special, yet. It has allowed us to learn some important concepts and is interesting, but, as with a lot of great art and design, it is missing constraints. Constraints breed craft. They allow you to guide the randomness of generative art in a way that keeps what is interesting while still controlling the visual output to fit within a pattern that will resonate with you and the people viewing your art.

Color Constraint

Let's look at how we can use constraints on a single dimension — color. We will use two different methods to get two very different outputs.

In the example that we have been building so far the color for each square is completely random. It could be any of the available colors. How can we take the idea of constraint to increase the quality of the art that our visual algorithm is creating?

There are a lot of approaches we could take, but a common approach to more visually pleasing color is using a gradient. Instead of complete chaos, we can add a feeling of flows moving from one side to another by limiting what colors are available based on the x and y coordinates of the pixel being rendered.

So, let’s update our visual algorithm so that the output has cooler colors in the top left and moves to warmer colors in the bottom right.

/**
* Here is a manually setup array of colors that will map to percentages.
* There are 10 indexed positions that will allow us to have a color grouping
* for every 10th of either the x or y axis.
**/
const colors = [
	['fill-blue', 'fill-purple', 'fill-purple'],
	['fill-green', 'fill-blue', 'fill-purple'],
	['fill-green', 'fill-blue', 'fill-purple'],
	['fill-lime', 'fill-green', 'fill-blue'],
	['fill-yellow', 'fill-lime', 'fill-green', 'fill-blue'],
	['fill-yellow', 'fill-lime', 'fill-green'],
	['fill-pink', 'fill-yellow', 'fill-lime', 'fill-green'],
	['fill-pink', 'fill-yellow', 'fill-lime'],
	['fill-pink', 'fill-orange', 'fill-yellow'],
	['fill-pink', 'fill-orange', 'fill-orange'],
]

/**
* This function is used to return a random positive integer (whole number)
* that will never be any larger than the max integer you pass in.
* This is extremely useful when trying to get a randomized value from
* an array.
**/
function getRandomPositiveIntWithin(max: number) {
  return Math.floor(Math.random() * max)
}

/**
* This function will give us a random color from the array of colors
* we have defined using the `getRandomPositiveIntWithin`. We use the
* length of the colors array to make sure the index lookup will be
* guaranteed to find a match.
**/
function generateColor(colors: string[]) {
  return colors[getRandomPositiveIntWithin(colors.length)]
}

/**
* This function will allow us to pass a length we need and array to be
* and will return an array where each value in the array is its index.
* Useful for creating rows and columns as seen below.
**/
function createArrayOfLength(length: number): number[] {
	return Array.from(Array(length), (_, i) => i)
}

function ColorBox({
	xPercent,
	yPercent,
	...props
}: ComponentProps<'rect'> & { xPercent: number; yPercent: number }) {
	const color = generateColor([...colors[xPercent], ...colors[yPercent]])
	return 
}

function ColorGrid() {
  const rows = createArrayOfLength(getRandomPositiveIntWithin(100))
  const columns = createArrayOfLength(getRandomPositiveIntWithin(40))
  const boxSize = 6
  return (
    
		{rows.map(x =>
			columns.map(y => (
				
			)),
		)}
	
  )
}

Okay, now the output of our visual algorithm is feeling more intentional and designed. With this approach, we manually hardcoded our available values at each stage of the gradient. However, as you introduce more complex variables like distance or size in your work how can we add constraints without needing to manually enter and control every available value?

Pattern Algorithms

In generative art, there is a large set of common visual and mathematical algorithms that allow you to create patterns that have a feeling of intention and direction by adding constraints to the randomness in your work.

Here is a list of some commonly used ones:

  • Simplex / Perlin Noise
  • Fibonacci Sequence
  • L-System
  • Truchet Tiles

There is a wide world of available pattern algorithms to experiment with. I'm not kidding. There is literally a wide world of natural patterns that you can draw inspiration from.

For demonstration let me show you how we can use Simplex Noise in the example we have been building vs manually grouping colors.

Simplex Noise is an algorithm created by Ken Perlin. It is most commonly used in video game topography and generative art…obviously.
import { makeNoise2D } from 'open-simplex-noise'

/**
* Here we are just going back to our basic array of colors and will let the
* simplex noise algorithm pick what colors are being selected from this array
**/
const colors = [
	'fill-purple',
	'fill-blue',
	'fill-green',
	'fill-lime',
	'fill-yellow',
	'fill-pink',
	'fill-orange',
]

/**
* This function is used to return a random positive integer (whole number)
* that will never be any larger than the max integer you pass in.
* This is extremely useful when trying to get a randomized value from
* an array.
**/
function getRandomPositiveIntWithin(max: number) {
  return Math.floor(Math.random() * max)
}

/**
 * Simplex Noise generates a value between -1 and 1, but we are working with an
 * array that will not accept a negative index. We will be converting the original
 * noise range to fit the 0 to 1 scale we need.
 **/
function getColorByNoise(noise: number) {
	return colors[Math.floor(((noise + 1) / 2) * colors.length)]
}

/**
* This function will allow us to pass a length we need and array to be
* and will return an array where each value in the array is its index.
* Useful for creating rows and columns as seen below.
**/
function createArrayOfLength(length: number): number[] {
	return Array.from(Array(length === 0 ? 1 : length), (_, i) => i)
}

function ColorBox({
	noise,
	...props
}: ComponentProps<'rect'> & { noise: number }) {
	const color = getColorByNoise(noise)
	return 
}

function ColorGrid() {
  const rows = createArrayOfLength(getRandomPositiveIntWithin(100))
  const columns = createArrayOfLength(getRandomPositiveIntWithin(40))
  const boxSize = 6
  const xSmoothness = 20
  const ySmoothness = 20
  const noise2D = makeNoise2D(Math.random())
  return (
    
		{rows.map(x =>
			columns.map(y => (
				
			)),
		)}
	
  )
}

By substituting our Math.random() value out with a value generated from our simplex algorithm we have enforced a constraint that is apparent in the colors selected across the coordinates of our visual algorithms output.

The smoothness controls allow us to change the scale at which our noise is changing. If you drag the smoothness to the far left you will be able to see what the output looks like with the raw noise values. Since the noise is changing so quickly at that scale it looks barely better than completely random values.

Random, but Repeatable

As I close out this article there is one more VERY important detail. What happens in the examples above when you hit the refresh button? You get a brand new output!

What if when you press the button you like the output? What happens if your dev environment crashes or your browser tab gets closed?

That version that you liked is gone. Most likely forever because of the probabilities of so many completely random values being used.

So the question is, how to generate art that is random, but repeatable?

The Mighty Seed

In generative art, the ability to create repeatable outputs is captured in two concepts. A Pseudo Random Number Generator (PRNG) and a seed.

A PRNG is exactly like it sounds. It is an algorithm (like Math.random() under the hood) that outputs a randomized number.

However, the big difference between Math.random() and using a “real” PRNG is the ability to accept a seed.

A seed is just a number that we pass to the PRNG that guarantees that the numbers that are output are always the same. Let’s take an interactive look at this concept.

The example below uses the Alea PRNG, and when we pass the same seed you can see that the first four generated numbers will always be the same. Try changing the numbers around and matching them back up.

0.43341971561312675

0.5240533177275211

0.18853929452598095

0.5806567727122456

0.43341971561312675

0.5240533177275211

0.18853929452598095

0.5806567727122456

See? Repeatable randomness. This means we can save and track inputs that make great outputs. Let's implement seeding with our latest example for our color grid that is using simplex noise.

import Alea from 'alea'
import { makeNoise2D } from 'open-simplex-noise'

/**
* Here we are just going back to our basic array of colors and will let the
* simplex noise algorithm pick what colors are being selected from this array
**/
const colors = [
	'fill-purple',
	'fill-blue',
	'fill-green',
	'fill-lime',
	'fill-yellow',
	'fill-pink',
	'fill-orange',
]

/**
* This function is now updated to accept the random number and the max. This
* allows us to make sure that we are passing in a number we know is connected
* to our seeded PRNG.
**/
function getRandomPositiveIntWithin(randomNum: number, max: number) {
  return Math.floor(randomNum * max)
}

/**
 * Simplex Noise generates a value between -1 and 1, but we are working with an
 * array that will not accept a negative index. We will be converting the original
 * noise range to fit the 0 to 1 scale we need.
 **/
function getColorByNoise(noise: number) {
	return colors[Math.floor(((noise + 1) / 2) * colors.length)]
}

/**
* This function will allow us to pass a length we need and array to be
* and will return an array where each value in the array is its index.
* Useful for creating rows and columns as seen below.
**/
function createArrayOfLength(length: number): number[] {
	return Array.from(Array(length === 0 ? 1 : length), (_, i) => i)
}

function ColorBox({
	noise,
	...props
}: ComponentProps<'rect'> & { noise: number }) {
	const color = getColorByNoise(noise)
	return 
}

function ColorGrid() {
	const seed = 99
	const generator = Alea(seed)
	const rows = createArrayOfLength(getRandomPositiveIntWithin(generator(), 100))
	const columns = createArrayOfLength(getRandomPositiveIntWithin(generator(), 40))
	const boxSize = 6
	const xSmoothness = 20
	const ySmoothness = 20
	const noise2D = makeNoise2D(generator())
	return (
		
			{rows.map(x =>
				columns.map(y => (
					
				)),
			)}
		
	)
}

Go Forth and Generate

That wraps it up. What we have covered here is enough for you to go and get started making generative art.

The only real way to make great visual algorithms at this point is to get out there and start building! You will learn so much when you try to make your first one. If you would like to dive a little deeper, I have added some extra resources and topics below that will help you expand your capability to create visual algorithms.

I hope you enjoyed

There is a lot more coming...

If you want to get updates when I publish new guides, demos, and more just put your email in below.

More Resources: