Skip to content
Logo Theodo

Learning through Generative Art : a Journey with Rust

Mickaël Gregot10 min read

A minimalist maze

This post explores the concept of Generative Art, presents a method for producing ideas for artworks, and demonstrates its practical application with the Rust programming language. Generative Art lets you create easy yet fun side projects to discover new things.


What is generative art ❓

Definition

According to Wikipedia:

Generative art is generally digital art that uses algorithms to create self-generating or non-predetermined works.

We’re going to look at how to generate interesting images and animations using basic algorithms based on randomness, coded in the language of our choice.

❓ You mentioned non-digital generative art ❓

pendulum-painting-example.gif Pendulum Painting - WM DIY

Examples of generative art

Training generator

A colleague developed a mini-application to generate original training titles.

training-generator.png

There is a recurring pattern: each sentence contains 4 sections.

what I didhow I did ita catchy parta smiley face

talk-generator-deconstructed.png

Each section contains around ten predefined items, so all you have to do is randomly pick an item from each section and put them together.

The application isn’t complicated, yet you’ll find yourself trying it out several times just to see the possibilities! It’s this kind of generative art application that I find particularly elegant: simple and creative.

NFT - Bored Ape Yacht Club

bored-yacht-club.png

The Bored Ape Yacht Club, an NFT that created a buzz, uses the same random-draw principle.

This time we build an image from sub-images of different types: nose, mouth, ear, hat, background color, body

Each item has a level of rarity. The rarer it is, the less chance we have of finding it. Accumulating several rare items makes the NFT even rarer.

nft.png NFT sold at ETH 740 (~€3 million at the time)

So even a very basic generative art idea can do a lot of things!

Animation

Pictures are good, animations are stylish too.

Simulation of a hundred points in a force field. field-force-animation.gif

Why do Generative Art?

These algorithms are easy to implement in your favorite language, but try them out in a language you want to learn. The result is an easy, fun mini-project that will give you plenty to learn.

Level 1 : Turtle Python

logo-example.gif

Logo is an educational language (released in 1967!) which later inspired Turtle, a standard Python library. The principle is straightforward: we give elementary instructions (move forward, turn…) to a little turtle carrying a pen on a sheet of paper. Its movements produce drawings.

Children can then write down the instructions to produce a drawing, anticipating and reasoning about the turtle’s movement.

go 10 forwardturn 90°go 10 forwardturn 90°go 10 forwardturn 90°go 10 forward

turle-square.gif

We can introduce the notion of a loop and the principle of DRY - Don’t Repeat Yourself.

Repeat 4 times
- go 10 forward
- turn 90°

Despite Turtle’s simplicity, this opens the door not only to important algorithmic paradigms, but also to more complex worlds such as rose windows and fractals.

tree-fractal.png

Level 2 : Processing

Available in several different languages, it’s a simple, flexible sketchpad for rapid prototyping.

processing-example.gif 4 lines of setup and 10 of instructions are all it takes to make this animation.

Processing isn’t everything. The idea behind this animation is clever but not complicated: a black background on a grid of white circles whose diameter is proportional to their distance from the cursor.

This little animation lets you discover loop nesting, calculations, typing and the use of high-level framework functions.

How do you find inspiration?

Be curious and look around

3d-pipe-old-screen.gif

google-waiting-room.png

ilight-party-saint-jean.gif

But also Pinterest, Instagram, Reddit… the keywords Generative Art give endless ideas.

Redo a work of art you like

For example, this photo is really cool: the artist simply stretched each pixel in the last column.

lines-we-live-by.png credits : Frances Berry - Lines We Live By

Let’s reproduce this concept with Processing:

def setup():
    size(1200, 950)
	global picture
	picture = loadImage("helsinki01.jpg")

def draw():
	global picture
	image(picture, 0, 0)
	for line in range(picture.height):
		color = picture.pixels[line * picture.width + mouseX]
		stroke(color)
		line(mouseX, line, picture.width, line)

line-idea-1.gif

A kitschy wallpaper idea line-idea-2.gif

Looks a bit like a landscape on a train line-idea-3.gif

Let’s add a little blur 🚅 line-idea-4.gif

💡 Building on an existing idea leads to new ones.

Let’s get practical

We’ve discovered Generative Art, seen how much fun it can be and how to come up with new ideas. Now all you have to do is get your hands dirty in the language you’ve always wanted to try! In my case ✨ Rust ✨

Why Rust?

Rust, the world’s most beloved language

Rust is the language most loved by developers for the 7th year running. We can question the relevance of this poll, but this data remains intriguing!

Most loved, dreaded, and wanted language - Stackoverflow rust-loved.png

Critical performance in Rust

PerformanceReliabilityProductivity
low memory requirementstyped languageefficient documentation
no runtime or garbage collectorborrow systemcompiler with clear error messages

🔎 Zoom in on Rust’s borrow system, based on three key concepts

This is a fundamental aspect of Rust: it avoids all pointer problems by avoiding the cost of a garbage collector. Errors are detected at compile time, not at runtime.

