I find myself, when I start a new project, spending a lot of time setting up a basic app before I get to the point where I can create something fun. I have to create a Rust application, set it up to use Nannou, create a basic Nannou app, test it to make sure it’s working (typos are inevitable, it seems). Then I have to add in any cool features I may have come up with from past apps I have done, which means remembering where I did it and copying in the code. The obvious fix for this is to make a template to use as a starting point for new projects. Unsurprisingly, that’s exactly what I did!
First, I set up a basic Rust project. If you have Rust installed , setting up a new project is easy. Just go to the directory where you want you project to be and run:
$ cargo new project
Use your project name instead of “project”. That will create a directory of that name and create a new project in it. That’s all that’s necessary. It even put a little bit of code there you can run to print a positive message of affirmation on the screen:
$ cd project
$ cargo run
Compiling project v0.1.0 (/Users/steve/Work/project)
Finished dev [unoptimized + debuginfo] target(s) in 4.41s
Running `target/debug/project`
Hello, world!
As I said, that’s a complete, albeit small, Rust project. However, I have taken mine a step further and set up a Cargo workspace . This lets me, if I want, set up multiple related projects together. Why would I want to do that? They might be different versions of the same project, or closely related in another way. Or they might have common code that they all need access to. I don’t need it all the time, but I’ve found it useful enough often enough to be worth the slight extra trouble. As an example, I recently did a series of four posts about noise. Each post has its own Nannou app project, but using workspaces, I grouped them all together .
After setting up the Rust project, I replaced the “Hello World” code with a basic Nannou app.
use std::process;
use nannou::prelude::*;
const WIDTH: u32 = 600;
const HEIGHT: u32 = 600;
const CAPTURE: bool = false;
fn main() {
nannou::app(model)
.update(update)
.run();
}
struct Model {
point: Vec2,
paused: bool,
ctrl_key_pressed: bool,
}
fn model(app: &App) -> Model {
app
.new_window()
.size(WIDTH, HEIGHT)
.view(view)
.key_pressed(key_pressed)
.key_released(key_released)
.build()
.unwrap();
Model {
point: vec2(0., 0.),
paused: false,
ctrl_key_pressed: false,
}
}
fn update(app: &App, model: &mut Model, _update: Update) {
if model.paused { return; }
let rect = app.window_rect();
model.point = vec2(
map_range(random_f32(), 0., 1., rect.left(), rect.right()),
map_range(random_f32(), 0., 1., rect.bottom(), rect.top()),
)
}
fn view(app: &App, model: &Model, frame: Frame) {
if model.paused { return; }
let draw = app.draw();
if app.elapsed_frames() == 1 {
draw.background().color(WHITE);
}
let color = random_f32();
let size = map_range(random_f32(), 0., 1., 3., 10.);
draw.ellipse()
.hsla(color, 1., 0.5, 0.5)
.w(size)
.h(size)
.xy(model.point);
draw.to_frame(app, &frame).unwrap();
if CAPTURE {
let file_path = captured_frame_path(app, &frame);
app.main_window().capture_frame(file_path);
}
}
/// React to key-presses
fn key_pressed(app: &App, model: &mut Model, key: Key) {
match key {
Key::C => {
if model.ctrl_key_pressed {
process::exit(0);
}
}
Key::S => {
let file_path = saved_image_path(app);
app.main_window().capture_frame(file_path);
}
Key::Space => {
model.paused = !model.paused;
}
Key::LControl => {
model.ctrl_key_pressed = true;
}
_other_key => {}
}
}
/// React to key releases
fn key_released(_app: &App, model: &mut Model, key: Key) {
match key {
Key::LControl => {
model.ctrl_key_pressed = false;
}
_other_key => {}
}
}
/// Get the path to the next captured frame
fn captured_frame_path(app: &App, frame: &Frame) -> std::path::PathBuf {
app.project_path()
.expect("failed to locate `project_path`")
.join("frames")
.join(format!("frame{:05}", frame.nth()))
.with_extension("png")
}
/// Get the path to the next saved image
fn saved_image_path(app: &App) -> std::path::PathBuf {
app.project_path()
.expect("failed to locate `project_path`")
.join("saved")
.join(format!("image{:05}", chrono::offset::Local::now()))
.with_extension("png")
}
There are a number of useful features built in.
The WIDTH
and HEIGHT
of the canvas are in constants at the top of the file (instead of being hardcoded further down.)
There is another constant, CAPTURE
. If you set it to true it will capture each frame as it is being generated, and save it in a frames
directory (warning: this could be a lot of saved images if you let the app run for a while, so only turn this on when you really need it!) This is useful if you want to make a video or an animated GIF of the app as it runs. It’s especially useful if your update()
function does so many calculations that it slows way down; making a video allows you to see how it would have run if you had infinite computing power. :)
There is a similar feature that saves only one image whenever you press the S
key. It saves the current frame in the saved
directory and puts a time-stamp in the file name. This is very useful for capturing images from a long-running app if you happen to like what you see on the screen.
Pressing the space bar will temporarily stop any updates or changes to the screen (press it again to get it to continue…) and pressing Control-C
exits the app.
What this template app does isn’t the point here, as it only exists to be replaced with what I really want to do. But if you must know, it draws an image like the one at the top of this post.
So when I want to start a new project, I copy all the files and directories in this template (renaming “project” to something moderately more descriptive), and I can start creating immediately instead of doing the busy-work of setting all this up first.
If you’d like to check out the full template, you can see it here . I’m, sure this will grow (and hopefully improve) over time. Please feel free to use it as a starting point for your own projects.
Have fun!