Norwegian version of this page

Creating a Tool for Generative Art

Creative Computing Hub Oslo (C2HO) presents a series of workshops featuring international creative computing practitioners who will explain the coding process and aesthetic considerations behind one of their digital artworks.

Image may contain: Font, Slope, Astronomical object, Science, Space.

Introduction

Hello, my name is Stevan Dedovic. This tutorial, and a companion video, was made for Creative Computing Hub Oslo. They requested everything be published to the public domain.

Except where otherwise noted, this work is licensed under CC-BY: https://creativecommons.org/licenses/by/3.0

—

For some background, around 2018 I visited the Metropolitan museum in New York. They had an exhibit on Abstractionism, and the Geometric Abstraction pieces really stood out to me. Then shortly after, I hosted an art show in Chicago and I needed to present some new work. The following pieces can be found on my website, dedovic.com, and recreating them is our goal today.

© 2018 Stevan Dedovic, All Rights Reserved© 2018 Stevan Dedovic, All Rights Reserved

This writeup is somewhere between a tutorial and explanation, and it will follow my creative process. It is intended for readers who know some Javascript and maybe p5.js.

P5 Setup

First, let’s set up an environment with useful functions and defaults.

This will set the color mode to HSB (also known as HSV), which is a little more intuitive than the RGB color space.

function setup() {
  colorMode(HSB, 360, 100, 100, 1.0);
}
 
function draw() {}

Next, let’s make the draw function to clear the background and draw a simple triangle on screen. We also need to create a canvas.

function setup() {
  colorMode(HSB, 360, 100, 100, 1.0);
 
  // 400px x 400px nearly fills the screen on my laptop
  createCanvas(400, 400); 
}
 
function draw() {
  // 0 saturation and 100 brightness means a white background
  background(0, 0, 100);
 
  // 0 brightness means a black stroke line
  stroke(0, 0, 0);
 
  // small triangle
  triangle(10, 10, 50, 50, 50, 10);
}

Already, there is a problem. The size of the canvas and the coordinates of the triangle are defined in pixels. This means that scaling the drawing later will be problematic, for example if we want a high DPI version for printing or publishing . This can be solved quite easily, though, by avoiding pixel coordinates in the first place, with some helper functions.

/** Convert normalised value [0, 1] to canvas coordinate [0, WIDTH] */
function w(val) {
  return val * width;
}
 
/** Convert normalised value [0, 1] to canvas coordinate [0, HEIGHT] */
function h(val) {
  return val * height;
}
 
...
 
function draw() {
  background(0, 0, 100);
  stroke(0, 0, 0);
 
  // this is necessary to scale the thickness of the stroke
  //  with the size of the canvas. The value is approximate,
  //  and looks consistent with the 1px line from before.
  strokeWeight(w(0.003));
 
  // draw triangle using coordinates relative to the canvas
  triangle(w(0.1), h(0.1), w(0.9), h(0.9), w(0.9), h(0.1));
}

Now, the canvas size can be changed but the drawing remains consistent. Try it out.

Link to p5.js Sketch

Note: It’s worth defining our data types ahead of time.

  • in javascript, number types may be either integer or decimal (floating point) numbers, so the distinction is not necessary
  • a point is a 2-list of numbers, e.g. [0, 0]or [3.1, 9.2]
  • a triangle is a 3-list of points, e.g. [[0, 0], [1, 0], [1, 1]]

Triangle Subdivision

This is the first of a handful of patterns in the generative artists toolbox I will cover today. There are many algorithms and off-the-shelf libraries to perform triangle subdivision, but we will write our own.

Let’s define “triangle subdivision” as a function that takes a triangle, cuts it in half, and outputs a list of the two smaller halves. Additionally, subdivision should look pleasing. This means creating nicely spaced triangles by subdividing the longest side of the triangle. Finally, it needs to be a little random and allow us to tweak how random . The high-level algorithm is as follows:

  1. Find the lengths of all the sides of the triangle
  2. Find the longest side
  3. Select a point m along the longest side, close to the middle
  4. Split the triangle into two

Here is that, as code:

/** Calculates the equality between two floats. */
function equalf(a, b) {
    return Math.abs(a - b) < Number.EPSILON;
}
 
