// Some notes on terminology as used in this project: // // Body frame (or body-fixed frame): // - A coordinate frame that moves and rotates with the body. // - The origin of the body is fixed at the origin of this frame, and points on the body // have constant coordinates relative to the origin. For example, the ship's nose // is always at a fixed distance along the x-axis in this frame. // // Local frame (or co-moving frame): // - A coordinate frame that moves with the object's position (e.g., center of mass) // but does not rotate with it. // - Points on the body may rotate relative to this frame as the object rotates in space. // // World frame (or inertial frame): // - A fixed coordinate frame that does not move or rotate with the body. // - All positions and orientations of the object are expressed relative to this absolute frame. // // This should be consistent with standard terminology. use std::f64; use std::collections::HashSet; use wasm_bindgen::prelude::*; use wasm_bindgen::JsCast; use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement}; use web_sys::console; mod cga; use cga::CGA; const PI: f64 = f64::consts::PI; const TWO_PI: f64 = 2.*PI; const ERROR: f64 = 0.0001; const WIDTH: f64 = 600.; const HEIGHT: f64 = 600.; const CENTER_X: f64 = 300.; const CENTER_Y: f64 = 300.; const RADIUS: f64 = 250.; const BACKGROUND: &str = "#ffffff"; const BLACK: &str = "#000000"; const RED: &str = "#ff0000"; const GREEN: &str = "#00ff00"; const BLUE: &str = "#0000ff"; const SCALAR: usize = 0; const E1: usize = 1; const E2: usize = 2; //const E3: usize = 3; //const E4: usize = 4; //const E5: usize = 5; const E12: usize = 6; const E13: usize = 7; const E14: usize = 8; const E15: usize = 9; const E23: usize = 10; const E24: usize = 11; const E25: usize = 12; const E34: usize = 13; const E35: usize = 14; const E45: usize = 15; //const E123: usize = 16; //const E124: usize = 17; //const E125: usize = 18; //const E134: usize = 19; //const E135: usize = 20; //const E145: usize = 21; //const E234: usize = 22; //const E235: usize = 23; //const E245: usize = 24; //const E345: usize = 25; //const E1234: usize = 26; //const E1235: usize = 27; //const E1245: usize = 28; //const E1345: usize = 29; //const E2345: usize = 30; //const E12345: usize = 31; const DRAG: f64 = 1.; const ACCEL_STR: f64 = 1.; struct Ship { com_rotor: CGA, verts: (CGA, CGA, CGA), vel: CGA, orientation: CGA, } impl Ship { fn update(&mut self, dt_s: f64, keys: &HashSet) { let vel = self.vel.clone(); //Velocity at beginning of frame let mut vel_2 = self.vel.clone(); let mut a_vec = - DRAG * &vel_2; if keys.contains("w") { let orient = &self.orientation; //Transforms body frame to local frame let pos = &self.com_rotor; //Transforms local frame to world frame let accel_body = ACCEL_STR * CGA::e15(); //The acceleration in the body frame. let accel_local = orient * accel_body * orient.Reverse(); //The acceleration in the local frame. let accel_world = pos * accel_local * pos.Reverse(); //The acceleration in the world frame. a_vec = a_vec - accel_world; //Sum of drag and thrust. } if keys.contains("q") { self.orientation = bivector_exponential(&(dt_s * CGA::e12())) * &self.orientation; } if keys.contains("e") { self.orientation = bivector_exponential(&(-dt_s * CGA::e12())) * &self.orientation; } vel_2 = vel_2 + a_vec*dt_s; //Velocity at end of frame let vel_3 = 0.5*(vel+&vel_2); let delta = vel_3 * dt_s; let pos = bivector_exponential(&delta)*&self.com_rotor; //Update position. self.vel = vel_2; self.com_rotor = pos; } fn draw(&self, context: &CanvasRenderingContext2d) { let origin = gen_hyperbolic_point(&CGA::zero()); //Origin in the body frame. let orient_rotor = &self.orientation; //Transforms body frame to local frame. let com = &self.com_rotor; //Transforms local frame to world frame. let v0_rotor = com*orient_rotor*bivector_exponential(&self.verts.0); //Combined transformation to locate a vertex of the ship in the world frame. let v1_rotor = com*orient_rotor*bivector_exponential(&self.verts.1); //Combined transformation ... let v2_rotor = com*orient_rotor*bivector_exponential(&self.verts.2); //Combined transformation ... let p0 = &v0_rotor*&origin*&v0_rotor.Reverse(); //Produce a vertex of the ship in the world frame. let p1 = &v1_rotor*&origin*&v1_rotor.Reverse(); //... let p2 = &v2_rotor*&origin*&v2_rotor.Reverse(); //... draw_point(context, &(com*&origin*com.Reverse())); draw_point(context, &p0); draw_point(context, &p1); draw_point(context, &p2); draw_line_between(context, &p0, &p1); draw_line_between(context, &p1, &p2); draw_line_between(context, &p2, &p0); //Construct and draw line pointing straight ahead, to help orient the player. let com_p = com * gen_hyperbolic_point(&CGA::zero()) * com.Reverse(); let delta_gen = com * orient_rotor * CGA::e14() * &orient_rotor.Reverse() * com.Reverse(); let dot = delta_gen | &com_p; let line = CGA::e4() ^ com_p ^ dot ^ CGA::e3(); draw_line(context, &line); } } enum Geometry { Circle(f64, f64, f64), //x,y,r Line(f64, f64, f64), //x,r,theta Null, } /* Translation of a Euclidean Point. Need to use hyperbolic*/ fn point_to_cga(x: f64, y: f64) -> CGA { let x_vec = CGA::e1(); let y_vec = CGA::e2(); let _z_vec =CGA::e3(); //This game assumes everything takes place in the plane. let e_vec = CGA::e4(); let e_bar = CGA::e5(); let n_vec = &e_vec + &e_bar; //Null vector for the point at infinity. let n_bar = &e_vec - &e_bar; //Null vector for the origin. let mut ux = x; let mut uy = y; let mut mag_sqr = ux*ux + uy*uy; if mag_sqr > 1. { ux /= mag_sqr.sqrt(); uy /= mag_sqr.sqrt(); mag_sqr = 1.; } 0.5*mag_sqr*n_vec + ux*x_vec + uy*y_vec - 0.5*n_bar //F(x) = 1/2(x^2 n + 2x - nbar) = -1/2(x-e)n(x-e) } fn point_to_screen_space(x: f64, y: f64) -> (f64, f64) { let screen_x = x*RADIUS + CENTER_X; let screen_y = y*RADIUS + CENTER_Y; (screen_x, screen_y) } fn draw_point(context: &CanvasRenderingContext2d, point: &CGA) { //Naively assume we are given a valid point let (x,y) = get_hyperbolic_point(point); let (x,y) = point_to_screen_space(x, y); context.begin_path(); context.set_fill_style_str(RED); context.arc(x, y, 2., 0., TWO_PI).unwrap(); context.fill(); let (x,y) = get_point(point); let (x,y) = point_to_screen_space(x,y); context.begin_path(); context.set_fill_style_str(BLUE); context.arc(x, y, 2., 0., TWO_PI).unwrap(); context.fill(); } //TODO Integrate into get geometry fn get_hyperbolic_point(point: &CGA) -> (f64, f64) { let n_bar = CGA::e4() - CGA::e5(); //Construct r hat dir for the point. let mut x = point[E1]; let mut y = point[E2]; let mag_sqr = x*x + y*y; if mag_sqr > ERROR { x /= mag_sqr.sqrt(); y /= mag_sqr.sqrt(); //(x,y) is a unit vector pointing in the direction of our point } else { //mag_sqr is too small to be confident in our vector. //Assume vector is at the origin. return (0., 0.); } //A normalized hyperbolic point in the full CGA satisfies p dot e4 = -1, //where e4 is the positive squaring conformal vector. let norm = -(point | CGA::e4())[SCALAR]; //Get the normalize component of the n term. Note n dot n = 0 and n dot n_bar = 2 let n_term = (0.5/norm) * (point | &n_bar)[SCALAR]; //Solve for r^2 let r_sqr = n_term/(1.+n_term); let r = r_sqr.sqrt(); (r*x,r*y) } fn get_point(point: &CGA) -> (f64, f64) { let x = point[E1]; let y = point[E2]; let n = CGA::e4()+CGA::e5(); let norm = -(point | &n)[SCALAR]; (x/norm, y/norm) } fn get_geometry(circle: &CGA) -> Geometry { //Naively assume we are given a valid line/circle let n_bar = CGA::e4() - CGA::e5(); let n_vec = CGA::e4() + CGA::e5(); let dual = (&circle).Dual(); let l = (&dual | &n_vec)[SCALAR]; let c = (&dual | &n_bar)[SCALAR]; let a = (&dual | CGA::e1())[SCALAR]; let b = (&dual | CGA::e2())[SCALAR]; if l.abs() < ERROR { //LINE let theta = (-a).atan2(b); let norm_sqr = a*a + b*b; if norm_sqr < ERROR*ERROR { return Geometry::Null; } let x = a*c/norm_sqr/2.; let y = b*c/norm_sqr/2.; return Geometry::Line(x,y,theta); } else { //Circle let c_x = a/l; let c_y = b/l; let r_sqr = c_x*c_x + c_y*c_y + c/l; let r = r_sqr.sqrt(); return Geometry::Circle(-c_x, -c_y, r); } } fn draw_line(context: &CanvasRenderingContext2d, line: &CGA) { context.begin_path(); context.set_stroke_style_str(GREEN); let geometry = get_geometry(line); match geometry { Geometry::Circle(x,y,r) => { let (canvas_x, canvas_y) = point_to_screen_space(x, y); let canvas_r = r*RADIUS; context.arc(canvas_x, canvas_y, canvas_r, 0., TWO_PI).unwrap(); }, Geometry::Line(x,y,theta) => { let (p1x, p1y) = (x - theta.cos(), y - theta.sin()); let (p2x, p2y) = (x + theta.cos(), y + theta.sin()); let (canvas_x1, canvas_y1) = point_to_screen_space(p1x, p1y); let (canvas_x2, canvas_y2) = point_to_screen_space(p2x, p2y); //draw_point(&context, &point_to_cga(x,y)); context.move_to(canvas_x1, canvas_y1); context.line_to(canvas_x2, canvas_y2); }, Geometry::Null => {}, } context.stroke(); } fn draw_line_between(context: &CanvasRenderingContext2d, a: &CGA, b: &CGA) { let line = CGA::e4() ^ a ^ b ^ CGA::e3(); let geometry = get_geometry(&line); context.begin_path(); context.set_stroke_style_str(GREEN); match geometry { Geometry::Circle(x,y,r) => { let (a_x, a_y) = get_point(a); let (b_x, b_y) = get_point(b); let mut theta_a = (a_y-y).atan2(a_x-x); let mut theta_b = (b_y-y).atan2(b_x-x); if theta_a < 0. { theta_a += TWO_PI; } if theta_b < 0. { theta_b += TWO_PI; } let mut delta = theta_b - theta_a; if delta < -PI { delta += TWO_PI; } else if delta > PI { delta -= TWO_PI; } let (x,y) = point_to_screen_space(x,y); let r = RADIUS*r; if delta < 0. { if theta_a + delta < 0. { context.arc(x, y, r, 0., theta_a).unwrap(); context.arc(x, y, r, theta_a + delta + TWO_PI, TWO_PI).unwrap(); } else { context.arc(x, y, r, theta_a + delta, theta_a).unwrap(); } } else { if theta_a + delta > TWO_PI { context.arc(x, y, r, theta_a, TWO_PI).unwrap(); context.arc(x, y, r, 0., theta_a + delta - TWO_PI).unwrap(); } else { context.arc(x, y, r, theta_a, theta_a + delta).unwrap(); } } }, _ => {}, } context.stroke(); } fn _get_hyperbolic_translation(vec: &CGA) -> CGA { let vec_sqr = (vec * vec)[SCALAR]; if vec_sqr >= 1. { panic!("Translation Vector Out of Bounds"); } let t = 1.0 / ( 1. - vec_sqr).sqrt() * ( CGA::new(1., SCALAR) + CGA::e5()*vec); t } fn gen_hyperbolic_point(vec: &CGA) -> CGA { let vec_sqr = (vec * vec)[SCALAR]; if vec_sqr >= 1. { panic!("Vector out of bounds"); } let n = CGA::e4() + CGA::e5(); let p = 1. / (1. - vec_sqr)*(vec_sqr*n/*CGA::new(vec_sqr, SCALAR)*/ + 2.*vec - (CGA::e4() - CGA::e5())); p } fn bivector_exponential(bivector: &CGA) -> CGA { //Explicitly take only the bivector part. //The library doesn't provide a grade 2 selection. let a = bivector[E12]; let b = bivector[E13]; let c = bivector[E14]; let d = bivector[E15]; let e = bivector[E23]; let f = bivector[E24]; let g = bivector[E25]; let h = bivector[E34]; let i = bivector[E35]; let j = bivector[E45]; let bivector = a*CGA::e12() + b*CGA::e13() + c*CGA::e14() + d*CGA::e15() + e*CGA::e23() + f*CGA::e24() + g*CGA::e25() + h*CGA::e34() + i *CGA::e35() + j*CGA::e45(); let sqr = (&bivector*&bivector)[SCALAR]; if sqr.abs() < ERROR { CGA::new(1., SCALAR) + bivector } else if sqr < 0. { let theta = sqr.abs().sqrt(); CGA::new(theta.cos(), SCALAR) + theta.sin()/theta*bivector //Division by theta to normalize bivector } else { let t = sqr.sqrt(); CGA::new(t.cosh(), SCALAR) + t.sinh()/t*bivector //Division by t to normalize the bivector } } struct Asteroid { circle: CGA, vel: CGA, } impl Asteroid { fn new() -> Asteroid { let v = 0.1; let (v_x, v_y) = (v * 1.0_f64.cos(), v*1.0_f64.sin()); let vel = v_x*CGA::e15() + v_y*CGA::e25(); Self::new_from_coords(0.0_f64, 0.0_f64, 0.1_f64, vel) } fn new_from_coords(x: f64, y: f64, r: f64, vel: CGA) -> Asteroid { let (a_x, a_y) = (x + r * 0.0_f64.cos(), y + r * 0.0_f64.sin()); let (b_x, b_y) = (x + r * 1.0_f64.cos(), y + r * 1.0_f64.sin()); let (c_x, c_y) = (x + r * 2.0_f64.cos(), y + r * 2.0_f64.sin()); let a = point_to_cga(a_x, a_y); let b = point_to_cga(b_x, b_y); let c = point_to_cga(c_x, c_y); let circle = a^b^c^CGA::e3(); Asteroid { circle, vel } } fn update(&mut self, dt_s: f64) { let vel = &self.vel; let delta = vel * dt_s; let vel_rotor = bivector_exponential(&delta); let circle = &vel_rotor*&self.circle*&vel_rotor.Reverse(); self.circle = circle; } fn draw(&self, context: &CanvasRenderingContext2d) { draw_line(context, &self.circle); } } #[wasm_bindgen] pub struct Game { context: CanvasRenderingContext2d, ship: Ship, asteroids: Vec::, keys: HashSet, } #[wasm_bindgen] impl Game { #[wasm_bindgen(constructor)] pub fn new(canvas_id: &str) -> Self { let window = web_sys::window().unwrap(); let document = window.document().unwrap(); let canvas = document .get_element_by_id(canvas_id) .ok_or_else( || JsValue::from_str("Canvas not found")).unwrap() .dyn_into::().unwrap(); canvas.set_width(WIDTH as u32); canvas.set_height(HEIGHT as u32); let context = canvas .get_context("2d").unwrap() .ok_or_else( || JsValue::from_str("Failed to get context")).unwrap() .dyn_into::().unwrap(); let tx_gen = CGA::e15(); let ty_gen = CGA::e25(); let ship = Ship { com_rotor: CGA::new(1., SCALAR), verts: (-0.1*&tx_gen, 0.05*&tx_gen+0.02*&ty_gen, 0.05*&tx_gen-0.02*ty_gen), vel: CGA::zero(), orientation: CGA::new(1., SCALAR), }; Game { context: context, ship, asteroids: vec![Asteroid::new()], keys: HashSet::new(), } } pub fn draw(&self) { self.context.begin_path(); self.context.set_fill_style_str(BACKGROUND); self.context.fill_rect(0.0, 0.0, WIDTH, HEIGHT); self.context.fill(); self.context.set_stroke_style_str(BLACK); self.context.arc(CENTER_X, CENTER_Y, RADIUS, 0., TWO_PI).unwrap(); self.context.stroke(); self.ship.draw(&self.context); for asteroid in &self.asteroids { asteroid.draw(&self.context); } } pub fn update(&mut self, dt_m: f64) { let dt_s = if dt_m > 100. { 0.1 } else { dt_m / 1000. }; for asteroid in &mut self.asteroids { asteroid.update(dt_s); } self.ship.update(dt_s, &self.keys); } pub fn key_down(&mut self, key: String) { self.keys.insert(key); } pub fn key_up(&mut self, key: String) { self.keys.remove(&key); } } // Start entry point #[wasm_bindgen(start)] pub fn start() -> Result<(), JsValue> { // let _ship = Ship { _pos: point_to_cga(0.,0.), _vel: CGA::zero(), _orientation: CGA::zero() }; /* // Grab document and canvas let window = web_sys::window().unwrap(); let document = window.document().unwrap(); let canvas = document .get_element_by_id("game-canvas") .unwrap() .dyn_into::()?; canvas.set_width(WIDTH as u32); canvas.set_height(HEIGHT as u32); let context = canvas .get_context("2d")? .unwrap() .dyn_into::().unwrap(); //Set up play area. context.begin_path(); context.set_fill_style_str(BACKGROUND); context.fill_rect(0.0, 0.0, WIDTH, HEIGHT); context.fill(); context.set_stroke_style_str(BLACK); context.arc(CENTER_X, CENTER_Y, RADIUS, 0., TWO_PI).unwrap(); context.stroke(); */ /* //Testing example lines and circles. let point = point_to_cga(0.,0.); draw_point(&context, &point); let p1 = point_to_cga(0.9, 0.0); draw_point(&context, &p1); let p2 = point_to_cga(0.0, 0.9); draw_point(&context, &p2); let p3 = point_to_cga(0.7, 0.6); draw_point(&context, &p3); let circle = &p1 ^ &p2 ^ &p3 ^ &CGA::e3(); //wedge e3 to force into the plane draw_line(&context, &circle); let p1 = point_to_cga(0.9, 0.2); draw_point(&context, &p1); let p2 = point_to_cga(-0.9, -0.); draw_point(&context, &p2); let p3 = point_to_cga(0., 0.1); draw_point(&context, &p3); let circle = &p1 ^ &p2 ^ &p3 ^ &CGA::e3(); draw_line(&context, &circle); let p1 = point_to_cga(0.9, 0.1); draw_point(&context, &p1); let p2 = point_to_cga(-0.9, -0.1); draw_point(&context, &p2); let p3 = point_to_cga(0., 0.0); draw_point(&context, &p3); let circle = &p1 ^ &p2 ^ &p3 ^ &CGA::e3(); draw_line(&context, &circle); let p1 = point_to_cga(0.9, 0.); draw_point(&context, &p1); let p2 = point_to_cga(0.9, -0.1); draw_point(&context, &p2); let circle = &p1 ^ &p2 ^ &CGA::e4() ^ &CGA::e3(); //Wedge e4 to create a circle through p1 and p1 that is tangent to the unit circle. draw_line(&context, &circle); let p1 = point_to_cga(0.9, 0.); draw_point(&context, &p1); let p2 = point_to_cga(0.9, -0.1); draw_point(&context, &p2); let circle = &p1 ^ &p2 ^ &CGA::e5() ^ &CGA::e3(); //Orthogonal to the line at infinity draw_line(&context, &circle);*/ Ok(()) }