Creative Coding in Rust with Nannou

Ben Lovy - Nov 16 '19 - - Dev Community

Oxidize Your Life With One Weird Trick

We're going to build a small demo with the nannou creative coding framework for Rust. This example itself is very simple, but specifically over-engineered to prepare to scale even further for your own project.

This is a beginner-level post but with conditions. You should be able to follow along with the logic here in a general sense with comfort in any imperative language, but if you wanted to build your own app from this base as a total newcomer to Rust, I absolutely recommend you read at least some of The Rust Programming Language, freely available, or equivalent before tackling this framework.

Part of the strength of nannou as a framework is that it doesn't reinvent the wheel. It is instead intended to pull together and unify the best the excellent Rust ecosystem already has to offer for each subproblem in one unified package, innovating only where the need is not already met by the community. You could also get here yourself by adding these dependencies one-by-one and gluing everything together, but this is aiming to be a curated batteries-included distribution for creative coding in pure top-to-bottom Rust.

The final code can be found on GitHub.

Table Of Contents

The Motive

I was irrationally hell-bent on modelling a problem in Rust that was perfectly suited for Processing or its JavaScript or Python siblings. Luckily, the Rust ecosystem continues to pleasantly surprise, and it's already possible to do! This tool isn't trying to be Processing-the-library for Rust, and is still very much a work in progress, but it occupies a very similar space and is already quite usable, partially thanks to the wealth of strong component crates already available in the ecosystem to lean on.

Yes, Really, In Rust

Rust might seem like an oddly... ahem combative choice of tool for such a dynamic and exploratory domain. I've found that after the initial learning curve, Rust's expressivity and modelling power help me get tasks done correctly efficiently, which more than outweighs however much its strict and unique semantics slow me down. My argument is essentially that the benefit of using Rust for this sort of program is that to implement your logic, you get to use Rust. This is pretty subjective argument.

My more substantive take is that it's performant by default, has a rich set of expressive, high-level basic language components and a solid standard library, and a highly helpful compiler if you've modelled your code effectively among all languages I've tried. It does impose a strict, unique mental model but once you understand how it works even that is more a positive point than a negative as well, as it gently nudges you towards better code Pit of Success style.

To me the biggest drawback is compilation time, which is admittedly brutal. This can be frustrating when doing such exploratory, iterative work - nannou ain't no Jupyter notebook. Working with this sort of code was a test of that limitation. a warm debug takes about 4 seconds and a release build takes four and a half on my 2017 i7. I generally just use the release build. The library itself has a little under 250 dependencies to build, so a cold taking about five minutes. Even with this frustration I found the balance skewed heavily toward positive, as always, your preferences and mileage may vary.

The caveat to any of my pros, also, is familiarity and experiential bias. I'd love hear why you disagree and Language X is more objectively superior for this and should be used instead!

Setup

Let's get ourselves to a successful compile first.

Dependencies

  • Stable Rust 2018 - the default installation is sufficient. This code was written with rustc version 1.39.

  • Vulkan SDK - on Gentoo, I had to install dev-util/vulkan-tools, not just dev-util/vulkan-headers.

I tested this code on Linux, and I'm not sure how to run this code on OS X and don't have access easily to try it myself.

Set Up the Project

Create a new Rust project directory:

$ cargo new nannou_dots
$ cd nannou_dots
Enter fullscreen mode Exit fullscreen mode

Add the dependency:

# ..

# after other metadata
[dependencies]

nannou = "0.12"
Enter fullscreen mode Exit fullscreen mode

To demonstrate the overall structure of the app, start with this simple demonstration:

use nannou::{color::named, prelude::*};

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

struct Model {
    bg_color: String,
    x: f32,
    y: f32,
    radius: f32,
}

fn model(_app: &App) -> Model {
    Model {
        bg_color: "honeydew".to_string(),
        x: 0.0,
        y: 0.0,
        radius: 10.0,
    }
}

fn update(_app: &App, model: &mut Model, _update: Update) {
    if model.radius < 500.0 {
        model.radius += 1.0;
    }
}

