diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/lib.rs | 62 | ||||
| -rw-r--r-- | src/line.rs | 22 | ||||
| -rw-r--r-- | src/point.rs | 50 | ||||
| -rw-r--r-- | src/state.rs | 190 |
4 files changed, 322 insertions, 2 deletions
@@ -1,11 +1,69 @@ -use log::{Level, error, info}; +use std::rc::Rc; +use std::cell::RefCell; + +use log::{Level, info}; use wasm_bindgen::prelude::*; -use web_sys::js_sys; + +use state::State; + +mod line; +mod point; +mod state; + +struct RenderLoop { + animation_id: Option<i32>, + pub closure: Option<Closure<dyn FnMut(f64)>>, +} + +impl RenderLoop { + pub fn new() -> Self { + Self { + animation_id: None, + closure: None, + } + } +} + +fn window() -> web_sys::Window { + web_sys::window().expect("no window") +} #[wasm_bindgen(start)] pub fn init() -> Result<(), JsValue> { console_log::init_with_level(Level::Debug).expect("couldn't init console log"); info!("wasm init"); + let mut s = State::new()?; + s.render_frame(0.0)?; + + // + // from https://users.rust-lang.org/t/wasm-web-sys-how-to-use-window-request-animation-frame-resolved/20882 + // + + let render_loop: Rc<RefCell<RenderLoop>> = Rc::new(RefCell::new(RenderLoop::new())); + { + let closure: Closure<dyn FnMut(f64)> = { + let render_loop = render_loop.clone(); + Closure::wrap(Box::new(move |t| { + s.render_frame(t).expect("should render frame"); + if !s.paused { + let mut render_loop = render_loop.borrow_mut(); + render_loop.animation_id = if let Some(ref closure) = render_loop.closure { + Some(window().request_animation_frame(closure.as_ref().unchecked_ref()).expect("req anim frame")) + } else { + None + } + } + })) + }; + let mut render_loop = render_loop.borrow_mut(); + render_loop.animation_id = Some(window().request_animation_frame(closure.as_ref().unchecked_ref()).expect("req anim frame")); + render_loop.closure = Some(closure); + } + + // + // + // + Ok(()) } diff --git a/src/line.rs b/src/line.rs new file mode 100644 index 0000000..07a1f03 --- /dev/null +++ b/src/line.rs @@ -0,0 +1,22 @@ +use crate::point::Point; + +pub struct Line { + p1: Point, + p2: Point, +} + +impl Line { + pub fn new(p1: Point, p2: Point) -> Self { + Self { p1, p2 } + } + + pub fn vec(&self) -> Point { + Point::new(self.p2.x - self.p1.x, self.p2.y - self.p1.y) + } + + pub fn angle_between(&self, other: &Self) -> f64 { + let v1 = self.vec(); + let v2 = other.vec(); + (v1.dot(&v2) / (v1.mag() * v2.mag())).acos() + } +} diff --git a/src/point.rs b/src/point.rs new file mode 100644 index 0000000..76f9ced --- /dev/null +++ b/src/point.rs @@ -0,0 +1,50 @@ +use crate::state::{MAX_SPEED, TAU}; + +const POINT_RADIUS: f64 = 0.01; + +fn rand_color() -> String { + format!("rgb({} {} {})", fastrand::u8(0..255), fastrand::u8(0..255), fastrand::u8(0..255)) +} + +#[derive(Clone, Debug)] +pub struct Point { + pub x: f64, + pub y: f64, + pub radius: f64, + pub heading: f64, + pub speed: f64, + pub color: String, +} + +impl Point { + pub fn new(x: f64, y: f64) -> Self { + Self { + x, + y, + radius: POINT_RADIUS, + heading: fastrand::f64() * TAU, + speed: fastrand::f64() * MAX_SPEED, + color: rand_color(), + } + } + + pub fn equal_pos(&self, other: &Self) -> bool { + self.x == other.x && self.y == other.y + } + + pub fn dot(&self, other: &Self) -> f64 { + self.x * other.x + self.y * other.y + } + + pub fn mag(&self) -> f64 { + (self.x.powi(2) + self.y.powi(2)).sqrt() + } + + pub fn speed_x(&self) -> f64 { + self.speed * self.heading.cos() + } + + pub fn speed_y(&self) -> f64 { + self.speed * self.heading.sin() + } +} diff --git a/src/state.rs b/src/state.rs new file mode 100644 index 0000000..8941a3b --- /dev/null +++ b/src/state.rs @@ -0,0 +1,190 @@ +use std::f64::consts::PI; + +use log::debug; +use wasm_bindgen::prelude::*; +use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement, HtmlElement}; + +use crate::line::Line; +use crate::point::Point; + +pub const TAU: f64 = PI * 2.0; +pub const MAX_SPEED: f64 = 0.01; + +const NUM_POINTS: usize = 40; +const VELVEC_SCALE: f64 = 3.0; + +fn window() -> web_sys::Window { + web_sys::window().expect("no window") +} + +fn document() -> web_sys::Document { + window().document().expect("no document") +} + +fn canvas() -> Result<web_sys::HtmlCanvasElement, JsValue> { + let x = document() + .query_selector("canvas")? + .expect("canvas element"); + Ok(x.dyn_into::<web_sys::HtmlCanvasElement>()?) +} + +#[derive(Debug)] +pub struct State { + canvas: HtmlCanvasElement, + ctx: CanvasRenderingContext2d, + fps: HtmlElement, + points: Vec<Point>, + pub paused: bool, + last_time: Option<f64>, + inter_count: u16, +} + +impl State { + pub fn new() -> Result<Self, JsValue> { + debug!("State::new()"); + let canvas = canvas()?; + let ctx: CanvasRenderingContext2d = canvas + .get_context("2d")? + .expect("2d context on canvas") + .dyn_into()?; + ctx.scale(canvas.width().into(), canvas.height().into())?; + let fps = document().query_selector("#fps")?.expect("fps counter exists").dyn_into::<HtmlElement>().expect("is html element"); + + let points = (0..NUM_POINTS) + .map(|_| Point::new(fastrand::f64(), fastrand::f64())) + .collect(); + + Ok(Self { + canvas, + ctx, + fps, + points, + paused: false, + last_time: document().timeline().current_time(), + inter_count: 1, + }) + } + + pub fn render_frame(&mut self, t: f64) -> Result<(), JsValue> { + if let Some(last_time) = self.last_time { + if t > last_time { + let ic: f64 = self.inter_count.into(); + let val: f64 = (1_000.0 * ic / (t - last_time)).trunc(); + self.fps.set_text_content(Some(val.to_string().as_str())); + self.last_time = Some(t); + self.inter_count = 1; + } else { + self.inter_count += 1; + } + } + + self.ctx.clear_rect( + 0.0, + 0.0, + self.canvas.width().into(), + self.canvas.height().into(), + ); + self.render_points()?; + if self.bounce_points() { + //debug!("point bounced"); + } + self.move_points(); + + // poly finding assumes sorted + self.points.sort_by(|a, b| { + if a.y > b.y { + std::cmp::Ordering::Greater + } else if a.y == b.y { + std::cmp::Ordering::Equal + } else { + std::cmp::Ordering::Less + } + }); + let poly_points = self.find_poly(); + + self.ctx.set_line_width(0.005); + self.ctx.begin_path(); + self.ctx.move_to(poly_points[0].x, poly_points[0].y); + for p in poly_points { + self.ctx.line_to(p.x, p.y); + } + self.ctx.set_stroke_style_str("blue"); + self.ctx.stroke(); + + Ok(()) + } + + fn render_points(&self) -> Result<(), JsValue> { + for p in &self.points { + self.ctx.set_fill_style_str(&p.color); + self.ctx.begin_path(); + self.ctx.arc(p.x, p.y, p.radius, 0.0, TAU)?; + self.ctx.fill(); + + self.ctx.set_line_width(0.005); + self.ctx.set_stroke_style_str(&p.color); + self.ctx.begin_path(); + self.ctx.move_to(p.x, p.y); + self.ctx.line_to( + p.x + p.speed_x() * VELVEC_SCALE, + p.y + p.speed_y() * VELVEC_SCALE, + ); + self.ctx.stroke(); + } + Ok(()) + } + + fn find_poly(&self) -> Vec<Point> { + let mut last_line = Line::new(Point::new(0.0, 0.0), Point::new(1.0, 0.0)); + let mut xxx = 100; + let mut res = vec![self.points[0].clone()]; + loop { + let last = res.last().expect("something in res"); + let mut min_theta = TAU; + let mut min_p = &self.points[0]; + for p in &self.points { + if !last.equal_pos(p) { + let l2 = Line::new(last.clone(), p.clone()); + let theta = last_line.angle_between(&l2); + if theta < min_theta { + min_theta = theta; + min_p = p; + } + } + } + last_line = Line::new(res.last().expect("something in res").clone(), min_p.clone()); + res.push(min_p.clone()); + + xxx -= 1; + if xxx == 0 || res[0].equal_pos(res.last().expect("something in res")) { + break; + } + } + + res + } + + fn bounce_points(&mut self) -> bool { + let mut did_bounce = false; + for p in &mut self.points { + let x = p.x + p.speed_x(); + let y = p.y + p.speed_y(); + + if x < 0.0 || x >= 1.0 { + p.heading = PI - p.heading; + did_bounce = true; + } else if y < 0.0 || y >= 1.0 { + p.heading = -p.heading; + did_bounce = true; + } + } + did_bounce + } + + fn move_points(&mut self) { + for p in &mut self.points { + p.x += p.speed_x(); + p.y += p.speed_y(); + } + } +} |
