aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/lib.rs62
-rw-r--r--src/line.rs22
-rw-r--r--src/point.rs50
-rw-r--r--src/state.rs190
4 files changed, 322 insertions, 2 deletions
diff --git a/src/lib.rs b/src/lib.rs
index 66bafe7..223e28a 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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();
+ }
+ }
+}