fn view(app: &App, model: &Model, frame: &Frame) {
    let draw = app.draw();
    draw.background()
        .color(named::from_str(&model.bg_color).unwrap());
    draw.ellipse()
        .color(STEELBLUE)
        .w(model.radius)
        .h(model.radius)
        .x_y(model.x, model.y);
    draw.to_frame(app, &frame).unwrap();
}
Enter fullscreen mode Exit fullscreen mode

Even if you've never worked with a tool like this, take a moment to read through this code and try to understand what will happen when you run it. Once you think you've got it, give it a go with cargo run --release and go make a cup of tea. The first build will be intense as it compiles all the dependencies the first time, but re-compiles will be quicker! Granted, not quick - this is one of Rust's definite trade-offs, but even my nine year old low-end laptop could keep up enough to iterate without losing my head after an admittedly pretty nuts initial build. Come back when your tea is cool enough to sip and see if you were right! You can kill the program by using the X button in the corner and re-run it to start the animation over from the beginning. Then kill it again, quickly. It came out unexpectedly terrifying for a "hello, world".

The Structure

If you're already familiar with model/view/update application structure, skip down to Defensive Refactor.

The main() function only does one thing: instantiate a nannou app object and immediately call it's run() method. It then continually draws frames based on the parameters we define. Each frame is defined by a view:

fn view(app: &App, model: &Model, frame: &Frame) {
    let draw = app.draw();
    draw.background()
        .color(named::from_str(&model.bg_color).unwrap());
    draw.ellipse()
        .color(STEELBLUE)
        .w(model.radius)
        .h(model.radius)
        .x_y(model.x, model.y);
    draw.to_frame(app, &frame).unwrap();
}
Enter fullscreen mode Exit fullscreen mode

This contains instructions for drawing a single frame. You can actually use nannou with only this function if you'd like to experiment with stateless drawing ideas. Simply replace the main() entrypoint code with this:

fn main() {
    nannou::sketch(view);
}
Enter fullscreen mode Exit fullscreen mode

We use the library-provided draw() methods provided by the app parameter to interact with the frame. First, we set the background color and then draw an ellipse. These methods update the state of the app object, and then finally we draw the new state to the frame. We get all the parameters about what color to use and how to paint the ellipse from the model:

fn model(_app: &App) -> Model {
    Model {
        bg_color: "honeydew".to_string(),
        x: 0.0,
        y: 0.0,
        radius: 10.0,
    }
}
Enter fullscreen mode Exit fullscreen mode

The model is the application state. Here, it's just set to these parameters: "honeydew" is a lovely color for the background that corresponds to one of the defined constants, and our ellipse starts off super small and at the center of the frame. Between each frame, the model might update:

fn update(_app: &App, model: &mut Model, _update: Update) {
    if model.radius < 500.0 {
        model.radius += 1.0;
    }
}
Enter fullscreen mode Exit fullscreen mode

In this function, we're given a mutable borrow of the Model to manipulate. This demo will check if the radius smaller than 500 pixels. If so, it's going to bump it up slightly. If not, nothing else happens.

Pulled all together, this app should be expected to load a mostly "honeydew" (off-white) screen with a small blue circle in the center that will quickly animate to grow to a slightly larger size. It then stays that way until the process ends. I know, riveting so far:

dot gif

It's a choppy screen record, but the actual run will be smooth. This program is already a jumping off stub for any demo using this library, feel free to ditch my larger demonstration app and go sailing forward with the API docs on your own if you already know what to write!

To implement any new functionality in our demo, we'll need to write logic that appropriately extends our model, view, and update functions to show the user what we mean.

Scaling Out

Defensive Refactor

The above demo gets us up and running, but we don't want to code directly into the main structure. This is a perfect opportunity to refactor into something that we can grow with more easily. This adds a lot of verbosity, but with Rust the more we can help the tooling the more the tooling helps us, and with a properly configured development environment it's not even that much typing. I also, er, happen to find Rust refactoring therapeutic but that's beside the point.

Now, cross your fingers - we're going to wipe out this implementation in favor of a more idiomatic (and verbose) Rust approach. I'll elaborate below. For now, go ahead and replace the entire file contents of src/main.rs with this - there are no functional changes, ony structural:

use nannou::{color::named, prelude::*};
use std::string::ToString;

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

/// All colors used in this application
#[derive(Debug, Clone, Copy)]
enum Color {
    Honeydew,
    SteelBlue,
}

