Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Cover image for Creative Coding in Rust with Nannou
Ben Lovy
Ben Lovy

Posted on • Edited on

     

Creative Coding in Rust with Nannou

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 withrustc version 1.39.

  • Vulkan SDK - on Gentoo, I had to installdev-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
Enter fullscreen modeExit fullscreen mode

Add the dependency:

# ..# after other metadata[dependencies]nannou="0.12"
Enter fullscreen modeExit fullscreen mode

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();}
Enter fullscreen modeExit 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 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();}
Enter fullscreen modeExit fullscreen mode

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);}
Enter fullscreen modeExit fullscreen mode

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,}}
Enter fullscreen modeExit 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 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;}}
Enter fullscreen modeExit fullscreen mode

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:

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 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();}
Enter fullscreen modeExit 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 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

TheColorsum 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()}}
Enter fullscreen modeExit fullscreen mode

I've also defined a trait of my own:

/// Things that can be drawn to the screentraitNannou{fndisplay(&self,draw:&app::Draw);fnupdate(&mutself);}
Enter fullscreen modeExit 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 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;}
Enter fullscreen modeExit 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 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 }
Enter fullscreen modeExit fullscreen mode

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,}}}
Enter fullscreen modeExit 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 callclone() 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 haveCopy, I take it. Must implementClone.
  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 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;
Enter fullscreen modeExit fullscreen mode

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})}}
Enter fullscreen modeExit fullscreen mode
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()}}
Enter fullscreen modeExit fullscreen mode

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()}}
Enter fullscreen modeExit fullscreen mode

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);}//..}
Enter fullscreen modeExit fullscreen mode

Special shout-out to the aforementionedToString implementation to provideColor::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 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"
Enter fullscreen modeExit 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:

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,}
Enter fullscreen modeExit fullscreen mode

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"
Enter fullscreen modeExit fullscreen mode

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();}
Enter fullscreen modeExit 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.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]
Enter fullscreen modeExit 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 modeExit fullscreen mode

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"
Enter fullscreen modeExit fullscreen mode

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"));}
Enter fullscreen modeExit fullscreen mode

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,  }
Enter fullscreen modeExit fullscreen mode

Usecargo run --release -- -vv to get theinfo level:

info screenshot

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:

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)]pubenumProjectError{ErrorOne(String),ErrorTwo(u8,u8),ErrorOther,}
Enter fullscreen modeExit fullscreen mode

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)}}
Enter fullscreen modeExit 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)]enumProjectError{#[error("{0}")]StringError(String),#[error("expected {expected:?}, got {found:?}")]NumberMismatch{expected:u8,found:u8},#[error("Something went wrong!")]ErrorOther,}
Enter fullscreen modeExit fullscreen mode

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"
Enter fullscreen modeExit fullscreen mode

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,  }
Enter fullscreen modeExit fullscreen mode

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());    }  }
Enter fullscreen modeExit fullscreen mode

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+    }  }
Enter fullscreen modeExit fullscreen mode

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}}
Enter fullscreen modeExit 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 modeExit fullscreen mode

lots of 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:

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

Top comments(10)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss
CollapseExpand
 
dsaghliani profile image
verified_tinker
  • Location
    Tbilisi, Georgia
  • Education
    San Diego State University
  • Joined

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.

CollapseExpand
 
deciduously profile image
Ben Lovy
Just this guy, you know?
  • Email
  • Location
    Boston, MA, USA
  • Education
    Currently enrolled @ Champlain College Online
  • Work
    Rust Developer @ Tangram.dev
  • Joined
• Edited on• Edited

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.

CollapseExpand
 
coolshaurya profile image
Info Comment hidden by post author - thread only accessible via permalink
Shaurya
  • Joined

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`
Comment hidden by post author
CollapseExpand
 
deciduously profile image
Ben Lovy
Just this guy, you know?
  • Email
  • Location
    Boston, MA, USA
  • Education
    Currently enrolled @ Champlain College Online
  • Work
    Rust Developer @ Tangram.dev
  • Joined
• Edited on• Edited

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!

CollapseExpand
 
coolshaurya profile image
Shaurya
  • Joined

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.

Thread Thread
 
deciduously profile image
Ben Lovy
Just this guy, you know?
  • Email
  • Location
    Boston, MA, USA
  • Education
    Currently enrolled @ Champlain College Online
  • Work
    Rust Developer @ Tangram.dev
  • Joined
• Edited on• Edited

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.

CollapseExpand
 
minigamedev profile image
Mossa
  • Joined

Nice post. I am following it closely.

This is missing somewhere, as I am unable to otherwise runlazy_static!.

#[macro_use]externcratelazy_static;
CollapseExpand
 
deciduously profile image
Ben Lovy
Just this guy, you know?
  • Email
  • Location
    Boston, MA, USA
  • Education
    Currently enrolled @ Champlain College Online
  • Work
    Rust Developer @ Tangram.dev
  • Joined
• Edited on• Edited

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!

CollapseExpand
 
minigamedev profile image
Mossa
  • Joined

Great! I think maybe you could use

uselog::{info,trace,warn};

somewhere as well?

What a great blog-entry. I learned a few things from it. Cheers!

Thread Thread
 
deciduously profile image
Ben Lovy
Just this guy, you know?
  • Email
  • Location
    Boston, MA, USA
  • Education
    Currently enrolled @ Champlain College Online
  • Work
    Rust Developer @ Tangram.dev
  • Joined
• Edited on• Edited

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

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

Just this guy, you know?
  • Location
    Boston, MA, USA
  • Education
    Currently enrolled @ Champlain College Online
  • Work
    Rust Developer @ Tangram.dev
  • Joined

More fromBen Lovy

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp