
Oxidize Your Life With One Weird Trick
We're going to build a small demo with thenannou creative coding framework forRust. 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 ofThe Rust Programming Language, freely available, or equivalent before tackling this framework.
Part of the strength ofnannou
as a framework is that itdoesn'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 onGitHub.
Table Of Contents
The Motive
I was irrationally hell-bent on modelling a problem in Rust that was perfectly suited forProcessing or itsJavaScript orPython 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 codePit 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
StableRust 2018 - thedefault 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:
usenannou::{color::named,prelude::*};fnmain(){nannou::app(model).update(update).simple_window(view).run();}structModel{bg_color:String,x:f32,y:f32,radius:f32,}fnmodel(_app:&App)->Model{Model{bg_color:"honeydew".to_string(),x:0.0,y:0.0,radius:10.0,}}fnupdate(_app:&App,model:&mutModel,_update:Update){ifmodel.radius<500.0{model.radius+=1.0;}}fnview(app:&App,model:&Model,frame:&Frame){letdraw=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 withcargo 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, notquick - 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 toDefensive Refactor.
Themain()
function only does one thing: instantiate anannou
app object and immediately call it'srun()
method. It then continually draws frames based on the parameters we define. Each frame is defined by aview:
fnview(app:&App,model:&Model,frame:&Frame){letdraw=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 usenannou
withonly this function if you'd like to experiment with stateless drawing ideas. Simply replace themain()
entrypoint code with this:
fnmain(){nannou::sketch(view);}
We use the library-provideddraw()
methods provided by theapp
parameter to interact with the frame. First, we set the background color and then draw an ellipse. These methods update the state of theapp
object, and then finally we draw the new state to theframe
. We get all the parameters about what color to use and how to paint the ellipse from themodel:
fnmodel(_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 thedefined constants, and our ellipse starts off super small and at the center of the frame. Between each frame, the model mightupdate:
fnupdate(_app:&App,model:&mutModel,_update:Update){ifmodel.radius<500.0{model.radius+=1.0;}}
In this function, we're given a mutable borrow of theModel
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 theAPI 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 configureddevelopment 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 ofsrc/main.rs
with this - there are no functional changes, ony structural:
usenannou::{color::named,prelude::*};usestd::string::ToString;fnmain(){nannou::app(model).update(update).simple_window(view).run();}/// All colors used in this application#[derive(Debug,Clone,Copy)]enumColor{Honeydew,SteelBlue,}implToStringforColor{fnto_string(&self)->String{format!("{:?}",self).to_lowercase()}}/// Type alias for nannou color typetypeRgb=Srgb<u8>;implFrom<Color>forRgb{fnfrom(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)]structPoint{x:f32,y:f32,}implPoint{fnnew(x:f32,y:f32)->Self{Self{x,y}}}/// Things that can be drawn to the screentraitNannou{fndisplay(&self,draw:&app::Draw);fnupdate(&mutself);}/// A circle to paint#[derive(Debug,Clone,Copy)]structDot{color:Color,origin:Point,radius:f32,max_radius:f32,growth_rate:f32,}implDot{fnnew()->Self{Self::default()}}implNannouforDot{fndisplay(&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));}fnupdate(&mutself){ifself.radius<self.max_radius{self.radius+=self.growth_rate;}}}implDefaultforDot{fndefault()->Self{Self{color:Color::SteelBlue,origin:Point::default(),radius:10.0,max_radius:200.0,growth_rate:1.0,}}}/// The application state#[derive(Debug)]structModel{bg_color:Color,current_bg:usize,dot:Dot,}implDefaultforModel{fndefault()->Self{Self{bg_color:Color::Honeydew,current_bg:usize::default(),dot:Dot::new(),}}}implNannouforModel{/// Show this modelfndisplay(&self,draw:&app::Draw){draw.background().color(Rgb::from(self.bg_color));self.dot.display(draw);}/// Update this modelfnupdate(&mutself){self.dot.update();}}//// Nannou interface///// Nannou app modelfnmodel(_app:&App)->Model{Model::default()}/// Nannou app updatefnupdate(_app:&App,model:&mutModel,_update:Update){model.update();}/// Nannou app viewfnview(app:&App,model:&Model,frame:&Frame){letdraw=app.draw();// Draw modelmodel.display(&draw);// Render framedraw.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 frommain()
and literallyrubber-duck thecontrol 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. Runningcargo run --release
should produce an identical result to before the switch.
Traits And Composition
TheColor
sum type (orRustenum
) is the best example of Rust-style composition:
/// All colors used in this application#[derive(Debug,Clone,Copy)]enumColor{Honeydew,SteelBlue,}implToStringforColor{fnto_string(&self)->String{format!("{:?}",self).to_lowercase()}}/// Type alias for nannou color typetypeRgb=Srgb<u8>;implFrom<Color>forRgb{fnfrom(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 screentraitNannou{fndisplay(&self,draw:&app::Draw);fnupdate(&mutself);}
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 aCat
inheriting from anAnimal
superclass, because a cat "is-a" animal. Instead, everything is extended via composition, or a "has-a" relationship. OurCat
might know how tospeak()
and say something different than aDog
would with the same method, but have theVoiced
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 istraits. 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 thecompiler's source code as this, omitting the doc comment and version tag:
pubtraitDefault:Sized{fndefault()->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 manualimpl Trait for Struct
block yourself that matches the prescribed API. There's a great overview of the reasoning and usage inthis blog post byAaron Turon.
Debug
Debug
provides a simple pretty-print implementation of a data structure that can be used with the{:?}
or{:#?}
formatters inprintln!()
(etc) invocations. The default dot looks like this:
Dot { color: SteelBlue, origin: Point { x: 0.0, y: 0.0 }, radius: 10.0 }
Here's aplayground link
Default
Default
provides a methoddefault()
to be used as a default constructor. When derived just callsdefault()
on each member. If one or more of your members do not themselves have aDefault
implementation or you'd like to manually specify something else, you can manually define one:
implDefaultforDot{fndefault()->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 theprelude of library functions available to all Rust modules by default.
FromStr
This is not in the prelude and must be explicitly included:
usestd::str::FromStr;
You will need to importFromStr
in order to use or implement it, and allows your self-defined types toparse()
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)]structPoint{x:i32,y:i32}implFromStrforPoint{typeErr=ParseIntError;fnfrom_str(s:&str)->Result<Self,Self::Err>{letcoords:Vec<&str>=s.trim_matches(|p|p=='('||p==')').split(',').collect();letx_fromstr=coords[0].parse::<i32>()?;lety_fromstr=coords[1].parse::<i32>()?;Ok(Point{x:x_fromstr,y:y_fromstr})}}
ToString
You only need to importstd::string::ToString
if you plan to implement it, as I do for theColor
enum to map to the exact string values that the library has constants for:
/// All colors used in this application#[derive(Debug,Clone,Copy)]enumColor{Honeydew,SteelBlue,}implToStringforColor{fnto_string(&self)->String{format!("{:?}",self).to_lowercase()}}
ThisToString
implementation does rely on the fact thatDebug
is also implemented forColor
, so I can use the{:?}
formatter onself
.
From/Into
This is how to convert between types within your program. ImplementingFrom
gets youInto
and vice versa: you get both when you implement one. You only need to directly implementInto
if you're converting to some type outside the current crate. I useFrom
to get from my special personalColor
type to the library type thedraw
methods expect:
/// Type alias for nannou named color typetypeRgb=Srgb<u8>;implFrom<Color>forRgb{fnfrom(c:Color)->Self{named::from_str(&c.to_string()).unwrap()}}
Now I can use colors I know and convert withRgb::from()
, but have my own control overColor
behavior:
implNannouforModel{/// Show this modelfndisplay(&self,draw:&app::Draw){draw.background().color(Rgb::from(self.bg_color));// Color::Honeydewself.dot.display(draw);}//..}
Special shout-out to the aforementionedToString
implementation to provideColor::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 isstructopt
. This crate lets you define a struct with your options, and it custom-derives you an implementation. Add the dependency toCargo.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:
usestructopt::StructOpt;// ../// A nannou demonstration application#[derive(StructOpt,Debug)]#[structopt(name="nannou_dots")]pubstructOpt{/// 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 withlazy_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 tosrc/Cargo.toml
:
[dependencies]+ lazy_static = "1.4" nannou = "0.12" structopt = "0.3"
I generally handlelazy_static
usage that's not local to a specific function at the top of the file:
uselazy_static::lazy_static;lazy_static!{pubstaticrefOPT: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.0A nannou demonstration applicationUSAGE: nannou_dots [OPTIONS]FLAGS: -h, --help Prints help information -V, --version Prints version informationOPTIONS: -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 theBuilder 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 usingpretty_env_logger
, which dresses up the output fromenv_logger
with nice colors and formatting. This itself is a wrapper aroundlog
, which provides a bunch ofprintln!()
-esque macros likewarn!()
,debug!()
, andinfo!()
. Theenv_logger
crate (and thuspretty_env_logger
) read theRUST_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 toCargo.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:
uselog::*;usestd::env::{set_var,var};/// Start env_loggerfninit_logging(level:u8){// if RUST_BACKTRACE is set, ignore the arg given and set `trace` no matter whatletmutoverridden=false;letverbosity=ifstd::env::var("RUST_BACKTRACE").unwrap_or_else(|_|"0".into())=="1"{overridden=true;"trace"}else{matchlevel{0=>"error",1=>"warn",2=>"info",3=>"debug",_=>"trace",}};set_var("RUST_LOG",verbosity);pretty_env_logger::init();ifoverridden{warn!("RUST_BACKTRACE is set, overriding user verbosity level");}elseifverbosity=="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 ifRUST_BACKTRACE
is set.RUST_BACKTRACE
will override whatever is passed to this and setRUST_LOG
totrace
automatically. If you pass in atrace
level of 4 or higher it will automatically setRUST_BACKTRACE
for you. This behavior is usually what I want.
There's a handy way to collect this information built-in tostructopt
- 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, }
Usecargo run --release -- -vv
to get theinfo
level:
Looks like the Nannou window-handling dependencywinit
is onboard! You can specify per-module by setting, e.g.RUST_LOG=nannou_dots=info
to only apply to your own includedinfo!()
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)]pubenumProjectError{ErrorOne(String),ErrorTwo(u8,u8),ErrorOther,}
There's always an emptystd::error::Error
impl, aResult<T>
alias, and astd::fmt::Display
block so it can be used with the{}
formatter:
implstd::error::ErrorforProjectError{}pubtypeResult<T>=std::result::Result<T,ProjectError>;implstd::fmt::DisplayforProjectError{fnfmt(&self,f:&mutstd::fmt::Formatter)->std::fmt::Result{useProjectError::*;lete_str=matchself{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)]enumProjectError{#[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 thatanyhow
:
[dependencies]+ anyhow = "1.0" lazy_static = "1.4" log = "0.4" nannou = "0.12" pretty_env_logger = "0.3" structopt = "0.3"
Then, just adduse 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 thisResult<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 theModel
, we'll keep track of aVec
:
/// 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 theDot
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 aModel::init_dots()
associated function that the default constructor can use:
implModel{fninit_dots()->Vec<Dot>{letmutret=Vec::new();for_in0..OPT.num_dots{letpoint_x=rand::random_range(-500.0,500.0);letpoint_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 withnannou
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 intoseparate modules, instead of putting everything inmain.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.
Top comments(10)

Thanks for the great post, Ben! I must admit, more than the Nannou parts, it's the Rust tooling that I found most useful to me. And the idiomatic project structure. Lots of lessons learned here.

- Email
- LocationBoston, MA, USA
- EducationCurrently enrolled @ Champlain College Online
- WorkRust Developer @ Tangram.dev
- Joined
I'm so glad this was helpful! This post is getting a little old, so I have two suggestions about tooling. For one, you can now ditchstructopt
. Clap v3+ integrates this feature, you just need to enable thederive
feature and use theclap::Parser
trait to annotate your CLI structs. There's atutorial.
Also, instead ofpretty_env_logger
, I would now recommendtracing
. Can be a drop-in replacement but also does a lot more.

Running the code gives an error -
--> src/main.rs:60:1 |45 | struct Dot { | ---------- previous definition of the type `Dot` here...60 | struct Dot { | ^^^^^^^^^^ `Dot` redefined here | = note: `Dot` must be defined only once in the type namespace of this moduleerror[E0119]: conflicting implementations of trait `std::marker::Copy` for type `Dot`: --> src/main.rs:59:24 |44 | #[derive(Debug, Clone, Copy)] | ---- first implementation here...59 | #[derive(Debug, Clone, Copy)] | ^^^^ conflicting implementation for `Dot`error[E0119]: conflicting implementations of trait `std::clone::Clone` for type `Dot`: --> src/main.rs:59:17 |44 | #[derive(Debug, Clone, Copy)] | ----- first implementation here...59 | #[derive(Debug, Clone, Copy)] | ^^^^^ conflicting implementation for `Dot`error[E0119]: conflicting implementations of trait `std::fmt::Debug` for type `Dot`: --> src/main.rs:59:10 |44 | #[derive(Debug, Clone, Copy)] | ----- first implementation here...59 | #[derive(Debug, Clone, Copy)] | ^^^^^ conflicting implementation for `Dot`

- Email
- LocationBoston, MA, USA
- EducationCurrently enrolled @ Champlain College Online
- WorkRust Developer @ Tangram.dev
- Joined
Hi Shaurya,
It looks like you have theDot
struct defined twice in your source file. Compare to the final version foundhere and make sure you haven't accidentally included that code snippet twice!

But in the blog post code snippet, itis defined twice -
/// A circle to paint#[derive(Debug,Clone,Copy)]structDot{color:Color,origin:Point,radius:f32,}/// Things that can be drawn to the screentraitNannou{fndisplay(&self,draw:&app::Draw);fnupdate(&mutself);}/// A circle to paint#[derive(Debug,Clone,Copy)]structDot{color:Color,origin:Point,radius:f32,max_radius:f32,growth_rate:f32,}
I think you need to update your blog post.
Also the gh repository seems totally different from the code given here - there are more dependencies on the gh repo.

- Email
- LocationBoston, MA, USA
- EducationCurrently enrolled @ Champlain College Online
- WorkRust Developer @ Tangram.dev
- Joined
Ah, thank you! My mistake indeed, I've removed the extraDot
declaration from the post. It compiles as written on my machine. The lower of those two declarations, with all five members, is correct.
The GitHub repo represents the code snippet at the end of this post, and does extend theDefensive Refactor snippet in a few ways. The final dependency list should look like this:
[dependencies]anyhow="1.0"lazy_static="1.4"log="0.4"nannou="0.12"pretty_env_logger="0.3"structopt="0.3"
Everything other thannannou
is added one by one throughout the post.

- Email
- LocationBoston, MA, USA
- EducationCurrently enrolled @ Champlain College Online
- WorkRust Developer @ Tangram.dev
- Joined
Thank you! I've updated the post. In fact, in Rust 2018 they did away withextern crate
, you no longer need to mark it withmacro_use
. You can import macros directly from any crate specified inCargo.toml
:
uselazy_static::lazy_static;
Previously you didn't need ause
statement, just theextern crate
, now there's noextern crate
but you DO need theuse
. I have noticed that RLS will not autofill the macro name as I type it (it tries to suggest LazyStatic), but it will work when compiled!

- Email
- LocationBoston, MA, USA
- EducationCurrently enrolled @ Champlain College Online
- WorkRust Developer @ Tangram.dev
- Joined
True enough! Yikes, note to self, these snippets needed a little massaging. Inmain.rs on GitHub I just went withuse log::*;
to pull them all in at once.
I'm glad to hear it, despite the hiccups!
Some comments have been hidden by the post's author -find out more
For further actions, you may consider blocking this person and/orreporting abuse