impl ToString for Color {
    fn to_string(&self) -> String {
        format!("{:?}", self).to_lowercase()
    }
}

/// Type alias for nannou color type
type Rgb = Srgb<u8>;

impl From<Color> for Rgb {
    fn from(c: Color) -> Self {
        named::from_str(&c.to_string()).unwrap()
    }
}

/// A coordinate pair - the (0,0) default is the center of the frame
#[derive(Debug, Default, Clone, Copy)]
struct Point {
    x: f32,
    y: f32,
}

impl Point {
    fn new(x: f32, y: f32) -> Self {
        Self { x, y }
    }
}

/// Things that can be drawn to the screen
trait Nannou {
    fn display(&self, draw: &app::Draw);
    fn update(&mut self);
}

/// A circle to paint
#[derive(Debug, Clone, Copy)]
struct Dot {
    color: Color,
    origin: Point,
    radius: f32,
    max_radius: f32,
    growth_rate: f32,
}

impl Dot {
    fn new() -> Self {
        Self::default()
    }
}

impl Nannou for Dot {
    fn display(&self, draw: &app::Draw) {
        draw.ellipse()
            .w(self.radius)
            .h(self.radius)
            .x_y(self.origin.x, self.origin.y)
            .color(Rgb::from(self.color));
    }
    fn update(&mut self) {
        if self.radius < self.max_radius {
            self.radius += self.growth_rate;
        }
    }
}

impl Default for Dot {
    fn default() -> Self {
        Self {
            color: Color::SteelBlue,
            origin: Point::default(),
            radius: 10.0,
            max_radius: 200.0,
            growth_rate: 1.0,
        }
    }
}

/// The application state
#[derive(Debug)]
struct Model {
    bg_color: Color,
    current_bg: usize,
    dot: Dot,
}

impl Default for Model {
    fn default() -> Self {
        Self {
            bg_color: Color::Honeydew,
            current_bg: usize::default(),
            dot: Dot::new(),
        }
    }
}

impl Nannou for Model {
    /// Show this model
    fn display(&self, draw: &app::Draw) {
        draw.background().color(Rgb::from(self.bg_color));
        self.dot.display(draw);
    }
    /// Update this model
    fn update(&mut self) {
        self.dot.update();
    }
}

//
// Nannou interface
//

/// Nannou app model
fn model(_app: &App) -> Model {
    Model::default()
}

/// Nannou app update
fn update(_app: &App, model: &mut Model, _update: Update) {
    model.update();
}

/// Nannou app view
fn view(app: &App, model: &Model, frame: &Frame) {
    let draw = app.draw();
    // Draw model
    model.display(&draw);
    // Render frame
    draw.to_frame(&app, &frame).unwrap();
}
Enter fullscreen mode Exit fullscreen mode

As before, I'd urge to take your time and step through this sample as well. I haven't actually changed anything at all functionally, just gotten myself organized. Start from main() and literally rubber-duck the control flow if you don't follow this just yet. I know, it's a lot bigger, but everything is right where it belongs. This is a much better base to build from. Running cargo run --release should produce an identical result to before the switch.

Traits And Composition

The Color sum type (or Rust enum) is the best example of Rust-style composition:

/// All colors used in this application
#[derive(Debug, Clone, Copy)]
enum Color {
    Honeydew,
    SteelBlue,
}

impl ToString for Color {
    fn to_string(&self) -> String {
        format!("{:?}", self).to_lowercase()
    }
}

/// Type alias for nannou color type
type Rgb = Srgb<u8>;

impl From<Color> for Rgb {
    fn from(c: Color) -> Self {
        named::from_str(&c.to_string()).unwrap()
    }
}
Enter fullscreen mode Exit fullscreen mode

I've also defined a trait of my own:

/// Things that can be drawn to the screen
trait Nannou {
    fn display(&self, draw: &app::Draw);
    fn update(&mut self);
}
Enter fullscreen mode Exit fullscreen mode

If you're already a regular Rust user, this will likely not be new - skip down to the next header. It was one of the more unfamiliar bits for me at the outset, though, and the sooner you embrace code that looks like this the sooner Rust will click. It's not nearly as complicated as it looks at first.

