From bf2c799ce294c64bb953d3bfaf0d859c87a8332d Mon Sep 17 00:00:00 2001 From: djmil Date: Sun, 28 Apr 2024 22:01:57 +0200 Subject: [PATCH] initial commit --- Cargo.toml | 10 +++ src/circle.rs | 58 ++++++++++++++++ src/main.rs | 182 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 250 insertions(+) create mode 100755 Cargo.toml create mode 100644 src/circle.rs create mode 100755 src/main.rs diff --git a/Cargo.toml b/Cargo.toml new file mode 100755 index 0000000..8057ffe --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "egui-circles" +version = "0.1.0" +edition = "2021" +rust-version = "1.56" + +[dependencies] +eframe = "0.24.0" # Gives us egui, epi and web+native backends +emath = "0.24.0" +rand = "0.8" \ No newline at end of file diff --git a/src/circle.rs b/src/circle.rs new file mode 100644 index 0000000..e0c892c --- /dev/null +++ b/src/circle.rs @@ -0,0 +1,58 @@ +pub mod circle { + + use emath::Vec2; + use emath::Pos2; + use emath::Rect; + use rand::Rng; + + pub static FRICTION: f32 = 0.995; + + pub struct Circle { + pub v: Vec2, + pub c: Pos2, + pub r: f32, + } + + impl Default for Circle { + fn default() -> Self { + let r = rand::thread_rng().gen_range(20.0..50.0); + Self { + r: r, + c: Pos2 { + x: rand::thread_rng().gen_range(r..400.0-r), + y: rand::thread_rng().gen_range(r..400.0-r)}, + v: Vec2 { + x: rand::thread_rng().gen_range(-2.0..2.0), + y: rand::thread_rng().gen_range(-2.0..2.0)}, + } + } + } + + impl Circle { + pub fn apply_force(&mut self, bb: &Rect) { + self.v *= FRICTION; + self.c += self.v; + + if self.v.x > 0.0 { + if self.c.x + self.r > bb.right() { + self.v.x *= -1.0 + } + } else { + if self.c.x - self.r < bb.left() { + self.v.x *= -1.0 + } + } + + if self.v.y > 0.0 { + if self.c.y + self.r > bb.bottom() { + self.v.y *= -1.0 + } + } else { + if self.c.y - self.r < bb.top() { + self.v.y *= -1.0 + } + } + } + } +} + diff --git a/src/main.rs b/src/main.rs new file mode 100755 index 0000000..d2b0b9c --- /dev/null +++ b/src/main.rs @@ -0,0 +1,182 @@ +use emath::Vec2; +use eframe::egui; +use egui::Color32; +use egui::Stroke; +use rand::Rng; + +mod circle; +pub use circle::circle::Circle; + +struct ExampleApp { + circles: Vec, + circles_count: usize, + + colors: Vec, +} + +impl ExampleApp { + fn name() -> &'static str { + "egui-circles" + } +} + +impl Default for ExampleApp { + fn default() -> Self { + Self { + circles: Vec::new(), + circles_count: 2, + colors: Vec::new(), + } + } +} + +impl eframe::App for ExampleApp { + fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + // Looks better on 4k montior + ctx.set_pixels_per_point(1.5); + + egui::CentralPanel::default().show(ctx, |ui| { + if ui.button("halt").clicked() { + //frame.quit() + self.circles.iter_mut().for_each(|c| (*c).v = Vec2{x: 0.0, y: 0.0}); + }; + + if ui.button("push").clicked() { + self.circles.iter_mut().for_each(|c| (*c).v = Vec2{ + x: rand::thread_rng().gen_range(-2.0..2.0), + y: rand::thread_rng().gen_range(-2.0..2.0)}); + }; + + + ui.add(egui::Slider::new(&mut self.circles_count, 0..=25).text("circles count")); + let diff = (self.circles.len() as i32) - (self.circles_count as i32); + if diff > 0 { + self.circles.truncate(self.circles_count); + } else { + for _ in diff..0 { + self.circles.push(Circle::default()); + self.colors.push( + egui::Color32::from_rgba_premultiplied( + rand::thread_rng().gen_range(0..255), + rand::thread_rng().gen_range(0..255), + rand::thread_rng().gen_range(0..255), + 64) + ); + } + } + + let painter = ui.painter(); + + let (hover_pos, any_down, any_released) = ctx.input(|input| (input.pointer.hover_pos(), input.pointer.any_down(), input.pointer.any_released())); + + if let Some(mousepos) = hover_pos { + + self.circles.iter_mut().for_each(|circle|{ + let d = (*circle).c - mousepos; + if d.length() < (*circle).r { + if any_down { + painter.line_segment( + [(*circle).c, (*circle).c +d], + Stroke{width: 1.0, color: Color32::from_rgb(128, 255, 255)}); + } + + if any_released { + (*circle).v += d.normalized() * (d.length() / (*circle).r) *8.0; + } + } + }); + } + + for i in 0..self.circles_count { + painter.circle( + self.circles[i].c, + self.circles[i].r, + self.colors[i] /*Color32::TRANSPARENT*/, + Stroke{width: 2.0, color: Color32::from_rgb(255, 255, 255)} + ); + } + }); + + for circle in &mut self.circles { + (*circle).apply_force(&ctx.used_rect()); + } + + // Naive N^2 Colition detection + // Optimization https://en.wikipedia.org/wiki/Sweep_and_prune + for i in 0..self.circles_count { + for j in i+1..self.circles_count { + + if (i + j) % 3 == 0 { + continue; // skip collsions for every third ball + } + + let dc = self.circles[i].c - self.circles[j].c; + let dr = self.circles[i].r + self.circles[j].r; + + if dc.length() < dr { + (self.circles[i].v, self.circles[j].v) = collision(&self.circles[i], &self.circles[j]); + } + } + } + + // This is how to go into continuous mode - uncomment this to see example of continuous mode + ctx.request_repaint(); + } +} + +fn main() -> eframe::Result<()> { + let native_options = eframe::NativeOptions { + viewport: egui::ViewportBuilder::default().with_inner_size((800.0, 600.0)), + ..eframe::NativeOptions::default() + }; + + eframe::run_native( + ExampleApp::name(), + native_options, + Box::new(|_| Box::::default()), + ) +} + +fn collision(c1: &Circle, c2: &Circle) -> (Vec2, Vec2) { + let m1 = c1.r; + let m2 = c2.r; + + // collision normal + let n = Vec2{ + x: c2.c.x - c1.c.x, + y: c2.c.y - c1.c.y + }; + + // normal vector unit + let un = n.normalized(); + + // collision tangen + let ut = Vec2{x: -un.y, y: un.x}; + + // 3 + let v1n = un.dot(c1.v); + let v1t = ut.dot(c1.v); + + let v2n = un.dot(c2.v); + let v2t = ut.dot(c2.v); + + // 4 + let v1t_new = v1t; + let v2t_new = v2t; + + // 5 + let v1n_new = (v1n * (m1 - m2) + 2.0 * m2 * v2n) / (m1 + m2); + let v2n_new = (v2n * (m2 - m1) + 2.0 * m1 * v1n) / (m1 + m2); + + // 6 + let vec1n = v1n_new * un; + let vec1t = v1t_new * ut; + + let vec2n = v2n_new * un; + let vec2t = v2t_new * ut; + + return ( + vec1n + vec1t, + vec2n + vec2t + ); +} \ No newline at end of file