🐣
Nannouのテンプレートを作った
GIFの仕様でカクカクしてるけど実際はぬるぬる
はじめに
追加していくより消す方が簡単なので全部入りテンプレートを最初に作っておく。
コード
template.rs
// ドキュメント
// https://docs.rs/nannou/latest/nannou/
//
// 色名一覧
// https://docs.rs/nannou/latest/nannou/color/named/index.html
use nannou::prelude::*;
// 1つのオブジェクトの例
// モジュール化する例としてわかりやすい
mod ball;
use crate::ball::Ball;
// 複数のオブジェクトを管理する1つのオブジェクトの例
// モジュール化する例としてわかりやすい
mod particle_system;
use crate::particle_system::ParticleSystem;
// 繰り返し処理で便利なのでとりあえず入れとこう
#[allow(unused_imports)]
use itertools::Itertools;
struct Model {
any_value1: isize, // 何か
mouse_down: bool, // 左クリックで true になる
ball: Ball, // 中央をまわる物体
ps: ParticleSystem, // マウス位置から噴射するツブツブを管理してるやつ
}
impl Model {
// Model 内のメンバー変数を操作するまとまった処理はここに書く
// それがまとまったら Ball みたいに別のクラスにする
fn any_value1_change(&mut self, sign: isize) {
self.any_value1 += sign;
}
}
fn main() {
// お約束
nannou::app(model).event(event).update(update).run();
}
fn model(app: &App) -> Model {
// ここらへんもお約束
app.new_window()
.size(640, 480) // あとからは app.window_rect() でわかるので定数にしなくてもよかろう
.mouse_pressed(mouse_pressed)
.mouse_released(mouse_released)
.view(view)
.build()
.unwrap();
// Windowタイトルを入れる場合
if true {
app.main_window().set_title("Nannou all in one template");
}
// Model のインスタンスを返す決まりになっている
Model {
any_value1: 0,
mouse_down: false,
ball: Ball::new(DODGERBLUE),
ps: ParticleSystem::new(),
}
}
fn event(app: &App, model: &mut Model, event: Event) {
if let Event::WindowEvent {
simple: Some(KeyPressed(key)),
..
} = event
{
// キーを叩いたタイミングで任意の処理を1回だけ行うとき用
// 押しっぱなしにはならない
match key {
Key::Z => {},
Key::X => {},
Key::C => {},
Key::Space => {},
Key::Left => model.any_value1_change(-1),
Key::Right => model.any_value1_change(1),
// 垂直同期モード (デフォルト)
// 常時何か動いているとき用
Key::Key1 => app.set_loop_mode(LoopMode::RefreshSync),
// FPS指定モード (だけど効いてなくない?)
Key::Key3 => app.set_loop_mode(LoopMode::rate_fps(30.0)),
Key::Key6 => app.set_loop_mode(LoopMode::rate_fps(60.0)),
// マウスが動いたりしたときだけ描画するモード
// ほぼ静止画でいいとき用
Key::Key0 => app.set_loop_mode(LoopMode::Wait),
// 1回描画したらもう何もしないモード (PAUSEとして使える)
Key::P => app.set_loop_mode(LoopMode::loop_once()),
// 全画面モードは設定しなくても最初から Command + f で行える
// Key::F => app.main_window().set_fullscreen(true),
// アプリ終了 (最初からEscでも終了できるようになっている)
Key::Q => app.quit(),
// フレームをPNG画像として保存する
Key::S => app
.main_window()
.capture_frame(app.exe_name().unwrap() + ".png"),
_ => {}
}
}
}
fn mouse_pressed(_app: &App, model: &mut Model, button: MouseButton) {
match button {
MouseButton::Left => model.mouse_down = true,
MouseButton::Right => {},
_ => {}
}
}
fn mouse_released(_app: &App, model: &mut Model, button: MouseButton) {
match button {
MouseButton::Left => model.mouse_down = false,
MouseButton::Right => {},
_ => {}
}
}
// 何か動かすときの処理
fn update(app: &App, model: &mut Model, _update: Update) {
// ボールを動かす
if true {
model.ball.update(&app);
}
// パーティクルを動かす
if true {
model.ps.origin = pt2(app.mouse.x, app.mouse.y);
// app.time は1秒間で1.0進むので60倍すればフレーム数カウンタのように使える
// なので % 3 すれば3フレーム毎に処理を行える
if (((app.time * 60.0) as usize) % 3) == 0 { // 3フレーム毎に1発
model.ps.add_particle();
}
model.ps.update();
}
}
// 表示処理
// 行儀の良いコードを書くならここで model の更新はしない方がいい
fn view(app: &App, model: &Model, frame: Frame) {
let draw = app.draw();
let win = app.window_rect();
// 全消去 (残像なし)
if true {
frame.clear(hsl(240.0 / 360.0, 1.0, 0.03));
}
// 全消去 (残像あり)
if false {
draw.rect()
.wh(app.window_rect().wh())
.hsla(240.0 / 360.0, 1.0, 0.03, 0.1);
}
// 左上からマウスに線
if true {
let start = win.top_left();
let mouse = app.mouse.position();
draw.line().points(start, mouse).color(DODGERBLUE);
}
// 左上に長方形
if true {
let r = Rect::from_w_h(100.0, 100.0);
let r = r.top_left_of(win.pad(30.0)); // 余白
draw.rect()
.xy(r.xy())
.wh(r.wh())
.hsla(1.0, 1.0, 1.0, 0.0)
.stroke_weight(1.0) // 枠を書くときは必須
.stroke(DODGERBLUE);
}
// 右下に円
if true {
let r = Rect::from_w_h(100.0, 100.0);
let r = r.bottom_right_of(win.pad(30.0)); // 余白
draw.ellipse()
.xy(r.xy())
.wh(r.wh())
.hsla(1.0, 1.0, 1.0, 0.0)
.stroke_weight(1.0) // 枠を書くときは必須
.stroke(DODGERBLUE);
}
// 右上に5角形
if true {
let r = Rect::from_w_h(100.0, 100.0);
let r = r.top_right_of(win.pad(30.0)); // 余白
draw.ellipse()
.resolution(5.0) // 3.0 なら三角で 4.0 なら◇
.xy(r.xy())
.wh(r.wh())
.no_fill()
.stroke_weight(1.0) // 枠を書くときは必須
.stroke(DODGERBLUE);
}
// 左下に三角
// 3点が決まっている場合は tri() でよいけど
// ただの三角であれば draw.ellipse().resolution(3.0) の方が扱いやすい
if true {
let r = Rect::from_w_h(100.0, 100.0);
let r = r.bottom_left_of(win.pad(30.0)); // 余白
draw.tri()
.points(r.bottom_left(), r.mid_top(), r.bottom_right())
.no_fill()
.stroke_weight(1.0)
.stroke(DODGERBLUE);
}
// 左下に三角
if true {
let r = Rect::from_w_h(100.0, 100.0);
let r = r.bottom_left_of(win.pad(30.0)); // 余白
draw.tri()
.points(r.bottom_left(), r.mid_top(), r.bottom_right())
.hsla(1.0, 1.0, 1.0, 0.0)
.stroke_weight(1.0)
// .rotate(app.time)
.stroke(DODGERBLUE);
}
// 中央からマウスに矢印
if true {
draw.arrow()
.start_cap_round()
.weight(3.0) // 太さ
.points(vec2(0.0, 0.0), app.mouse.position())
.color(DODGERBLUE);
}
// 中央から十字矢印
if true {
let crosshair_color = DODGERBLUE;
let ends = [
win.mid_top(),
win.mid_right(),
win.mid_bottom(),
win.mid_left(),
];
ends.iter().for_each(|&end| {
draw.arrow()
.start_cap_round()
.head_length(16.0)
.head_width(8.0)
.color(crosshair_color)
.end(end);
})
}
// マウス座標をマウス位置に表示
if true {
let mouse = app.mouse.position();
let pos = format!("[{:.1}, {:.1}]", mouse.x, mouse.y);
draw.text(&pos)
.xy(mouse + vec2(0.0, 20.0))
.font_size(14)
.color(DODGERBLUE);
}
// マウスの衛星
if true {
let x = (app.time * 5.0 * 0.3).cos() * win.w() / 2.0;
let y = (app.time * 6.0 * 0.3).sin() * win.h() / 2.0;
// map_range を使う場合
// nannou のサンプルでは単に * win.w() / 2.0 などとせず map_range で広げている
// let x = (app.time * 5.0 * 0.3).cos();
// let y = (app.time * 6.0 * 0.3).sin();
// let x = map_range(x, -1.0, 1.0, win.left(), win.right());
// let y = map_range(y, -1.0, 1.0, win.bottom(), win.top());
draw.ellipse().x_y(x, y).color(DODGERBLUE);
}
// 何か押しているとき何かする処理
// キーのチェックは fn event 内で行わないといけないわけではない
if true {
if app.keys.down.contains(&Key::Z) {
println!("{:?}", app.keys.down);
}
if app.keys.down.contains(&Key::X) {
println!("{:?}", app.keys.down);
}
if app.keys.down.contains(&Key::C) {
println!("{:?}", app.keys.down);
}
}
// 描画処理はここに書かずに委譲する例
if true {
model.ball.display(&draw);
model.ps.draw(&draw);
}
// アプリ情報を表示
if true {
let r = Rect::from_w_h(win.w(), 22.0).bottom_left_of(win.pad(0.0));
// draw.rect().xy(r.xy()).wh(r.wh()).rgb(0.0, 0.0, 0.4);
let text = format!("{:?} {}", model.mouse_down, model.any_value1);
draw.text(&text).xy(r.xy()).wh(r.wh()).color(CORNFLOWERBLUE);
}
// システム情報を表示
// 毎フレーム描画しているのかわかるように描画数やFPSは常に表示しておくのがおすすめ
if true {
let r = Rect::from_w_h(win.w(), 22.0).top_left_of(win.pad(0.0));
let text = format!(
"{} {:.2} {:.0}fps {:?} {}x{} M({:.0},{:.0}) {:.0} ",
frame.nth(),
app.time,
app.fps(),
app.loop_mode(),
win.w(),
win.h(),
app.mouse.x,
app.mouse.y,
app.mouse.position().length(),
);
draw.text(&text)
.xy(r.xy())
.wh(r.wh())
.left_justify()
// .align_text_top()
.color(CORNFLOWERBLUE);
}
draw.to_frame(app, &frame).unwrap();
}
ball.rs
// 参考
// https://github.com/nannou-org/nannou/blob/master/examples/rust_basics/7_modules/ball.rs
use nannou::prelude::*;
pub struct Ball {
pub position: Point2,
color: Srgb<u8>,
alive_counter: f32,
}
impl Ball {
pub fn new(color: Srgb<u8>) -> Self {
Ball {
color: color,
position: pt2(0.0, 0.0),
alive_counter: 0.0,
}
}
pub fn update(&mut self, app: &App) {
self.position = pt2(app.mouse.x, app.mouse.y);
self.alive_counter += 1.0;
}
pub fn display(&self, draw: &Draw) {
let x = self.position.x + (self.alive_counter * 0.10).cos() * 50.0;
let y = self.position.y + (self.alive_counter * 0.10).sin() * 50.0;
draw.ellipse()
.x_y(x, y)
.radius(8.0)
.color(self.color);
}
}
particle_system.rs
// ↓オリジナル
// https://github.com/nannou-org/nannou/blob/master/nature_of_code/chp_04_systems/4_03_exercise_moving_particle_system.rs
//
// The Nature of Code
// Daniel Shiffman
// http://natureofcode.com
//
// example 4-03: Exercise Moving Particle System
use nannou::prelude::*;
struct Particle {
position: Point2,
velocity: Vec2,
acceleration: Vec2,
alive_counter: f32,
}
impl Particle {
fn new(l: Point2) -> Self {
let acceleration = vec2(0.0, 0.08);
let velocity = vec2(random_f32() * 1.0 - 0.5, -4.0 + random_f32() * -4.0);
let position = l;
let alive_counter = 0.0;
Particle {
acceleration,
velocity,
position,
alive_counter,
}
}
fn update(&mut self) {
self.velocity += self.acceleration;
self.position -= self.velocity;
self.alive_counter += 1.0;
}
fn display(&self, draw: &Draw) {
draw.rect()
.xy(self.position)
.w_h(4.0, 4.0)
.color(DODGERBLUE);
}
fn is_dead(&self) -> bool {
self.alive_counter >= 60.0 * 4.0
}
}
pub struct ParticleSystem {
particles: Vec<Particle>,
pub origin: Point2,
}
impl ParticleSystem {
pub fn new() -> Self {
let origin = pt2(0.0, 0.0);
let particles = Vec::new();
ParticleSystem { origin, particles }
}
pub fn add_particle(&mut self) {
self.particles.push(Particle::new(self.origin));
}
pub fn update(&mut self) {
// FIXME: もうちょっと綺麗に書けないものか
for i in (0..self.particles.len()).rev() {
self.particles[i].update(); // FIXME: 添字アクセスは止めたい
if self.particles[i].is_dead() {
self.particles.remove(i);
}
}
}
pub fn draw(&self, draw: &Draw) {
for p in self.particles.iter() {
p.display(&draw);
}
}
}
備忘録としてのNannou導入手順
~/src/nannou-playground
に作るとする。
shell
cd ~/src
cargo new nannou-playground
cd nannou-playground
cargo add nannou
cargo add itertools
mkdir -p examples
cd examples
として examples のなかにファイルを置いていく。
そこで examples/foo.rs
を実行するには cargo run --example foo
とする。
src/main.rs
に書いてもいいけど、いろいろ試したいときは examples の中に作った方がファイル毎にエントリーポイントを切り替えることができて開発しやすい。
参考
Discussion