Rust does not have traditional inheritance at all, which represents an "is-a" relationship between related instances. Think of a Cat inheriting from an Animal superclass, because a cat "is-a" animal. Instead, everything is extended via composition, or a "has-a" relationship. Our Cat might know how to speak() and say something different than a Dog would with the same method, but have the Voiced trait provide it. Cats and dogs both "has-a" voice. They can manage their own behavior behind the common API instead of overriding a base class implementation. The mechanism for this is traits. They fall somewhere in between (I think) a Java interface and a Haskell typeclass, and are very simple to define. For instance, std::default::Default is defined in the compiler's source code as this, omitting the doc comment and version tag:

pub trait Default: Sized {
    fn default() -> Self;
}
Enter fullscreen mode Exit fullscreen mode

The trait only defines a single method that a type needs to define, returning some instance of itself that works as a default value. The trait itself doesn't care how, the compiler will decide whether a specific implementation of this trait checks out. The Rust compiler can statically OR dynamically verify whether a given object implements a trait.

Traits are powerful, and in fact not only permeate Rust usage but power exactly those benefits I listed above, and the Rust compiler is powerful enough to derive many useful ones for you. If you ever do want to override that behavior, you can always provide a manual impl Trait for Struct block yourself that matches the prescribed API. There's a great overview of the reasoning and usage in this blog post by Aaron Turon.

Debug

Debug provides a simple pretty-print implementation of a data structure that can be used with the {:?} or {:#?} formatters in println!() (etc) invocations. The default dot looks like this:

Dot { color: SteelBlue, origin: Point { x: 0.0, y: 0.0 }, radius: 10.0 }
Enter fullscreen mode Exit fullscreen mode

Here's a playground link

Default

Default provides a method default() to be used as a default constructor. When derived just calls default() on each member. If one or more of your members do not themselves have a Default implementation or you'd like to manually specify something else, you can manually define one:

impl Default for Dot {
    fn default() -> Self {
        Self {
            color: Color::SteelBlue,
            origin: Point::default(),
            radius: 10.0,
        }
    }
}
Enter fullscreen mode Exit fullscreen mode
Clone/Copy/PartialEq/PartialOrd

These aren't heavily used in this code, but are commonly found in general.

  1. Clone - duplicate an arbitrarily nested object - potentially expensive, will call clone() on any child.
  2. Copy - duplicate an object that's simple enough to just copy bits. My rule of thumb is that if I can have Copy, I take it. Must implement Clone.
  3. PartialEq/PartialOrd - allow two instances of this structure to be compared for equality/magnitude respectively.

These traits are often derived, and are included in the prelude of library functions available to all Rust modules by default.

FromStr

This is not in the prelude and must be explicitly included:

use std::str::FromStr;
Enter fullscreen mode Exit fullscreen mode

You will need to import FromStr in order to use or implement it, and allows your self-defined types to parse() from string values like primitives. It's relatively straightforward to implement, but does include an associated type - I don't use it in this program but it does come up often. From the docs:

#[derive(Debug, PartialEq)]
struct Point {
    x: i32,
    y: i32
}

impl FromStr for Point {
    type Err = ParseIntError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let coords: Vec<&str> = s.trim_matches(|p| p == '(' || p == ')' )
                                 .split(',')
                                 .collect();

        let x_fromstr = coords[0].parse::<i32>()?;
        let y_fromstr = coords[1].parse::<i32>()?;

        Ok(Point { x: x_fromstr, y: y_fromstr })
    }
}
Enter fullscreen mode Exit fullscreen mode
ToString

You only need to import std::string::ToString if you plan to implement it, as I do for the Color enum to map to the exact string values that the library has constants for:

/// All colors used in this application
#[derive(Debug, Clone, Copy)]
enum Color {
    Honeydew,
    SteelBlue,
}

impl ToString for Color {
    fn to_string(&self) -> String {
        format!("{:?}", self).to_lowercase()
    }
}
Enter fullscreen mode Exit fullscreen mode

This ToString implementation does rely on the fact that Debug is also implemented for Color, so I can use the {:?} formatter on self.

From/Into

