diff --git a/.github/workflows/action.yml b/.github/workflows/action.yml index 92c10cf..ee43ea6 100644 --- a/.github/workflows/action.yml +++ b/.github/workflows/action.yml @@ -22,7 +22,7 @@ jobs: # wasm - run: cargo install wasm-pack - - run: wasm-pack build --target web + - run: wasm-pack build js: runs-on: ubuntu-latest steps: @@ -32,7 +32,7 @@ jobs: # wasm - run: cargo install wasm-pack - - run: wasm-pack build --target web + - run: wasm-pack build # js - run: npm ci diff --git a/src/js/index.html b/src/js/index.html index 4628a45..30cbf6a 100644 --- a/src/js/index.html +++ b/src/js/index.html @@ -1,12 +1,12 @@ - + galaxy-gen - - + + - +
diff --git a/src/js/lib/application.tsx b/src/js/lib/application.tsx index 8f0936b..ad7ef1b 100644 --- a/src/js/lib/application.tsx +++ b/src/js/lib/application.tsx @@ -6,9 +6,9 @@ import * as galaxy from "./galaxy"; const wasm = import("galaxy_gen_backend/galaxy_gen_backend"); export function Interface() { - const [galaxySize, setGalaxySize] = React.useState(100); - const [galaxySeedMass, setGalaxySeedMass] = React.useState(5); - const [minStarMass, setMinStarMass] = React.useState(1000); + const [galaxySize, setGalaxySize] = React.useState(50); + const [galaxySeedMass, setGalaxySeedMass] = React.useState(25); + const [timeModifier, setTimeModifier] = React.useState(0.01); let wasmModule: any = null; let galaxyFrontend: galaxy.Frontend = null; @@ -61,24 +61,24 @@ export function Interface() { ); - const handleMinStarMassChange = ( + const handletimeModifierChange = ( event: React.ChangeEvent ) => { const value = parseInt(event.target.value); - setMinStarMass(Number.isNaN(value) ? 0 : value); + setTimeModifier(Number.isNaN(value) ? 0 : value); }; - const minStarMassInput = ( + const timeModifierInput = (
- Min Star Mass: + Time Modifier:
); @@ -88,7 +88,7 @@ export function Interface() { console.error("wasm not yet loaded"); } else { console.log("initializing galaxy"); - galaxyFrontend = new galaxy.Frontend(galaxySize, minStarMass); + galaxyFrontend = new galaxy.Frontend(galaxySize, timeModifier); dataviz.initViz(galaxyFrontend); } }; @@ -103,7 +103,7 @@ export function Interface() { if (galaxyFrontend === null) { console.error("galaxy not yet initialized"); } else { - console.log("seeding galaxy"); + console.log("adding mass to the galaxy"); galaxyFrontend.seed(galaxySeedMass); dataviz.initData(galaxyFrontend); } @@ -116,7 +116,9 @@ export function Interface() { ); const handleTickClick = () => { - galaxyFrontend.tick(minStarMass); + console.log("advancing time"); + galaxyFrontend.tick(timeModifier); + dataviz.initData(galaxyFrontend); }; const tickButton = ( @@ -135,15 +137,13 @@ export function Interface() { {galaxySizeInput} {galaxySeedMassInput} - {minStarMassInput} + {timeModifierInput}
{initButton} {seedButton} {tickButton}
-
-
-
+
); } diff --git a/src/js/lib/dataviz.tsx b/src/js/lib/dataviz.tsx index e3f70b4..056678c 100644 --- a/src/js/lib/dataviz.tsx +++ b/src/js/lib/dataviz.tsx @@ -5,13 +5,12 @@ import * as galaxy from "./galaxy"; const margin = { top: 40, right: 40, bottom: 40, left: 40 }; function getSizeModifier(galaxyFrontend: galaxy.Frontend) { - return Math.sqrt(galaxyFrontend.galaxySize); + // TODO: flexible scaling + return Math.sqrt(galaxyFrontend.galaxySize * 10); } export function initViz(galaxyFrontend: galaxy.Frontend) { const sizeModifier = getSizeModifier(galaxyFrontend); - const width = galaxyFrontend.galaxySize + margin.left + margin.right; - const height = galaxyFrontend.galaxySize + margin.top + margin.bottom; // remove old svg d3.select("#dataviz svg").remove(); @@ -20,9 +19,9 @@ export function initViz(galaxyFrontend: galaxy.Frontend) { const svg = d3 .select("#dataviz") .append("svg") - .attr("width", width * sizeModifier + margin.left + margin.right) - .attr("height", height * sizeModifier + margin.top + margin.bottom) + .style("overflow", "visible") .append("g") + .attr("id", "axis") .attr("transform", `translate(${margin.left}, ${margin.top})`); // Add X axis @@ -30,43 +29,36 @@ export function initViz(galaxyFrontend: galaxy.Frontend) { .scaleLinear() .domain([0, galaxyFrontend.galaxySize]) .range([0, galaxyFrontend.galaxySize * sizeModifier]); - svg - .append("g") - .attr( - "transform", - `translate(0, ${galaxyFrontend.galaxySize * sizeModifier})` - ) - .call(d3.axisBottom(x)); // Add Y axis const y = d3 .scaleLinear() .domain([0, galaxyFrontend.galaxySize]) .range([galaxyFrontend.galaxySize * sizeModifier, 0]); - svg.append("g").call(d3.axisLeft(y)); } export function initData(galaxyFrontend: galaxy.Frontend) { const sizeModifier = getSizeModifier(galaxyFrontend); // remove old data - d3.select("#dataviz svg circle").remove(); + d3.select("#dataviz svg #data").remove(); // append the svg object to the body of the page const svg = d3.select("#dataviz svg"); svg .append("g") + .attr("id", "data") .selectAll("dot") .data(galaxyFrontend.cells()) .join("circle") .attr("cx", function (c: galaxy.Cell) { - return c.x * sizeModifier + margin.left; + return Math.round(c.x * sizeModifier + margin.left); }) .attr("cy", function (c: galaxy.Cell) { - return c.y * sizeModifier + margin.top; + return Math.round(c.y * sizeModifier + margin.top); }) .attr("r", function (c: galaxy.Cell) { - return c.mass; + return Math.log(c.mass) > 0 ? Math.log(c.mass) : 0; }) .style("fill", "#69b3a2"); } diff --git a/src/js/lib/galaxy.ts b/src/js/lib/galaxy.ts index b3f3866..7bfaa00 100644 --- a/src/js/lib/galaxy.ts +++ b/src/js/lib/galaxy.ts @@ -23,15 +23,17 @@ export class Frontend { this.galaxy = this.galaxy.seed(additionalMass); } - public tick(gravityReach: number): void { - this.galaxy = this.galaxy.tick(gravityReach); + public tick(timeModifier: number): void { + this.galaxy = this.galaxy.tick(timeModifier); } public cells(): Cell[] { + const cells: Cell[] = []; + const mass = this.galaxy.mass(); const x = this.galaxy.x(); const y = this.galaxy.y(); - const cells: Cell[] = []; + for (let i = 0; i < this.galaxySize ** 2; i++) { cells.push({ mass: mass[i], diff --git a/src/rust/galaxy.rs b/src/rust/galaxy.rs index af0cc8d..1f7a4eb 100644 --- a/src/rust/galaxy.rs +++ b/src/rust/galaxy.rs @@ -1,27 +1,18 @@ +use std::collections::HashMap; + use rand::Rng; use wasm_bindgen::prelude::*; // types -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, PartialEq)] pub struct Cell { pub mass: u16, - pub accel_mangitude: u16, - pub accel_degree: u16, + pub accel_magnitude: f32, + pub accel_degrees: f32, // Cells have an x and y position that is not stored here. // It is generated by looking that the cell's index in the galaxy. } -// defaults -impl Default for Cell { - fn default() -> Cell { - Cell { - mass: 0, - accel_mangitude: 0, - accel_degree: 0, - } - } -} - // types #[wasm_bindgen] pub struct Galaxy { @@ -32,9 +23,42 @@ pub struct Galaxy { // defaults impl Galaxy { - pub const MAX_STAR_MASS: u16 = u16::MAX; + // --------------------------------------------------------------- // + // An explanation of the mechanics of the different types of cells // + // --------------------------------------------------------------- // + + // Nebula + // Nebulas are the most common type of cell. It is the default type. + // Nebulas gravitate towards each other, but do not naturally diffuse. + // Nebulas can by created by stellar wind (e.g. from a star or white hole). + + // Planet + // Planets are created by the aggregation of gas. + // Planets here are understood to be gas giants, hot jupiters, and proto stars. + // Planets do not gravitate towards gases. + // Planets do not diffuse. + + // Star + // Stars are created by aggregating gas around a planet. + // Stars gravitate towards each other and planets. + // Stars diffuse at a medium rate. + + // White Hole + // White holes are created by the aggregation of stars. + // White holes do not experience gravitation. + // White holes diffuse at an incredible rate. + // White holes exist to repel objects away from the max mass (eg. u16 max size). + pub const TYPE_INDEX_GAS: u8 = 0; + pub const TYPE_INDEX_PLANET: u8 = 1; pub const TYPE_INDEX_STAR: u8 = 2; + pub const TYPE_INDEX_WHITE_HOLE: u8 = 3; + pub const STAR_MAX_MASS: u16 = u16::MAX / 2; +} + +impl Galaxy { + // https://en.wikipedia.org/wiki/Newton%27s_law_of_universal_gravitation + pub const GRAVATIONAL_CONSTANT: f32 = 0.0000000000667408; } // public methods @@ -51,7 +75,8 @@ impl Galaxy { cells: vec![ Cell { mass: cell_initial_mass, - ..Default::default() + accel_magnitude: 0.0, + accel_degrees: 0.0, }; (size as u32).pow(2) as usize ], @@ -65,7 +90,8 @@ impl Galaxy { let mass = self.cells[index as usize].mass; Cell { mass: mass + rng.gen_range(0..additional + 1), - ..Default::default() + accel_magnitude: 0.0, + accel_degrees: 0.0, } }) .collect(); @@ -75,21 +101,24 @@ impl Galaxy { min_star_mass: self.min_star_mass, }; } - pub fn tick(&self, reach: u16) -> Galaxy { + pub fn tick(&self, time: f32) -> Galaxy { // advance the galaxy one tick - // TODO: not yet implemented - let next: Vec = (0..self.size.pow(2)) + let with_gravitation: Vec = (0..self.size.pow(2)) .map(|index| { let _cell = self.cells[index as usize]; - let _neighbours = self.neighbours(index, reach); + let _neighbours = self.neighbours(index, u16::MAX); + let (magnitude, degrees) = self.gravitate(index, _neighbours); Cell { - ..Default::default() + mass: _cell.mass, + accel_magnitude: magnitude, + accel_degrees: degrees, } }) .collect(); + let after_acceleration = self.apply_acceleration(with_gravitation, time); return Galaxy { size: self.size, - cells: next, + cells: after_acceleration, min_star_mass: self.min_star_mass, }; } @@ -114,7 +143,7 @@ impl Galaxy { return self.size; } fn gas_reach_range(&self) -> u16 { - return (self.size as f64).sqrt() as u16; + return (self.size as f32).sqrt() as u16; } fn reach_of_type(&self, type_index: u8) -> u16 { match type_index { @@ -123,6 +152,17 @@ impl Galaxy { _ => unreachable!(), } } + fn get_type_index(&self, cell: Cell) -> u8 { + if cell.mass < self.min_star_mass { + return Galaxy::TYPE_INDEX_GAS; + } else { + return Galaxy::TYPE_INDEX_STAR; + } + } + fn check_if_type(&self, cell: Cell, type_index: u8) -> bool { + let this_type_index = self.get_type_index(cell); + return type_index == this_type_index; + } fn neighbours_of_my_type(&self, index: u16) -> Vec<(u16, u16)> { let type_index = self.get_type_index(self.cells[index as usize]); return self.neighbours_of_type(index, type_index); @@ -150,22 +190,6 @@ impl Galaxy { } return neighbours; } - fn get_type_index(&self, cell: Cell) -> u8 { - if cell.mass < self.min_star_mass { - return Galaxy::TYPE_INDEX_GAS; - } else { - return Galaxy::TYPE_INDEX_STAR; - } - } - fn check_if_type(&self, cell: Cell, type_index: u8) -> bool { - if (type_index == Galaxy::TYPE_INDEX_GAS) & (cell.mass < self.min_star_mass) { - return true; - } else if (type_index == Galaxy::TYPE_INDEX_STAR) & (cell.mass >= self.min_star_mass) { - return true; - } else { - return false; - } - } fn col_row_to_index(&self, col: u16, row: u16) -> u16 { return row * self.size + col; } @@ -189,7 +213,7 @@ impl Galaxy { } fn reach_range_end(&self, index: u16, reach: u16) -> u16 { let end; - if index + reach > self.size { + if index.saturating_add(reach) >= self.size { end = self.size - 1; } else { end = index + reach; @@ -198,6 +222,223 @@ impl Galaxy { } } +impl Galaxy { + fn apply_acceleration(&self, cells: Vec, time: f32) -> Vec { + let mut cell_vec: Vec = Vec::new(); + let mut cell_map: HashMap = HashMap::new(); + + // This loop iterates over the cells, and adds them to a hashmap. + // The hashmap is used to combine the cells that have moved to the same location. + // The next loop iterates over the hashmap, and adds the cells to the new_cells vector. + for (index, cell) in cells.iter().enumerate() { + let (row, col) = self.index_to_row_col(index as u16); + + // break the acceleration into x and y components + let acc_x = cell.accel_magnitude * cell.accel_degrees.to_radians().cos(); + let acc_y = cell.accel_magnitude * cell.accel_degrees.to_radians().sin(); + + // modify the acceleration by the time, to enable faster or slower movement + let new_x = (acc_x * time.powf(2.0)) as i16 + col as i16; + let new_y = (acc_y * time.powf(2.0)) as i16 + row as i16; + + // clamp x and y to the edges of the galaxy + let new_x = self.clamp(new_x, 0, self.size - 1); + let new_y = self.clamp(new_y, 0, self.size - 1); + let new_index = self.col_row_to_index(new_x, new_y); + + // get mass of the cell at the new index + let existing_cell = cell_map.get(&new_index).unwrap_or(&Cell { + mass: 0, + accel_magnitude: 0.0, + accel_degrees: 0.0, + }); + let new_mass = existing_cell.mass + cell.mass; + + // add the cell to the map + cell_map.insert( + new_index, + Cell { + mass: new_mass, + accel_magnitude: 0.0, + accel_degrees: 0.0, + }, + ); + } + + // iterate over the size of the galaxy + // adding the cells from the hashmap to the new_cells vector + for index in 0..self.size.pow(2) { + let cell = cell_map + .get(&index) + .unwrap_or(&Cell { + mass: 0, + accel_magnitude: 0.0, + accel_degrees: 0.0, + }) + .clone(); + cell_vec.push(cell); + } + + return cell_vec; + } + fn clamp(&self, value: i16, min: u16, max: u16) -> u16 { + if value < (min as i16) { + return max - (value.abs() as u16 % max); + } else if value > (max as i16) { + return value as u16 % max; + } else { + return value.abs() as u16; + } + } +} + +// private methods - the methods needed for gravitation +impl Galaxy { + fn gravitate(&self, index: u16, neighbours: Vec<(u16, u16)>) -> (f32, f32) { + let mut sum_accel_magnitude = self.cells[index as usize].accel_magnitude; + let mut sum_accel_degrees = self.cells[index as usize].accel_degrees; + for neighbour_coords in neighbours { + let (new_accel_magnitude, new_accel_degrees) = + self.acceleration(index, neighbour_coords); + (sum_accel_magnitude, sum_accel_degrees) = self.combine_vectors( + sum_accel_magnitude, + sum_accel_degrees, + new_accel_magnitude, + new_accel_degrees, + ); + } + return (sum_accel_magnitude, sum_accel_degrees); + } + fn combine_vectors( + &self, + init_magnitude: f32, + init_degrees: f32, + new_magnitude: f32, + new_degrees: f32, + ) -> (f32, f32) { + let init_x = init_magnitude * init_degrees.to_radians().cos(); + let init_y = init_magnitude * init_degrees.to_radians().sin(); + let new_x = new_magnitude * new_degrees.to_radians().cos(); + let new_y = new_magnitude * new_degrees.to_radians().sin(); + let x = init_x + new_x; + let y = init_y + new_y; + let magnitude = (x.powi(2) + y.powi(2)).sqrt(); + let degrees = (x.atan2(y)).to_degrees(); + return (magnitude, degrees); + } + fn acceleration(&self, index: u16, neighbour_coords: (u16, u16)) -> (f32, f32) { + let cell = self.cells[index as usize]; + let neighbour_index = self.col_row_to_index(neighbour_coords.0, neighbour_coords.1); + let neighbour_cell = self.cells[neighbour_index as usize]; + let distance = self.distance(index, neighbour_coords); + let degrees = self.degrees(index, neighbour_coords); + let gravitation = self.gravitation(cell.mass, neighbour_cell.mass, distance); + let acceleration = gravitation / cell.mass as f32; + return (acceleration, degrees); + } + fn gravitation(&self, mass_one: u16, mass_two: u16, distance: f32) -> f32 { + // https://en.wikipedia.org/wiki/Newton%27s_law_of_universal_gravitation + // F = G * ((m1 * m2) / r^2) + let force = + Galaxy::GRAVATIONAL_CONSTANT * ((mass_one * mass_two) as f32 / distance.powi(2)); + return force; + } + fn degrees(&self, index: u16, neighbour_coords: (u16, u16)) -> f32 { + let (cell_x, cell_y) = self.index_to_row_col(index); + let x = neighbour_coords.0 as i16 - cell_x as i16; + let y = neighbour_coords.1 as i16 - cell_y as i16; + let radians = (x as f32).atan2(y as f32); + let degrees = radians.to_degrees(); + return degrees; + } + fn distance(&self, index: u16, neighbour_coords: (u16, u16)) -> f32 { + let (cell_x, cell_y) = self.index_to_row_col(index); + let x = (cell_x as i16 - neighbour_coords.0 as i16).pow(2); + let y = (cell_y as i16 - neighbour_coords.1 as i16).pow(2); + let distance = (x as f32 + y as f32).sqrt(); + return distance; + } +} + +#[cfg(test)] +mod tests_combine_vectors {} + +#[cfg(test)] +mod tests_distance { + use super::*; + #[test] + fn test_distance_one() { + let galaxy = Galaxy::new(3, 0, 1000); + let index: u16 = 0; + let neighbour_coords = (1, 1); + let mut distance = galaxy.distance(index, neighbour_coords); + distance = (distance * 100.0).round() / 100.0; + assert_eq!(distance, 1.41); + } + #[test] + fn test_distance_two() { + let galaxy = Galaxy::new(3, 0, 1000); + let index = 0; + let neighbour_coords = (2, 2); + let mut distance = galaxy.distance(index, neighbour_coords); + distance = (distance * 100.0).round() / 100.0; + assert_eq!(distance, 2.83); + } + #[test] + fn test_distance_two_linear() { + let galaxy = Galaxy::new(3, 0, 1000); + let index = 0; + let neighbour_coords = (0, 2); + let mut distance = galaxy.distance(index, neighbour_coords); + distance = (distance * 100.0).round() / 100.0; + assert_eq!(distance, 2.00); + } +} + +mod tests_degreess { + use super::*; + #[test] + fn test_degreess_x() { + let galaxy = Galaxy::new(3, 0, 1000); + let index = 0; + let neighbour_coords = (2, 0); + let degrees = galaxy.degrees(index, neighbour_coords).round() as u16; + assert_eq!(degrees, 90, "neighbour_coords: {:?}, x", neighbour_coords); + } + #[test] + fn test_degreess_y() { + let galaxy = Galaxy::new(3, 0, 1000); + let index = 0; + let neighbour_coords = (0, 2); + let degrees = galaxy.degrees(index, neighbour_coords).round() as u16; + assert_eq!(degrees, 0, "neighbour_coords: {:?}, y", neighbour_coords); + } + #[test] + fn test_degreess_z_one() { + let galaxy = Galaxy::new(3, 0, 1000); + let index = 0; + let neighbour_coords = (2, 2); + let degrees = galaxy.degrees(index, neighbour_coords).round() as u16; + assert_eq!(degrees, 45, "neighbour_coords: {:?}, xy", neighbour_coords); + } + #[test] + fn test_degreess_z_two() { + let galaxy = Galaxy::new(3, 0, 1000); + let index = 0; + let neighbour_coords = (1, 2); + let degrees = galaxy.degrees(index, neighbour_coords).round() as u16; + assert_eq!(degrees, 27, "neighbour_coords: {:?}, xy", neighbour_coords); + } + #[test] + fn test_degreess_z_three() { + let galaxy = Galaxy::new(3, 0, 1000); + let index = 0; + let neighbour_coords = (2, 1); + let degrees = galaxy.degrees(index, neighbour_coords).round() as u16; + assert_eq!(degrees, 63, "neighbour_coords: {:?}, xy", neighbour_coords); + } +} + #[cfg(test)] mod tests_intial_generation { use super::*; @@ -211,7 +452,7 @@ mod tests_intial_generation { } #[test] fn test_seed_tick_no_panic() { - Galaxy::new(10, 1, 1000).seed(1).tick(1); + Galaxy::new(10, 1, 1000).seed(1).tick(1.0); } #[test] fn test_seed_alters_data() { @@ -372,11 +613,13 @@ mod tests_neighbors_and_reach { fn test_neighbor_size_larger() { let galaxy = Galaxy::new(10, 0, 1000); assert_eq!(galaxy.neighbours(0, 2).len(), 8); + assert_eq!(galaxy.neighbours(0, u16::MAX).len(), 99); } #[test] fn test_neighbor_size_center() { let galaxy = Galaxy::new(3, 0, 1000); assert_eq!(galaxy.neighbours(4, 1).len(), 8); + assert_eq!(galaxy.neighbours(4, u16::MAX).len(), 8); } #[test] fn test_neighbor_size_differs_for_large_galaxy() { @@ -385,13 +628,15 @@ mod tests_neighbors_and_reach { // gas galaxy.cells[index] = Cell { mass: 1, - ..Default::default() + accel_magnitude: 0.0, + accel_degrees: 0.0, }; let gas_neighbours = galaxy.neighbours_of_my_type(0).len(); // star galaxy.cells[index] = Cell { mass: 65535, - ..Default::default() + accel_magnitude: 0.0, + accel_degrees: 0.0, }; let star_neighbours = galaxy.neighbours_of_my_type(0).len(); assert_ne!(gas_neighbours, star_neighbours); @@ -403,13 +648,15 @@ mod tests_neighbors_and_reach { // gas galaxy.cells[index] = Cell { mass: 1, - ..Default::default() + accel_magnitude: 0.0, + accel_degrees: 0.0, }; let gas_neighbours = galaxy.neighbours_of_my_type(0).len(); // star galaxy.cells[index] = Cell { mass: 59999, - ..Default::default() + accel_magnitude: 0.0, + accel_degrees: 0.0, }; let star_neighbours = galaxy.neighbours_of_my_type(0).len(); assert_eq!(gas_neighbours, star_neighbours);