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 justdev-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
Add the dependency:
# ..
# after other metadata
[dependencies]
nannou = "0.12"
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();
}
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();
}
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);
}
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,
}
}
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;
}
}
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:
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();
}
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()
}
}
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);
}
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;
}
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 }
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,
}
}
}
Clone/Copy/PartialEq/PartialOrd
These aren't heavily used in this code, but are commonly found in general.
-
Clone
- duplicate an arbitrarily nested object - potentially expensive, will callclone()
on any child. -
Copy
- duplicate an object that's simple enough to just copy bits. My rule of thumb is that if I can haveCopy
, I take it. Must implementClone
. -
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;
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 })
}
}
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()
}
}
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()
}
}
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);
}
//..
}
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...
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"
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,
}
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"
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();
}
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]
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
+ }
}
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"
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")
);
}
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,
}
Use cargo run --release -- -vv
to get the info
level:
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
:
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,
}
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)
}
}
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,
}
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"
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,
}
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());
}
}
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
+ }
}
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
}
}
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(),
}
}
}
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:
- Add some sounds.
- Make the dots move.
- Make the dots different colors.
- Use the
noise
module to distribute the dots. - Add sliders to control parameters.