This is how to convert between types within your program. Implementing From gets you Into and vice versa: you get both when you implement one. You only need to directly implement Into if you're converting to some type outside the current crate. I use From to get from my special personal Color type to the library type the draw methods expect:

/// Type alias for nannou named color type
type Rgb = Srgb<u8>;

impl From<Color> for Rgb {
    fn from(c: Color) -> Self {
        named::from_str(&c.to_string()).unwrap()
    }
}
Enter fullscreen mode Exit fullscreen mode

Now I can use colors I know and convert with Rgb::from(), but have my own control over Color behavior:

impl Nannou for Model {
    /// Show this model
    fn display(&self, draw: &app::Draw) {
        draw.background().color(Rgb::from(self.bg_color)); // Color::Honeydew
        self.dot.display(draw);
    }
    //..
}
Enter fullscreen mode Exit fullscreen mode

Special shout-out to the aforementioned ToString implementation to provide Color::to_string(), which is where I've defined how to produce the library constants...

kronk meme

Quality of Life Crates

We're expecting this codebase to grow, and the Rust ecosystem has a few other tidbits that can help us spend time working on the problem, not the environment.

Command Line Arguments

I find the easiest way to get this done is structopt. This crate lets you define a struct with your options, and it custom-derives you an implementation. Add the dependency to Cargo.toml:

  [dependencies]

  nannou = "0.12"
+ structopt = "0.3"
Enter fullscreen mode Exit fullscreen mode

Let's test it out by letting the user control the parameters with command-line options. Add the import tot he top and define the struct:

use structopt::StructOpt;

// ..

/// A nannou demonstration application
#[derive(StructOpt, Debug)]
#[structopt(name = "nannou_dots")]
pub struct Opt {
    /// Set dot growth rate
    #[structopt(short, long, default_value = "1.0")]
    rate: f32,
}
Enter fullscreen mode Exit fullscreen mode

We want this to load when the application starts and be available everywhere. One way to accomplish this is with lazy_static, which lets you define static values that have some runtime initialization. This code will get run once the first time this object is accessed and cache the result for future use throughout your program. This is convenient for things like image assets, which are referred to all over the place making for some potentially sticky ownership problems, and any options passed at runtime that will be true for the entire lifetime of the program.

First, add the dependency to src/Cargo.toml:

  [dependencies]

+ lazy_static = "1.4"
  nannou = "0.12"
  structopt = "0.3"
Enter fullscreen mode Exit fullscreen mode

I generally handle lazy_static usage that's not local to a specific function at the top of the file:

use lazy_static::lazy_static;

lazy_static! {
    pub static ref OPT: Opt = Opt::from_args();
}
Enter fullscreen mode Exit fullscreen mode

Check out where that gets us by running the auto-generated --help/-h flag:

$ cargo run --release -- -h
   Compiling nannou_dots v0.1.0 (/home/ben/code/nannou_dots)
    Finished release [optimized] target(s) in 4.83s
     Running `target/release/nannou_dots -h`
nannou_dots 0.1.0
A nannou demonstration application

USAGE:
    nannou_dots [OPTIONS]

FLAGS:
    -h, --help       Prints help information
    -V, --version    Prints version information

OPTIONS:
    -r, --rate <rate>    Set dot growth rate [default: 1.0]
Enter fullscreen mode Exit fullscreen mode

Good stuff. Notice how the triple-slashed doc comment for the struct member became the help string in this message, and the one for the struct itself is displayed at the top. To hook it up, make the following changes:

  impl Dot {
      fn new() -> Self {
-         Self::default()
+         Self::default().set_growth_rate(OPT.rate)
      }
+     fn set_growth_rate(mut self, rate: f32) -> Self {
+         self.growth_rate = rate;
+         self
+     }
  }
Enter fullscreen mode Exit fullscreen mode

I still leverage the default constructor, but instead give myself a method to set the growth rate of the model. It follows the Builder Pattern so that my code remains flexible for changing my mind and experimenting.

Logging

Another way to set ourselves up for success is by hooking up the standard Rust logging tooling. Now, I'm going to toss three crates at you but don't panic, it's all just one thing. I'm using pretty_env_logger, which dresses up the output from env_logger with nice colors and formatting. This itself is a wrapper around log, which provides a bunch of println!()-esque macros like warn!(), debug!(), and info!(). The env_logger crate (and thus pretty_env_logger) read the RUST_LOG environment variable at runtime to determine which statements to show without needing to recompile. No more commenting out print statements, just pick their debug level and leave 'em all in.

