Rustでもクリエイティブコーディング。グラフィク描画、音の取り扱い、GUIを使ったインタラクティブなアプリ等に対応している。
READMEに
The project was started out of a desire for a creative coding framework inspired by Processing, OpenFrameworks and Cinder, but for Rust.
とある通り、ProcessingやOpenFrameworksやCinderの影響を受けているプロジェクト。
Processingにあるような機能はだいたいある(もしくは実装予定がある)。
原点はデフォルトで画面中央であり、変更する手段があるのかどうかはよくわからない。
examples/simple_draw.rsを見ればグラフィックスAPIの雰囲気をつかめる。
Nannou Examplesの手順に従えば各種サンプルを実行できる。
ただし、プロジェクト開始から1年経過しておらず、未完成な点や未調整な点が多数ある。一応動く、程度の状態。tri()
が最速でrect()
は問題なし、だがellipse()
は体感できるほど遅かったりもする。
バイナリが巨大になる(1GB程度)ことやコンパイル時間の長さに対処するため、スケッチをいくつも作って遊ぶような用途ではCargoのワークスペース機能で一括管理するとよいだろうか。
こんな感じに。
playground
├── Cargo.lock
├── Cargo.toml
├── sketch_one
│ ├── Cargo.toml
│ └── src
│ └── main.rs
├── sketch_two
│ ├── Cargo.toml
│ └── src
│ └── main.rs
└── target
├── ...
ライフゲームを作る例
nannouにはviewモードとappモードがある。
viewモードではメインの関数を一つ定義し、その関数を毎フレーム実行し続ける。
appモードでは、モデル作成関数とイベントハンドリング関数と描画関数を用意し、イベントハンドリングと描画を毎フレーム行っていく。
上述のsimple_draw.rsでは前者を使っている。この例では後者を使う。
extern crate nannou;
use nannou::prelude::*;
fn main() {
nannou::run(model, event, view);
}
main
関数でrun
関数を呼び出し、model
とevent
とview
を渡す。modelとeventとviewはそれぞれ特定の引数を受け取る関数である。
#[derive(Copy, Clone, PartialEq)]
enum Cell {
Live,
Dead,
}
pub struct Model {
grid_size: Vector2<f32>,
cell_size: f32,
width : f32,
height : f32,
game : Vec<Cell>
}
Model
構造体を定義する。このModel
構造体は、model
関数内でインスタンスを作成し、event
関数で内容を更新し、view
関数から閲覧される。
また、今回はライフゲーム用にCell列挙型も定義している。
続いてmodel
関数で準備をする。ここでは最終的にModel
構造体のインスタンスを返せればよいが、Model
構造体と関係なくても最初の1回だけ実行したい処理もここに一緒に書いている。
fn model(app: &App) -> Model {
use self::Cell::*;
app.set_loop_mode(LoopMode::rate_fps(12.0));
let grid_size = vec2(9, 9);
let cell_size = 20.0;
let width = (grid_size.x as f32) * cell_size;
let height = (grid_size.y as f32) * cell_size;
app.main_window().set_title("Conway's Game of Life");
app.main_window()
.set_inner_size_pixels(width as u32, height as u32);
Model {
grid_size, cell_size, width, height,
game: vec![
Dead, Dead, Dead, Dead, Dead, Dead, Dead, Dead, Dead,
Dead, Dead, Dead, Dead, Dead, Dead, Dead, Dead, Dead,
Dead, Dead, Dead, Dead, Dead, Dead, Dead, Dead, Dead,
Dead, Dead, Dead, Dead, Live, Dead, Dead, Dead, Dead,
Dead, Dead, Dead, Dead, Dead, Live, Dead, Dead, Dead,
Dead, Dead, Dead, Live, Live, Live, Dead, Dead, Dead,
Dead, Dead, Dead, Dead, Dead, Dead, Dead, Dead, Dead,
Dead, Dead, Dead, Dead, Dead, Dead, Dead, Dead, Dead,
Dead, Dead, Dead, Dead, Dead, Dead, Dead, Dead, Dead,
],
}
}
event
を定義する。ライフゲームのルールに則り次世代を作成し、modelに再代入して返している。
fn event(_app: &App, mut model: Model, event: Event) -> Model {
use self::Cell::*;
match event {
Event::Update(_update) => {
let x_len = model.grid_size.x;
let y_len = model.grid_size.y;
let next_generation = model.game.iter().enumerate()
.map(|(i, current_cell)| {
let middle = i / x_len;
let center = i % x_len;
let top = ((middle+y_len - 1) % y_len) * x_len;
let bottom = ((middle + 1) % y_len) * x_len;
let left = (center+x_len - 1) % x_len;
let right = (center + 1) % x_len;
let middle = middle * x_len;
let around = [
top + left,
top + center,
top + right,
middle + left,
middle + right,
bottom + left,
bottom + center,
bottom + right,
]
.iter()
.filter(|around_cell_i| model.game[**around_cell_i] == Live)
.count();
match around {
3 => Live,
2 => *current_cell,
_ => Dead
}
})
.collect();
model.game = next_generation;
}
_ => ()
}
model
}
最後にview
関数。
fn view(app: &App, model: &Model, frame: Frame) -> Frame {
let draw = app.draw();
draw.background().color(BLACK);
model.game.iter().enumerate()
.for_each(|(i, cell)| {
let color = match cell {
Cell::Live => WHITE,
Cell::Dead => return,
};
let xi = i % model.grid_size.x;
let yi = i / model.grid_size.x;
let x = model.cell_size/2.0 - model.width/2.0
+ (xi as f32) * model.cell_size;
let y = -model.cell_size/2.0 + model.height/2.0
- (yi as f32) * model.cell_size;
draw.rect()
.x_y(x, y)
.w_h(model.cell_size, model.cell_size)
.color(color);
});
draw.to_frame(app, &frame).unwrap();
frame
}
これで
$ cargo run
すれば、白いグライダーが右下に向かって進んでいくはずだ。