From 69591cc5483d36bc819c75dce9347b08b04e33bf Mon Sep 17 00:00:00 2001 From: brian cully Date: Sat, 27 Dec 2025 12:58:44 -0500 Subject: add rust/wasm impl --- src/state.rs | 190 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 src/state.rs (limited to 'src/state.rs') 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 { + let x = document() + .query_selector("canvas")? + .expect("canvas element"); + Ok(x.dyn_into::()?) +} + +#[derive(Debug)] +pub struct State { + canvas: HtmlCanvasElement, + ctx: CanvasRenderingContext2d, + fps: HtmlElement, + points: Vec, + pub paused: bool, + last_time: Option, + inter_count: u16, +} + +impl State { + pub fn new() -> Result { + 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::().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 { + 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(); + } + } +} -- cgit v1.3