The borrow system encourages more modular and functional code design, avoiding undesirable side-effects and favoring clear separation of responsibilities. It also facilitates concurrent programming.

The drawback is that writing time is longer, but debugging time is reduced. A compiler with clear messages is therefore essential.

This concept highlights Rust’s critical performance.

The right tool for the right need

🛠 “I suppose it’s tempting, if the only tool you have is a hammer, to think of everything as a nail.”

Rust is mainly used to make command-line (CLI), network or embedded tools, thanks to its performance. It is also used in WebAssambly, which opens up possible solutions for Theodo.

Rust is used in production by many companies such as Mozilla, Sentry, OVH, Dropbox, Deliveroo, Atlassian, 1Password or Figma.

Nannou: a Generative Art framework for Rust

Let’s get back to our goal: to have a little side project that lets you discover Rust.

Artist Alexis André set himself the challenge of making one little animation a day using Rust and the Nannou framework. alexis-andre-example.gif

Checkpoints for a generative art framework:

Nannou ticks all the boxes, we can use it!

Nannou is based on the desing pattern MVC model view controller

mvc.png Wikipedia

Goal

We’re going to create a minimalist image of an imperfect labyrinth.

imperfect-labyrinth.png Imperfect: some parts are not reachable

Algorithm

Imperfection provides an easy algorithm for generating this labyrinth. A picture is squared and a diagonal is drawn in each square, either in one direction / or the other /. diags.png

algo-labyrinth.gif

Nominal Code

Let’s start by making a circle on a colored background.

use nannou::prelude::*;

fn main() {
    nannou::app(model)
        .update(update)
        .simple_window(view)
        .size(900, 900)
        .run();
}

struct Model {}

fn model(_app: &App) -> Model {
    Model {}
}

fn update(_app: &App, _model: &mut Model, _update: Update) {}

fn view(app: &App, _model: &Model, frame: Frame) {
    let draw = app.draw();
    draw.background().color(NAVY);
    draw.ellipse().color(MEDIUMAQUAMARINE);
    draw.to_frame(app, &frame).unwrap();
}

Notes

💡 Nannou’s guide is very good for more details on the nominal part.

Result

nominal-nannou.png

We need these different bricks for the labyrinth:

Draw a diagonal

Nannou uses the Rect object, a rectangle from which we can retrieve corner coordinates. To draw a line, Nannou follows the builder design pattern, simply specifying the start, end and additional parameters such as thickness and color.

let rect = app.window_rect();
draw.line()
    .start(rect.bottom_right())
    .end(rect.top_left())
    .weight(10.0)
    .color(BLACK);
draw.to_frame(app, &frame).unwrap();

diagonal-nannou.png

Subdivide into boxes

The subdivisions_iter() method of Rect allows you to subdivide the rectangle into 4 new rectangles in one iterator. By nesting for loops on each new rectangle, you can achieve the desired subdivision.

draw.background().color(BLACK);
for rect in app.window_rect().subdivisions_iter() {
    for rect in rect.subdivisions_iter() {
        for rect in rect.subdivisions_iter() {
            draw.rect().color(WHITE).x(rect.x()).y(rect.y());
        }
    }
}

grid-nannou.png

Randomly draw a Boolean

Nannou offers helpers in its rand module for randomly drawing a Boolean.

use nannou::{prelude::*, rand};
rand::random();

Assembling in the Viewer

fn view(app: &App, _model: &Model, frame: Frame) {
    frame.clear(WHITE);

    let draw = app.draw();

    for rect in app.window_rect().subdivisions_iter() {
        for rect in rect.subdivisions_iter() {
            for rect in rect.subdivisions_iter() {
                if rand::random() {
                    draw.line()
                        .start(rect.bottom_right())
                        .end(rect.top_left())
                        .weight(10.0)
                        .color(BLACK);
                } else {
                    draw.line()
                        .start(rect.top_right())
                        .end(rect.bottom_left())
                        .weight(10.0)
                        .color(BLACK);
                }
            }
        }
    }

    draw.to_frame(app, &frame).unwrap();
}

bug-labyrinth.gif

To understand the bug 🐞 you need to understand what the view does.

The application’s view function is called each time the application is ready to retrieve a new image that will be displayed to a screen.

The program quickly generates and displays new images by recalculating the random print each time “view” is called, creating flicker due to frequent changes in diagonal direction.

Using the Model

As you can see, it’s not the role of the screen (= the view) to calculate whether a diagonal is one way or the other, but it is the role of the Model.

✨ Result ✨

labyrinth-nannou.png

Additional

From the labyrinth we created, we can imagine variations:

Once we have a base, coming up with ideas for iterations comes easily. Be curious and get your hands in there!

You can check out this talk by Tim Holman - Generative Art Speedrun which is full of brilliant ideas, as well as their tutorial.

Conclusion

This mini-tutorial introduced us to some of Rust’s concepts and philosophy, as well as various design patterns shared by other languages, such as builder, MVC and iterators.

You now have all the keys you need to create mini Generative Art projects in the framework you’ve always dreamed of testing!

Liked this article?