Blog Logo

06 Dec 2023 ~ 3 min read

Using nannou in Rust


Simple app in nannou

The library nannou is an “open-source creative-coding framework for Rust”. Among the simplest graphics app one always find some sort of “projectile app” or “rocket app”. Let’s see how such an app looks like in nannou.

A nannou app features three components:

  • the Model: The model function is returns a Model struct that contains the initial state of the app.

  • the View (aka the ‘rendering’): The view function is used to draw from the current state of the application

  • the Update: Think of the update as a function that is repeatedly called within an infinite game loop. The update together with all user interaction takes the place of the controller in the MVC (Model-View-Controller).

The main objects

The projectiles will be thrown from a little cannon (described by the angle at which it is pointing and its power).

struct Model {
    cannon: Cannon,
    projectiles: Vec<Projectile>,
}

struct Cannon {
    angle: f32,
    power: f32,
}

struct Projectile {
    position: Point2,
    velocity: Vec2,
}

Model-View-Update

The model

fn model(app: &App) -> Model {
    app.new_window()
       .size(800, 600)
       .view(view)
       .build()
       .unwrap();

    Model {
        cannon: Cannon {
            angle: PI / 4.0,
            power: 10.0,
        },
        projectiles: Vec::new(),
    }
}

The view

fn view(app: &App, model: &Model, frame: Frame) {
    let draw = app.draw();
    draw.background().color(WHITE);

    draw_cannon(&draw, &model.cannon);
    draw_projectiles(&draw, &model.projectiles);

    draw.to_frame(app, &frame).unwrap();
}

The update

fn update(app: &App, model: &mut Model, _update: Update) {
    if app.elapsed_frames() % 60 == 0 {
        fire_projectile(model);
    }

    update_projectiles(model);
}

How it looks like

nannou projectile

The entire app code


use nannou::prelude::*;

struct Model {
    cannon: Cannon,
    projectiles: Vec<Projectile>,
}

struct Cannon {
    angle: f32,
    power: f32,
}

struct Projectile {
    position: Point2,
    velocity: Vec2,
}

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

fn model(app: &App) -> Model {
    app.new_window()
       .size(800, 600)
       .view(view)
       .build()
       .unwrap();

    Model {
        cannon: Cannon {
            angle: PI / 4.0,
            power: 10.0,
        },
        projectiles: Vec::new(),
    }
}

fn update(app: &App, model: &mut Model, _update: Update) {
    if app.elapsed_frames() % 60 == 0 {
        fire_projectile(model);
    }

    update_projectiles(model);
}

fn fire_projectile(model: &mut Model) {
    let velocity = Vec2::new(
        model.cannon.power * model.cannon.angle.cos(),
        model.cannon.power * model.cannon.angle.sin(),
    );
    let projectile = Projectile {
        position: pt2(0.0, 0.0),
        velocity,
    };
    model.projectiles.push(projectile);
}

fn update_projectiles(model: &mut Model) {
    for projectile in &mut model.projectiles {
        projectile.velocity.y -= 0.1; // gravity
        projectile.position += projectile.velocity;
    }
}

fn view(app: &App, model: &Model, frame: Frame) {
    let draw = app.draw();
    draw.background().color(WHITE);

    draw_cannon(&draw, &model.cannon);
    draw_projectiles(&draw, &model.projectiles);

    draw.to_frame(app, &frame).unwrap();
}

fn draw_cannon(draw: &Draw, cannon: &Cannon) {
    draw.line()
        .start(pt2(0.0, 0.0))
        .end(pt2(
            50.0 * cannon.angle.cos(),
            50.0 * cannon.angle.sin(),
        ))
        .color(BLACK);
}

fn draw_projectiles(draw: &Draw, projectiles: &[Projectile]) {
    for projectile in projectiles {
        draw.ellipse()
            .xy(projectile.position)
            .radius(5.0)
            .color(RED);
    }
}

Headshot of Sylvain Bonnot

I'm Sylvain. I'm a data scientist based near Paris. You can see some of my work on GitHub.