/** Subdivides a triangle. Returns a list of triangles. */
function subdivide(tri) {
  // destructure the individual coordinates
  const [[ax, ay], [bx, by], [cx, cy]] = tri;
 
  // Step 1 - find lengths of sides
  const lenAB = dist(ax, ay, bx, by);
  const lenBC = dist(bx, by, cx, cy);
  const lenCA = dist(cx, cy, ax, ay);
 
  // Step 2 - determine longest side
  const longest = Math.max(lenAB, lenBC, lenCA);
 
  // Helper Function
  // Subdivide the traingle ABC along the side AB. 
  // Picks a point random, but close to the midpoint of AB.
  const subdivideAB = (a, b, c) => {
    const [ax, ay] = a;
    const [bx, by] = b;
 
    // Step 3 - select random point close to midpoint
    const n = randomGaussian(0, 0.09);
    const xm = lerp(ax, bx, 0.5 + n);
    const ym = lerp(ay, by, 0.5 + n);
    const p = [xm, ym];
 
    // Step 4 - create two new triangles, splitting side AB
    return [
        [p, a, c],
        [p, b, c]
    ];
  };
 
  // Step 3 - subdivide the longest side
  const [a, b, c] = tri;
  if (equalf(lenAB, longest))
    return subdivideAB(a, b, c);
  if (equalf(lenBC, longest))
    return subdivideAB(b, c, a);
  if (equalf(lenCA, longest))
    return subdivideAB(c, a, b);
}

I’ll explain a few decisions in the next sections, feel free to skip this section if you don’t need an in-depth explanation. But first, to use the above function, we must change draw a bit:

// pull our initial triangle out of the draw function
const defaultTri = [[0.1, 0.1], [0.9, 0.9], [0.9, 0.1]];
 
function draw() {
  background(0, 0, 100);
  stroke(0, 0, 0);
  strokeWeight(w(0.003));
 
  // since subdivide returns a list of triangles, we must iterate over them
  for(tri of subdivide(defaultTri)) {
    const [[x1, y1], [x2, y2], [x3, y3]] = tri;
    triangle(w(x1), h(y1), w(x2), h(y2), w(x3), h(y3));
  }
}

Link to p5.js Sketch

If you’re interested in exploring triangle subdivision further, I recommend checking out this 2017 article by artist Tyler Hobbs.

float equality

Floating point numbers can be tricky when it comes to ordering and equality. They have finite precision, which means some number, like 1/3, cannot be represented without information loss. This topic is well covered on the internet. Equality between floats must be treated with care. Above, I’ve defined a helper function, equalf, that compares two floats and returns if they are close enough to be considered equal. This isn’t strictly necessary, but I err towards caution. It’s usually better to write a bit more code than deal with weird float precision bugs. Also, check out the documentation for the Javascript Math.EPSILON for more info.

Note - Julia Evan’s “wizard zines” has an amazingly fun and informative zine, titled “How Integers and Floats Work” and I cannot recommend it enough!

lerp

I used the lerp or linear interpolation function above, to select a midpoint between two values. If you’re not familiar, it returns the value of interpolating from the first parameter to the second parameter by an amount from 0 to 1. I like using lerp as it is very flexible and performant.

randomGaussian

Using randomGaussian lets us randomize the location of the midpoint, along the side AB. But, unlike the random function, the selected value is not uniformly selected; with a Gaussian distribution values near the mean are more likely than values far away from the mean. The randomGaussian function in P5js takes in two parameters - the mean and standard deviation . The way its used in conjunction with lerp in the above code effectively becomes , “select a point randomly but likely close to the midpoint of the line”.

Interaction

Next on our journey is to set up some live interaction. We’ll start small - I want to subdivide the initial triangle whenever I hit the s key on the keyboard. P5js has a very easy way to do this - the keyPressed function. This is similar to setup and draw in that it is defined by us and then the P5 runtime executes it.

In order to subdivide the triangle, we have to do some state management. Currently, the draw function is where subdivide is called, and that means it is called once per render. Instead, we want to store a list of all the triangles to draw, and:

  • update by subdividing when the key s is pressed
  • iterate over the triangles when rendering in the draw function

Lets start by externalizing the triangles we plan to render:

// keep the default as it is useful
const defaultTri = [[0.1, 0.1], [0.9, 0.9], [0.9, 0.1]];
 
// this is our list of triangles to draw. 
// Note the `let` instead of `const` - this is necessary as we will reassign the value when subdividing.
let toDraw = [defaultTri];
 
function draw() {
  background(0, 0, 100);
  stroke(0, 0, 0);
  strokeWeight(w(0.003));
 
  // this will _not_ perform subdivision
  for(tri of toDraw) {
    const [[x1, y1], [x2, y2], [x3, y3]] = tri;
    triangle(w(x1), h(y1), w(x2), h(y2), w(x3), h(y3));
  }
}