First, add some new dependencies to Cargo.toml:

  [dependencies]

+ log = "0.4"
  nannou = "0.12"
+ pretty_env_logger = "0.3"
Enter fullscreen mode Exit fullscreen mode

To start it up, here's a function I just kinda copy-paste into new projects:

use log::*;
use std::env::{set_var, var};

/// Start env_logger
fn init_logging(level: u8) {
    // if RUST_BACKTRACE is set, ignore the arg given and set `trace` no matter what
    let mut overridden = false;
    let verbosity = if std::env::var("RUST_BACKTRACE").unwrap_or_else(|_| "0".into()) == "1" {
        overridden = true;
        "trace"
    } else {
        match level {
            0 => "error",
            1 => "warn",
            2 => "info",
            3 => "debug",
            _ => "trace",
        }
    };
    set_var("RUST_LOG", verbosity);

    pretty_env_logger::init();

    if overridden {
        warn!("RUST_BACKTRACE is set, overriding user verbosity level");
    } else if verbosity == "trace" {
        set_var("RUST_BACKTRACE", "1");
        trace!("RUST_BACKTRACE has been set");
    };
    info!(
        "Set verbosity to {}",
        var("RUST_LOG").expect("Should set RUST_LOG environment variable")
    );
}
Enter fullscreen mode Exit fullscreen mode

This function takes a number as a level, but also checks if RUST_BACKTRACE is set. RUST_BACKTRACE will override whatever is passed to this and set RUST_LOG to trace automatically. If you pass in a trace level of 4 or higher it will automatically set RUST_BACKTRACE for you. This behavior is usually what I want.

There's a handy way to collect this information built-in to structopt - it can handle arguments from number of occurrences:

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

  /// A nannou demonstration application
  #[derive(StructOpt, Debug)]
  #[structopt(name = "nannou_dots")]
  struct Opt {
      /// Set dot growth rate
      #[structopt(short, long, default_value = "1.0")]
      rate: f32,
+     /// Verbose mode (-v: warn, -vv: info, -vvv: debug, , -vvvv or more: trace)
+     #[structopt(short, long, parse(from_occurrences))]
+     verbosity: u8,
  }
Enter fullscreen mode Exit fullscreen mode

Use cargo run --release -- -vv to get the info level:

info screenshot

Looks like the Nannou window-handling dependency winit is onboard! You can specify per-module by setting, e.g. RUST_LOG=nannou_dots=info to only apply to your own included info!() statements.

To run it with a backtrace you can just specify 4 (or more) vs to the program with -vvvv:

trace screenshot

This allows you to basically do "print" debugging without having to comment things out and recompile. Instead, you leave everything in and just specify how much to dump out at runtime.

Error Handling

This is more of an honorable mention, but nearly every time I write a Rust project, I end up with some enum:

#[derive(Debug)]
pub enum ProjectError {
    ErrorOne(String),
    ErrorTwo(u8, u8),
    ErrorOther,
}
Enter fullscreen mode Exit fullscreen mode

There's always an empty std::error::Error impl, a Result<T> alias, and a std::fmt::Display block so it can be used with the {} formatter:

impl std::error::Error for ProjectError {}

pub type Result<T> = std::result::Result<T, ProjectError>;

impl std::fmt::Display for ProjectError {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        use ProjectError::*;
        let e_str = match self {
            ErrorOne(s) => &format!("{}", s),
            ErrorTwo(x, y) => &format!("expected {}, got {}", x, y),
            ErrorOther => "Something went wrong!",
        };
        write!(f, "Error: {}", e_str)
    }
}
Enter fullscreen mode Exit fullscreen mode

I do this every time, and it works, and it's nice to have this control. Luckily, there's a crate for that: thiserror, which provides some custom-derive magic on it. We could replace the entire above example with this:

/// Error types
#[derive(Error, Debug)]
enum ProjectError {
    #[error("{0}")]
    StringError(String),
    #[error("expected {expected:?}, got {found:?}")]
    NumberMismatch { expected: u8, found: u8 },
    #[error("Something went wrong!")]
    ErrorOther,
}
Enter fullscreen mode Exit fullscreen mode

