From 0c6d978442b244ca3f29c1ffdd44b5007ae7ad93 Mon Sep 17 00:00:00 2001 From: Ilion Beyst Date: Wed, 29 Dec 2021 21:24:57 +0100 Subject: separate out visualizer library --- web/planetwars-rs/src/lib.rs | 373 +++++++++++++++++++++++++++++++++++++++++ web/planetwars-rs/src/types.rs | 45 +++++ web/planetwars-rs/src/utils.rs | 65 +++++++ 3 files changed, 483 insertions(+) create mode 100644 web/planetwars-rs/src/lib.rs create mode 100644 web/planetwars-rs/src/types.rs create mode 100644 web/planetwars-rs/src/utils.rs (limited to 'web/planetwars-rs/src') diff --git a/web/planetwars-rs/src/lib.rs b/web/planetwars-rs/src/lib.rs new file mode 100644 index 0000000..f2ba7e1 --- /dev/null +++ b/web/planetwars-rs/src/lib.rs @@ -0,0 +1,373 @@ +extern crate serde; +#[macro_use] +extern crate serde_derive; +extern crate octoon_math; +extern crate serde_json; +extern crate voronoi; + +use octoon_math::Mat3; +use voronoi::{make_polygons, voronoi, Point}; + +mod types; +mod utils; + +use std::collections::HashMap; +use wasm_bindgen::prelude::*; + +macro_rules! console_log { + // Note that this is using the `log` function imported above during + // `bare_bones` + ($($t:tt)*) => (log(&format_args!($($t)*).to_string())) +} + +// When the `wee_alloc` feature is enabled, use `wee_alloc` as the global +// allocator. +#[cfg(feature = "wee_alloc")] +#[global_allocator] +static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; + +#[derive(Debug, Clone)] +pub struct Circle { + r: f32, + x: f32, + y: f32, + a0: f32, + ad: f32, + distance: usize, +} + +use std::f32::consts::PI; +fn spr(from: f32) -> f32 { + let pi2 = PI * 2.; + ((from % pi2) + pi2) % pi2 +} + +impl Circle { + pub fn new(p1: &types::Planet, p2: &types::Planet) -> Self { + let x1 = p1.x; + let y1 = p1.y; + let x2 = p2.x; + let y2 = p2.y; + + // Distance between planets + let q = ((x2 - x1).powi(2) + (y2 - y1).powi(2)).sqrt(); + // Center of between planets + let x3 = (x1 + x2) / 2.0; + let y3 = (y1 + y2) / 2.0; + + // Radius of circle + let r = q * 1.0; + + // Center of circle + let x = x3 + (r.powi(2) - (q / 2.0).powi(2)).sqrt() * (y1 - y2) / q; + let y = y3 + (r.powi(2) - (q / 2.0).powi(2)).sqrt() * (x2 - x1) / q; + // console_log!("{},{} -> {},{} ({},{} r={})", x1, y1, x2, y2, x, y, r); + + let a0 = spr((y - y1).atan2(x - x1)); + let a2 = spr((y - y2).atan2(x - x2)); + + let mut ad = spr(a0 - a2); + if ad > PI { + ad = spr(a2 - a0); + } + // console_log!("a1 {} a2 {} ad {}", a0/PI * 180.0, a2/PI * 180.0, ad/PI*180.0); + + let distance = q.ceil() as usize + 1; + Self { + r, + x, + y, + a0, + ad, + distance, + } + } + + pub fn get_for_remaining(&self, remaining: usize) -> ((Mat3, f32), (Mat3, f32)) { + ( + self.get_remaining(remaining), + self.get_remaining((remaining + 1).min(self.distance - 1)), + ) + } + + fn get_remaining(&self, remaining: usize) -> (Mat3, f32) { + let alpha = self.a0 + (1.0 - (remaining as f32 / self.distance as f32)) * self.ad; + + let cos = alpha.cos(); + let sin = alpha.sin(); + ( + Mat3::new( + 0.3, + 0.0, + 0.0, + 0.0, + 0.3, + 0.0, + -self.x + cos * self.r, + -self.y + sin * self.r, + 0.3, + ), + alpha, + ) + } +} + +fn create_voronoi(planets: &Vec, bbox: f32) -> (Vec, Vec) { + let mut verts: Vec<[f32; 2]> = planets.iter().map(|p| [p.x, p.y]).collect(); + let mut ids = Vec::new(); + + let vor_points = planets + .iter() + .map(|p| Point::new(p.x as f64, p.y as f64)) + .collect(); + + let vor = voronoi(vor_points, bbox as f64); + let vor = make_polygons(&vor); + + for poly in vor.iter() { + // Get planet index for planet that is inside this poligon + let idx = 0; + + let mut prev = ids.len() + poly.len() - 1; + for p in poly.iter() { + let now = verts.len(); + verts.push([p.x.0 as f32, p.y.0 as f32]); + + ids.push(idx); + ids.push(now); + ids.push(prev); + prev = now; + } + } + + (verts.concat(), ids) +} + +#[wasm_bindgen] +pub struct Game { + states: Vec, + turn: usize, + + planet_map: HashMap<(String, String), Circle>, + + /* put extra shit here */ + view_box: Vec, + + planets: Vec, + planet_ships: Vec, + + ship_locations: Vec, + ship_label_locations: Vec, + ship_colours: Vec, + ship_counts: Vec, + + current_planet_colours: Vec, + + voronoi_vertices: Vec, + voronoi_colors: Vec, + voronoi_indices: Vec, +} + +#[wasm_bindgen] +impl Game { + pub fn new(file: &str) -> Self { + utils::set_panic_hook(); + + // First line is fucked but we just filter out things that cannot parse + let states: Vec = file + .split("\n") + .filter_map(|line| serde_json::from_str(line).ok()) + .collect(); + + let mut planet_map = HashMap::new(); + + // Iterator? + for p1 in states[0].planets.iter() { + for p2 in states[0].planets.iter() { + planet_map.insert((p1.name.clone(), p2.name.clone()), Circle::new(&p1, &p2)); + } + } + let view_box = utils::caclulate_viewbox(&states[0].planets); + + let (voronoi_vertices, voronoi_indices) = + create_voronoi(&states[0].planets, view_box[2].max(view_box[3])); + + let voronoi_colors: Vec = voronoi_indices + .iter() + .map(|_| [0.0, 0.0, 0.0]) + .collect::>() + .concat(); // Init these colours on black + + Self { + planets: utils::get_planets(&states[0].planets, 2.0), + planet_ships: Vec::new(), + view_box, + + planet_map, + turn: 0, + states, + ship_locations: Vec::new(), + ship_label_locations: Vec::new(), + ship_colours: Vec::new(), + ship_counts: Vec::new(), + current_planet_colours: Vec::new(), + + voronoi_vertices, + voronoi_indices, + voronoi_colors, + } + } + + pub fn push_state(&mut self, state_str: &str) { + if let Ok(state) = serde_json::from_str(state_str) { + self.states.push(state); + } + } + + pub fn get_viewbox(&self) -> Vec { + self.view_box.clone() + } + + pub fn get_planets(&self) -> Vec { + self.planets.clone() + } + + pub fn get_planet_ships(&self) -> Vec { + self.planet_ships.clone() + } + + pub fn get_planet_colors(&self) -> Vec { + self.current_planet_colours.clone() + } + + pub fn turn_count(&self) -> usize { + self.states.len() + } + + pub fn update_turn(&mut self, turn: usize) -> usize { + self.turn = turn.min(self.states.len() - 1); + + self.update_planet_ships(); + self.update_planet_colours(); + self.update_voronoi_colors(); + self.update_ship_locations(); + self.update_ship_counts(); + + self.turn + } + + fn update_planet_ships(&mut self) { + self.planet_ships = self.states[self.turn] + .planets + .iter() + .map(|p| p.ship_count as usize) + .collect(); + } + + fn update_voronoi_colors(&mut self) { + for (i, p) in self.states[self.turn].planets.iter().enumerate() { + let color = utils::COLORS[p.owner.unwrap_or(0) as usize % utils::COLORS.len()]; + self.voronoi_colors[i * 3 + 0] = color[0]; + self.voronoi_colors[i * 3 + 1] = color[1]; + self.voronoi_colors[i * 3 + 2] = color[2]; + } + } + + fn update_planet_colours(&mut self) { + let mut new_vec: Vec<[f32; 3]> = Vec::new(); + let planets_now = self.states[self.turn].planets.iter(); + let planets_later = self.states[(self.turn + 1).min(self.states.len() - 1)] + .planets + .iter(); + + for (p1, p2) in planets_now.zip(planets_later) { + new_vec + .push(utils::COLORS[p1.owner.unwrap_or(0) as usize % utils::COLORS.len()].into()); + new_vec + .push(utils::COLORS[p2.owner.unwrap_or(0) as usize % utils::COLORS.len()].into()); + } + + self.current_planet_colours = new_vec.concat::(); + } + + fn update_ship_locations(&mut self) { + let mut new_sl = Vec::new(); + let mut new_sll = Vec::new(); + + let t = Mat3::new(0.2, 0., 0., 0., 0.2, 0.0, 0., -0.5, 0.2); + + for ship in self.states[self.turn].expeditions.iter() { + let ((o1, a1), (o2, a2)) = self + .planet_map + .get(&(ship.origin.clone(), ship.destination.clone())) + .unwrap() + .get_for_remaining(ship.turns_remaining as usize); + new_sl.push((o1 * Mat3::rotate_z(a1)).to_array()); + new_sl.push((o2 * Mat3::rotate_z(a2)).to_array()); + + new_sll.push((o1 + t).to_array()); + new_sll.push((o2 + t).to_array()); + } + + self.ship_locations = new_sl.concat(); + self.ship_label_locations = new_sll.concat(); + + self.ship_colours = self.states[self.turn] + .expeditions + .iter() + .map(|s| utils::COLORS[s.owner as usize % utils::COLORS.len()]) + .collect::>() + .concat(); + } + + fn update_ship_counts(&mut self) { + self.ship_counts = self.states[self.turn] + .expeditions + .iter() + .map(|s| s.ship_count as usize) + .collect(); + } + + pub fn get_max_ships(&self) -> usize { + self.states + .iter() + .map(|s| s.expeditions.len()) + .max() + .unwrap() + } + + pub fn get_ship_locations(&self) -> Vec { + self.ship_locations.clone() + } + + pub fn get_ship_label_locations(&self) -> Vec { + self.ship_label_locations.clone() + } + + pub fn get_ship_colours(&self) -> Vec { + self.ship_colours.clone() + } + + pub fn get_ship_counts(&self) -> Vec { + self.ship_counts.clone() + } + + pub fn get_voronoi_verts(&self) -> Vec { + self.voronoi_vertices.clone() + } + + pub fn get_voronoi_colours(&self) -> Vec { + self.voronoi_colors.clone() + } + + pub fn get_voronoi_inds(&self) -> Vec { + self.voronoi_indices.clone() + } +} + +#[wasm_bindgen] +extern "C" { + fn alert(s: &str); + #[wasm_bindgen(js_namespace = console)] + fn log(s: &str); +} diff --git a/web/planetwars-rs/src/types.rs b/web/planetwars-rs/src/types.rs new file mode 100644 index 0000000..2d7d8c0 --- /dev/null +++ b/web/planetwars-rs/src/types.rs @@ -0,0 +1,45 @@ +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Expedition { + pub id: u64, + pub ship_count: u64, + pub origin: String, + pub destination: String, + pub owner: u64, + pub turns_remaining: u64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Planet { + pub ship_count: u64, + pub x: f32, + pub y: f32, + pub owner: Option, + pub name: String, +} + +use std::hash::{Hash, Hasher}; +use std::mem; + +impl Hash for Planet { + fn hash(&self, state: &mut H) { + unsafe { + let x: u32 = mem::transmute_copy(&self.x); + let y: u32 = mem::transmute_copy(&self.y); + state.write_u32(x); + state.write_u32(y); + } + } +} + +impl PartialEq for Planet { + fn eq(&self, other: &Self) -> bool { + (self.x - other.x).abs() < 0.0001 && (self.y - other.y).abs() < 0.0001 + } +} +impl Eq for Planet {} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct State { + pub planets: Vec, + pub expeditions: Vec, +} diff --git a/web/planetwars-rs/src/utils.rs b/web/planetwars-rs/src/utils.rs new file mode 100644 index 0000000..a903912 --- /dev/null +++ b/web/planetwars-rs/src/utils.rs @@ -0,0 +1,65 @@ +pub fn set_panic_hook() { + // When the `console_error_panic_hook` feature is enabled, we can call the + // `set_panic_hook` function at least once during initialization, and then + // we will get better error messages if our code ever panics. + // + // For more details see + // https://github.com/rustwasm/console_error_panic_hook#readme + #[cfg(feature = "console_error_panic_hook")] + console_error_panic_hook::set_once(); +} + +/// this is total extra, so it the planet viewbox is like 100px wide, it will now be in total 110 pixels wide +static VIEWBOX_SCALE: f32 = 0.1; + +pub static COLORS: [[f32; 3]; 10] = [ + [0.5, 0.5, 0.5], + [1.0, 0.50, 0.0], // #FF8000 + [0.0, 0.50, 1.0], // #0080ff + [1.0, 0.4, 0.58], // #FF6693 + [0.24, 0.79, 0.33], // #3fcb55 + [0.79, 0.76, 0.24], // #cbc33f + [0.81, 0.25, 0.91], // #cf40e9 + [0.94, 0.32, 0.32], // #FF3F0D + [0.11, 0.93, 0.94], // #1beef0 + [0.05, 0.77, 1.0], // #0DC5FF +]; + +use super::types; + +pub fn caclulate_viewbox(planets: &Vec) -> Vec { + let mut iter = planets.iter(); + + let init = match iter.next() { + Some(p) => (p.x, p.y, p.x, p.y), + None => return vec![0.0, 0.0, 0.0, 0.0], + }; + let (min_x, min_y, max_x, max_y) = + planets + .iter() + .fold(init, |(min_x, min_y, max_x, max_y), p| { + ( + min_x.min(p.x), + min_y.min(p.y), + max_x.max(p.x), + max_y.max(p.y), + ) + }); + + let (width, height) = (max_x - min_x, max_y - min_y); + let (dx, dy) = ( + (VIEWBOX_SCALE * width).max(6.0), + (VIEWBOX_SCALE * height).max(6.0), + ); + + vec![min_x - dx / 2.0, min_y - dy / 2.0, width + dx, height + dy] +} + +pub fn get_planets(planets: &Vec, r: f32) -> Vec { + planets.iter().fold(Vec::new(), |mut cum, p| { + cum.push(p.x); + cum.push(p.y); + cum.push(r); + cum + }) +} -- cgit v1.2.3