Running the above code will simply render the default triangle. Nothing new yet, so lets implement the keyPressed function.

// called by P5js whenever a key is pressed.
// See P5 docs for semantics and comparison with other key interaction functions

function keyPressed() {
    if (key === 's') {
        toDraw = toDraw.flatMap(subdivide);
    }
}

This function is quite simple - when the s key is pressed, it reassigns the value of toDraw by subdividing each triangle in toDraw. The flatMap function is akin to performing a map and then concatenating the results.

So now, running this all together, we get an interactive little app. Try changing the the stroke weight, second parameter of randomGaussian, or the defaultTri. Notice, also, how long it takes to subdivide - the performance can be quite bad at 5+ iterations. It can also completely crash your browser, so beware!

Link to p5.js Sketch

Offsetting

Next up, I want to shrink all the triangles. This is called offsetting, and for triangles is simple enough. We can offset a triangle by finding the center and moving each vertex slightly inwards. While triangles have multiple centers, I will use the centroid, due to personal preference and the simplicity of the centroid algorithm.

// Offsets or insets the triangle by the specified
//  amount. Positive values shrink, negative values grow.   
function offset(tri, amount) {
    const [[x1, y1], [x2, y2], [x3, y3]] = tri;
 
    // calculate centroid coordinates  
    const cx = (x1 + x2 + x3) / 3;
    const cy = (y1 + y2 + y3) / 3;
 
    const moveToward = (px, py, tx, ty) => {
      // atan2 gives us the angle between two points
      const theta = atan2(ty - py, tx - px);
      return [
        px + amount * cos(theta),
        py + amount * sin(theta)
      ];
    };
 
    return [
        moveToward(x1, y1, cx, cy),
        moveToward(x2, y2, cx, cy),
        moveToward(x3, y3, cx, cy),
    ];
}

And wiring up the offsetting algorithm to our keyboard input handling:

function keyPressed() {
    ...
 
    if (key === 'j') {
        toDraw = toDraw.map(tri => offset(tri, 0.01));
    }
    if (key === 'k') {
        toDraw = toDraw.map(tri => offset(tri, -0.01));
    }
}

Now you can rerun the app and play around with subdividing, growing, and shrinking all the triangles. But there’s a few small issues that can be improved.

Holding Down a Button

First, instead of using keyPressed, we can move the offsetting to happen in draw, using the keyIsDown function. This means we handle holding not pressing to change the shape.

function draw() {
  // setup code
  ...
 
 
  if (keyIsDown(74)) { // "j"
    toDraw = toDraw.map(tri => offset(tri, 0.005));
  }
  if (keyIsDown(75)) { // "k"
    toDraw = toDraw.map(tri => offset(tri, -0.005));
  }
 
  // drawing code
  ...
}

Two things to note:

  • keyIsDown works on key codes, not a character. The P5js docs reference this site to lookup key codes. It’s worth noting they are case-sensitive.
  • because draw is called for each frame, I lowered the offset strength to 0.002.

Link to p5.js Sketch

Fixing the Offsetting

One thing you may notice is that the offsetting algorithm is distorting the shape of the triangle as it works. This is because we need to move the points of the triangle proportionately, relative to their distance from the centroid.

The way this can be done is by linearly scaling the amount of offsetting based on how far each point is from the centroid, and using the longest distance as the baseline amount. In code, this means modifying the offset function as follows,

function offset(tri, amount) {
  ...
 
  // longest distance from triangle point to centroid -
  //   this is the baseline
  const lenLongest = Math.max(
    dist(x1, y1, cx, cy),
    dist(x2, y2, cx, cy),
    dist(x3, y3, cx, cy)
  );
 
  const moveToward = (px, py, tx, ty) => {
    const theta = atan2(ty - py, tx - px);
 
    // figure out scaling factor based on length from this
    //  triangle point to centroid and baseline      
    const len = dist(px, py, tx, ty);
    const scaleFactor = len / lenLongest;
 
    // now scale the offsetting amount by the above factor  
    return [
      px + amount * scaleFactor * cos(theta),
      py + amount * scaleFactor * sin(theta)
    ];
  };
 
  ...
}

Now that is much better! There are probably better algorithms to do this, but I like this approach as it lets me tweak some of these values later so I can change the behavior of the offsetting in a unique way.

Link to p5.js Sketch

