Getting Started with creative coding in Rust and Nannou.

  • rust
  • creative-coding
  • nannou

If you have had anything to do with programming in the past couple years you've probably heard about Rust, likely a lot of good things. It's topped Stack Overflow's "most loved programming language" for the past four years, primarily for it's developer productivity, ease of use, tooling, enthusiastic community and different way of handling memory management.

Like C++, Java, JavaScript and numerous other languages, Rust also has it's own creative coding framework, Nannou. Whilst maybe not as mature as the likes of OpenFrameworks, Processing or other similar frameworks, development is still ongoing and Nannou benefits from the the enthusiasm in the Rust community.

Okay then. Show me some code.

If you've ever used Processing or OpenFrameworks you'll be familiar with the typical structure of a creative coding app, generally in the form of a setup function and a draw function. Nannou is no different, with a few exceptions relating to it being written in Rust.

I'm going to recommend you go through this guide on setting up a Rust project first and come back here when you've got things up and running.

Open up cargo.toml and add nannou = "0.9" into your dependencies.

Now open up main.rs in your code editor of choice...

First we need to import our deps...

use nannou::prelude::*;

The above code essentially imports all the basic modules you need to get a Nannou app up and running.

Like most grown up languages such as C and C++, entry into a Rust application is done through the `main` function. The app will not compile if there is no entry point. For now we'll leave this empty and will come back to it later.

main() {
    // stuff will go in here
}

Next up we declare our `Model` struct.

struct Model {
    color: usize,
}

Okay, wait, wat?

The model in a Nannou app is where all of the application state is stored. An object of this shape will be available through all stages of the app, to use / update as required. Think of it as similar to the model in an MVC app.

Note that the above isn't an actual variable but essentially a type declaration, there is no data stored in the above declaration. All we are doing above is saying that there will be data structures of type Model that contains one field, color of the type usize. Why we are declaring a field of type usize will become apparent shortly. However, if you're interested in finding out more info on Rust datatypes, have a look here.

We're also going to declare an array of colours as a const. Luckily, in Nannou colours are already declared as public constants. So we can use them like so:

const COLORS: [Rgba<f32>; 6] = [ // declares the name, type and length of the array.
    DARK_ORANGE,
    ORANGE,
    GREEN,
    BLUE,
    PURPLE,
    LIGHT_BLUE
];

Okay so now we've got our data model type declared, we need to initialise the state somehow. So how do we do that? We need to declare a model function which will be passed to Nannou as part of the set-up.

fn model(app: &App) -> Model {
    Model {
        color: 0,
    }
}

Okay, so there's a reasonable amount to unpack there, what exactly is going on?

Let's get the easy wins out of the way first.

Functions in Rust are declared with the fn keyword. In simple terms we are declaring a function that accepts a reference of type App does some stuff and returns a struct (of type Model) with color set to 0.

The -> Model part simply says that we will return objects of type Model from the function.

The tricky part to get your head round is the use of an ampersand & before our App type delaration. This tells the compiler that we want to pass a reference of the object to the function and not the actual `app` object. Why is that? This is to do with Rust's ownership feature. If you were to pass the actual `app` object this alters the ownership of `app` and will subsequently cause an error. For more info on this in detail have a look at this SO question.

Another thing to note about the above code is that there is no return statement and no semi-colon after creating the model. Why?

Rust is an expression based language, meaning that every expression evaluates to a value. So, because a function block is essentially an expression, the final value of the block is that blocks value. Cool, huh? More on that here.

Okay, so now that we've declared our `model` function, we need to pass it to our Nannou app and initialise our state. Back to our `main` function we go...

fn main() {
    nannou::app(model)
}

Here we create our Nannou app and pass our `model` function as an argument. But wait! We don't declare our function until later. That's okay, functions are hoisted in Rust, similar to JS, you can use them before they are formally declared.

The :: may be a little WTF at first, but essentially you can think of it as roughly equivalent to a static method in other OOP languages. Caveat: it's not quite, but for this post, it's close enough. Have a gander at this thread for a more in depth discussion.

So next up we need an update function to be called before every frame

fn update(app: &App, model: &mut Model, _update: Update) {
    model.color = app.time.floor() as usize % COLORS.len();
}

So again, there's a bit to unpack here.

On the first line we declare our parameters:
* The reference to our app
* The actual data model. Note the &mut, variables are immutable by default in Rust, this means we can mutate our data model directly

On the second line we assign a value to our color field.
app.time gives us the current running time of the app in milliseconds as an f32 (32-bit floating point number), COLORS.len() gives us the length of the COLORS array as a usize, so we need to cast app.time to a usize in order to perform a modulo operation on both numbers. In Rust you can use the as keyword to do this.

This then gives us a number between 0 and the length of the COLORS array - 1 of the type usize. Why we need this will become apparent shortly, but you've maybe already guessed.

Once we've done that, we need to register the udpate function to our app with the .update() method...

fn main() {
    nannou::app(model)
        .update(update)
}

Still awake? We're almost there, we've just got one more step, creating the view...

fn view(_app: &App, model: &Model, frame: Frame) -> Frame {
    let index: usize = model.color;

    frame.clear(COLORS[index]);

    frame
}

Hopefully things should be becoming more obvious as we work through. Our view function accepts our app, a reference to the model, the current frame, clears the page using `frame.clear()` returns the next frame object.

Array indices must be of type usize (hence why we had to cast to this type in update).

Head back to your project in the command line and hit:

cargo run

Ideally, you should something like this:

HOLY CRAP MIND BLOWN

Yeah, that seemed like a lot of chat for minimal output. Remember tho, baby steps.

Here's the full code to copy / paste play around with.

use nannou::prelude::*;

// use random;

const COLORS: [Rgba<f32>; 6] = [
    DARK_ORANGE,
    ORANGE,
    GREEN,
    BLUE,
    PURPLE,
    LIGHT_BLUE
];

struct Model {
    color: usize,
}

fn model(_app: &App) -> Model { // initialises the model
    Model {
        color: 0,
    }
}

fn update(app: &App, model: &mut Model, _update: Update) {
    let color_index = app.time.floor() as usize % COLORS.len();

    model.color = color_index;
}

fn view(_app: &App, model: &Model, frame: Frame) -> Frame {
    let index: usize = model.color;

    frame.clear(COLORS[index]);

    frame
}

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

Usual caveats apply, I'm learning too here, so any questions / comments / rebuttals / insults / cash advances should be directed towards my twitter.

Enjoy!