はじめに
RustでGUIを持ったアプリを作ろうと思い、即時モードのGUIフレームワークであるeguiを使っています。
以前の投稿では、簡単なお絵描きをeguiで行う方法について紹介しました。
今回は、静止画を連続的に描くことによって、簡単なアニメーションを作ってみたので、その紹介をしたいと思います。
なお、本記事においては、eguiクレートの機能的な側面や、Rustの文法に関しては一切触れていません。まずは、手っ取り早くeguiで描画してみたい、という方向けに記述しています。
Rust, egui のバージョンについて
本記事では
- Rust 1.71.1
- egui/eframe 0.23.0
を使用しています。
eguiはバージョンアップで容赦なく仕様を変えており、バージョン違いでコンパイルエラーが頻発しますので、eguiに関する記事を参照する際には、バージョンを十分確認したほうが良いです。
また、eguiの導入については、以前の記事を参照ください。
Main Windowの作成
まずは、main.rst を以下の状態から始めます。
use eframe::{egui::*};
#[derive(Default)]
pub struct EguiSample {}
impl EguiSample {
fn new(_cc: &eframe::CreationContext<'_>) -> Self {Self::default()}
}
impl eframe::App for EguiSample {
fn update(&mut self, _ctx: &Context, _frame: &mut eframe::Frame) {}
}
fn main() {
let native_options = eframe::NativeOptions {
initial_window_size: Some((800.0, 800.0).into()),
//resizable: false,
..eframe::NativeOptions::default()
};
let _ = eframe::run_native(
"egui_sample",
native_options,
Box::new(|cc| Box::new(EguiSample::new(cc)))
);
}
initial_window_size で縦横それぞれ800pixelに設定しています。
resizable: false をコメントアウトしているのは、今回ウインドウサイズを変更できるようにしたことを示すためです。
EguiSampleでは、継承元の eframe::App からupdate()関数のみを実装しています。
この後、コンソールで以下のようにタイプして、コンパイル&実行します。
cargo run --release
時間管理の仕組みを実装
EguiSampleの構造体に時間管理関係の変数を追加。また、update()関数内に時間管理用の処理を追加します。
use std::time::{Duration, Instant};
pub struct EguiSample {
cnt: i32,
instant: Instant,
}
impl EguiSample {
fn new(_cc: &eframe::CreationContext<'_>) -> Self {
Self {
cnt: 0,
instant: Instant::now(),
}
}
}
impl eframe::App for EguiSample {
fn update(&mut self, ctx: &Context, _frame: &mut eframe::Frame) {
ctx.request_repaint_after(Duration::from_millis(25));
if self.instant.elapsed() >= Duration::from_millis(50) {
self.cnt += 1;
self.instant = Instant::now();
}
}
}
ctx.request_repaint_after()では、このupdate()を次回強制的に呼ぶ時間を設定しています。update()は表示イベントや、入力イベントがあったときに自動的に呼ばれますが、自動的に図形が動くアニメーションを表示するために、定期的に呼ぶようにします。
また、プログラムが起動してからの時間を得るために、EguiSampleにInstantというメンバー変数を追加し、Instant::now()で現在までの累計時間を得て、self.instantを更新。この処理を、この値から50msecを超えたら呼ばれるようにして、その回数をself.cntに記録します。
これによって、このアプリ内の時間をself.cntの値で管理できるようにしています。この値が1増えると、50msecが経過したということになります。
今回、ctx.request_repaint_after()で25msecで設定しつつ、50msecを超えたらself.cntをインクリメントする処理を同時に実装しているのは、request_repaint_after()ではupdate()が呼ばれる時間の正確な周期性を保証していないのではないかと考えるからです。
今回の実装では、20FPS(50msecに一度)を保証しつつ、update()の再呼び出し時間は50msecではなく、その周期をより短い(今回は半分)設定とすることで、少々の処理の無駄があっても描画の時間正確性を確保しました。
表示オブジェクト(水紋)の実装
実際に表示される図形の描画処理を作ります。
pub struct Object {
para1: f32,
para2: f32,
para3: f32,
time: i32,
}
impl Object {
const DISAPPEAR_RATE: f32 = 300.0;
const RIPPLE_SIZE: i32 = 32;
const BRIGHTNESS: f32 = 255.0; // Max 255
const RIPPLE_SIZE_F: f32 = (Self::RIPPLE_SIZE-1) as f32;
fn disp(&self, crnt_time: i32, ui: &mut Ui) -> bool {
let cnt = (crnt_time - self.time)*4;
if cnt as f32 > Self::DISAPPEAR_RATE {return false;}
for i in 0..Self::RIPPLE_SIZE {
let phase = std::f32::consts::PI*(i as f32)/16.0; // 波の密度
let gray = Self::BRIGHTNESS*(1.0-phase.sin().abs()); // 波パターンの関数(sinの絶対値)
let gray_scl = (gray*
(self.para3/100.0)*
((Self::RIPPLE_SIZE_F-(i as f32))/Self::RIPPLE_SIZE_F)* // 厚さと濃淡
((Self::DISAPPEAR_RATE-(cnt as f32))/Self::DISAPPEAR_RATE) // 消えゆく速さ
) as u8; // 白/Alpha値への変換
if i < cnt {
ui.painter().circle_stroke(
Pos2 {x:self.para1, y:self.para2}, // location
(cnt-i) as f32, // radius
Stroke {width:1.0, color:Color32::from_white_alpha(gray_scl)}
);
}
}
true
}
}
今回は、池に石を投げた時に起きるような水紋のような模様を表示します。
上記のオブジェクトが、一つの波紋を表示します。
物理的な正確性は全くないですが、今回は下図のようにsin波を絶対値にして(正の方向に折り返して)、逆さまにした形状としています。
波紋は同心円状に広がり、各位置は時間が経つほど振幅が減っていきます。
このオブジェクトが生成されるとき、以下の4つのパラメータを受信します。
- para1:中心のx座標
- para2:中心のy座標
- para3:波全体の大きさ
- time:絶対時間
また、上記のプログラムでは、時間による減衰量、波の周期などを数値で設定できるようにしています。
描画の際、振幅値をcolor:Color32::from_white_alpha()の引数として、白とアルファ値の値として設定しています。
Main Windowに波紋をランダムに表示
上記の水紋オブジェクトを、EguiSampleから呼び出します。
use std::time::{Duration, Instant};
use rand::{thread_rng, Rng, rngs};
pub struct EguiSample {
cnt: i32,
instant: Instant,
size: Pos2,
rndm: rngs::ThreadRng,
nobj: Vec<Object>,
}
impl EguiSample {
fn new(_cc: &eframe::CreationContext<'_>) -> Self {
Self {
cnt: 0,
instant: Instant::now(),
size: Pos2 {x:400.0, y:400.0},
rndm: thread_rng(),
nobj: vec![Object {para1:200.0, para2:200.0, para3:127.0, time:0}],
}
}
}
impl eframe::App for EguiSample {
fn update(&mut self, ctx: &Context, frame: &mut eframe::Frame) {
ctx.request_repaint_after(Duration::from_millis(25));
if self.instant.elapsed() >= Duration::from_millis(50) {
self.cnt += 1;
self.instant = Instant::now();
if self.cnt%20 == 0 { // 1sec
let orgx = self.size.x;
let orgy = self.size.y;
self.size.x = frame.info().window_info.size.x;
self.size.y = frame.info().window_info.size.y;
if orgx != self.size.x || orgy != self.size.y {
println!("x:{},y:{}", self.size.x, self.size.y);
}
// create new object
let rndx: f32 = self.rndm.gen();
let rndy: f32 = self.rndm.gen();
let mut rnd_strength: f32 = self.rndm.gen();
rnd_strength = rnd_strength*99.0 + 1.0;
self.nobj.push(
Object {para1:self.size.x*rndx, para2:self.size.y*rndy, para3:rnd_strength, time:self.cnt}
);
}
}
CentralPanel::default().show(ctx, |ui| {
let nlen = self.nobj.len();
let mut rls = vec![true; nlen];
for (i, obj) in self.nobj.iter_mut().enumerate() {
if obj.disp(self.cnt, ui) == false {
rls[i] = false;
}
}
for i in 0..nlen { // 一度に一つ消去
if !rls[i] {self.nobj.remove(i); break;}
}
});
}
}
EguiSampleのメンバー変数に、ウインドウサイズ、ランダム値出力、それから水紋オブジェクトを格納するベクタを追加しました。
update()の中に、1秒に一回呼ばれる処理を作り、その中に
- Main Window のサイズの取得
- 新しいObjectを生成し、引数の生成位置や水紋の大きさに乱数生成
の処理を追加しました。
また、update()の後半で、各オブジェクトの描画処理disp()を呼び、描画とともに、広がり切ったオブジェクトを消去しています。
これで今回のプログラムは完成です。完成した全プログラムは、下の関連リンクより参照ください。
実際に画面が動いている様子をスクショしたのが以下の画像です。
終わりに
eguiで、水紋が広がって消えていく様子をアニメーションにしてみました。
これらのオブジェクトの移動や当たり判定などの機能を実装することで、簡単なゲームを作ることも出来そうです。
Processingのように、グラフィックにフォーカスしたプログラミングにおいても、Rustが一般的になったら面白いですね。