Turning off the Background Redraw

One of my favorite tricks is to disable redrawing the background of an interactive artwork. Removing fill and lowering the stroke opacity is also a good idea.

function draw() {
  // commented out now
  // background(0, 0, 100);     
 
  noFill();
  stroke(0, 0, 0, 0.05);
 
  ...
}

Interactions, Pt. 2

I’m going to take a quick detour and improve the overall experience of the sketch. It’s going to make it a better overall tool.

Toggling Drawing

First up is toggling the drawing of the triangles and turning it off by default.

// optionally set this to true so that is draws by default 
let shouldDraw = false;
 
function keyPressed() {
  ...
 
 
  if (key === 'd') {
    shouldDraw = !shouldDraw;
  }
}
 
function draw() {
  ...
 
  if (shouldDraw) {
    for(tri of toDraw) {
      const [[x1, y1], [x2, y2], [x3, y3]] = tri;
      triangle(w(x1), h(y1), w(x2), h(y2), w(x3), h(y3));
    }
  }
 
  ...
}

Note: since only the drawing part of the draw function is conditional, we will always subdivide, grow, shrink the triangles. None of the interactions are affected.

Resetting

This one is pretty simple. When I hit r, I want the whole sketch to reset. That means resetting to the default triangle, disabling drawing, and clearing out the canvas. Here it is:

function keyPressed() {
  ...
 
 
  // reset key
  if (key === 'r') {
    shouldDraw = false;
    toDraw = [defaultTri];
    background(0, 0, 100); 
  }
}

Link to p5.js Sketch

Sand Painting

I’ll quote the Anders Hoff / Inconvergent article, Grains of Sand, to explain the concept:

Basically it works by drawing several, almost transparent, dots at random along a line. This is akin to spreading a fixed number of grains of sand along a line—hence the name. If you always use the same number of grains, this will have the effect of making the line appear dense whenever the points are close to each other. And, conversely, less dense when they are farther apart. 

I love using this as a “drawing” technique, viewing it as an emulated digital medium.

Basic Algorithm

The algorithm is quite simple: given a number of grains, n, randomly place them along the line AB. To start, we will place the grains uniformly randomly, and along the edges of the triangles we are already drawing. The trick is keeping the lines a consistent density, i.e. adding more grains if the line is longer.

function paintLine(a, b) {
  const [x1, y1] = a;
  const [x2, y2] = b;
  const len = dist(x1, y1, x2, y2);
 
  // density might need to be tweaked to look pleasant   
  const density = 500;
  for (let i = 0; i < len * density; i++) {
 
    // random number [0, 1]
    const r = random();
 
    const xr = lerp(x1, x2, r);
    const yr = lerp(y1, y2, r);
    point(w(xr), h(yr))
  }
}

Link to p5.js Sketch

Domain Warping

To give it some more character, we’ll employ a technique called domain warping. This technique boils down to mapping the inputs of the grain positioning through a noise function. There is a wonderful article by Inigo Quilez, domain warping, that covers the concept well.

function paintLine(a, b) {
  ...  
 
  const rez = 0.01
  for (let i = 0; i < len * density; i++) {
 
    // because `noise` is nonuniform and continuous,
    //  we get a really visually pleasing effect   
    const r = random();
    const n = noise(x1 * rez, y1 * rez, r);
 
    const xr = lerp(x1, x2, n);
    const yr = lerp(y1, y2, n);
 
    point(w(xr), h(yr))
  }
} 

I’m using the noise function in P5js, which I will not cover in depth here. There are many other articles that cover gradient noise elsewhere.

One small issue with the noise function is the output range - it returns values from [-1, 1] whereas lerp takes in values from [0, 1]. This can be solved a number of ways, but for now I’ll simply scale it up a bit and make it “wrap”. Unfortunately, Javascript does not have a built-in function to do this, so we’ll reimplement the fmod function from C.

function fmod(n, d) {
    return n - (Math.floor(n / d) * d);
}
 
function paintLine(a, b) {
  ...  
 
  const rez = 0.01
  for (let i = 0; i < len * density; i++) {
 
    const r = random();
    let n = noise(x1 * rez, y1 * rez, r, frameCount);
 
    // scale by 10 and only keep the fractional part
    n = fmod(n * 10, 1.0);
 
    const xr = lerp(x1, x2, n);
    const yr = lerp(y1, y2, n);
 
    point(w(xr), h(yr))
  }
}

Interactions and Change

