モチベ
Nannouに慣れるために以下の過去作と同じアニメーションを作った:
https://openprocessing.org/sketch/545616
つくったもの
コード
use nannou::prelude::*;
const BEAM_MAX_LIFETIME: f32 = 7.0;
const BEAM_MAX_RADIUS_STEP: i32 = 7;
const BEAM_RADIUS_STEP_SIZE: f32 = 50.0;
enum ShapeType {
Circle,
Polygon(u32),
}
struct Beam {
pos: Point2,
radius: f32,
lifetime: f32,
timestep: f32,
shape_type: ShapeType,
rot: f32,
col: Srgb,
alive: bool,
}
fn hex2rgb(c: i32) -> Rgb {
let r: i32 = (c >> 16) & 0x0000FF;
let g: i32 = (c >> 8) & 0x0000FF;
let b: i32 = c & 0x0000FF;
rgb(r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0)
}
impl Beam {
pub fn new() -> Beam {
Beam {
pos: pt2(0.0, 0.0),
radius: 64.0,
lifetime: 0.0,
timestep: random_range(0.025, 0.075),
shape_type: ShapeType::Circle,
rot: 0.0,
col: rgb(1.0, 0.0, 0.0),
alive: true,
}
}
fn generate_beam(pos: Point2, color_palette: &Vec<Rgb>) -> Beam {
let nvert = random_range(3, 8);
let shape_type = if nvert == 7 {
ShapeType::Circle
} else {
ShapeType::Polygon(nvert)
};
let beam = Beam::new();
Beam {
pos,
shape_type: shape_type,
col: color_palette[random_range(0, color_palette.len())],
..beam
}
}
pub fn compute_radius(t: f32, r: f32) -> f32 {
let a: f32 = (-t).exp() - (-3.0*t).exp();
let b: f32 = (3.0.pow(-0.5) - 3.0.pow(-3.0/2.0)) as f32;
(r * a / b).max(0.0)
}
pub fn update(&mut self) {
self.lifetime = self.lifetime + self.timestep;
if self.lifetime >= BEAM_MAX_LIFETIME {
self.alive = false
}
}
pub fn view(&self, draw: &Draw) {
let radius = Beam::compute_radius(self.lifetime, self.radius);
match self.shape_type {
ShapeType::Circle => {
draw.ellipse()
.no_fill()
.stroke_color(self.col)
.stroke_weight(2.0)
.w_h(radius, radius)
.xy(self.pos);
},
ShapeType::Polygon(n) => {
let points = (0..n).map(|k| {
let t = k as f32 / n as f32 * (2.0 * PI);
let point = pt2(
radius*t.cos(),
radius*t.sin()
);
point + self.pos
});
draw.polygon()
.stroke_color(self.col)
.stroke_weight(2.0)
.no_fill()
.rotate(self.rot)
.points(points);
},
}
}
}
struct Model {
_window: window::Id,
color_palette: Vec<Rgb>,
beams: Vec<Beam>,
recording: bool,
}
fn main() {
nannou::app(model).update(update).run();
}
fn model(app: &App) -> Model {
let _window = app.new_window()
.view(view)
.event(event)
.size(600, 600)
.build().unwrap();
let mut color_palette: Vec<Rgb> = Vec::new();
color_palette.push(hex2rgb(0x95ECD7));
color_palette.push(hex2rgb(0x6BB8Fa));
color_palette.push(hex2rgb(0xEC889a));
color_palette.push(hex2rgb(0xFBC52a));
let color_palette = color_palette;
let n = 128;
let beams: Vec<Beam> = (0..n).map(|k| {
let t = k as f32 / n as f32 * (2.0 * PI);
let r = random_range(32.0, 128.0);
let pos = pt2(r*t.cos(), r*t.sin());
Beam::generate_beam(pos, &color_palette)
}).collect();
Model {
_window,
color_palette,
beams,
recording: false
}
}
fn update(app: &App, model: &mut Model, _update: Update) {
let elapsed_frames = app.elapsed_frames() as f32;
let r_base = (elapsed_frames) / 60.0 % BEAM_MAX_RADIUS_STEP as f32;
for k in 0..model.beams.len() {
if model.beams[k].alive {
model.beams[k].update();
} else {
let t = random_range(0.0, 2.0*PI);
let r = random_range(
r_base * BEAM_RADIUS_STEP_SIZE as f32,
(r_base + 1.0) * BEAM_RADIUS_STEP_SIZE as f32
);
let pos = pt2(r*t.cos(), r*t.sin());
model.beams[k] = Beam::generate_beam(pos, &model.color_palette)
}
}
}
fn view(app: &App, model: &Model, frame: Frame) {
let draw = app.draw();
draw.background().color(BLACK);
for beam in &model.beams {
if beam.alive {
beam.view(&draw);
}
}
draw.to_frame(app, &frame).unwrap();
if model.recording {
// Capture the frame!
let file_path = captured_frame_path(app, &frame);
app.main_window().capture_frame(file_path);
}
}
fn event(_app: &App, model: &mut Model, event: WindowEvent) {
match event {
KeyPressed(key) => {
match key {
Key::S => {
model.recording = !model.recording;
println!("recording: {}", model.recording);
},
_ => {},
}
}
_other => {}
}
}
fn captured_frame_path(app: &App, frame: &Frame) -> std::path::PathBuf {
// Create a path that we want to save this frame to.
app.project_path()
.expect("failed to locate `project_path`")
.join("./cap/")
.join(app.exe_name().unwrap())
.join(format!("{:04}", frame.nth()))
.with_extension("png")
}
Sキーを押すとフレームの保存を開始してくれる(もう一度押すとおわる).
動画化するときは下記のffmpeg
コマンドでできる:
ffmpeg -y -r 60 -i %04d.png -vcodec libx264 -pix_fmt yuv420p -qscale 0 -r 60 output.mp4
Beam
のインスタンスは生成と破棄を繰り返してるので一度作ったもののパラメタを初期化して使いまわせば動作がもっと軽量になるかもしれない.
NannouをProcessingやp5.jsと比べた感想
感想
draw
に更新と描画処理を両方書くテンプレとdraw
とupdate
に分けて書くテンプレが用意されている- 今回は
draw
とupdate
にわけた - openFrameworksに近い使用感
- 今回は
frameCount
に相当するものはapp.elapsed_frames
で得られる- 図形のサイズ,位置,色などはメソッドチェーンで指定するので他のフレームワークと比べるとクセ強め
- 三角関数や指数関数の計算も
x.sin()
とかx.exp()
なのでRust自体にそういう風習があるのかもしれない
- 三角関数や指数関数の計算も
- Rustの文法やマナーを知らないとサンプルコードが全く読めない
- 宣言したけど使ってない変数の頭に
_
をつける - 所有権と借用の概念
- 列挙型の概念
- スコープは戻り値を持つことができる
if
やmatch
は文ではなく式なので値を返すのにも使う
- 宣言したけど使ってない変数の頭に
- エラーメッセージは親切なのでRustをある程度理解してたら何が悪いのかはすぐわかる
- スケッチごとにRustのプロジェクトを作ると(
cargo new xxxxx
),Nannou自体のビルドに時間がかかってディスクも喰うのでひとつのでかいプロジェクトに複数スケッチを作る構成にするのが良さそうsrc/bin
の中にsketch_211231a.rs
を置いて,cargo run --bin sketch_211231a
で実行するかんじ
所有権と借用について
NannouやるならRustのテキストは絶対読んだ方が良い.自分は6章までは読んだ.
The Rust Programming Language 日本語版
所有権と借用は難しいけど,
- 代入・関数呼び出しで
&
をつけたら「値を貸し出した」と読む(&mut
なら書き換えも許可する) - 関数の引数で
&
をつけたら「値を借りてきた」と読む(&mut
なら書き換えも許可してもらえてる)
と考えられれば大丈夫なはず. これを理解してると,以下の宣言:
fn update(app: &App, model: &mut Model, update: Update) {}
fn view(app: &App, model: &Model, frame: Frame) {}
を見たとき
update
はモデルの更新処理なのでmodel: &mut Model
(モデルの書き換えを許可してもらえてる)view
は描画をするだけなのでmodel: &Model
(モデルの読み取りだけ許可されてる)
というのがすぐわかる.万が一権限を勘違いしていてもコンパイルの時点で弾かれるのでわかりやすい. Nannouはコンピュータに管理されたい人にオススメなクリエイティブコーディング環境だと思う.
おわり