However, in this app, I'm too lazy to even muck around with that. Who's got time for that anyhow:

  [dependencies]

+ anyhow = "1.0"
  lazy_static = "1.4"
  log = "0.4"
  nannou = "0.12"
  pretty_env_logger = "0.3"
  structopt = "0.3"
Enter fullscreen mode Exit fullscreen mode

Then, just add use anyhow::Result to the top of your file and get ? for free. If any function you write calls an operation that can fail, just make your function return this Result<T> and it'll just work.

Lots Of Dots

Let's take our newfound structure for a spin by upping the number of dots. First, add a parameter for the user:

  /// A nannou demonstration application
  #[derive(StructOpt, Debug)]
  #[structopt(name = "nannou_dots")]
  pub struct Opt {
+     /// How many dots to render
+     #[structopt(short, long, default_value = "1")]
+     num_dots: u8,
      /// Set dot growth rate
      #[structopt(short, long, default_value = "1.0")]
      rate: f32,
      /// Verbose mode (-v: warn, -vv: info, -vvv: debug, , vvvv or more: trace)
      #[structopt(short, long, parse(from_occurrences))]
      verbosity: u8,
  }
Enter fullscreen mode Exit fullscreen mode

The user can specify 0 through 255 dots. In the Model, we'll keep track of a Vec:

  /// The application state
  #[derive(Debug)]
  struct Model {
      bg_color: Color,
      current_bg: usize,
-     dot: Dot,
+     dots: Vec<Dot>,
  }

  impl Nannou for Model {
    /// Show this model
    fn display(&self, draw: &app::Draw) {
        draw.background().color(Rgb::from(self.bg_color));
-       self.dot.display(draw);
+       self.dots.iter().for_each(|d| d.display(&draw));
    }
    /// Update this model
    fn update(&mut self) {
-       self.dot.update();
+       self.dots.iter_mut().for_each(|d| d.update());
    }
  }
Enter fullscreen mode Exit fullscreen mode

Before we initialize them, let's flesh out the Dot definition to allow us to specify a start location:

  impl Dot {
-     fn new() -> Self {
-         Self::default().set_growth_rate(OPT.rate)
+     fn new(point: Option<Point>) -> Self {
+       let mut ret = Self::default();
+       if let Some(loc) = point {
+           ret.set_location(loc);
+       }
+       ret.set_growth_rate(OPT.rate)
      }
      fn set_growth_rate(mut self, rate: f32) -> Self {
          self.growth_rate = rate;
          self
      }
+     fn set_location(mut self, loc: Point) -> Self {
+       self.origin = loc;
+       self
+    }
  }
Enter fullscreen mode Exit fullscreen mode

Now we can make a Model::init_dots() associated function that the default constructor can use:

impl Model {
    fn init_dots() -> Vec<Dot> {
        let mut ret = Vec::new();
        for _ in 0..OPT.num_dots {
            let point_x = rand::random_range(-500.0, 500.0);
            let point_y = rand::random_range(-500.0, 500.0);
            ret.push(Dot::new(Some(Point::new(point_x, point_y))));
        }
        ret
    }
}
Enter fullscreen mode Exit fullscreen mode

Just swap it in:

  impl Default for Model {
      fn default() -> Self {
          Self {
              bg_color: Color::Honeydew,
              current_bg: usize::default(),
-             dot: Dot::new(),
+             dots: Self::init_dots(),
          }
      }
  }
Enter fullscreen mode Exit fullscreen mode

lots of dots

Wrapping Up

Some features I didn't touch on bundled with nannou include UI components, math functions, image handling, and noise generation, things you'd otherwise manually include a crate yourself for. Nannou aims to be a complete, all-in-one solution leveraging the best of the Rust ecosystem to fit this domain, and by my estimation hits the mark.

Challenges

Before moving further, my recommendation would be to split this logic into separate modules, instead of putting everything in main.rs. You do you though. Here's a few things you could try next:

  1. Add some sounds.
  2. Make the dots move.
  3. Make the dots different colors.
  4. Use the noise module to distribute the dots.
  5. Add sliders to control parameters.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .