diff --git a/.gitignore b/.gitignore index a1a5044..79dc8b0 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,5 @@ Cargo.lock # MSVC Windows builds of rustc generate these, which store debugging information *.pdb +# trunk output folder +dist diff --git a/Cargo.toml b/Cargo.toml index 8057ffe..0c627f4 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,42 @@ [package] name = "egui-circles" -version = "0.1.0" +version = "0.2.0" +authors = ["Andriy Djmil "] edition = "2021" -rust-version = "1.56" +rust-version = "1.76" + +[package.metadata.docs.rs] +all-features = true +targets = ["x86_64-unknown-linux-gnu", "wasm32-unknown-unknown"] [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 +egui = "0.28" +eframe = { version = "0.28", default-features = false, features = [ +# "accesskit", # Make egui compatible with screen readers. NOTE: adds a lot of dependencies. + "default_fonts", # Embed the default egui fonts. + "glow", # Use the glow rendering backend. Alternative: "wgpu". +# "persistence", # Enable restoring app state when restarting the app. +] } +log = "0.4" +emath = "0.28.0" +rand = "0.8" +getrandom = { version = "0.2", features = ["js"] } + +# native: +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +env_logger = "0.10" + +# web: +[target.'cfg(target_arch = "wasm32")'.dependencies] +wasm-bindgen-futures = "0.4" + +# to access the DOM (to hide the loading text) +[target.'cfg(target_arch = "wasm32")'.dependencies.web-sys] +version = "0.3.4" + +[profile.release] +opt-level = 2 # fast and small wasm + +# Optimize all dependencies even in debug builds: +[profile.dev.package."*"] +opt-level = 2 diff --git a/assets/favicon.ico b/assets/favicon.ico new file mode 100644 index 0000000..b205b42 Binary files /dev/null and b/assets/favicon.ico differ diff --git a/assets/icon-192.png b/assets/icon-192.png new file mode 100644 index 0000000..d8e9760 Binary files /dev/null and b/assets/icon-192.png differ diff --git a/assets/manifest.json b/assets/manifest.json new file mode 100644 index 0000000..f85e117 --- /dev/null +++ b/assets/manifest.json @@ -0,0 +1,17 @@ +{ + "name": "eGUI Circles", + "short_name": "circles", + "icons": [ + { + "src": "/icon-192.png", + "sizes": "192x192", + "type": "image/png" + } + ], + "lang": "en-US", + "id": "/index.html", + "start_url": "./index.html", + "display": "standalone", + "background_color": "white", + "theme_color": "white" +} \ No newline at end of file diff --git a/assets/sw.js b/assets/sw.js new file mode 100644 index 0000000..7ecd229 --- /dev/null +++ b/assets/sw.js @@ -0,0 +1,25 @@ +var cacheName = 'egui-template-pwa'; +var filesToCache = [ + './', + './index.html', + './eframe_template.js', + './eframe_template_bg.wasm', +]; + +/* Start the service worker and cache all of the app's content */ +self.addEventListener('install', function (e) { + e.waitUntil( + caches.open(cacheName).then(function (cache) { + return cache.addAll(filesToCache); + }) + ); +}); + +/* Serve cached content when offline */ +self.addEventListener('fetch', function (e) { + e.respondWith( + caches.match(e.request).then(function (response) { + return response || fetch(e.request); + }) + ); +}); diff --git a/index.html b/index.html new file mode 100644 index 0000000..a40b89c --- /dev/null +++ b/index.html @@ -0,0 +1,136 @@ + + + + + + + + + + egui Circles + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000..f936714 --- /dev/null +++ b/src/app.rs @@ -0,0 +1,172 @@ +extern crate emath; +extern crate rand; +extern crate eframe; + +use self::emath::Vec2; +use self::eframe::egui; +use self::egui::Color32; +use self::egui::Stroke; +use self::rand::Rng; + +use crate::circle::Circle; + +pub struct Simulation { + circles: Vec, + circles_count: usize, + + colors: Vec, +} + +impl Default for Simulation { + fn default() -> Self { + Self { + circles: Vec::new(), + circles_count: 2, + colors: Vec::new(), + } + } +} + +impl eframe::App for Simulation { + 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() { + 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 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 + ); +} + +//} diff --git a/src/circle.rs b/src/circle.rs index e0c892c..f4508f4 100644 --- a/src/circle.rs +++ b/src/circle.rs @@ -1,58 +1,58 @@ -pub mod circle { +extern crate emath; +extern crate rand; - use emath::Vec2; - use emath::Pos2; - use emath::Rect; - use rand::Rng; +use self::emath::Vec2; +use self::emath::Pos2; +use self::emath::Rect; +use self::rand::Rng; - pub static FRICTION: f32 = 0.995; +pub static FRICTION: f32 = 0.995; - pub struct Circle { - pub v: Vec2, - pub c: Pos2, - pub r: f32, - } +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 - } - } +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 index d2b0b9c..bc29e8f 100755 --- a/src/main.rs +++ b/src/main.rs @@ -1,182 +1,64 @@ -use emath::Vec2; -use eframe::egui; -use egui::Color32; -use egui::Stroke; -use rand::Rng; - +mod app; 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(); - } -} +// When compiling natively: +#[cfg(not(target_arch = "wasm32"))] fn main() -> eframe::Result<()> { + //env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`). + let native_options = eframe::NativeOptions { - viewport: egui::ViewportBuilder::default().with_inner_size((800.0, 600.0)), - ..eframe::NativeOptions::default() + viewport: eframe::egui::ViewportBuilder::default() + .with_inner_size([800.0, 600.0]) + .with_min_inner_size([300.0, 220.0]) + .with_icon( + // NOTE: Adding an icon is optional + eframe::icon_data::from_png_bytes(&include_bytes!("../assets/icon-192.png")[..]) + .expect("Failed to load icon"), + ), + ..Default::default() }; eframe::run_native( - ExampleApp::name(), + "egui-circles", native_options, - Box::new(|_| Box::::default()), + Box::new(|_| Ok(Box::::default())), ) } -fn collision(c1: &Circle, c2: &Circle) -> (Vec2, Vec2) { - let m1 = c1.r; - let m2 = c2.r; +// When compiling to web using trunk: +#[cfg(target_arch = "wasm32")] +fn main() { + // Redirect `log` message to `console.log` and friends: + eframe::WebLogger::init(log::LevelFilter::Debug).ok(); - // collision normal - let n = Vec2{ - x: c2.c.x - c1.c.x, - y: c2.c.y - c1.c.y - }; + let web_options = eframe::WebOptions::default(); - // normal vector unit - let un = n.normalized(); + wasm_bindgen_futures::spawn_local(async { + let start_result = eframe::WebRunner::new() + .start( + "the_canvas_id", + web_options, + //Box::new(|cc| Ok(Box::new(eframe_template::TemplateApp::new(cc)))), + Box::new(|_| Ok(Box::::default())), + ) + .await; - // 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 - ); + // Remove the loading text and spinner: + let loading_text = web_sys::window() + .and_then(|w| w.document()) + .and_then(|d| d.get_element_by_id("loading_text")); + if let Some(loading_text) = loading_text { + match start_result { + Ok(_) => { + loading_text.remove(); + } + Err(e) => { + loading_text.set_inner_html( + "

The app has crashed. See the developer console for details.

", + ); + panic!("Failed to start eframe: {e:?}"); + } + } + } + }); } \ No newline at end of file