A few further ideas, I leave up to the reader.

  • Pull the rez variable out to a global, and use some new keys (e.g. h, l) to decrease and increase the value , respectively.
  • Tweak the n = fmod(...) call. Maybe by scaling the inputs a bit.
  • add a third parameter to the noise function call, maybe based on frameCount?

Link to p5.js Sketch

Finishing Up

The sketch is coming together really well. There’s only a few small things that I need to add before it’s a faithful recreation of the original works.

Color

I like the interactivity of the rest of the sketch, so color selection will be interactive as well. Colors can be defined as a list of HSV values, and selected by hitting the number keys. A clever way to handle key inputs is to use keyCode again, knowing that the numbers 0-9 on the keyboard have incrementing keyCode values.

const colorPalette = [
    [47, 35, 92],
    [33, 100, 100],
    [276, 16, 80],
];
 
// default color is black
let currentColor = [0, 0, 0];         
 
function setup() {
  ... 
 
  // this was moved out of draw
  stroke(0, 0, 0, 0.05);
}
 
function keyPressed() {
  ...
 
  // "0"
  if (keyCode == 48) {
    stroke(0, 0, 0, 0.05)
  }
 
  // "1"-"9"
  if (keyCode >= 49 && keyCode <= 57) {
    const idx = min(keyCode - 49, colorPalette.length - 1);
    const currentColor = [...colorPalette[idx], 0.05];
    stroke.apply(null, currentColor);
  }
}

It’s worth noting - the actual color values are not mine. I stole them from a book I purchased a while ago, A Dictionary of Color Combinations.

Link to p5.js Sketch

Masking

Masking is a simple concept. I want to isolate sections of the image from drawing. One option is to use multiple p5.Graphicsrenderers, which can be thought of as Layers in GIMP/Photoshop/similar photo editing software. Another option is to perform the masking work ourselves in the draw function. I originally took the approach of doing it myself because in a high resolution print the little “grain” placed during sand painting can be visible, and possibly span more than one pixel. I don’t want to crop my graphic, I want to simple omit drawing a grain. Unfortunately, this isn’t very performant.

For a simple circle, calculating whether a point is inside or outside is done by making sure the distance between our point and the center of the circle is smaller than the radius of the circle. Here’s the code:

function testInCircle(testPoint, circleCenter, circleRadius) {
    const [px, py] = testPoint;
    const [cx, cy] = circleCenter;
 
    return dist(px, py, cx, cy) < circleRadius;
}  

Now adding that to the drawing function lets us clip the drawn triangle.

function paintLine(a, b) {
  ...
 
  // point in center with a diameter of 0.8
  if (testInCircle([xr, yr], [0.5, 0.5], 0.4)) {
    point(w(xr), h(yr)) 
  }
 
  ...
}

There are ways to do more complex shapes, and I’ll direct the reader to SDFs, or Signed Distance Functions. Here is another Inigo Quilez article that demonstrates some 2D SDF examples with code. Unfortunately, a general solution to the Point-In-Polygon problem is not so simple, and out of the scope of this tutorial.

Link to p5.js Sketch

Fin

While there is a lot more that can be done, I need to wrap things up. One thing I’ve purposely left out is a small helper function to randomize the starting triangle. I leave this up to the reader, but I include it in my final sketch. If you need a hint - I do it by placing triangle coordinates along a big circle.

Link to Final p5.js Sketch

I’ll leave a couple ideas and links to further materials by some of my favorite artists and coder.

Taking it Further

  • Similar to the previous point, try different shapes than triangles. General polygon triangulation, also known as tessellation, is a huge field and a really fun area to explore.
  • Add multiple color palettes. Use the control key + a number to switch between palettes.
  • Try other SDFs and combining them using boolean operations
  • Try other masking code. For instance, by sampling a bitmap or image
  • Take the domain warping one step further by using the current value of noise as an input into another noise call
  • Change the shape of the grains of sand to a square or triangle

References

Other Artists

This… was a long writeup. If you find any inaccuracies or have questions, please feel free to reach out via email, Instagram, or Matrix. You can contact me at stevanproductions@gmail.com

Again thanks so much to Creative Computing Hub Oslo for this opportunity!

© Stevan Dedovic 2024, Some Rights Reserved

Except where otherwise noted, this work is licensed under https://creativecommons.org/licenses/by/4.0/ 

Tags: C2HO, Creative Computing, Creative Coding By Stevan Dedovic
Published Jan. 8, 2024 8:10 PM - Last modified Feb. 18, 2026 8:57 PM