Starting with order: an approach to generative art

PUBLISHED ON JUN 6, 2020

I came across an interesting approach in Anders Hoff’s writings:

When experimenting with generative algorithms it is sometimes useful to start with a highly organized structure. Then try to find interesting ways to gradually disrupt it.

Let’s try this out. Here’s a starting point:

  • let there be particles arranged on the circumference of a circle
  • let each particle have a target position that it desires to reach, and initialize this to the opposite side of the circle(ie: 180° away on the circle)
  • iterate forever: each particle takes a tiny step towards its target
  • every frame we draw the position of the particle with a slight transparency

Concretely, we initialize the particles like:

setup() {
    for(var i=0; i<N; i++) {
        const angle = i * 2.0 * PI / N;
        const x = cos(angle) * radius_x + center_x;
        const y = sin(angle) * radius_y + center_y;

        const target_x = cos(PI + angle) * radius_x + center_x;
        const target_y = sin(PI + angle) * radius_y + center_y;

        particles.push(new Particle(x, y, target_x, target_y));
    }
}

In the update function for each particle we find a vector towards the target, and scale it by a magnitude. Then, take a step in that direction:

class Particle {
    // Particle update function
    update() {
        const v_x = (this.target_x - this.x) * this.vmag_x;
        const v_y = (this.target_y - this.y) * this.vmag_y;

        this.x += v_x;
        this.y += v_y;
    }

    // ...
}

// Global update function
update() {
    for(var i=0; i<N; i++) {
        particles[i].update();
    }
}

Now, what if we changed the particle’s target to 90° away?

setup() {
    for(var i=0; i<N; i++) {
        const angle = i * 2.0 * PI / N;
        const x = cos(angle) * radius_x + center_x;
        const y = sin(angle) * radius_y + center_y;

        const target_x = cos(PI/2.0 + angle) * radius_x + center_x; // angle changed to PI/2.0
        const target_y = sin(PI/2.0 + angle) * radius_y + center_y; // angle changed to PI/2.0

        particles.push(new Particle(x, y, target_x, target_y));
    }
}

What if the particles moved twice as fast in the y direction vs the x direction?

setup() {
    // ... 
    // same as before
    // ...

    for(var i=0; i<N; i++) {
        particles[i].set_vmag(0.01, 0.02);
    }
}

What if the target positions for the particles kept changing every frame? Lets keep moving the target along the circle.

// Global update function
update() {
    steps++;

    for(var i=0; i<N; i++) {
        const angle = i * 2.0 * PI / N;

        // We use steps*0.01 to increase the angle on the circle by a small amount every update
        const target_x = cos(PI*(0.5 + steps*0.01) + angle) * radius_x + center_x;
        const target_y = sin(PI*(0.5 + steps*0.01) + angle) * radius_y + center_y;

        particles[i].set_target(target_x, target_y);
        particles[i].update();
    }
}

(Notice how the particles stay near the center because their target destination keeps moving to the other side of the circle before they can reach it.)

Lets add a sine wave to the way the target changes on the circle:

update() {
    steps++;

    for(var i=0; i<N; i++) {
        const angle = i * 2.0 * PI / N;

        // We add sin(0.001 * steps) every update
        const target_x = cos(PI*(0.5 + steps*0.01 + sin(0.001*steps)) + angle) * radius_x + center_x;
        const target_y = sin(PI*(0.5 + steps*0.01 + sin(0.001*steps)) + angle) * radius_y + center_y;

        particles[i].set_target(target_x, target_y);
        particles[i].update();
    }
}

We can get a lot more interesting patterns by building on this. You also might have noticed that we never even used random numbers throughout the process. The source code for the final version is available here. You can see a few more variations on this theme here.