From 96ee00242f350b391dcc1cf4d72a442c969f565e Mon Sep 17 00:00:00 2001 From: Ilion Beyst Date: Wed, 22 Dec 2021 13:59:13 +0100 Subject: [PATCH] add pw-frontend --- web/pw-frontend/.editorconfig | 2 + web/pw-frontend/.gitignore | 5 + web/pw-frontend/README.md | 48 + web/pw-frontend/index.html | 19 + web/pw-frontend/package.json | 30 + web/pw-frontend/planetwars-rs/.gitignore | 2 + web/pw-frontend/planetwars-rs/Cargo.toml | 44 + web/pw-frontend/planetwars-rs/src/lib.rs | 373 ++++ web/pw-frontend/planetwars-rs/src/types.rs | 45 + web/pw-frontend/planetwars-rs/src/utils.rs | 65 + .../public/assets/images/earth.svg | 114 ++ .../public/assets/images/jupiter.svg | 118 ++ web/pw-frontend/public/assets/images/mars.svg | 114 ++ .../public/assets/images/neptune.svg | 186 ++ .../public/assets/images/rocket.svg | 79 + .../public/assets/images/uranus.svg | 150 ++ .../public/assets/images/venus.svg | 114 ++ web/pw-frontend/public/favicon.ico | Bin 0 -> 1150 bytes web/pw-frontend/public/match.log | 43 + .../public/static/res/assets/earth.svg | 114 ++ .../public/static/res/assets/font.png | Bin 0 -> 912 bytes .../public/static/res/assets/jupiter.svg | 118 ++ .../public/static/res/assets/leaves.jpg | Bin 0 -> 36902 bytes .../public/static/res/assets/mars.svg | 114 ++ .../public/static/res/assets/neptune.svg | 186 ++ .../public/static/res/assets/ship.svg | 78 + .../public/static/res/assets/uranus.svg | 150 ++ .../public/static/res/assets/venus.svg | 114 ++ .../public/static/shaders/frag/image.glsl | 14 + .../public/static/shaders/frag/simple.glsl | 15 + .../public/static/shaders/frag/vor.glsl | 18 + .../public/static/shaders/vert/image.glsl | 33 + .../public/static/shaders/vert/simple.glsl | 36 + .../public/static/shaders/vert/vor.glsl | 43 + web/pw-frontend/src/App.svelte | 47 + web/pw-frontend/src/assets/svelte.png | Bin 0 -> 5185 bytes web/pw-frontend/src/lib/Visualizer.svelte | 49 + .../src/lib/visualizer/LICENSE-MIT | 25 + web/pw-frontend/src/lib/visualizer/README.md | 1 + web/pw-frontend/src/lib/visualizer/index.html | 102 + web/pw-frontend/src/lib/visualizer/index.ts | 666 +++++++ .../src/lib/visualizer/src/games.ts | 47 + web/pw-frontend/src/lib/visualizer/style.css | 309 +++ .../lib/visualizer/voronoi/voronoi-core.d.ts | 56 + .../lib/visualizer/voronoi/voronoi-core.js | 1726 +++++++++++++++++ .../src/lib/visualizer/voronoi/voronoi.ts | 165 ++ .../src/lib/visualizer/webgl/buffer.ts | 55 + .../src/lib/visualizer/webgl/index.ts | 122 ++ .../src/lib/visualizer/webgl/renderer.ts | 157 ++ .../src/lib/visualizer/webgl/shader.ts | 327 ++++ .../src/lib/visualizer/webgl/text.ts | 192 ++ .../src/lib/visualizer/webgl/texture.ts | 106 + .../src/lib/visualizer/webgl/util.ts | 229 +++ .../visualizer/webgl/vertexBufferLayout.ts | 115 ++ web/pw-frontend/src/main.ts | 9 + web/pw-frontend/src/vite-env.d.ts | 2 + web/pw-frontend/svelte.config.js | 7 + web/pw-frontend/tsconfig.json | 20 + web/pw-frontend/vite.config.js | 12 + 59 files changed, 7130 insertions(+) create mode 100644 web/pw-frontend/.editorconfig create mode 100644 web/pw-frontend/.gitignore create mode 100644 web/pw-frontend/README.md create mode 100644 web/pw-frontend/index.html create mode 100644 web/pw-frontend/package.json create mode 100644 web/pw-frontend/planetwars-rs/.gitignore create mode 100644 web/pw-frontend/planetwars-rs/Cargo.toml create mode 100644 web/pw-frontend/planetwars-rs/src/lib.rs create mode 100644 web/pw-frontend/planetwars-rs/src/types.rs create mode 100644 web/pw-frontend/planetwars-rs/src/utils.rs create mode 100644 web/pw-frontend/public/assets/images/earth.svg create mode 100644 web/pw-frontend/public/assets/images/jupiter.svg create mode 100644 web/pw-frontend/public/assets/images/mars.svg create mode 100644 web/pw-frontend/public/assets/images/neptune.svg create mode 100644 web/pw-frontend/public/assets/images/rocket.svg create mode 100644 web/pw-frontend/public/assets/images/uranus.svg create mode 100644 web/pw-frontend/public/assets/images/venus.svg create mode 100644 web/pw-frontend/public/favicon.ico create mode 100644 web/pw-frontend/public/match.log create mode 100644 web/pw-frontend/public/static/res/assets/earth.svg create mode 100644 web/pw-frontend/public/static/res/assets/font.png create mode 100644 web/pw-frontend/public/static/res/assets/jupiter.svg create mode 100644 web/pw-frontend/public/static/res/assets/leaves.jpg create mode 100644 web/pw-frontend/public/static/res/assets/mars.svg create mode 100644 web/pw-frontend/public/static/res/assets/neptune.svg create mode 100644 web/pw-frontend/public/static/res/assets/ship.svg create mode 100644 web/pw-frontend/public/static/res/assets/uranus.svg create mode 100644 web/pw-frontend/public/static/res/assets/venus.svg create mode 100644 web/pw-frontend/public/static/shaders/frag/image.glsl create mode 100644 web/pw-frontend/public/static/shaders/frag/simple.glsl create mode 100644 web/pw-frontend/public/static/shaders/frag/vor.glsl create mode 100644 web/pw-frontend/public/static/shaders/vert/image.glsl create mode 100644 web/pw-frontend/public/static/shaders/vert/simple.glsl create mode 100644 web/pw-frontend/public/static/shaders/vert/vor.glsl create mode 100644 web/pw-frontend/src/App.svelte create mode 100644 web/pw-frontend/src/assets/svelte.png create mode 100644 web/pw-frontend/src/lib/Visualizer.svelte create mode 100644 web/pw-frontend/src/lib/visualizer/LICENSE-MIT create mode 100644 web/pw-frontend/src/lib/visualizer/README.md create mode 100644 web/pw-frontend/src/lib/visualizer/index.html create mode 100644 web/pw-frontend/src/lib/visualizer/index.ts create mode 100644 web/pw-frontend/src/lib/visualizer/src/games.ts create mode 100644 web/pw-frontend/src/lib/visualizer/style.css create mode 100644 web/pw-frontend/src/lib/visualizer/voronoi/voronoi-core.d.ts create mode 100644 web/pw-frontend/src/lib/visualizer/voronoi/voronoi-core.js create mode 100644 web/pw-frontend/src/lib/visualizer/voronoi/voronoi.ts create mode 100644 web/pw-frontend/src/lib/visualizer/webgl/buffer.ts create mode 100644 web/pw-frontend/src/lib/visualizer/webgl/index.ts create mode 100644 web/pw-frontend/src/lib/visualizer/webgl/renderer.ts create mode 100644 web/pw-frontend/src/lib/visualizer/webgl/shader.ts create mode 100644 web/pw-frontend/src/lib/visualizer/webgl/text.ts create mode 100644 web/pw-frontend/src/lib/visualizer/webgl/texture.ts create mode 100644 web/pw-frontend/src/lib/visualizer/webgl/util.ts create mode 100644 web/pw-frontend/src/lib/visualizer/webgl/vertexBufferLayout.ts create mode 100644 web/pw-frontend/src/main.ts create mode 100644 web/pw-frontend/src/vite-env.d.ts create mode 100644 web/pw-frontend/svelte.config.js create mode 100644 web/pw-frontend/tsconfig.json create mode 100644 web/pw-frontend/vite.config.js diff --git a/web/pw-frontend/.editorconfig b/web/pw-frontend/.editorconfig new file mode 100644 index 0000000..3007779 --- /dev/null +++ b/web/pw-frontend/.editorconfig @@ -0,0 +1,2 @@ +[*.{js,ts,html,svelte}] +indent_size = 2 \ No newline at end of file diff --git a/web/pw-frontend/.gitignore b/web/pw-frontend/.gitignore new file mode 100644 index 0000000..012c26e --- /dev/null +++ b/web/pw-frontend/.gitignore @@ -0,0 +1,5 @@ +/node_modules/ +/dist/ +/.vscode/ +.DS_Store +package-lock.json \ No newline at end of file diff --git a/web/pw-frontend/README.md b/web/pw-frontend/README.md new file mode 100644 index 0000000..a9d516a --- /dev/null +++ b/web/pw-frontend/README.md @@ -0,0 +1,48 @@ +# Svelte + TS + Vite + +This template should help get you started developing with Svelte and TypeScript in Vite. + +## Recommended IDE Setup + +[VSCode](https://code.visualstudio.com/) + [Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode). + +## Need an official Svelte framework? + +Check out [SvelteKit](https://github.com/sveltejs/kit#readme), which is also powered by Vite. Deploy anywhere with its serverless-first approach and adapt to various platforms, with out of the box support for TypeScript, SCSS, and Less, and easily-added support for mdsvex, GraphQL, PostCSS, Tailwind CSS, and more. + +## Technical considerations + +**Why use this over SvelteKit?** + +- It brings its own routing solution which might not be preferable for some users. +- It is first and foremost a framework that just happens to use Vite under the hood, not a Vite app. + `vite dev` and `vite build` wouldn't work in a SvelteKit environment, for example. + +This template contains as little as possible to get started with Vite + TypeScript + Svelte, while taking into account the developer experience with regards to HMR and intellisense. It demonstrates capabilities on par with the other `create-vite` templates and is a good starting point for beginners dipping their toes into a Vite + Svelte project. + +Should you later need the extended capabilities and extensibility provided by SvelteKit, the template has been structured similarly to SvelteKit so that it is easy to migrate. + +**Why `global.d.ts` instead of `compilerOptions.types` inside `jsconfig.json` or `tsconfig.json`?** + +Setting `compilerOptions.types` shuts out all other types not explicitly listed in the configuration. Using triple-slash references keeps the default TypeScript setting of accepting type information from the entire workspace, while also adding `svelte` and `vite/client` type information. + +**Why include `.vscode/extensions.json`?** + +Other templates indirectly recommend extensions via the README, but this file allows VS Code to prompt the user to install the recommended extension upon opening the project. + +**Why enable `allowJs` in the TS template?** + +While `allowJs: false` would indeed prevent the use of `.js` files in the project, it does not prevent the use of JavaScript syntax in `.svelte` files. In addition, it would force `checkJs: false`, bringing the worst of both worlds: not being able to guarantee the entire codebase is TypeScript, and also having worse typechecking for the existing JavaScript. In addition, there are valid use cases in which a mixed codebase may be relevant. + +**Why is HMR not preserving my local component state?** + +HMR state preservation comes with a number of gotchas! It has been disabled by default in both `svelte-hmr` and `@sveltejs/vite-plugin-svelte` due to its often surprising behavior. You can read the details [here](https://github.com/rixo/svelte-hmr#svelte-hmr). + +If you have state that's important to retain within a component, consider creating an external store which would not be replaced by HMR. + +```ts +// store.ts +// An extremely simple external store +import { writable } from 'svelte/store' +export default writable(0) +``` diff --git a/web/pw-frontend/index.html b/web/pw-frontend/index.html new file mode 100644 index 0000000..dc46fa0 --- /dev/null +++ b/web/pw-frontend/index.html @@ -0,0 +1,19 @@ + + + + + + + + + + + Planetwars + + +
+ + + diff --git a/web/pw-frontend/package.json b/web/pw-frontend/package.json new file mode 100644 index 0000000..194f7cf --- /dev/null +++ b/web/pw-frontend/package.json @@ -0,0 +1,30 @@ +{ + "name": "pw-frontend", + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "check": "svelte-check --tsconfig ./tsconfig.json" + }, + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^1.0.0-next.30", + "@tsconfig/svelte": "^2.0.1", + "rollup-plugin-polyfill-node": "^0.8.0", + "svelte": "^3.44.0", + "svelte-check": "^2.2.7", + "svelte-preprocess": "^4.9.8", + "tslib": "^2.3.1", + "typescript": "^4.4.4", + "vite": "^2.7.2" + }, + "dependencies": { + "buffer": "^6.0.3", + "extract-svg-path": "^2.1.0", + "load-svg": "^1.0.0", + "planetwars-rs": "file:./planetwars-rs/pkg", + "svg-mesh-3d": "^1.1.0", + "ts-heap": "^1.1.3" + } +} diff --git a/web/pw-frontend/planetwars-rs/.gitignore b/web/pw-frontend/planetwars-rs/.gitignore new file mode 100644 index 0000000..a04eea2 --- /dev/null +++ b/web/pw-frontend/planetwars-rs/.gitignore @@ -0,0 +1,2 @@ +pkg/** +target/** diff --git a/web/pw-frontend/planetwars-rs/Cargo.toml b/web/pw-frontend/planetwars-rs/Cargo.toml new file mode 100644 index 0000000..31b4d6c --- /dev/null +++ b/web/pw-frontend/planetwars-rs/Cargo.toml @@ -0,0 +1,44 @@ +[package] +name = "planetwars-rs" +version = "0.1.0" +authors = ["ajuvercr "] +edition = "2018" + +[package.metadata.wasm-pack.profile.release] +wasm-opt = ["-Oz", "--enable-mutable-globals"] + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +default = ["console_error_panic_hook"] + +[dependencies] +wasm-bindgen = "0.2" + +# The `console_error_panic_hook` crate provides better debugging of panics by +# logging them with `console.error`. This is great for development, but requires +# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for +# code size when deploying. +console_error_panic_hook = { version = "0.1.1", optional = true } + +# `wee_alloc` is a tiny allocator for wasm that is only ~1K in code size +# compared to the default allocator's ~10K. It is slower than the default +# allocator, however. +# +# Unfortunately, `wee_alloc` requires nightly Rust when targeting wasm for now. +wee_alloc = { version = "0.4.2", optional = true } +serde = "1.0.100" +serde_derive = "1.0.100" +serde_json = "1.0" +octoon-math = "0.1.7" +voronoi = "0.1.4" + +[dev-dependencies] +wasm-bindgen-test = "0.2" + +[profile.release] +# Tell `rustc` to optimize for small code size. +opt-level = "s" + +[workspace] \ No newline at end of file diff --git a/web/pw-frontend/planetwars-rs/src/lib.rs b/web/pw-frontend/planetwars-rs/src/lib.rs new file mode 100644 index 0000000..f2ba7e1 --- /dev/null +++ b/web/pw-frontend/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/pw-frontend/planetwars-rs/src/types.rs b/web/pw-frontend/planetwars-rs/src/types.rs new file mode 100644 index 0000000..2d7d8c0 --- /dev/null +++ b/web/pw-frontend/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/pw-frontend/planetwars-rs/src/utils.rs b/web/pw-frontend/planetwars-rs/src/utils.rs new file mode 100644 index 0000000..a903912 --- /dev/null +++ b/web/pw-frontend/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 + }) +} diff --git a/web/pw-frontend/public/assets/images/earth.svg b/web/pw-frontend/public/assets/images/earth.svg new file mode 100644 index 0000000..f1be561 --- /dev/null +++ b/web/pw-frontend/public/assets/images/earth.svg @@ -0,0 +1,114 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/pw-frontend/public/assets/images/jupiter.svg b/web/pw-frontend/public/assets/images/jupiter.svg new file mode 100644 index 0000000..e5fcd0f --- /dev/null +++ b/web/pw-frontend/public/assets/images/jupiter.svg @@ -0,0 +1,118 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/pw-frontend/public/assets/images/mars.svg b/web/pw-frontend/public/assets/images/mars.svg new file mode 100644 index 0000000..d92851d --- /dev/null +++ b/web/pw-frontend/public/assets/images/mars.svg @@ -0,0 +1,114 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/pw-frontend/public/assets/images/neptune.svg b/web/pw-frontend/public/assets/images/neptune.svg new file mode 100644 index 0000000..995d440 --- /dev/null +++ b/web/pw-frontend/public/assets/images/neptune.svg @@ -0,0 +1,186 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/pw-frontend/public/assets/images/rocket.svg b/web/pw-frontend/public/assets/images/rocket.svg new file mode 100644 index 0000000..162cb46 --- /dev/null +++ b/web/pw-frontend/public/assets/images/rocket.svg @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web/pw-frontend/public/assets/images/uranus.svg b/web/pw-frontend/public/assets/images/uranus.svg new file mode 100644 index 0000000..fb192a4 --- /dev/null +++ b/web/pw-frontend/public/assets/images/uranus.svg @@ -0,0 +1,150 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/pw-frontend/public/assets/images/venus.svg b/web/pw-frontend/public/assets/images/venus.svg new file mode 100644 index 0000000..3bebb10 --- /dev/null +++ b/web/pw-frontend/public/assets/images/venus.svg @@ -0,0 +1,114 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/pw-frontend/public/favicon.ico b/web/pw-frontend/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..d75d248ef0b15096a95054643a4d97f5d9b60846 GIT binary patch literal 1150 zcmaKqSxA*Z7>4KMS_;~x;8i!JU{-`tpyx!I(n2NFMU)w2L{WiMS3#Lcp@vrpA*5Yp zSy6|`AkYfDD(e{`n8yX0pLf20X1@3RmKkGw`Vte3=0)aUq%ldx zm^49K+Hw0b#^`KboP)QXJOwbuVUFxlAs{RfqJ+twGylWfOp{Hc$s#253LlN1nsFVc zKa>40?h5(7PTC6ltDx)(Y&Ze2xggCq(kK? zTA`;gAfKD!+uFjpxc_A3+Ma(L28W=z4Gvs@r*ECk`;c45=S#;=oA|abt`f&j5&uJO z3Dn+&^gZ%h4JidsaTR{{!_Y8PUx(-%PosPy2gi@qIvBMMYz;e3L1{f~mrd9RdB>pZ zD}4R|sk_C`;=cT&r)c=8u>7h9)u32*SbL`xiq3(pq5C^5-sSOw;<|fv@nfXfl&U`2 z81K5ExDp;bf#DISW%IY%k&2-noShOoz-;kb(u?5RFX-ro?87j3GZdCXrFc8bTx}jd zz_n@djWnxc*TbbCjEq80FPyG}1zQwvjq7R6ZSWuQ@_#A*LN5n<3$BI?X}q%iD!B-s zdSFcNp!EgpJr6CAK?klug4>=)Tv z+F#{yt>6EK)3NU=L&y_W3UNaC?Tg=6YE0)^V;(0Mb0$WJ7>7@Lg0~+3x9d)!Pd + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/pw-frontend/public/static/res/assets/font.png b/web/pw-frontend/public/static/res/assets/font.png new file mode 100644 index 0000000000000000000000000000000000000000..1724e0dd27e96c4c0977e4bf4b4939553d6e79dd GIT binary patch literal 912 zcmV;B18@9^P)e zSad^gZEa<4bO1wgWnpw>WFU8GbZ8()Nlj2!fese{00Q_)L_t(&-tAiLj^iK<%%Hsg zBcJa_kHi>%LfT#3Zlp@x7#m}ZXG|Bz{Sf;O1%Rh{%Z~*x^(#W<2a79D=PyACmlISqiU#=_9J~Q^Njg@gDe1Q zl$BYeBS@rW`ibmk=Feua8UVgSD?6B|CDk1+Wk?^B@m~$V#ts31TLUUPoLDU8wT<&v z9xPVjuS5YWI!h>U{PZV^QbtY-_TkhFTJV3F$25Kh`^TyQmYl!y^rQOHot0w~_DT3g zf$ua0pcfva06~#IdVacd3jw@%fItC~D4@yDWU-;_0#*amowK0KgSZ_OYt8~mqnX|U zyd6+%Q-STJxj^KfTLX+#!*2BDS$r6lF8>52PBGOi3RJ7?DvEpp1%9#rsU1d=3GVWU z9eKF^wuqy0484;_ROD42v2!0#>c=_EeW#~YE4a}OMj3)nG$LLrtoT0%z=J3U=gE*i zSEwE!L6(2#Za-b@<--LUM~Rdp1dByheAvEK-9U-z%`+7dsM4+eY##8Ad-=?*O(jFJ zjE<@q+_E?u;JnKt5=|9ld4>}Z(+AK0Gh3vb01ZE?7(9Y9{d|UJ7)6e}R@y19FBmj% zd`dO1!fv#(2o`6_5fHIdC)&Cp=SGO4#V2?~SZx*iurkb}>sEj*9s`Hcn(6C}G5T@q zcLQwLR|}o82JbfuJi!A#h0|3OsPdHG`v4=taCV58z8ps)!dy;;=u17zPGn#AuQ@o& z4)CAhqZyxNd_pNlsqv6{vk&sUw13Zk`vDq?gx$}Bq%*khQP7R|cXhcJkbB3wo~6%& z@{-Ro?nD23`u_kHh<`+8G&E060NON(C69OcQ0##0#b-Otre<$*`u!<|^vQ48ILUY> zyWDe-f3)-#iy$pv-W=L9-3QeBc`F0v0O}bM? + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/pw-frontend/public/static/res/assets/leaves.jpg b/web/pw-frontend/public/static/res/assets/leaves.jpg new file mode 100644 index 0000000000000000000000000000000000000000..529b94e1221b38b2fc5792de76e8a32021375786 GIT binary patch literal 36902 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 + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/pw-frontend/public/static/res/assets/neptune.svg b/web/pw-frontend/public/static/res/assets/neptune.svg new file mode 100644 index 0000000..995d440 --- /dev/null +++ b/web/pw-frontend/public/static/res/assets/neptune.svg @@ -0,0 +1,186 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/pw-frontend/public/static/res/assets/ship.svg b/web/pw-frontend/public/static/res/assets/ship.svg new file mode 100644 index 0000000..ee202ff --- /dev/null +++ b/web/pw-frontend/public/static/res/assets/ship.svg @@ -0,0 +1,78 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/web/pw-frontend/public/static/res/assets/uranus.svg b/web/pw-frontend/public/static/res/assets/uranus.svg new file mode 100644 index 0000000..fb192a4 --- /dev/null +++ b/web/pw-frontend/public/static/res/assets/uranus.svg @@ -0,0 +1,150 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/pw-frontend/public/static/res/assets/venus.svg b/web/pw-frontend/public/static/res/assets/venus.svg new file mode 100644 index 0000000..3bebb10 --- /dev/null +++ b/web/pw-frontend/public/static/res/assets/venus.svg @@ -0,0 +1,114 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/pw-frontend/public/static/shaders/frag/image.glsl b/web/pw-frontend/public/static/shaders/frag/image.glsl new file mode 100644 index 0000000..69c8b91 --- /dev/null +++ b/web/pw-frontend/public/static/shaders/frag/image.glsl @@ -0,0 +1,14 @@ +#ifdef GL_ES +precision mediump float; +#endif + +// Passed in from the vertex shader. +varying vec2 v_texCoord; + +// The texture. +uniform sampler2D u_texture; + +void main() { + gl_FragColor = texture2D(u_texture, v_texCoord); +// gl_FragColor = vec4(0.7, 0.7, 0.0, 1.0); +} diff --git a/web/pw-frontend/public/static/shaders/frag/simple.glsl b/web/pw-frontend/public/static/shaders/frag/simple.glsl new file mode 100644 index 0000000..1292569 --- /dev/null +++ b/web/pw-frontend/public/static/shaders/frag/simple.glsl @@ -0,0 +1,15 @@ +#ifdef GL_ES +precision mediump float; +#endif + +uniform float u_step_interval; +uniform float u_time; +uniform vec3 u_color; +uniform vec3 u_color_next; + +void main() { + + vec3 color = mix(u_color, u_color_next, u_time); + + gl_FragColor = vec4(color, 1.0); +} diff --git a/web/pw-frontend/public/static/shaders/frag/vor.glsl b/web/pw-frontend/public/static/shaders/frag/vor.glsl new file mode 100644 index 0000000..faaa68f --- /dev/null +++ b/web/pw-frontend/public/static/shaders/frag/vor.glsl @@ -0,0 +1,18 @@ +#ifdef GL_ES +precision mediump float; +#endif + +#define PI 3.141592 + +uniform float u_step_interval; +uniform float u_time; +uniform bool u_vor; + +varying float v_intensity; +varying float v_dist; +varying vec3 v_color; +varying vec2 v_pos; + +void main() { + gl_FragColor = vec4(v_color, (1.0 - pow(1.0 - v_intensity, 1.23)) * 0.7); +} diff --git a/web/pw-frontend/public/static/shaders/vert/image.glsl b/web/pw-frontend/public/static/shaders/vert/image.glsl new file mode 100644 index 0000000..5dd3f56 --- /dev/null +++ b/web/pw-frontend/public/static/shaders/vert/image.glsl @@ -0,0 +1,33 @@ +#ifdef GL_ES +precision mediump float; +#endif + +attribute vec2 a_position; +attribute vec2 a_texCoord; + +uniform float u_time; + +uniform vec4 u_viewbox; // [x, y, width, height] +uniform vec2 u_resolution; +uniform mat3 u_trans; + +varying vec2 v_pos; +varying vec2 v_texCoord; + +void main() { + vec3 pos = vec3(a_position, 1.0); + + pos = u_trans * pos; + + vec2 uv = pos.xy; + + // Viewbox's center is top left, a_position's is in the center to the screen + // So translate and scale the viewbox** + uv -= u_viewbox.xy + (u_viewbox.zw * 0.5); + uv /= u_viewbox.zw * 0.5; + + v_pos = (uv.xy + 1.0) * 0.5; + + gl_Position = vec4(uv.xy, 0.0, 1.0); + v_texCoord = a_texCoord; +} diff --git a/web/pw-frontend/public/static/shaders/vert/simple.glsl b/web/pw-frontend/public/static/shaders/vert/simple.glsl new file mode 100644 index 0000000..935decf --- /dev/null +++ b/web/pw-frontend/public/static/shaders/vert/simple.glsl @@ -0,0 +1,36 @@ +#ifdef GL_ES +precision mediump float; +#endif + +attribute vec2 a_position; +attribute vec2 a_texCoord; + +uniform float u_time; + +uniform vec4 u_viewbox; // [x, y, width, height] +uniform vec2 u_resolution; +uniform mat3 u_trans; +uniform mat3 u_trans_next; + +varying vec2 v_pos; +varying vec2 v_texCoord; + +void main() { + vec3 pos = vec3(a_position, 1.0); + + mat3 trans = (u_trans_next * (1.0 - u_time)) + (u_trans * u_time); + + pos = trans * pos; + + vec2 uv = pos.xy; + + // Viewbox's center is top left, a_position's is in the center to the screen + // So translate and scale the viewbox** + uv -= u_viewbox.xy + (u_viewbox.zw * 0.5); + uv /= u_viewbox.zw * 0.5; + + v_pos = (uv.xy + 1.0) * 0.5; + + gl_Position = vec4(uv.xy, 0.0, 1.0); + v_texCoord = a_texCoord; +} diff --git a/web/pw-frontend/public/static/shaders/vert/vor.glsl b/web/pw-frontend/public/static/shaders/vert/vor.glsl new file mode 100644 index 0000000..75747c4 --- /dev/null +++ b/web/pw-frontend/public/static/shaders/vert/vor.glsl @@ -0,0 +1,43 @@ +#ifdef GL_ES +precision mediump float; +#endif + +attribute vec2 a_pos; +attribute vec2 a_center; +attribute float a_own; +attribute float a_intensity; + +uniform vec3 u_planet_colours[$PLANETS * 2]; +uniform vec4 u_viewbox; // [x, y, width, height] +uniform vec2 u_resolution; +uniform float u_time; + +varying float v_intensity; +varying float v_dist; +varying vec2 v_pos; +varying vec3 v_color; + +void main() { + v_intensity = a_intensity; + v_dist = distance(a_pos * u_resolution , a_center * u_resolution); + + int own = int(a_own); + + vec2 uv = a_pos; + + // Viewbox's center is top left, a_position's is in the center to the screen + // So translate and scale the viewbox** + uv -= u_viewbox.xy + (u_viewbox.zw * 0.5); + uv /= u_viewbox.zw * 0.5; + v_pos = uv.xy; + + // v_pos = (uv.xy + 1.0) * 0.5; + + if (own < 0) { + v_color = vec3(0., 0., 0.); + } else { + v_color = mix(u_planet_colours[own * 2], u_planet_colours[own * 2 + 1], u_time); + } + + gl_Position = vec4(uv.xy, 0.0, 1.0); +} diff --git a/web/pw-frontend/src/App.svelte b/web/pw-frontend/src/App.svelte new file mode 100644 index 0000000..614cf6f --- /dev/null +++ b/web/pw-frontend/src/App.svelte @@ -0,0 +1,47 @@ + + +
+ + + +
+ + diff --git a/web/pw-frontend/src/assets/svelte.png b/web/pw-frontend/src/assets/svelte.png new file mode 100644 index 0000000000000000000000000000000000000000..e673c91c7bcb0e3a1be737745af56990613a641e GIT binary patch literal 5185 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 literal 0 HcmV?d00001 diff --git a/web/pw-frontend/src/lib/Visualizer.svelte b/web/pw-frontend/src/lib/Visualizer.svelte new file mode 100644 index 0000000..35b0677 --- /dev/null +++ b/web/pw-frontend/src/lib/Visualizer.svelte @@ -0,0 +1,49 @@ + + +
+ +
+
+ +
+
0 / 0
+
+ Ms per frame:  + +
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+ + diff --git a/web/pw-frontend/src/lib/visualizer/LICENSE-MIT b/web/pw-frontend/src/lib/visualizer/LICENSE-MIT new file mode 100644 index 0000000..8d459d1 --- /dev/null +++ b/web/pw-frontend/src/lib/visualizer/LICENSE-MIT @@ -0,0 +1,25 @@ +Copyright (c) 2018 Arthur Vercruysse + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/web/pw-frontend/src/lib/visualizer/README.md b/web/pw-frontend/src/lib/visualizer/README.md new file mode 100644 index 0000000..aaba256 --- /dev/null +++ b/web/pw-frontend/src/lib/visualizer/README.md @@ -0,0 +1 @@ +Original by the amazing Arthur Vercruysse! \ No newline at end of file diff --git a/web/pw-frontend/src/lib/visualizer/index.html b/web/pw-frontend/src/lib/visualizer/index.html new file mode 100644 index 0000000..c2b2c33 --- /dev/null +++ b/web/pw-frontend/src/lib/visualizer/index.html @@ -0,0 +1,102 @@ + + + + + Hello wasm-pack! + + + + +
+ +
+ +
+
+ +
+
+ 0 / 0 +
+
+ Ms per frame:  + +
+
+ +
+
+
+ +
+
+ Option one +
+
+ Option two +
+
+ Option three +
+
+ Option three +
+
+ Option three +
+
+ Option three +
+
+ Option three +
+
+ Option three +
+
+ Option three +
+
+ Option three +
+ Option three +
+
+ Option three +
+
+ Option three +
+
+ Option three +
+
+ Option three +
+
+ Option three +
+ Option three +
+
+ Option three +
+
+ +
+ + + + + + + diff --git a/web/pw-frontend/src/lib/visualizer/index.ts b/web/pw-frontend/src/lib/visualizer/index.ts new file mode 100644 index 0000000..363a1c5 --- /dev/null +++ b/web/pw-frontend/src/lib/visualizer/index.ts @@ -0,0 +1,666 @@ +import { Game } from "planetwars-rs"; +// import { memory } from "planetwars-rs/planetwars_rs_bg"; +// const memory = planetwars_bg.memory; +import type { Dictionary } from './webgl/util'; +import type { BBox } from "./voronoi/voronoi-core"; + +import { + Resizer, + resizeCanvasToDisplaySize, + FPSCounter, + url_to_mesh, + Mesh, +} from "./webgl/util"; +import { + Shader, + Uniform4f, + Uniform3fv, + Uniform1f, + Uniform2f, + ShaderFactory, + Uniform3f, + UniformMatrix3fv, + UniformBool, +} from "./webgl/shader"; +import { Renderer } from "./webgl/renderer"; +import { VertexBuffer, IndexBuffer } from "./webgl/buffer"; +import { VertexBufferLayout, VertexArray } from "./webgl/vertexBufferLayout"; +import { defaultLabelFactory, LabelFactory, Align, Label } from "./webgl/text"; +import { VoronoiBuilder } from "./voronoi/voronoi"; + +// svg-mesh requires global to exist +(window as any).global = window; + + + +function to_bbox(box: number[]): BBox { + return { + xl: box[0], + xr: box[0] + box[2], + yt: box[1], + yb: box[1] + box[3], + }; +} + +// function f32v(ptr: number, size: number): Float32Array { +// return new Float32Array(memory.buffer, ptr, size); +// } + +// function i32v(ptr: number, size: number): Int32Array { +// return new Int32Array(memory.buffer, ptr, size); +// } + +export function set_game_name(name: string) { + ELEMENTS["name"].innerHTML = name; +} + +export function set_loading(loading: boolean) { + if (loading) { + if (!ELEMENTS["main"].classList.contains("loading")) { + ELEMENTS["main"].classList.add("loading"); + } + } else { + ELEMENTS["main"].classList.remove("loading"); + } +} + +const ELEMENTS: any = {}; +var CANVAS: any; +var RESOLUTION: any; +var GL: any; +var ms_per_frame: any; + +const LAYERS = { + vor: -1, // Background + planet: 1, + planet_label: 2, + ship: 3, + ship_label: 4, +}; + +const COUNTER = new FPSCounter(); + + + +export function init() { + [ + "name", + "turnCounter", + "main", + "turnSlider", + "fileselect", + "speed", + "canvas", + ].forEach((n) => (ELEMENTS[n] = document.getElementById(n))); + + CANVAS = ELEMENTS["canvas"]; + RESOLUTION = [CANVAS.width, CANVAS.height]; + + ms_per_frame = parseInt(ELEMENTS["speed"].value); + + GL = CANVAS.getContext("webgl"); + + GL.clearColor(0, 0, 0, 1); + GL.clear(GL.COLOR_BUFFER_BIT); + + GL.enable(GL.BLEND); + GL.blendFunc(GL.SRC_ALPHA, GL.ONE_MINUS_SRC_ALPHA); + + window.addEventListener( + "resize", + function () { + resizeCanvasToDisplaySize(CANVAS); + + if (game_instance) { + game_instance.on_resize(); + } + }, + { capture: false, passive: true } + ); + + ELEMENTS["turnSlider"].oninput = function () { + if (game_instance) { + game_instance.updateTurn(parseInt(ELEMENTS["turnSlider"].value)); + } + }; + + ELEMENTS["speed"].onchange = function () { + ms_per_frame = parseInt(ELEMENTS["speed"].value); + }; +} + +export class GameInstance { + resizer: Resizer; + game: Game; + + shader: Shader; + vor_shader: Shader; + image_shader: Shader; + + text_factory: LabelFactory; + planet_labels: Label[]; + ship_labels: Label[]; + + ship_ibo: IndexBuffer; + ship_vao: VertexArray; + // TODO: find a better way + max_num_ships: number; + + renderer: Renderer; + planet_count: number; + + vor_builder: VoronoiBuilder; + + vor_counter = 3; + use_vor = true; + playing = true; + time_stopped_delta = 0; + last_time = 0; + frame = -1; + + turn_count = 0; + + constructor( + game: Game, + meshes: Mesh[], + ship_mesh: Mesh, + shaders: Dictionary + ) { + this.game = game; + const planets = game.get_planets(); + this.planet_count = planets.length; + + this.shader = shaders["normal"].create_shader(GL, { + MAX_CIRCLES: "" + planets.length, + }); + this.image_shader = shaders["image"].create_shader(GL); + this.vor_shader = shaders["vor"].create_shader(GL, { + PLANETS: "" + planets.length, + }); + + this.text_factory = defaultLabelFactory(GL, this.image_shader); + this.planet_labels = []; + this.ship_labels = []; + + this.resizer = new Resizer(CANVAS, [...game.get_viewbox()], true); + this.renderer = new Renderer(); + this.game.update_turn(0); + + // Setup key handling + document.addEventListener("keydown", this.handleKey.bind(this)); + + // List of [(x, y, r)] for all planets + this._create_voronoi(planets); + this._create_planets(planets, meshes); + + // create_shipes + this.ship_ibo = new IndexBuffer(GL, ship_mesh.cells); + const ship_positions = new VertexBuffer(GL, ship_mesh.positions); + const ship_layout = new VertexBufferLayout(); + ship_layout.push(GL.FLOAT, 3, 4, "a_position"); + this.ship_vao = new VertexArray(); + this.ship_vao.addBuffer(ship_positions, ship_layout); + this.max_num_ships = 0; + + // Set slider correctly + this.turn_count = game.turn_count(); + ELEMENTS["turnSlider"].max = this.turn_count - 1 + ""; + } + + push_state(state: string) { + this.game.push_state(state); + + if (this.frame == this.turn_count - 1) { + this.playing = true; + } + + // Set slider correctly + this.turn_count = this.game.turn_count(); + this.updateTurnCounters(); + } + + _create_voronoi(planets: Float32Array) { + const planet_points = []; + for (let i = 0; i < planets.length; i += 3) { + planet_points.push({ x: -planets[i], y: -planets[i + 1] }); + } + + const bbox = to_bbox(this.resizer.get_viewbox()); + + this.vor_builder = new VoronoiBuilder( + GL, + this.vor_shader, + planet_points, + bbox + ); + this.renderer.addRenderable(this.vor_builder.getRenderable(), LAYERS.vor); + } + + _create_planets(planets: Float32Array, meshes: Mesh[]) { + for (let i = 0; i < this.planet_count; i++) { + { + const transform = new UniformMatrix3fv([ + 1, + 0, + 0, + 0, + 1, + 0, + -planets[i * 3], + -planets[i * 3 + 1], + 1, + ]); + + const indexBuffer = new IndexBuffer( + GL, + meshes[i % meshes.length].cells + ); + const positionBuffer = new VertexBuffer( + GL, + meshes[i % meshes.length].positions + ); + + const layout = new VertexBufferLayout(); + layout.push(GL.FLOAT, 3, 4, "a_position"); + const vao = new VertexArray(); + vao.addBuffer(positionBuffer, layout); + + this.renderer.addToDraw( + indexBuffer, + vao, + this.shader, + { + u_trans: transform, + u_trans_next: transform, + }, + [], + LAYERS.planet + ); + } + + { + const transform = new UniformMatrix3fv([ + 1, + 0, + 0, + 0, + 1, + 0, + -planets[i * 3], + -planets[i * 3 + 1] - 1.2, + 1, + ]); + + const label = this.text_factory.build(GL, transform); + this.planet_labels.push(label); + this.renderer.addRenderable(label.getRenderable(), LAYERS.planet_label); + } + } + } + + on_resize() { + this.resizer = new Resizer(CANVAS, [...this.game.get_viewbox()], true); + const bbox = to_bbox(this.resizer.get_viewbox()); + this.vor_builder.resize(GL, bbox); + } + + _update_state() { + this._update_planets(); + this._update_ships(); + } + + _update_planets() { + const colours = this.game.get_planet_colors(); + const planet_ships = this.game.get_planet_ships(); + + this.vor_shader.uniform(GL, "u_planet_colours", new Uniform3fv(colours)); + + for (let i = 0; i < this.planet_count; i++) { + const u = new Uniform3f( + colours[i * 6], + colours[i * 6 + 1], + colours[i * 6 + 2] + ); + this.renderer.updateUniform( + i, + (us) => (us["u_color"] = u), + LAYERS.planet + ); + const u2 = new Uniform3f( + colours[i * 6 + 3], + colours[i * 6 + 4], + colours[i * 6 + 5] + ); + this.renderer.updateUniform( + i, + (us) => (us["u_color_next"] = u2), + LAYERS.planet + ); + + this.planet_labels[i].setText( + GL, + "*" + planet_ships[i], + Align.Middle, + Align.Begin + ); + } + } + + _update_ships() { + const ships = this.game.get_ship_locations(); + const labels = this.game.get_ship_label_locations(); + const ship_counts = this.game.get_ship_counts(); + const ship_colours = this.game.get_ship_colours(); + + for (let i = this.max_num_ships; i < ship_counts.length; i++) { + this.renderer.addToDraw( + this.ship_ibo, + this.ship_vao, + this.shader, + {}, + [], + LAYERS.ship + ); + + const label = this.text_factory.build(GL); + this.ship_labels.push(label); + this.renderer.addRenderable(label.getRenderable(), LAYERS.ship_label); + } + if (ship_counts.length > this.max_num_ships) + this.max_num_ships = ship_counts.length; + + // TODO: actually remove obsolete ships + for (let i = 0; i < this.max_num_ships; i++) { + if (i < ship_counts.length) { + this.ship_labels[i].setText( + GL, + "" + ship_counts[i], + Align.Middle, + Align.Middle + ); + + this.renderer.enableRenderable(i, LAYERS.ship); + this.renderer.enableRenderable(i, LAYERS.ship_label); + + const u = new Uniform3f( + ship_colours[i * 3], + ship_colours[i * 3 + 1], + ship_colours[i * 3 + 2] + ); + + const t1 = new UniformMatrix3fv(ships.slice(i * 18, i * 18 + 9)); + const t2 = new UniformMatrix3fv(ships.slice(i * 18 + 9, i * 18 + 18)); + + const tl1 = new UniformMatrix3fv(labels.slice(i * 18, i * 18 + 9)); + const tl2 = new UniformMatrix3fv(labels.slice(i * 18 + 9, i * 18 + 18)); + + this.renderer.updateUniform( + i, + (us) => { + us["u_color"] = u; + us["u_color_next"] = u; + us["u_trans"] = t1; + us["u_trans_next"] = t2; + }, + LAYERS.ship + ); + + this.renderer.updateUniform( + i, + (us) => { + us["u_trans"] = tl1; + us["u_trans_next"] = tl2; + }, + LAYERS.ship_label + ); + } else { + this.renderer.disableRenderable(i, LAYERS.ship); + this.renderer.disableRenderable(i, LAYERS.ship_label); + } + } + } + + render(time: number) { + COUNTER.frame(time); + + if (COUNTER.delta(time) < 30) { + this.vor_counter = Math.min(3, this.vor_counter + 1); + } else { + this.vor_counter = Math.max(-3, this.vor_counter - 1); + } + + if (this.vor_counter < -2) { + this.use_vor = false; + } + + // If not playing, still reder with different viewbox, so people can still pan etc. + if (!this.playing) { + this.last_time = time; + + this.shader.uniform( + GL, + "u_viewbox", + new Uniform4f(this.resizer.get_viewbox()) + ); + this.vor_shader.uniform( + GL, + "u_viewbox", + new Uniform4f(this.resizer.get_viewbox()) + ); + this.image_shader.uniform( + GL, + "u_viewbox", + new Uniform4f(this.resizer.get_viewbox()) + ); + + this.renderer.render(GL); + return; + } + + // Check if turn is still correct + if (time > this.last_time + ms_per_frame) { + this.last_time = time; + this.updateTurn(this.frame + 1); + if (this.frame == this.turn_count - 1) { + this.playing = false; + } + } + + // Do GL things + GL.bindFramebuffer(GL.FRAMEBUFFER, null); + GL.viewport(0, 0, GL.canvas.width, GL.canvas.height); + GL.clear(GL.COLOR_BUFFER_BIT | GL.DEPTH_BUFFER_BIT); + + this.vor_shader.uniform( + GL, + "u_time", + new Uniform1f((time - this.last_time) / ms_per_frame) + ); + this.vor_shader.uniform( + GL, + "u_viewbox", + new Uniform4f(this.resizer.get_viewbox()) + ); + this.vor_shader.uniform(GL, "u_resolution", new Uniform2f(RESOLUTION)); + this.vor_shader.uniform(GL, "u_vor", new UniformBool(this.use_vor)); + + this.shader.uniform( + GL, + "u_time", + new Uniform1f((time - this.last_time) / ms_per_frame) + ); + this.shader.uniform( + GL, + "u_mouse", + new Uniform2f(this.resizer.get_mouse_pos()) + ); + this.shader.uniform( + GL, + "u_viewbox", + new Uniform4f(this.resizer.get_viewbox()) + ); + this.shader.uniform(GL, "u_resolution", new Uniform2f(RESOLUTION)); + + this.image_shader.uniform( + GL, + "u_time", + new Uniform1f((time - this.last_time) / ms_per_frame) + ); + this.image_shader.uniform( + GL, + "u_mouse", + new Uniform2f(this.resizer.get_mouse_pos()) + ); + this.image_shader.uniform( + GL, + "u_viewbox", + new Uniform4f(this.resizer.get_viewbox()) + ); + this.image_shader.uniform(GL, "u_resolution", new Uniform2f(RESOLUTION)); + + // Render + this.renderer.render(GL); + + COUNTER.frame_end(); + } + + updateTurn(turn: number) { + this.frame = Math.max(0, turn); + const new_frame = this.game.update_turn(this.frame); + if (new_frame < this.frame) { + this.frame = new_frame; + this.playing = false; + } else { + this._update_state(); + this.playing = true; + } + + this.updateTurnCounters(); + } + + updateTurnCounters() { + ELEMENTS["turnCounter"].innerHTML = + this.frame + " / " + (this.turn_count - 1); + ELEMENTS["turnSlider"].value = this.frame + ""; + ELEMENTS["turnSlider"].max = this.turn_count - 1 + ""; + } + + handleKey(event: KeyboardEvent) { + // Space + if (event.keyCode == 32) { + if (this.playing) { + this.playing = false; + } else { + this.playing = true; + } + } + + // Arrow left + if (event.keyCode == 37) { + // This feels more natural than -1 what it should be, I think + this.updateTurn(this.frame - 2); + } + + // Arrow right + if (event.keyCode == 39) { + this.updateTurn(this.frame + 1); + } + + // d key + if (event.keyCode == 68) { + ELEMENTS["speed"].value = ms_per_frame + 10 + ""; + ELEMENTS["speed"].onchange(undefined); + } + + // a key + if (event.keyCode == 65) { + ELEMENTS["speed"].value = Math.max(ms_per_frame - 10, 0) + ""; + ELEMENTS["speed"].onchange(undefined); + } + } +} + +var game_instance: GameInstance; +var meshes: Mesh[]; +var shaders: Dictionary; + +export async function set_instance(source: string): Promise { + if (!meshes || !shaders) { + const mesh_promises = ["ship.svg", "earth.svg", "mars.svg", "venus.svg"] + .map((name) => "/static/res/assets/" + name) + .map(url_to_mesh); + + const shader_promies = [ + (async () => + <[string, ShaderFactory]>[ + "normal", + await ShaderFactory.create_factory( + "/static/shaders/frag/simple.glsl", + "/static/shaders/vert/simple.glsl" + ), + ])(), + (async () => + <[string, ShaderFactory]>[ + "vor", + await ShaderFactory.create_factory( + "/static/shaders/frag/vor.glsl", + "/static/shaders/vert/vor.glsl" + ), + ])(), + (async () => + <[string, ShaderFactory]>[ + "image", + await ShaderFactory.create_factory( + "/static/shaders/frag/image.glsl", + "/static/shaders/vert/simple.glsl" + ), + ])(), + ]; + let shaders_array: [string, ShaderFactory][]; + [meshes, shaders_array] = await Promise.all([ + Promise.all(mesh_promises), + Promise.all(shader_promies), + ]); + + shaders = {}; + shaders_array.forEach(([name, fac]) => (shaders[name] = fac)); + } + + resizeCanvasToDisplaySize(CANVAS); + + game_instance = new GameInstance( + Game.new(source), + meshes.slice(1), + meshes[0], + shaders + ); + + set_loading(false); + start(); + return game_instance; +} + +var _animating = false; + +export function start() { + if (_animating) { + // already running + return; + } + _animating = true; + requestAnimationFrame(step); +} + +export function stop() { + _animating = false; +} + +function step(time: number) { + if (game_instance) { + game_instance.render(time); + } + + if (_animating) { + requestAnimationFrame(step); + } +} diff --git a/web/pw-frontend/src/lib/visualizer/src/games.ts b/web/pw-frontend/src/lib/visualizer/src/games.ts new file mode 100644 index 0000000..4b9e7e2 --- /dev/null +++ b/web/pw-frontend/src/lib/visualizer/src/games.ts @@ -0,0 +1,47 @@ + +import { set_game_name, set_loading, set_instance } from '../index' + +var game_name, game_file; + +document.getElementById("addbutton").onclick = function () { + const loc = window.location; + const query = `?game=${game_file}&name=${game_name}`; + navigator.clipboard.writeText(loc.origin + loc.pathname + encodeURI(query)).then(() => { + console.log("Success"); + }, () => { + console.log("Failed"); + }); +} + +async function on_load() { + const options = document.getElementsByClassName("options"); + const urlVars = new URLSearchParams(window.location.search); + + if (urlVars.get("game") && urlVars.get("name")) { + console.log(urlVars.get("game") + ' ' + urlVars.get("name")) + handle(urlVars.get("game"), urlVars.get("name")) + } else if (options[0]) { + const options_div = options[0]; + if (options_div.children[0]) { + options_div.children[0].dispatchEvent(new Event('click')); + } + } +} + +window.addEventListener("load", on_load, false); + +export function handle(location: string, name: string) { + game_file = location; + game_name = name; + + set_loading(true); + + fetch(location) + .then((r) => r.text()) + .then((response) => { + set_instance(response); + set_game_name(name); + }).catch(console.error); +} + +on_load(); diff --git a/web/pw-frontend/src/lib/visualizer/style.css b/web/pw-frontend/src/lib/visualizer/style.css new file mode 100644 index 0000000..8c5119e --- /dev/null +++ b/web/pw-frontend/src/lib/visualizer/style.css @@ -0,0 +1,309 @@ + * { + margin: 0; + padding: 0; + } + + body { + background-color: #333; + } + + p { + padding: 3px 0; + } + + #wrapper { + max-height: 100%; + min-height: 100%; + width: 100%; + height: 100%; + display: flex; + } + + #main { + height: 100%; + flex-grow: 1; + position: relative; + } + + #name { + position: absolute; + top: 10px; + left: 10px; + color: white; + } + + #meta { + padding: 10px 2%; + color: white; + position: absolute; + bottom: 0; + width: 96%; + } + + .options { + background-color: black; + max-width: 300px; + width: 20%; + min-width: 150px; + height: 100%; + display: flex; + flex-direction: column; + overflow-y: auto; + } + + .option { + color: white; + margin: 0 0 20px 0; + padding: 10px; + background-color: #333; + } + + .option:last-child { + margin: 0; + } + + .option:hover { + background-color: #777; + } + + .lds-roller { + display: none; + } + + .loading>.lds-roller { + display: inline-block; + } + + .lds-roller { + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + width: 80px; + height: 80px; + } + + .lds-roller div { + animation: lds-roller 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite; + transform-origin: 40px 40px; + } + + .lds-roller div:after { + content: " "; + display: block; + position: absolute; + width: 7px; + height: 7px; + border-radius: 50%; + background: #fff; + margin: -4px 0 0 -4px; + } + + .lds-roller div:nth-child(1) { + animation-delay: -0.036s; + } + + .lds-roller div:nth-child(1):after { + top: 63px; + left: 63px; + } + + .lds-roller div:nth-child(2) { + animation-delay: -0.072s; + } + + .lds-roller div:nth-child(2):after { + top: 68px; + left: 56px; + } + + .lds-roller div:nth-child(3) { + animation-delay: -0.108s; + } + + .lds-roller div:nth-child(3):after { + top: 71px; + left: 48px; + } + + .lds-roller div:nth-child(4) { + animation-delay: -0.144s; + } + + .lds-roller div:nth-child(4):after { + top: 72px; + left: 40px; + } + + .lds-roller div:nth-child(5) { + animation-delay: -0.18s; + } + + .lds-roller div:nth-child(5):after { + top: 71px; + left: 32px; + } + + .lds-roller div:nth-child(6) { + animation-delay: -0.216s; + } + + .lds-roller div:nth-child(6):after { + top: 68px; + left: 24px; + } + + .lds-roller div:nth-child(7) { + animation-delay: -0.252s; + } + + .lds-roller div:nth-child(7):after { + top: 63px; + left: 17px; + } + + .lds-roller div:nth-child(8) { + animation-delay: -0.288s; + } + + .lds-roller div:nth-child(8):after { + top: 56px; + left: 12px; + } + + @keyframes lds-roller { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } + } + + #speed { + width: 100px; + } + + #canvas { + position: relative; + background-color: black; + width: 100%; + height: 100%; + } + + .button { + position: absolute; + top: 10px; + right: 10px; + width: 0; + height: 0; + } + + .button::before { + content: ""; + display: block; + float: left; + margin: 0 -20px 0 0; + font-family: 'fontawesome'; + content: "\f1e1"; + transform: translate(-1em, 0px); + color: antiquewhite; + font-size: 1.5em; + } + + .button:hover::before { + color: #ff7f00; + } + /* ---------------------------------------------- + * Generated by Animista on 2019-9-17 14:35:13 + * Licensed under FreeBSD License. + * See http://animista.net/license for more info. + * w: http://animista.net, t: @cssanimista + * ---------------------------------------------- */ + /** + * ---------------------------------------- + * animation slide-top + * ---------------------------------------- + */ + + @-webkit-keyframes slide-top { + 0% { + -webkit-transform: translate(-50%, 50%); + transform: translate(-50%, 50%); + } + 100% { + -webkit-transform: translate(-50%, -150%); + transform: translate(-50%, -150%); + } + } + + @keyframes slide-top { + 0% { + -webkit-transform: translate(-50%, 50%); + transform: translate(-50%, 50%); + } + 100% { + -webkit-transform: translate(-50%, -150%); + transform: translate(-50%, -150%); + } + } + /** + * ---------------------------------------- + * Copy from https://www.w3schools.com/howto/howto_js_rangeslider.asp + * ---------------------------------------- + */ + + .slidecontainer { + margin-top: 10px; + width: 100%; + /* Width of the outside container */ + } + + .slider { + -webkit-appearance: none; + width: 100%; + height: 15px; + border-radius: 5px; + background: #d3d3d3; + outline: none; + opacity: 0.7; + -webkit-transition: .2s; + transition: opacity .2s; + } + + .slider::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 25px; + height: 25px; + border-radius: 50%; + background: #ff7000; + cursor: pointer; + } + + .slider::-moz-range-thumb { + width: 25px; + height: 25px; + border-radius: 50%; + background: #ff7000; + cursor: pointer; + } + + ::-webkit-scrollbar-track { + -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); + box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); + border-radius: 10px; + background-color: #444; + border-radius: 10px; + } + + ::-webkit-scrollbar { + width: 10px; + background-color: #444; + } + + ::-webkit-scrollbar-thumb { + border-radius: 10px; + background-color: #F90; + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .2) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .2) 50%, rgba(255, 255, 255, .2) 75%, transparent 75%, transparent) + } \ No newline at end of file diff --git a/web/pw-frontend/src/lib/visualizer/voronoi/voronoi-core.d.ts b/web/pw-frontend/src/lib/visualizer/voronoi/voronoi-core.d.ts new file mode 100644 index 0000000..e908fbb --- /dev/null +++ b/web/pw-frontend/src/lib/visualizer/voronoi/voronoi-core.d.ts @@ -0,0 +1,56 @@ + +declare namespace Voronoi { + class Point { + x: number; + y: number; + } + + class Site { + x: number; + y: number; + voronoiId: number; + } + + class Cell { + site: Site; + halfedges: HalfEdge[]; + closeMe: boolean; + } + + class Edge { + lSite: Site; + rSite: Site; + vb: Point; + va: Point; + } + + class HalfEdge { + site: Site; + edge: Edge; + angle: number; + getStartpoint(): Point; + getEndpoint(): Point; + } + + class BBox { + xl: number; + xr: number; + yt: number; + yb: number; + } + + class VoronoiDiagram { + site: any; + cells: Cell[]; + edges: Edge[]; + vertices: Point[]; + execTime: number; + } +} + +declare class Voronoi { + constructor(); + compute(sites: Voronoi.Point[], bbox: Voronoi.BBox): Voronoi.VoronoiDiagram; +} + +export = Voronoi; diff --git a/web/pw-frontend/src/lib/visualizer/voronoi/voronoi-core.js b/web/pw-frontend/src/lib/visualizer/voronoi/voronoi-core.js new file mode 100644 index 0000000..9dcc5b3 --- /dev/null +++ b/web/pw-frontend/src/lib/visualizer/voronoi/voronoi-core.js @@ -0,0 +1,1726 @@ +/*! +Copyright (C) 2010-2013 Raymond Hill: https://github.com/gorhill/Javascript-Voronoi +MIT License: See https://github.com/gorhill/Javascript-Voronoi/LICENSE.md +*/ +/* +Author: Raymond Hill (rhill@raymondhill.net) +Contributor: Jesse Morgan (morgajel@gmail.com) +File: rhill-voronoi-core.js +Version: 0.98 +Date: January 21, 2013 +Description: This is my personal Javascript implementation of +Steven Fortune's algorithm to compute Voronoi diagrams. + +License: See https://github.com/gorhill/Javascript-Voronoi/LICENSE.md +Credits: See https://github.com/gorhill/Javascript-Voronoi/CREDITS.md +History: See https://github.com/gorhill/Javascript-Voronoi/CHANGELOG.md + +## Usage: + + var sites = [{x:300,y:300}, {x:100,y:100}, {x:200,y:500}, {x:250,y:450}, {x:600,y:150}]; + // xl, xr means x left, x right + // yt, yb means y top, y bottom + var bbox = {xl:0, xr:800, yt:0, yb:600}; + var voronoi = new Voronoi(); + // pass an object which exhibits xl, xr, yt, yb properties. The bounding + // box will be used to connect unbound edges, and to close open cells + result = voronoi.compute(sites, bbox); + // render, further analyze, etc. + +Return value: + An object with the following properties: + + result.vertices = an array of unordered, unique Voronoi.Vertex objects making + up the Voronoi diagram. + result.edges = an array of unordered, unique Voronoi.Edge objects making up + the Voronoi diagram. + result.cells = an array of Voronoi.Cell object making up the Voronoi diagram. + A Cell object might have an empty array of halfedges, meaning no Voronoi + cell could be computed for a particular cell. + result.execTime = the time it took to compute the Voronoi diagram, in + milliseconds. + +Voronoi.Vertex object: + x: The x position of the vertex. + y: The y position of the vertex. + +Voronoi.Edge object: + lSite: the Voronoi site object at the left of this Voronoi.Edge object. + rSite: the Voronoi site object at the right of this Voronoi.Edge object (can + be null). + va: an object with an 'x' and a 'y' property defining the start point + (relative to the Voronoi site on the left) of this Voronoi.Edge object. + vb: an object with an 'x' and a 'y' property defining the end point + (relative to Voronoi site on the left) of this Voronoi.Edge object. + + For edges which are used to close open cells (using the supplied bounding + box), the rSite property will be null. + +Voronoi.Cell object: + site: the Voronoi site object associated with the Voronoi cell. + halfedges: an array of Voronoi.Halfedge objects, ordered counterclockwise, + defining the polygon for this Voronoi cell. + +Voronoi.Halfedge object: + site: the Voronoi site object owning this Voronoi.Halfedge object. + edge: a reference to the unique Voronoi.Edge object underlying this + Voronoi.Halfedge object. + getStartpoint(): a method returning an object with an 'x' and a 'y' property + for the start point of this halfedge. Keep in mind halfedges are always + countercockwise. + getEndpoint(): a method returning an object with an 'x' and a 'y' property + for the end point of this halfedge. Keep in mind halfedges are always + countercockwise. + +TODO: Identify opportunities for performance improvement. + +TODO: Let the user close the Voronoi cells, do not do it automatically. Not only let + him close the cells, but also allow him to close more than once using a different + bounding box for the same Voronoi diagram. +*/ + +/*global Math */ + +// --------------------------------------------------------------------------- + +function Voronoi() { + this.vertices = null; + this.edges = null; + this.cells = null; + this.toRecycle = null; + this.beachsectionJunkyard = []; + this.circleEventJunkyard = []; + this.vertexJunkyard = []; + this.edgeJunkyard = []; + this.cellJunkyard = []; + } + +// --------------------------------------------------------------------------- + +Voronoi.prototype.reset = function() { + if (!this.beachline) { + this.beachline = new this.RBTree(); + } + // Move leftover beachsections to the beachsection junkyard. + if (this.beachline.root) { + var beachsection = this.beachline.getFirst(this.beachline.root); + while (beachsection) { + this.beachsectionJunkyard.push(beachsection); // mark for reuse + beachsection = beachsection.rbNext; + } + } + this.beachline.root = null; + if (!this.circleEvents) { + this.circleEvents = new this.RBTree(); + } + this.circleEvents.root = this.firstCircleEvent = null; + this.vertices = []; + this.edges = []; + this.cells = []; + }; + +Voronoi.prototype.sqrt = Math.sqrt; +Voronoi.prototype.abs = Math.abs; +Voronoi.prototype.ε = Voronoi.ε = 1e-9; +Voronoi.prototype.invε = Voronoi.invε = 1.0 / Voronoi.ε; +Voronoi.prototype.equalWithEpsilon = function(a,b){return this.abs(a-b)<1e-9;}; +Voronoi.prototype.greaterThanWithEpsilon = function(a,b){return a-b>1e-9;}; +Voronoi.prototype.greaterThanOrEqualWithEpsilon = function(a,b){return b-a<1e-9;}; +Voronoi.prototype.lessThanWithEpsilon = function(a,b){return b-a>1e-9;}; +Voronoi.prototype.lessThanOrEqualWithEpsilon = function(a,b){return a-b<1e-9;}; + +// --------------------------------------------------------------------------- +// Red-Black tree code (based on C version of "rbtree" by Franck Bui-Huu +// https://github.com/fbuihuu/libtree/blob/master/rb.c + +Voronoi.prototype.RBTree = function() { + this.root = null; + }; + +Voronoi.prototype.RBTree.prototype.rbInsertSuccessor = function(node, successor) { + var parent; + if (node) { + // >>> rhill 2011-05-27: Performance: cache previous/next nodes + successor.rbPrevious = node; + successor.rbNext = node.rbNext; + if (node.rbNext) { + node.rbNext.rbPrevious = successor; + } + node.rbNext = successor; + // <<< + if (node.rbRight) { + // in-place expansion of node.rbRight.getFirst(); + node = node.rbRight; + while (node.rbLeft) {node = node.rbLeft;} + node.rbLeft = successor; + } + else { + node.rbRight = successor; + } + parent = node; + } + // rhill 2011-06-07: if node is null, successor must be inserted + // to the left-most part of the tree + else if (this.root) { + node = this.getFirst(this.root); + // >>> Performance: cache previous/next nodes + successor.rbPrevious = null; + successor.rbNext = node; + node.rbPrevious = successor; + // <<< + node.rbLeft = successor; + parent = node; + } + else { + // >>> Performance: cache previous/next nodes + successor.rbPrevious = successor.rbNext = null; + // <<< + this.root = successor; + parent = null; + } + successor.rbLeft = successor.rbRight = null; + successor.rbParent = parent; + successor.rbRed = true; + // Fixup the modified tree by recoloring nodes and performing + // rotations (2 at most) hence the red-black tree properties are + // preserved. + var grandpa, uncle; + node = successor; + while (parent && parent.rbRed) { + grandpa = parent.rbParent; + if (parent === grandpa.rbLeft) { + uncle = grandpa.rbRight; + if (uncle && uncle.rbRed) { + parent.rbRed = uncle.rbRed = false; + grandpa.rbRed = true; + node = grandpa; + } + else { + if (node === parent.rbRight) { + this.rbRotateLeft(parent); + node = parent; + parent = node.rbParent; + } + parent.rbRed = false; + grandpa.rbRed = true; + this.rbRotateRight(grandpa); + } + } + else { + uncle = grandpa.rbLeft; + if (uncle && uncle.rbRed) { + parent.rbRed = uncle.rbRed = false; + grandpa.rbRed = true; + node = grandpa; + } + else { + if (node === parent.rbLeft) { + this.rbRotateRight(parent); + node = parent; + parent = node.rbParent; + } + parent.rbRed = false; + grandpa.rbRed = true; + this.rbRotateLeft(grandpa); + } + } + parent = node.rbParent; + } + this.root.rbRed = false; + }; + +Voronoi.prototype.RBTree.prototype.rbRemoveNode = function(node) { + // >>> rhill 2011-05-27: Performance: cache previous/next nodes + if (node.rbNext) { + node.rbNext.rbPrevious = node.rbPrevious; + } + if (node.rbPrevious) { + node.rbPrevious.rbNext = node.rbNext; + } + node.rbNext = node.rbPrevious = null; + // <<< + var parent = node.rbParent, + left = node.rbLeft, + right = node.rbRight, + next; + if (!left) { + next = right; + } + else if (!right) { + next = left; + } + else { + next = this.getFirst(right); + } + if (parent) { + if (parent.rbLeft === node) { + parent.rbLeft = next; + } + else { + parent.rbRight = next; + } + } + else { + this.root = next; + } + // enforce red-black rules + var isRed; + if (left && right) { + isRed = next.rbRed; + next.rbRed = node.rbRed; + next.rbLeft = left; + left.rbParent = next; + if (next !== right) { + parent = next.rbParent; + next.rbParent = node.rbParent; + node = next.rbRight; + parent.rbLeft = node; + next.rbRight = right; + right.rbParent = next; + } + else { + next.rbParent = parent; + parent = next; + node = next.rbRight; + } + } + else { + isRed = node.rbRed; + node = next; + } + // 'node' is now the sole successor's child and 'parent' its + // new parent (since the successor can have been moved) + if (node) { + node.rbParent = parent; + } + // the 'easy' cases + if (isRed) {return;} + if (node && node.rbRed) { + node.rbRed = false; + return; + } + // the other cases + var sibling; + do { + if (node === this.root) { + break; + } + if (node === parent.rbLeft) { + sibling = parent.rbRight; + if (sibling.rbRed) { + sibling.rbRed = false; + parent.rbRed = true; + this.rbRotateLeft(parent); + sibling = parent.rbRight; + } + if ((sibling.rbLeft && sibling.rbLeft.rbRed) || (sibling.rbRight && sibling.rbRight.rbRed)) { + if (!sibling.rbRight || !sibling.rbRight.rbRed) { + sibling.rbLeft.rbRed = false; + sibling.rbRed = true; + this.rbRotateRight(sibling); + sibling = parent.rbRight; + } + sibling.rbRed = parent.rbRed; + parent.rbRed = sibling.rbRight.rbRed = false; + this.rbRotateLeft(parent); + node = this.root; + break; + } + } + else { + sibling = parent.rbLeft; + if (sibling.rbRed) { + sibling.rbRed = false; + parent.rbRed = true; + this.rbRotateRight(parent); + sibling = parent.rbLeft; + } + if ((sibling.rbLeft && sibling.rbLeft.rbRed) || (sibling.rbRight && sibling.rbRight.rbRed)) { + if (!sibling.rbLeft || !sibling.rbLeft.rbRed) { + sibling.rbRight.rbRed = false; + sibling.rbRed = true; + this.rbRotateLeft(sibling); + sibling = parent.rbLeft; + } + sibling.rbRed = parent.rbRed; + parent.rbRed = sibling.rbLeft.rbRed = false; + this.rbRotateRight(parent); + node = this.root; + break; + } + } + sibling.rbRed = true; + node = parent; + parent = parent.rbParent; + } while (!node.rbRed); + if (node) {node.rbRed = false;} + }; + +Voronoi.prototype.RBTree.prototype.rbRotateLeft = function(node) { + var p = node, + q = node.rbRight, // can't be null + parent = p.rbParent; + if (parent) { + if (parent.rbLeft === p) { + parent.rbLeft = q; + } + else { + parent.rbRight = q; + } + } + else { + this.root = q; + } + q.rbParent = parent; + p.rbParent = q; + p.rbRight = q.rbLeft; + if (p.rbRight) { + p.rbRight.rbParent = p; + } + q.rbLeft = p; + }; + +Voronoi.prototype.RBTree.prototype.rbRotateRight = function(node) { + var p = node, + q = node.rbLeft, // can't be null + parent = p.rbParent; + if (parent) { + if (parent.rbLeft === p) { + parent.rbLeft = q; + } + else { + parent.rbRight = q; + } + } + else { + this.root = q; + } + q.rbParent = parent; + p.rbParent = q; + p.rbLeft = q.rbRight; + if (p.rbLeft) { + p.rbLeft.rbParent = p; + } + q.rbRight = p; + }; + +Voronoi.prototype.RBTree.prototype.getFirst = function(node) { + while (node.rbLeft) { + node = node.rbLeft; + } + return node; + }; + +Voronoi.prototype.RBTree.prototype.getLast = function(node) { + while (node.rbRight) { + node = node.rbRight; + } + return node; + }; + +// --------------------------------------------------------------------------- +// Diagram methods + +Voronoi.prototype.Diagram = function(site) { + this.site = site; + }; + +// --------------------------------------------------------------------------- +// Cell methods + +Voronoi.prototype.Cell = function(site) { + this.site = site; + this.halfedges = []; + this.closeMe = false; + }; + +Voronoi.prototype.Cell.prototype.init = function(site) { + this.site = site; + this.halfedges = []; + this.closeMe = false; + return this; + }; + +Voronoi.prototype.createCell = function(site) { + var cell = this.cellJunkyard.pop(); + if ( cell ) { + return cell.init(site); + } + return new this.Cell(site); + }; + +Voronoi.prototype.Cell.prototype.prepareHalfedges = function() { + var halfedges = this.halfedges, + iHalfedge = halfedges.length, + edge; + // get rid of unused halfedges + // rhill 2011-05-27: Keep it simple, no point here in trying + // to be fancy: dangling edges are a typically a minority. + while (iHalfedge--) { + edge = halfedges[iHalfedge].edge; + if (!edge.vb || !edge.va) { + halfedges.splice(iHalfedge,1); + } + } + + // rhill 2011-05-26: I tried to use a binary search at insertion + // time to keep the array sorted on-the-fly (in Cell.addHalfedge()). + // There was no real benefits in doing so, performance on + // Firefox 3.6 was improved marginally, while performance on + // Opera 11 was penalized marginally. + halfedges.sort(function(a,b){return b.angle-a.angle;}); + return halfedges.length; + }; + +// Return a list of the neighbor Ids +Voronoi.prototype.Cell.prototype.getNeighborIds = function() { + var neighbors = [], + iHalfedge = this.halfedges.length, + edge; + while (iHalfedge--){ + edge = this.halfedges[iHalfedge].edge; + if (edge.lSite !== null && edge.lSite.voronoiId != this.site.voronoiId) { + neighbors.push(edge.lSite.voronoiId); + } + else if (edge.rSite !== null && edge.rSite.voronoiId != this.site.voronoiId){ + neighbors.push(edge.rSite.voronoiId); + } + } + return neighbors; + }; + +// Compute bounding box +// +Voronoi.prototype.Cell.prototype.getBbox = function() { + var halfedges = this.halfedges, + iHalfedge = halfedges.length, + xmin = Infinity, + ymin = Infinity, + xmax = -Infinity, + ymax = -Infinity, + v, vx, vy; + while (iHalfedge--) { + v = halfedges[iHalfedge].getStartpoint(); + vx = v.x; + vy = v.y; + if (vx < xmin) {xmin = vx;} + if (vy < ymin) {ymin = vy;} + if (vx > xmax) {xmax = vx;} + if (vy > ymax) {ymax = vy;} + // we dont need to take into account end point, + // since each end point matches a start point + } + return { + x: xmin, + y: ymin, + width: xmax-xmin, + height: ymax-ymin + }; + }; + +// Return whether a point is inside, on, or outside the cell: +// -1: point is outside the perimeter of the cell +// 0: point is on the perimeter of the cell +// 1: point is inside the perimeter of the cell +// +Voronoi.prototype.Cell.prototype.pointIntersection = function(x, y) { + // Check if point in polygon. Since all polygons of a Voronoi + // diagram are convex, then: + // http://paulbourke.net/geometry/polygonmesh/ + // Solution 3 (2D): + // "If the polygon is convex then one can consider the polygon + // "as a 'path' from the first vertex. A point is on the interior + // "of this polygons if it is always on the same side of all the + // "line segments making up the path. ... + // "(y - y0) (x1 - x0) - (x - x0) (y1 - y0) + // "if it is less than 0 then P is to the right of the line segment, + // "if greater than 0 it is to the left, if equal to 0 then it lies + // "on the line segment" + var halfedges = this.halfedges, + iHalfedge = halfedges.length, + halfedge, + p0, p1, r; + while (iHalfedge--) { + halfedge = halfedges[iHalfedge]; + p0 = halfedge.getStartpoint(); + p1 = halfedge.getEndpoint(); + r = (y-p0.y)*(p1.x-p0.x)-(x-p0.x)*(p1.y-p0.y); + if (!r) { + return 0; + } + if (r > 0) { + return -1; + } + } + return 1; + }; + +// --------------------------------------------------------------------------- +// Edge methods +// + +Voronoi.prototype.Vertex = function(x, y) { + this.x = x; + this.y = y; + }; + +Voronoi.prototype.Edge = function(lSite, rSite) { + this.lSite = lSite; + this.rSite = rSite; + this.va = this.vb = null; + }; + +Voronoi.prototype.Halfedge = function(edge, lSite, rSite) { + this.site = lSite; + this.edge = edge; + // 'angle' is a value to be used for properly sorting the + // halfsegments counterclockwise. By convention, we will + // use the angle of the line defined by the 'site to the left' + // to the 'site to the right'. + // However, border edges have no 'site to the right': thus we + // use the angle of line perpendicular to the halfsegment (the + // edge should have both end points defined in such case.) + if (rSite) { + this.angle = Math.atan2(rSite.y-lSite.y, rSite.x-lSite.x); + } + else { + var va = edge.va, + vb = edge.vb; + // rhill 2011-05-31: used to call getStartpoint()/getEndpoint(), + // but for performance purpose, these are expanded in place here. + this.angle = edge.lSite === lSite ? + Math.atan2(vb.x-va.x, va.y-vb.y) : + Math.atan2(va.x-vb.x, vb.y-va.y); + } + }; + +Voronoi.prototype.createHalfedge = function(edge, lSite, rSite) { + return new this.Halfedge(edge, lSite, rSite); + }; + +Voronoi.prototype.Halfedge.prototype.getStartpoint = function() { + return this.edge.lSite === this.site ? this.edge.va : this.edge.vb; + }; + +Voronoi.prototype.Halfedge.prototype.getEndpoint = function() { + return this.edge.lSite === this.site ? this.edge.vb : this.edge.va; + }; + + + +// this create and add a vertex to the internal collection + +Voronoi.prototype.createVertex = function(x, y) { + var v = this.vertexJunkyard.pop(); + if ( !v ) { + v = new this.Vertex(x, y); + } + else { + v.x = x; + v.y = y; + } + this.vertices.push(v); + return v; + }; + +// this create and add an edge to internal collection, and also create +// two halfedges which are added to each site's counterclockwise array +// of halfedges. + +Voronoi.prototype.createEdge = function(lSite, rSite, va, vb) { + var edge = this.edgeJunkyard.pop(); + if ( !edge ) { + edge = new this.Edge(lSite, rSite); + } + else { + edge.lSite = lSite; + edge.rSite = rSite; + edge.va = edge.vb = null; + } + + this.edges.push(edge); + if (va) { + this.setEdgeStartpoint(edge, lSite, rSite, va); + } + if (vb) { + this.setEdgeEndpoint(edge, lSite, rSite, vb); + } + this.cells[lSite.voronoiId].halfedges.push(this.createHalfedge(edge, lSite, rSite)); + this.cells[rSite.voronoiId].halfedges.push(this.createHalfedge(edge, rSite, lSite)); + return edge; + }; + +Voronoi.prototype.createBorderEdge = function(lSite, va, vb) { + var edge = this.edgeJunkyard.pop(); + if ( !edge ) { + edge = new this.Edge(lSite, null); + } + else { + edge.lSite = lSite; + edge.rSite = null; + } + edge.va = va; + edge.vb = vb; + this.edges.push(edge); + return edge; + }; + +Voronoi.prototype.setEdgeStartpoint = function(edge, lSite, rSite, vertex) { + if (!edge.va && !edge.vb) { + edge.va = vertex; + edge.lSite = lSite; + edge.rSite = rSite; + } + else if (edge.lSite === rSite) { + edge.vb = vertex; + } + else { + edge.va = vertex; + } + }; + +Voronoi.prototype.setEdgeEndpoint = function(edge, lSite, rSite, vertex) { + this.setEdgeStartpoint(edge, rSite, lSite, vertex); + }; + +// --------------------------------------------------------------------------- +// Beachline methods + +// rhill 2011-06-07: For some reasons, performance suffers significantly +// when instanciating a literal object instead of an empty ctor +Voronoi.prototype.Beachsection = function() { + }; + +// rhill 2011-06-02: A lot of Beachsection instanciations +// occur during the computation of the Voronoi diagram, +// somewhere between the number of sites and twice the +// number of sites, while the number of Beachsections on the +// beachline at any given time is comparatively low. For this +// reason, we reuse already created Beachsections, in order +// to avoid new memory allocation. This resulted in a measurable +// performance gain. + +Voronoi.prototype.createBeachsection = function(site) { + var beachsection = this.beachsectionJunkyard.pop(); + if (!beachsection) { + beachsection = new this.Beachsection(); + } + beachsection.site = site; + return beachsection; + }; + +// calculate the left break point of a particular beach section, +// given a particular sweep line +Voronoi.prototype.leftBreakPoint = function(arc, directrix) { + // http://en.wikipedia.org/wiki/Parabola + // http://en.wikipedia.org/wiki/Quadratic_equation + // h1 = x1, + // k1 = (y1+directrix)/2, + // h2 = x2, + // k2 = (y2+directrix)/2, + // p1 = k1-directrix, + // a1 = 1/(4*p1), + // b1 = -h1/(2*p1), + // c1 = h1*h1/(4*p1)+k1, + // p2 = k2-directrix, + // a2 = 1/(4*p2), + // b2 = -h2/(2*p2), + // c2 = h2*h2/(4*p2)+k2, + // x = (-(b2-b1) + Math.sqrt((b2-b1)*(b2-b1) - 4*(a2-a1)*(c2-c1))) / (2*(a2-a1)) + // When x1 become the x-origin: + // h1 = 0, + // k1 = (y1+directrix)/2, + // h2 = x2-x1, + // k2 = (y2+directrix)/2, + // p1 = k1-directrix, + // a1 = 1/(4*p1), + // b1 = 0, + // c1 = k1, + // p2 = k2-directrix, + // a2 = 1/(4*p2), + // b2 = -h2/(2*p2), + // c2 = h2*h2/(4*p2)+k2, + // x = (-b2 + Math.sqrt(b2*b2 - 4*(a2-a1)*(c2-k1))) / (2*(a2-a1)) + x1 + + // change code below at your own risk: care has been taken to + // reduce errors due to computers' finite arithmetic precision. + // Maybe can still be improved, will see if any more of this + // kind of errors pop up again. + var site = arc.site, + rfocx = site.x, + rfocy = site.y, + pby2 = rfocy-directrix; + // parabola in degenerate case where focus is on directrix + if (!pby2) { + return rfocx; + } + var lArc = arc.rbPrevious; + if (!lArc) { + return -Infinity; + } + site = lArc.site; + var lfocx = site.x, + lfocy = site.y, + plby2 = lfocy-directrix; + // parabola in degenerate case where focus is on directrix + if (!plby2) { + return lfocx; + } + var hl = lfocx-rfocx, + aby2 = 1/pby2-1/plby2, + b = hl/plby2; + if (aby2) { + return (-b+this.sqrt(b*b-2*aby2*(hl*hl/(-2*plby2)-lfocy+plby2/2+rfocy-pby2/2)))/aby2+rfocx; + } + // both parabolas have same distance to directrix, thus break point is midway + return (rfocx+lfocx)/2; + }; + +// calculate the right break point of a particular beach section, +// given a particular directrix +Voronoi.prototype.rightBreakPoint = function(arc, directrix) { + var rArc = arc.rbNext; + if (rArc) { + return this.leftBreakPoint(rArc, directrix); + } + var site = arc.site; + return site.y === directrix ? site.x : Infinity; + }; + +Voronoi.prototype.detachBeachsection = function(beachsection) { + this.detachCircleEvent(beachsection); // detach potentially attached circle event + this.beachline.rbRemoveNode(beachsection); // remove from RB-tree + this.beachsectionJunkyard.push(beachsection); // mark for reuse + }; + +Voronoi.prototype.removeBeachsection = function(beachsection) { + var circle = beachsection.circleEvent, + x = circle.x, + y = circle.ycenter, + vertex = this.createVertex(x, y), + previous = beachsection.rbPrevious, + next = beachsection.rbNext, + disappearingTransitions = [beachsection], + abs_fn = Math.abs; + + // remove collapsed beachsection from beachline + this.detachBeachsection(beachsection); + + // there could be more than one empty arc at the deletion point, this + // happens when more than two edges are linked by the same vertex, + // so we will collect all those edges by looking up both sides of + // the deletion point. + // by the way, there is *always* a predecessor/successor to any collapsed + // beach section, it's just impossible to have a collapsing first/last + // beach sections on the beachline, since they obviously are unconstrained + // on their left/right side. + + // look left + var lArc = previous; + while (lArc.circleEvent && abs_fn(x-lArc.circleEvent.x)<1e-9 && abs_fn(y-lArc.circleEvent.ycenter)<1e-9) { + previous = lArc.rbPrevious; + disappearingTransitions.unshift(lArc); + this.detachBeachsection(lArc); // mark for reuse + lArc = previous; + } + // even though it is not disappearing, I will also add the beach section + // immediately to the left of the left-most collapsed beach section, for + // convenience, since we need to refer to it later as this beach section + // is the 'left' site of an edge for which a start point is set. + disappearingTransitions.unshift(lArc); + this.detachCircleEvent(lArc); + + // look right + var rArc = next; + while (rArc.circleEvent && abs_fn(x-rArc.circleEvent.x)<1e-9 && abs_fn(y-rArc.circleEvent.ycenter)<1e-9) { + next = rArc.rbNext; + disappearingTransitions.push(rArc); + this.detachBeachsection(rArc); // mark for reuse + rArc = next; + } + // we also have to add the beach section immediately to the right of the + // right-most collapsed beach section, since there is also a disappearing + // transition representing an edge's start point on its left. + disappearingTransitions.push(rArc); + this.detachCircleEvent(rArc); + + // walk through all the disappearing transitions between beach sections and + // set the start point of their (implied) edge. + var nArcs = disappearingTransitions.length, + iArc; + for (iArc=1; iArc falls somewhere before the left edge of the beachsection + if (dxl > 1e-9) { + // this case should never happen + // if (!node.rbLeft) { + // rArc = node.rbLeft; + // break; + // } + node = node.rbLeft; + } + else { + dxr = x-this.rightBreakPoint(node,directrix); + // x greaterThanWithEpsilon xr => falls somewhere after the right edge of the beachsection + if (dxr > 1e-9) { + if (!node.rbRight) { + lArc = node; + break; + } + node = node.rbRight; + } + else { + // x equalWithEpsilon xl => falls exactly on the left edge of the beachsection + if (dxl > -1e-9) { + lArc = node.rbPrevious; + rArc = node; + } + // x equalWithEpsilon xr => falls exactly on the right edge of the beachsection + else if (dxr > -1e-9) { + lArc = node; + rArc = node.rbNext; + } + // falls exactly somewhere in the middle of the beachsection + else { + lArc = rArc = node; + } + break; + } + } + } + // at this point, keep in mind that lArc and/or rArc could be + // undefined or null. + + // create a new beach section object for the site and add it to RB-tree + var newArc = this.createBeachsection(site); + this.beachline.rbInsertSuccessor(lArc, newArc); + + // cases: + // + + // [null,null] + // least likely case: new beach section is the first beach section on the + // beachline. + // This case means: + // no new transition appears + // no collapsing beach section + // new beachsection become root of the RB-tree + if (!lArc && !rArc) { + return; + } + + // [lArc,rArc] where lArc == rArc + // most likely case: new beach section split an existing beach + // section. + // This case means: + // one new transition appears + // the left and right beach section might be collapsing as a result + // two new nodes added to the RB-tree + if (lArc === rArc) { + // invalidate circle event of split beach section + this.detachCircleEvent(lArc); + + // split the beach section into two separate beach sections + rArc = this.createBeachsection(lArc.site); + this.beachline.rbInsertSuccessor(newArc, rArc); + + // since we have a new transition between two beach sections, + // a new edge is born + newArc.edge = rArc.edge = this.createEdge(lArc.site, newArc.site); + + // check whether the left and right beach sections are collapsing + // and if so create circle events, to be notified when the point of + // collapse is reached. + this.attachCircleEvent(lArc); + this.attachCircleEvent(rArc); + return; + } + + // [lArc,null] + // even less likely case: new beach section is the *last* beach section + // on the beachline -- this can happen *only* if *all* the previous beach + // sections currently on the beachline share the same y value as + // the new beach section. + // This case means: + // one new transition appears + // no collapsing beach section as a result + // new beach section become right-most node of the RB-tree + if (lArc && !rArc) { + newArc.edge = this.createEdge(lArc.site,newArc.site); + return; + } + + // [null,rArc] + // impossible case: because sites are strictly processed from top to bottom, + // and left to right, which guarantees that there will always be a beach section + // on the left -- except of course when there are no beach section at all on + // the beach line, which case was handled above. + // rhill 2011-06-02: No point testing in non-debug version + //if (!lArc && rArc) { + // throw "Voronoi.addBeachsection(): What is this I don't even"; + // } + + // [lArc,rArc] where lArc != rArc + // somewhat less likely case: new beach section falls *exactly* in between two + // existing beach sections + // This case means: + // one transition disappears + // two new transitions appear + // the left and right beach section might be collapsing as a result + // only one new node added to the RB-tree + if (lArc !== rArc) { + // invalidate circle events of left and right sites + this.detachCircleEvent(lArc); + this.detachCircleEvent(rArc); + + // an existing transition disappears, meaning a vertex is defined at + // the disappearance point. + // since the disappearance is caused by the new beachsection, the + // vertex is at the center of the circumscribed circle of the left, + // new and right beachsections. + // http://mathforum.org/library/drmath/view/55002.html + // Except that I bring the origin at A to simplify + // calculation + var lSite = lArc.site, + ax = lSite.x, + ay = lSite.y, + bx=site.x-ax, + by=site.y-ay, + rSite = rArc.site, + cx=rSite.x-ax, + cy=rSite.y-ay, + d=2*(bx*cy-by*cx), + hb=bx*bx+by*by, + hc=cx*cx+cy*cy, + vertex = this.createVertex((cy*hb-by*hc)/d+ax, (bx*hc-cx*hb)/d+ay); + + // one transition disappear + this.setEdgeStartpoint(rArc.edge, lSite, rSite, vertex); + + // two new transitions appear at the new vertex location + newArc.edge = this.createEdge(lSite, site, undefined, vertex); + rArc.edge = this.createEdge(site, rSite, undefined, vertex); + + // check whether the left and right beach sections are collapsing + // and if so create circle events, to handle the point of collapse. + this.attachCircleEvent(lArc); + this.attachCircleEvent(rArc); + return; + } + }; + +// --------------------------------------------------------------------------- +// Circle event methods + +// rhill 2011-06-07: For some reasons, performance suffers significantly +// when instanciating a literal object instead of an empty ctor +Voronoi.prototype.CircleEvent = function() { + // rhill 2013-10-12: it helps to state exactly what we are at ctor time. + this.arc = null; + this.rbLeft = null; + this.rbNext = null; + this.rbParent = null; + this.rbPrevious = null; + this.rbRed = false; + this.rbRight = null; + this.site = null; + this.x = this.y = this.ycenter = 0; + }; + +Voronoi.prototype.attachCircleEvent = function(arc) { + var lArc = arc.rbPrevious, + rArc = arc.rbNext; + if (!lArc || !rArc) {return;} // does that ever happen? + var lSite = lArc.site, + cSite = arc.site, + rSite = rArc.site; + + // If site of left beachsection is same as site of + // right beachsection, there can't be convergence + if (lSite===rSite) {return;} + + // Find the circumscribed circle for the three sites associated + // with the beachsection triplet. + // rhill 2011-05-26: It is more efficient to calculate in-place + // rather than getting the resulting circumscribed circle from an + // object returned by calling Voronoi.circumcircle() + // http://mathforum.org/library/drmath/view/55002.html + // Except that I bring the origin at cSite to simplify calculations. + // The bottom-most part of the circumcircle is our Fortune 'circle + // event', and its center is a vertex potentially part of the final + // Voronoi diagram. + var bx = cSite.x, + by = cSite.y, + ax = lSite.x-bx, + ay = lSite.y-by, + cx = rSite.x-bx, + cy = rSite.y-by; + + // If points l->c->r are clockwise, then center beach section does not + // collapse, hence it can't end up as a vertex (we reuse 'd' here, which + // sign is reverse of the orientation, hence we reverse the test. + // http://en.wikipedia.org/wiki/Curve_orientation#Orientation_of_a_simple_polygon + // rhill 2011-05-21: Nasty finite precision error which caused circumcircle() to + // return infinites: 1e-12 seems to fix the problem. + var d = 2*(ax*cy-ay*cx); + if (d >= -2e-12){return;} + + var ha = ax*ax+ay*ay, + hc = cx*cx+cy*cy, + x = (cy*ha-ay*hc)/d, + y = (ax*hc-cx*ha)/d, + ycenter = y+by; + + // Important: ybottom should always be under or at sweep, so no need + // to waste CPU cycles by checking + + // recycle circle event object if possible + var circleEvent = this.circleEventJunkyard.pop(); + if (!circleEvent) { + circleEvent = new this.CircleEvent(); + } + circleEvent.arc = arc; + circleEvent.site = cSite; + circleEvent.x = x+bx; + circleEvent.y = ycenter+this.sqrt(x*x+y*y); // y bottom + circleEvent.ycenter = ycenter; + arc.circleEvent = circleEvent; + + // find insertion point in RB-tree: circle events are ordered from + // smallest to largest + var predecessor = null, + node = this.circleEvents.root; + while (node) { + if (circleEvent.y < node.y || (circleEvent.y === node.y && circleEvent.x <= node.x)) { + if (node.rbLeft) { + node = node.rbLeft; + } + else { + predecessor = node.rbPrevious; + break; + } + } + else { + if (node.rbRight) { + node = node.rbRight; + } + else { + predecessor = node; + break; + } + } + } + this.circleEvents.rbInsertSuccessor(predecessor, circleEvent); + if (!predecessor) { + this.firstCircleEvent = circleEvent; + } + }; + +Voronoi.prototype.detachCircleEvent = function(arc) { + var circleEvent = arc.circleEvent; + if (circleEvent) { + if (!circleEvent.rbPrevious) { + this.firstCircleEvent = circleEvent.rbNext; + } + this.circleEvents.rbRemoveNode(circleEvent); // remove from RB-tree + this.circleEventJunkyard.push(circleEvent); + arc.circleEvent = null; + } + }; + +// --------------------------------------------------------------------------- +// Diagram completion methods + +// connect dangling edges (not if a cursory test tells us +// it is not going to be visible. +// return value: +// false: the dangling endpoint couldn't be connected +// true: the dangling endpoint could be connected +Voronoi.prototype.connectEdge = function(edge, bbox) { + // skip if end point already connected + var vb = edge.vb; + if (!!vb) {return true;} + + // make local copy for performance purpose + var va = edge.va, + xl = bbox.xl, + xr = bbox.xr, + yt = bbox.yt, + yb = bbox.yb, + lSite = edge.lSite, + rSite = edge.rSite, + lx = lSite.x, + ly = lSite.y, + rx = rSite.x, + ry = rSite.y, + fx = (lx+rx)/2, + fy = (ly+ry)/2, + fm, fb; + + // if we reach here, this means cells which use this edge will need + // to be closed, whether because the edge was removed, or because it + // was connected to the bounding box. + this.cells[lSite.voronoiId].closeMe = true; + this.cells[rSite.voronoiId].closeMe = true; + + // get the line equation of the bisector if line is not vertical + if (ry !== ly) { + fm = (lx-rx)/(ry-ly); + fb = fy-fm*fx; + } + + // remember, direction of line (relative to left site): + // upward: left.x < right.x + // downward: left.x > right.x + // horizontal: left.x == right.x + // upward: left.x < right.x + // rightward: left.y < right.y + // leftward: left.y > right.y + // vertical: left.y == right.y + + // depending on the direction, find the best side of the + // bounding box to use to determine a reasonable start point + + // rhill 2013-12-02: + // While at it, since we have the values which define the line, + // clip the end of va if it is outside the bbox. + // https://github.com/gorhill/Javascript-Voronoi/issues/15 + // TODO: Do all the clipping here rather than rely on Liang-Barsky + // which does not do well sometimes due to loss of arithmetic + // precision. The code here doesn't degrade if one of the vertex is + // at a huge distance. + + // special case: vertical line + if (fm === undefined) { + // doesn't intersect with viewport + if (fx < xl || fx >= xr) {return false;} + // downward + if (lx > rx) { + if (!va || va.y < yt) { + va = this.createVertex(fx, yt); + } + else if (va.y >= yb) { + return false; + } + vb = this.createVertex(fx, yb); + } + // upward + else { + if (!va || va.y > yb) { + va = this.createVertex(fx, yb); + } + else if (va.y < yt) { + return false; + } + vb = this.createVertex(fx, yt); + } + } + // closer to vertical than horizontal, connect start point to the + // top or bottom side of the bounding box + else if (fm < -1 || fm > 1) { + // downward + if (lx > rx) { + if (!va || va.y < yt) { + va = this.createVertex((yt-fb)/fm, yt); + } + else if (va.y >= yb) { + return false; + } + vb = this.createVertex((yb-fb)/fm, yb); + } + // upward + else { + if (!va || va.y > yb) { + va = this.createVertex((yb-fb)/fm, yb); + } + else if (va.y < yt) { + return false; + } + vb = this.createVertex((yt-fb)/fm, yt); + } + } + // closer to horizontal than vertical, connect start point to the + // left or right side of the bounding box + else { + // rightward + if (ly < ry) { + if (!va || va.x < xl) { + va = this.createVertex(xl, fm*xl+fb); + } + else if (va.x >= xr) { + return false; + } + vb = this.createVertex(xr, fm*xr+fb); + } + // leftward + else { + if (!va || va.x > xr) { + va = this.createVertex(xr, fm*xr+fb); + } + else if (va.x < xl) { + return false; + } + vb = this.createVertex(xl, fm*xl+fb); + } + } + edge.va = va; + edge.vb = vb; + + return true; + }; + +// line-clipping code taken from: +// Liang-Barsky function by Daniel White +// http://www.skytopia.com/project/articles/compsci/clipping.html +// Thanks! +// A bit modified to minimize code paths +Voronoi.prototype.clipEdge = function(edge, bbox) { + var ax = edge.va.x, + ay = edge.va.y, + bx = edge.vb.x, + by = edge.vb.y, + t0 = 0, + t1 = 1, + dx = bx-ax, + dy = by-ay; + // left + var q = ax-bbox.xl; + if (dx===0 && q<0) {return false;} + var r = -q/dx; + if (dx<0) { + if (r0) { + if (r>t1) {return false;} + if (r>t0) {t0=r;} + } + // right + q = bbox.xr-ax; + if (dx===0 && q<0) {return false;} + r = q/dx; + if (dx<0) { + if (r>t1) {return false;} + if (r>t0) {t0=r;} + } + else if (dx>0) { + if (r0) { + if (r>t1) {return false;} + if (r>t0) {t0=r;} + } + // bottom + q = bbox.yb-ay; + if (dy===0 && q<0) {return false;} + r = q/dy; + if (dy<0) { + if (r>t1) {return false;} + if (r>t0) {t0=r;} + } + else if (dy>0) { + if (r 0, va needs to change + // rhill 2011-06-03: we need to create a new vertex rather + // than modifying the existing one, since the existing + // one is likely shared with at least another edge + if (t0 > 0) { + edge.va = this.createVertex(ax+t0*dx, ay+t0*dy); + } + + // if t1 < 1, vb needs to change + // rhill 2011-06-03: we need to create a new vertex rather + // than modifying the existing one, since the existing + // one is likely shared with at least another edge + if (t1 < 1) { + edge.vb = this.createVertex(ax+t1*dx, ay+t1*dy); + } + + // va and/or vb were clipped, thus we will need to close + // cells which use this edge. + if ( t0 > 0 || t1 < 1 ) { + this.cells[edge.lSite.voronoiId].closeMe = true; + this.cells[edge.rSite.voronoiId].closeMe = true; + } + + return true; + }; + +// Connect/cut edges at bounding box +Voronoi.prototype.clipEdges = function(bbox) { + // connect all dangling edges to bounding box + // or get rid of them if it can't be done + var edges = this.edges, + iEdge = edges.length, + edge, + abs_fn = Math.abs; + + // iterate backward so we can splice safely + while (iEdge--) { + edge = edges[iEdge]; + // edge is removed if: + // it is wholly outside the bounding box + // it is looking more like a point than a line + if (!this.connectEdge(edge, bbox) || + !this.clipEdge(edge, bbox) || + (abs_fn(edge.va.x-edge.vb.x)<1e-9 && abs_fn(edge.va.y-edge.vb.y)<1e-9)) { + edge.va = edge.vb = null; + edges.splice(iEdge,1); + } + } + }; + +// Close the cells. +// The cells are bound by the supplied bounding box. +// Each cell refers to its associated site, and a list +// of halfedges ordered counterclockwise. +Voronoi.prototype.closeCells = function(bbox) { + var xl = bbox.xl, + xr = bbox.xr, + yt = bbox.yt, + yb = bbox.yb, + cells = this.cells, + iCell = cells.length, + cell, + iLeft, + halfedges, nHalfedges, + edge, + va, vb, vz, + lastBorderSegment, + abs_fn = Math.abs; + + while (iCell--) { + cell = cells[iCell]; + // prune, order halfedges counterclockwise, then add missing ones + // required to close cells + if (!cell.prepareHalfedges()) { + continue; + } + if (!cell.closeMe) { + continue; + } + // find first 'unclosed' point. + // an 'unclosed' point will be the end point of a halfedge which + // does not match the start point of the following halfedge + halfedges = cell.halfedges; + nHalfedges = halfedges.length; + // special case: only one site, in which case, the viewport is the cell + // ... + + // all other cases + iLeft = 0; + while (iLeft < nHalfedges) { + va = halfedges[iLeft].getEndpoint(); + vz = halfedges[(iLeft+1) % nHalfedges].getStartpoint(); + // if end point is not equal to start point, we need to add the missing + // halfedge(s) up to vz + if (abs_fn(va.x-vz.x)>=1e-9 || abs_fn(va.y-vz.y)>=1e-9) { + + // rhill 2013-12-02: + // "Holes" in the halfedges are not necessarily always adjacent. + // https://github.com/gorhill/Javascript-Voronoi/issues/16 + + // find entry point: + switch (true) { + + // walk downward along left side + case this.equalWithEpsilon(va.x,xl) && this.lessThanWithEpsilon(va.y,yb): + lastBorderSegment = this.equalWithEpsilon(vz.x,xl); + vb = this.createVertex(xl, lastBorderSegment ? vz.y : yb); + edge = this.createBorderEdge(cell.site, va, vb); + iLeft++; + halfedges.splice(iLeft, 0, this.createHalfedge(edge, cell.site, null)); + nHalfedges++; + if ( lastBorderSegment ) { break; } + va = vb; + // fall through + + // walk rightward along bottom side + case this.equalWithEpsilon(va.y,yb) && this.lessThanWithEpsilon(va.x,xr): + lastBorderSegment = this.equalWithEpsilon(vz.y,yb); + vb = this.createVertex(lastBorderSegment ? vz.x : xr, yb); + edge = this.createBorderEdge(cell.site, va, vb); + iLeft++; + halfedges.splice(iLeft, 0, this.createHalfedge(edge, cell.site, null)); + nHalfedges++; + if ( lastBorderSegment ) { break; } + va = vb; + // fall through + + // walk upward along right side + case this.equalWithEpsilon(va.x,xr) && this.greaterThanWithEpsilon(va.y,yt): + lastBorderSegment = this.equalWithEpsilon(vz.x,xr); + vb = this.createVertex(xr, lastBorderSegment ? vz.y : yt); + edge = this.createBorderEdge(cell.site, va, vb); + iLeft++; + halfedges.splice(iLeft, 0, this.createHalfedge(edge, cell.site, null)); + nHalfedges++; + if ( lastBorderSegment ) { break; } + va = vb; + // fall through + + // walk leftward along top side + case this.equalWithEpsilon(va.y,yt) && this.greaterThanWithEpsilon(va.x,xl): + lastBorderSegment = this.equalWithEpsilon(vz.y,yt); + vb = this.createVertex(lastBorderSegment ? vz.x : xl, yt); + edge = this.createBorderEdge(cell.site, va, vb); + iLeft++; + halfedges.splice(iLeft, 0, this.createHalfedge(edge, cell.site, null)); + nHalfedges++; + if ( lastBorderSegment ) { break; } + va = vb; + // fall through + + // walk downward along left side + lastBorderSegment = this.equalWithEpsilon(vz.x,xl); + vb = this.createVertex(xl, lastBorderSegment ? vz.y : yb); + edge = this.createBorderEdge(cell.site, va, vb); + iLeft++; + halfedges.splice(iLeft, 0, this.createHalfedge(edge, cell.site, null)); + nHalfedges++; + if ( lastBorderSegment ) { break; } + va = vb; + // fall through + + // walk rightward along bottom side + lastBorderSegment = this.equalWithEpsilon(vz.y,yb); + vb = this.createVertex(lastBorderSegment ? vz.x : xr, yb); + edge = this.createBorderEdge(cell.site, va, vb); + iLeft++; + halfedges.splice(iLeft, 0, this.createHalfedge(edge, cell.site, null)); + nHalfedges++; + if ( lastBorderSegment ) { break; } + va = vb; + // fall through + + // walk upward along right side + lastBorderSegment = this.equalWithEpsilon(vz.x,xr); + vb = this.createVertex(xr, lastBorderSegment ? vz.y : yt); + edge = this.createBorderEdge(cell.site, va, vb); + iLeft++; + halfedges.splice(iLeft, 0, this.createHalfedge(edge, cell.site, null)); + nHalfedges++; + if ( lastBorderSegment ) { break; } + // fall through + + default: + throw "Voronoi.closeCells() > this makes no sense!"; + } + } + iLeft++; + } + cell.closeMe = false; + } + }; + +// --------------------------------------------------------------------------- +// Debugging helper +/* +Voronoi.prototype.dumpBeachline = function(y) { + console.log('Voronoi.dumpBeachline(%f) > Beachsections, from left to right:', y); + if ( !this.beachline ) { + console.log(' None'); + } + else { + var bs = this.beachline.getFirst(this.beachline.root); + while ( bs ) { + console.log(' site %d: xl: %f, xr: %f', bs.site.voronoiId, this.leftBreakPoint(bs, y), this.rightBreakPoint(bs, y)); + bs = bs.rbNext; + } + } + }; +*/ + +// --------------------------------------------------------------------------- +// Helper: Quantize sites + +// rhill 2013-10-12: +// This is to solve https://github.com/gorhill/Javascript-Voronoi/issues/15 +// Since not all users will end up using the kind of coord values which would +// cause the issue to arise, I chose to let the user decide whether or not +// he should sanitize his coord values through this helper. This way, for +// those users who uses coord values which are known to be fine, no overhead is +// added. + +Voronoi.prototype.quantizeSites = function(sites) { + var ε = this.ε, + n = sites.length, + site; + while ( n-- ) { + site = sites[n]; + site.x = Math.floor(site.x / ε) * ε; + site.y = Math.floor(site.y / ε) * ε; + } + }; + +// --------------------------------------------------------------------------- +// Helper: Recycle diagram: all vertex, edge and cell objects are +// "surrendered" to the Voronoi object for reuse. +// TODO: rhill-voronoi-core v2: more performance to be gained +// when I change the semantic of what is returned. + +Voronoi.prototype.recycle = function(diagram) { + if ( diagram ) { + if ( diagram instanceof this.Diagram ) { + this.toRecycle = diagram; + } + else { + throw 'Voronoi.recycleDiagram() > Need a Diagram object.'; + } + } + }; + +// --------------------------------------------------------------------------- +// Top-level Fortune loop + +// rhill 2011-05-19: +// Voronoi sites are kept client-side now, to allow +// user to freely modify content. At compute time, +// *references* to sites are copied locally. + +Voronoi.prototype.compute = function(sites, bbox) { + // to measure execution time + var startTime = new Date(); + + // init internal state + this.reset(); + + // any diagram data available for recycling? + // I do that here so that this is included in execution time + if ( this.toRecycle ) { + this.vertexJunkyard = this.vertexJunkyard.concat(this.toRecycle.vertices); + this.edgeJunkyard = this.edgeJunkyard.concat(this.toRecycle.edges); + this.cellJunkyard = this.cellJunkyard.concat(this.toRecycle.cells); + this.toRecycle = null; + } + + // Initialize site event queue + var siteEvents = sites.slice(0); + siteEvents.sort(function(a,b){ + var r = b.y - a.y; + if (r) {return r;} + return b.x - a.x; + }); + + // process queue + var site = siteEvents.pop(), + siteid = 0, + xsitex, // to avoid duplicate sites + xsitey, + cells = this.cells, + circle; + + // main loop + for (;;) { + // we need to figure whether we handle a site or circle event + // for this we find out if there is a site event and it is + // 'earlier' than the circle event + circle = this.firstCircleEvent; + + // add beach section + if (site && (!circle || site.y < circle.y || (site.y === circle.y && site.x < circle.x))) { + // only if site is not a duplicate + if (site.x !== xsitex || site.y !== xsitey) { + // first create cell for new site + cells[siteid] = this.createCell(site); + site.voronoiId = siteid++; + // then create a beachsection for that site + this.addBeachsection(site); + // remember last site coords to detect duplicate + xsitey = site.y; + xsitex = site.x; + } + site = siteEvents.pop(); + } + + // remove beach section + else if (circle) { + this.removeBeachsection(circle.arc); + } + + // all done, quit + else { + break; + } + } + + // wrapping-up: + // connect dangling edges to bounding box + // cut edges as per bounding box + // discard edges completely outside bounding box + // discard edges which are point-like + this.clipEdges(bbox); + + // add missing edges in order to close opened cells + this.closeCells(bbox); + + // to measure execution time + var stopTime = new Date(); + + // prepare return values + var diagram = new this.Diagram(); + diagram.cells = this.cells; + diagram.edges = this.edges; + diagram.vertices = this.vertices; + diagram.execTime = stopTime.getTime()-startTime.getTime(); + + // clean up + this.reset(); + + return diagram; + }; + +/******************************************************************************/ + +if ( typeof module !== 'undefined' ) { + module.exports = Voronoi; +} + +export default Voronoi; diff --git a/web/pw-frontend/src/lib/visualizer/voronoi/voronoi.ts b/web/pw-frontend/src/lib/visualizer/voronoi/voronoi.ts new file mode 100644 index 0000000..a63bc9a --- /dev/null +++ b/web/pw-frontend/src/lib/visualizer/voronoi/voronoi.ts @@ -0,0 +1,165 @@ +import type { Shader } from "../webgl/shader"; +import type { BBox, Point } from "./voronoi-core"; +import Voronoi from "./voronoi-core"; +import { DefaultRenderable } from "../webgl/renderer"; +import { IndexBuffer, VertexBuffer } from "../webgl/buffer"; +import { VertexBufferLayout, VertexArray } from "../webgl/vertexBufferLayout"; + +function arcctg(x: number): number { return Math.PI / 2 - Math.atan(x); } + +function to_key(p: Point): string { + return [p.x, p.y] + ""; +} + +function round_point(center: Point, point: Point, amount_fn = (b: number) => 0.7): Point { + const d = dist(center, point, true); + const x = center.x + amount_fn(d) * (point.x - center.x); + const y = center.y + amount_fn(d) * (point.y - center.y); + return { 'x': x, 'y': y }; +} + +function median_point(c: Point, p: Point, n: Point, d = 0.1): number[] { + const dd = 1.0 - 2 * d; + return [ + dd * c.x + d * p.x + d * n.x, + dd * c.y + d * p.y + d * n.y, + ] +} + +function build_point_map(es: Voronoi.HalfEdge[]): (point: Point) => Point { + const mean = es.map(e => dist(e.getStartpoint(), e.getEndpoint())).reduce((a, b) => a + b, 0) / es.length; + const map = {}; + + for (let edge of es) { + const start = edge.getStartpoint(); + const end = edge.getEndpoint(); + + if (dist(start, end) < 0.03 * mean) { // These points have to be merged + const middle = { 'x': (start.x + end.x) / 2, 'y': (start.y + end.y) / 2 }; + map[to_key(start)] = middle; + map[to_key(end)] = middle; + } + } + + return (p) => map[to_key(p)] || p; +} + +function get_round_fn(dist_mean: number, amount = 0.7): (d: number) => number { + return (d) => arcctg((d - dist_mean) / dist_mean) / Math.PI + 0.6; +} + +function dist(a: Point, b: Point, norm = false): number { + const dx = a.x - b.x; + const dy = a.y - b.y; + if (norm) return Math.sqrt(dx * dx + dy * dy); + return dx * dx + dy * dy; +} + +export class VoronoiBuilder { + inner: DefaultRenderable; + + vor: Voronoi; + planets: Point[]; + + + constructor(gl: WebGLRenderingContext, shader: Shader, planets: Point[], bbox: BBox) { + this.vor = new Voronoi(); + this.planets = planets; + + const ib = new IndexBuffer(gl, []); + const vb = new VertexBuffer(gl, []); + + const layout = new VertexBufferLayout(); + layout.push(gl.FLOAT, 2, 4, "a_pos"); + layout.push(gl.FLOAT, 2, 4, "a_center"); + layout.push(gl.FLOAT, 1, 4, "a_own"); + layout.push(gl.FLOAT, 1, 4, "a_intensity"); + + const vao = new VertexArray(); + vao.addBuffer(vb, layout); + + this.inner = new DefaultRenderable(ib, vao, shader, [], {}); + + this.resize(gl, bbox); + } + + getRenderable(): DefaultRenderable { + return this.inner; + } + + resize(gl: WebGLRenderingContext, bbox: BBox) { + const start = new Date().getTime(); + + // This voronoi sorts the planets, then owners don't align anymore + const own_map = {}; + this.planets.forEach((p, i) => own_map[to_key(p)] = i); + + const vor = this.vor.compute(this.planets, bbox); + + const attrs = []; + const ids = []; + + let vertCount = 0; + + for (let i = 0; i < vor.cells.length; i++) { + const cell = vor.cells[i]; + const planetId = own_map[to_key(cell.site)]; + const point_map = build_point_map(cell.halfedges); + + const centerId = vertCount++; + + attrs.push(cell.site.x, cell.site.y); + attrs.push(cell.site.x, cell.site.y); + attrs.push(planetId); + attrs.push(1); + + const dist_mean = cell.halfedges.map(e => { + const start = e.getStartpoint(); + const end = e.getEndpoint(); + return dist(cell.site, start, true) + dist(cell.site, { 'x': (start.x + end.x) / 2, 'y': (start.y + end.y) / 2 }, true) + }).reduce((a, b) => a + b, 0) / cell.halfedges.length / 2; + const round_fn = get_round_fn(dist_mean); + + for (let edge of cell.halfedges) { + let start = point_map(edge.getStartpoint()); + let end = point_map(edge.getEndpoint()); + let center = { 'x': (start.x + end.x) / 2, 'y': (start.y + end.y) / 2 }; + + if (to_key(start) == to_key(end)) continue; + + start = round_point(cell.site, start, round_fn); + center = round_point(cell.site, center, round_fn); + end = round_point(cell.site, end, round_fn); + + ids.push(centerId); + ids.push(vertCount++); + attrs.push(start.x, start.y); + attrs.push(cell.site.x, cell.site.y); + attrs.push(planetId); + attrs.push(0); + + ids.push(vertCount++); + attrs.push(center.x, center.y); + attrs.push(cell.site.x, cell.site.y); + attrs.push(planetId); + attrs.push(0); + + ids.push(centerId); + ids.push(vertCount - 1); + + ids.push(vertCount++); + attrs.push(end.x, end.y); + attrs.push(cell.site.x, cell.site.y); + attrs.push(planetId); + attrs.push(0); + } + } + + this.inner.updateIndexBuffer(gl, ids); + this.inner.updateVAOBuffer(gl, 0, attrs); + + console.log(`Vor things took ${new Date().getTime() - start} ms!`) + } +} + +export default VoronoiBuilder; \ No newline at end of file diff --git a/web/pw-frontend/src/lib/visualizer/webgl/buffer.ts b/web/pw-frontend/src/lib/visualizer/webgl/buffer.ts new file mode 100644 index 0000000..2739fbe --- /dev/null +++ b/web/pw-frontend/src/lib/visualizer/webgl/buffer.ts @@ -0,0 +1,55 @@ + +export class Buffer { + buffer: WebGLBuffer; + data: any; + count: number; + type: number; + + constructor(gl: WebGLRenderingContext, data: number[], type: number) { + this.buffer = gl.createBuffer(); + this.type = type; + + if (data) + this.updateData(gl, data); + } + + _toArray(data: number[]): any { + return new Float32Array(data); + } + + updateData(gl: WebGLRenderingContext, data: number[]) { + this.data = data; + this.count = data.length; + gl.bindBuffer(this.type, this.buffer); + gl.bufferData(this.type, this._toArray(data), gl.STATIC_DRAW); + } + + bind(gl: WebGLRenderingContext) { + gl.bindBuffer(this.type, this.buffer); + } + + getCount(): number { + return this.count; + } +} + +export class VertexBuffer extends Buffer { + constructor(gl: WebGLRenderingContext, data: any) { + super(gl, data, gl.ARRAY_BUFFER); + } + + _toArray(data: number[]): any { + return new Float32Array(data); + } +} + + +export class IndexBuffer extends Buffer { + constructor(gl: WebGLRenderingContext, data: any) { + super(gl, data, gl.ELEMENT_ARRAY_BUFFER); + } + + _toArray(data: number[]): any { + return new Uint16Array(data); + } +} diff --git a/web/pw-frontend/src/lib/visualizer/webgl/index.ts b/web/pw-frontend/src/lib/visualizer/webgl/index.ts new file mode 100644 index 0000000..fdb7886 --- /dev/null +++ b/web/pw-frontend/src/lib/visualizer/webgl/index.ts @@ -0,0 +1,122 @@ +import { Uniform4f, Uniform1f, Uniform2f, ShaderFactory, UniformMatrix3fv, Uniform3f } from './shader'; +import { resizeCanvasToDisplaySize, FPSCounter, onload2promise, Resizer, url_to_mesh } from "./util"; +import { VertexBuffer, IndexBuffer } from './buffer'; +import { VertexArray, VertexBufferLayout } from './vertexBufferLayout'; +import { Renderer } from './renderer'; +import { Texture } from './texture'; + +const URL = window.location.origin+window.location.pathname; +const LOCATION = URL.substring(0, URL.lastIndexOf("/") + 1); + +async function create_texture_from_svg(gl: WebGLRenderingContext, name: string, path: string, width: number, height: number): Promise { + + const [mesh, factory] = await Promise.all([ + url_to_mesh(path), + ShaderFactory.create_factory(LOCATION + "static/shaders/frag/static_color.glsl", LOCATION + "static/shaders/vert/svg.glsl") + ]); + + const program = factory.create_shader(gl); + const renderer = new Renderer(); + + var positionBuffer = new VertexBuffer(gl, mesh.positions); + var layout = new VertexBufferLayout(); + layout.push(gl.FLOAT, 3, 4, "a_position"); + + const vao = new VertexArray(); + vao.addBuffer(positionBuffer, layout); + + program.bind(gl); + vao.bind(gl, program); + + var indexBuffer = new IndexBuffer(gl, mesh.cells); + indexBuffer.bind(gl); + + renderer.addToDraw(indexBuffer, vao, program, {}); + + return Texture.fromRenderer(gl, name, width, height, renderer); +} + + +async function main() { + + // Get A WebGL context + var canvas = document.getElementById("c"); + const resolution = [canvas.width, canvas.height]; + + const resizer = new Resizer(canvas, [-10, -10, 20, 20], true); + + var gl = canvas.getContext("webgl"); + if (!gl) { + return; + } + + const mesh = await url_to_mesh("static/res/images/earth.svg"); + console.log(Math.max(...mesh.positions), Math.min(...mesh.positions)); + const renderer = new Renderer(); + + const factory = await ShaderFactory.create_factory(LOCATION + "static/shaders/frag/static_color.glsl", LOCATION + "static/shaders/vert/simple.glsl"); + const program = factory.create_shader(gl); + + var positionBuffer = new VertexBuffer(gl, mesh.positions); + var layout = new VertexBufferLayout(); + layout.push(gl.FLOAT, 3, 4, "a_position"); + // layout.push(gl.FLOAT, 2, 4, "a_tex"); + + const vao = new VertexArray(); + vao.addBuffer(positionBuffer, layout); + + resizeCanvasToDisplaySize(gl.canvas); + + // Tell WebGL how to convert from clip space to pixels + gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); + + // Clear the canvas + gl.clearColor(0, 0, 0, 0); + gl.clear(gl.COLOR_BUFFER_BIT); + + gl.enable(gl.BLEND); + gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); + + program.bind(gl); + vao.bind(gl, program); + + var indexBuffer = new IndexBuffer(gl, mesh.cells); + indexBuffer.bind(gl); + + renderer.addToDraw(indexBuffer, vao, program, {}); + + const counter = new FPSCounter(); + + const step = function (time: number) { + + // console.log(resizer.get_viewbox()); + + program.uniform(gl, "u_time", new Uniform1f(time * 0.001)); + program.uniform(gl, "u_mouse", new Uniform2f(resizer.get_mouse_pos())); + program.uniform(gl, "u_viewbox", new Uniform4f(resizer.get_viewbox())); + program.uniform(gl, "u_resolution", new Uniform2f(resolution)); + program.uniform(gl, "u_trans", new UniformMatrix3fv([1, 0, 0, 0, 1, 0, 0, 0, 1])); + program.uniform(gl, "u_color", new Uniform3f(1.0, 0.5, 0.0)); + + renderer.render(gl); + + counter.frame(time); + requestAnimationFrame(step); + } + + requestAnimationFrame(step); +} + + +main(); + +document.getElementById("loader").classList.remove("loading"); + +// const loader = document.getElementById("loader"); +// setInterval(() => { +// if (loader.classList.contains("loading")) { +// loader.classList.remove("loading") +// } else { +// loader.classList.add("loading"); +// } +// }, 2000); diff --git a/web/pw-frontend/src/lib/visualizer/webgl/renderer.ts b/web/pw-frontend/src/lib/visualizer/webgl/renderer.ts new file mode 100644 index 0000000..c3b219f --- /dev/null +++ b/web/pw-frontend/src/lib/visualizer/webgl/renderer.ts @@ -0,0 +1,157 @@ +import type { IndexBuffer } from './buffer'; +import type { VertexArray } from './vertexBufferLayout'; +import type { Texture } from './texture'; +import type { Dictionary } from './util'; +import type { Shader, Uniform } from './shader'; +import { Uniform1i } from './shader'; + +function sortedIndex(array, value) { + var low = 0, + high = array.length; + + while (low < high) { + var mid = (low + high) >>> 1; + if (array[mid] < value) low = mid + 1; + else high = mid; + } + return low; +} + +export interface Renderable { + getUniforms() : Dictionary; + render(gl: WebGLRenderingContext): void; + updateVAOBuffer(gl: WebGLRenderingContext, index: number, data: number[]); + updateIndexBuffer(gl: WebGLRenderingContext, data: number[]); +} + +export class DefaultRenderable implements Renderable { + ibo: IndexBuffer; + va: VertexArray; + shader: Shader; + textures: Texture[]; + uniforms: Dictionary; + + constructor( + ibo: IndexBuffer, + va: VertexArray, + shader: Shader, + textures: Texture[], + uniforms: Dictionary, + ) { + this.ibo = ibo; + this.va = va; + this.shader = shader; + this.textures = textures; + this.uniforms = uniforms; + } + + getUniforms(): Dictionary { + return this.uniforms; + } + + updateVAOBuffer(gl: WebGLRenderingContext, index: number, data: number[]) { + this.va.updateBuffer(gl, index, data); + } + + updateIndexBuffer(gl: WebGLRenderingContext, data: number[]) { + this.ibo.updateData(gl, data); + } + + render(gl: WebGLRenderingContext): void { + + const indexBuffer = this.ibo; + const vertexArray = this.va; + const uniforms = this.uniforms; + + const shader = this.shader; + const textures = this.textures; + let texLocation = 0; + + for (let texture of textures) { + + shader.uniform(gl, texture.name, new Uniform1i(texLocation)); + texture.bind(gl, texLocation); + + texLocation ++; + // if (texLocation > maxTextures) { + // console.error("Using too many textures, this is not supported yet\nUndefined behaviour!"); + // } + } + + if (vertexArray && shader && uniforms) { + for(let key in uniforms) { + shader.uniform(gl, key, uniforms[key]); + } + + vertexArray.bind(gl, shader); + + if (indexBuffer) { + indexBuffer.bind(gl); + gl.drawElements(gl.TRIANGLES, indexBuffer.getCount(), gl.UNSIGNED_SHORT, 0); + } else { + console.error("IndexBuffer is required to render, for now"); + } + } + + } +} + +export class Renderer { + renderables: { [id: number] : [Renderable, boolean][]; }; + renderable_layers: number[]; + + constructor() { + this.renderables = {}; + this.renderable_layers = []; + } + + updateUniform(i: number, f: (uniforms: Dictionary) => void, layer=0, ) { + f(this.renderables[layer][i][0].getUniforms()); + } + + disableRenderable(i: number, layer=0) { + this.renderables[layer][i][1] = false; + } + + enableRenderable(i: number, layer=0) { + this.renderables[layer][i][1] = true; + } + + addRenderable(item: Renderable, layer=0): number { + if(!this.renderables[layer]) { + const idx = sortedIndex(this.renderable_layers, layer); + this.renderable_layers.splice(idx, 0, layer); + this.renderables[layer] = []; + } + + this.renderables[layer].push([item, true]); + return this.renderables[layer].length - 1; + } + + addToDraw(indexBuffer: IndexBuffer, vertexArray: VertexArray, shader: Shader, uniforms?: Dictionary, texture?: Texture[], layer=0): number { + return this.addRenderable( + new DefaultRenderable( + indexBuffer, + vertexArray, + shader, + texture || [], + uniforms || {}, + ), layer + ); + } + + render(gl: WebGLRenderingContext, frameBuffer?: WebGLFramebuffer, width?: number, height?: number) { + gl.bindFramebuffer(gl.FRAMEBUFFER, frameBuffer); + gl.viewport(0, 0, width || gl.canvas.width, height || gl.canvas.height); + gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); + + const maxTextures = gl.getParameter(gl.MAX_VERTEX_TEXTURE_IMAGE_UNITS); + + for (let layer of this.renderable_layers) { + for (let [r, e] of this.renderables[layer]) { + if (!e) continue; + r.render(gl); + } + } + } +} diff --git a/web/pw-frontend/src/lib/visualizer/webgl/shader.ts b/web/pw-frontend/src/lib/visualizer/webgl/shader.ts new file mode 100644 index 0000000..942c4c2 --- /dev/null +++ b/web/pw-frontend/src/lib/visualizer/webgl/shader.ts @@ -0,0 +1,327 @@ +import type { Dictionary } from './util'; + +function error(msg: string) { + console.error(msg); +} + +const defaultShaderType = [ + "VERTEX_SHADER", + "FRAGMENT_SHADER" +]; + +/// Create Shader from Source string +function loadShader( + gl: WebGLRenderingContext, + shaderSource: string, + shaderType: number, + opt_errorCallback: any, +): WebGLShader { + var errFn = opt_errorCallback || error; + // Create the shader object + var shader = gl.createShader(shaderType); + + // Load the shader source + gl.shaderSource(shader, shaderSource); + + // Compile the shader + gl.compileShader(shader); + + // Check the compile status + var compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS); + if (!compiled) { + // Something went wrong during compilation; get the error + var lastError = gl.getShaderInfoLog(shader); + errFn("*** Error compiling shader '" + shader + "':" + lastError); + gl.deleteShader(shader); + return null; + } + + return shader; +} + +/// Actually Create Program with Shader's +function createProgram( + gl: WebGLRenderingContext, + shaders: WebGLShader[], + opt_attribs: string[], + opt_locations: number[], + opt_errorCallback: any, +): WebGLProgram { + var errFn = opt_errorCallback || error; + var program = gl.createProgram(); + shaders.forEach(function (shader) { + gl.attachShader(program, shader); + }); + if (opt_attribs) { + opt_attribs.forEach(function (attrib, ndx) { + gl.bindAttribLocation( + program, + opt_locations ? opt_locations[ndx] : ndx, + attrib); + }); + } + gl.linkProgram(program); + + // Check the link status + var linked = gl.getProgramParameter(program, gl.LINK_STATUS); + if (!linked) { + // something went wrong with the link + var lastError = gl.getProgramInfoLog(program); + errFn("Error in program linking:" + lastError); + + gl.deleteProgram(program); + return null; + } + return program; +} + +export class ShaderFactory { + frag_source: string; + vert_source: string; + + static async create_factory(frag_url: string, vert_url: string): Promise { + const sources = await Promise.all([ + fetch(frag_url).then((r) => r.text()), + fetch(vert_url).then((r) => r.text()), + ]); + + return new ShaderFactory(sources[0], sources[1]); + } + + constructor(frag_source: string, vert_source: string ) { + this.frag_source = frag_source; + this.vert_source = vert_source; + } + + create_shader( + gl: WebGLRenderingContext, + context?: Dictionary, + opt_attribs?: string[], + opt_locations?: number[], + opt_errorCallback?: any, + ): Shader { + let vert = this.vert_source.slice(); + let frag = this.frag_source.slice(); + for (let key in context) { + vert = vert.replace(new RegExp("\\$" + key, 'g'), context[key]); + frag = frag.replace(new RegExp("\\$" + key, 'g'), context[key]); + } + + const shaders = [ + loadShader(gl, vert, gl.VERTEX_SHADER, opt_errorCallback), + loadShader(gl, frag, gl.FRAGMENT_SHADER, opt_errorCallback), + ]; + + return new Shader(createProgram(gl, shaders, opt_attribs, opt_locations, opt_errorCallback)); + } +} + +export class Shader { + shader: WebGLProgram; + uniformCache: Dictionary; + attribCache: Dictionary; + + static async createProgramFromUrls( + gl: WebGLRenderingContext, + vert_url: string, + frag_url: string, + context?: Dictionary, + opt_attribs?: string[], + opt_locations?: number[], + opt_errorCallback?: any, + ): Promise { + const sources = (await Promise.all([ + fetch(vert_url).then((r) => r.text()), + fetch(frag_url).then((r) => r.text()), + ])).map(x => { + for (let key in context) { + x = x.replace(new RegExp("\\$" + key, 'g'), context[key]); + } + return x; + }); + + const shaders = [ + loadShader(gl, sources[0], 35633, opt_errorCallback), + loadShader(gl, sources[1], 35632, opt_errorCallback), + ]; + return new Shader(createProgram(gl, shaders, opt_attribs, opt_locations, opt_errorCallback)); + } + + constructor(shader: WebGLProgram) { + this.shader = shader; + this.uniformCache = {}; + this.attribCache = {}; + } + + bind(gl: WebGLRenderingContext) { + gl.useProgram(this.shader); + } + + // Different locations have different types :/ + getUniformLocation(gl: WebGLRenderingContext, name: string): WebGLUniformLocation { + if (this.uniformCache[name] === undefined) { + this.uniformCache[name] = gl.getUniformLocation(this.shader, name); + } + + return this.uniformCache[name]; + } + + getAttribLocation(gl: WebGLRenderingContext, name: string): number { + if (this.attribCache[name] === undefined) { + this.attribCache[name] = gl.getAttribLocation(this.shader, name); + } + + return this.attribCache[name]; + } + + uniform( + gl: WebGLRenderingContext, + name: string, + uniform: T, + ) { + this.bind(gl); + const location = this.getUniformLocation(gl, name); + if (location < 0) { + console.error("No location found with name " + name); + } + + uniform.setUniform(gl, location); + } + + clear(gl: WebGLRenderingContext) { + gl.deleteProgram(this.shader); + } +} + +export interface Uniform { + setUniform(gl: WebGLRenderingContext, location: WebGLUniformLocation): void; +} + +export class Uniform2fv implements Uniform { + data: number[] | Float32Array; + constructor(data: number[] | Float32Array) { + this.data = data; + } + + setUniform(gl: WebGLRenderingContext, location: WebGLUniformLocation) { + gl.uniform2fv(location, this.data); + } +} + +export class Uniform3fv implements Uniform { + data: number[] | Float32Array; + constructor(data: number[] | Float32Array) { + this.data = data; + } + + setUniform(gl: WebGLRenderingContext, location: WebGLUniformLocation) { + gl.uniform3fv(location, this.data); + } +} + +export class Uniform3f implements Uniform { + x: number; + y: number; + z: number; + + constructor(x: number, y: number, z: number) { + this.x = x; + this.y = y; + this.z = z; + } + + setUniform(gl: WebGLRenderingContext, location: WebGLUniformLocation) { + gl.uniform3f(location, this.x ,this.y, this.z); + } +} + +export class Uniform1iv implements Uniform { + data: number[] | Int32List; + constructor(data: number[] | Int32List) { + this.data = data; + } + + setUniform(gl: WebGLRenderingContext, location: WebGLUniformLocation) { + gl.uniform1iv(location, this.data); + } +} + +export class Uniform1i implements Uniform { + texture: number; + + constructor(texture: number) { + this.texture = texture; + } + + setUniform(gl: WebGLRenderingContext, location: WebGLUniformLocation) { + gl.uniform1i(location, this.texture); + } +} + +export class Uniform1f implements Uniform { + texture: number; + + constructor(texture: number) { + this.texture = texture; + } + + setUniform(gl: WebGLRenderingContext, location: WebGLUniformLocation) { + gl.uniform1f(location, this.texture); + } +} + +export class Uniform2f implements Uniform { + x: number; + y: number; + + constructor(xy: number[]) { + this.x = xy[0]; + this.y = xy[1]; + } + + setUniform(gl: WebGLRenderingContext, location: WebGLUniformLocation) { + gl.uniform2f(location, this.x, this.y); + } +} + +export class Uniform4f implements Uniform { + v0: number; + v1: number; + v2: number; + v3: number; + + constructor(xyzw: number[]) { + this.v0 = xyzw[0]; + this.v1 = xyzw[1]; + this.v2 = xyzw[2]; + this.v3 = xyzw[3]; + } + + setUniform(gl: WebGLRenderingContext, location: WebGLUniformLocation) { + gl.uniform4f(location, this.v0, this.v1, this.v2, this.v3); + } +} + +export class UniformMatrix3fv implements Uniform { + data: number[] | Float32Array; + constructor(data: number[] | Float32Array) { + this.data = data; + } + + setUniform(gl: WebGLRenderingContext, location: WebGLUniformLocation) { + gl.uniformMatrix3fv(location, false, this.data); + } +} + +export class UniformBool implements Uniform { + data: boolean; + constructor(data: boolean) { + this.data = data; + } + + setUniform(gl: WebGLRenderingContext, location: WebGLUniformLocation) { + gl.uniform1i(location, this.data ? 1 : 0); + } +} + +export default Shader; \ No newline at end of file diff --git a/web/pw-frontend/src/lib/visualizer/webgl/text.ts b/web/pw-frontend/src/lib/visualizer/webgl/text.ts new file mode 100644 index 0000000..3f1cec6 --- /dev/null +++ b/web/pw-frontend/src/lib/visualizer/webgl/text.ts @@ -0,0 +1,192 @@ +import type { Dictionary } from "./util"; +import type { Shader, UniformMatrix3fv } from "./shader"; +import { Texture } from "./texture"; +import { DefaultRenderable } from "./renderer"; +import { IndexBuffer, VertexBuffer } from "./buffer"; +import { VertexBufferLayout, VertexArray } from "./vertexBufferLayout"; + + +export enum Align { + Begin, + End, + Middle, +} + +export class GlypInfo { + x: number; + y: number; + width: number; +} + +export class FontInfo { + letterHeight: number; + spaceWidth: number; + spacing: number; + textureWidth: number; + textureHeight: number; + glyphInfos: Dictionary; +} + +export class LabelFactory { + texture: Texture; + font: FontInfo; + shader: Shader; + + constructor(gl: WebGLRenderingContext, loc: string, font: FontInfo, shader: Shader) { + this.texture = Texture.fromImage(gl, loc, 'font'); + this.font = font; + this.shader = shader; + } + + build(gl: WebGLRenderingContext, transform?: UniformMatrix3fv): Label { + return new Label(gl, this.shader, this.texture, this.font, transform); + } +} + +export class Label { + inner: DefaultRenderable; + + font: FontInfo; + + constructor(gl: WebGLRenderingContext, shader: Shader, tex: Texture, font: FontInfo, transform?: UniformMatrix3fv) { + this.font = font; + + const uniforms = transform ? { "u_trans": transform, "u_trans_next": transform, } : {}; + const ib = new IndexBuffer(gl, []); + const vb_pos = new VertexBuffer(gl, []); + const vb_tex = new VertexBuffer(gl, []); + + const layout_pos = new VertexBufferLayout(); + layout_pos.push(gl.FLOAT, 2, 4, "a_position"); + + const layout_tex = new VertexBufferLayout(); + layout_tex.push(gl.FLOAT, 2, 4, "a_texCoord"); + + const vao = new VertexArray(); + vao.addBuffer(vb_pos, layout_pos); + vao.addBuffer(vb_tex, layout_tex); + + this.inner = new DefaultRenderable(ib, vao, shader, [tex], uniforms); + } + + getRenderable(): DefaultRenderable { + return this.inner; + } + + setText(gl: WebGLRenderingContext, text: string, h_align = Align.Begin, v_align = Align.Begin) { + const idxs = []; + const verts_pos = []; + const verts_tex = []; + + const letterHeight = this.font.letterHeight / this.font.textureHeight; + let xPos = 0; + + switch (h_align) { + case Align.Begin: + break; + case Align.End: + xPos = -1 * [...text].map(n => this.font.glyphInfos[n] ? this.font.glyphInfos[n].width : this.font.spaceWidth).reduce((a, b) => a + b, 0) / this.font.letterHeight; + break; + case Align.Middle: + xPos = -1 * [...text].map(n => this.font.glyphInfos[n] ? this.font.glyphInfos[n].width : this.font.spaceWidth).reduce((a, b) => a + b, 0) / this.font.letterHeight / 2; + break; + } + let yStart = 0; + switch (v_align) { + case Align.Begin: + break; + case Align.End: + yStart = 1; + break; + case Align.Middle: + yStart = 0.5; + break; + } + + let j = 0; + for (let i = 0; i < text.length; i++) { + const info = this.font.glyphInfos[text[i]]; + if (info) { + const dx = info.width / this.font.letterHeight; + const letterWidth = info.width / this.font.textureWidth; + const x0 = info.x / this.font.textureWidth; + const y0 = info.y / this.font.textureHeight; + verts_pos.push(xPos, yStart); + verts_pos.push(xPos + dx, yStart); + verts_pos.push(xPos, yStart-1); + verts_pos.push(xPos + dx, yStart-1); + + verts_tex.push(x0, y0); + verts_tex.push(x0 + letterWidth, y0); + verts_tex.push(x0, y0 + letterHeight); + verts_tex.push(x0 + letterWidth, y0 + letterHeight); + + xPos += dx; + + idxs.push(j+0, j+1, j+2, j+1, j+2, j+3); + j += 4; + } else { + // Just move xPos + xPos += this.font.spaceWidth / this.font.letterHeight; + } + } + + this.inner.updateIndexBuffer(gl, idxs); + this.inner.updateVAOBuffer(gl, 0, verts_pos); + this.inner.updateVAOBuffer(gl, 1, verts_tex); + } +} + +export function defaultLabelFactory(gl: WebGLRenderingContext, shader: Shader): LabelFactory { + const fontInfo = { + letterHeight: 8, + spaceWidth: 8, + spacing: -1, + textureWidth: 64, + textureHeight: 40, + glyphInfos: { + 'a': { x: 0, y: 0, width: 8, }, + 'b': { x: 8, y: 0, width: 8, }, + 'c': { x: 16, y: 0, width: 8, }, + 'd': { x: 24, y: 0, width: 8, }, + 'e': { x: 32, y: 0, width: 8, }, + 'f': { x: 40, y: 0, width: 8, }, + 'g': { x: 48, y: 0, width: 8, }, + 'h': { x: 56, y: 0, width: 8, }, + 'i': { x: 0, y: 8, width: 8, }, + 'j': { x: 8, y: 8, width: 8, }, + 'k': { x: 16, y: 8, width: 8, }, + 'l': { x: 24, y: 8, width: 8, }, + 'm': { x: 32, y: 8, width: 8, }, + 'n': { x: 40, y: 8, width: 8, }, + 'o': { x: 48, y: 8, width: 8, }, + 'p': { x: 56, y: 8, width: 8, }, + 'q': { x: 0, y: 16, width: 8, }, + 'r': { x: 8, y: 16, width: 8, }, + 's': { x: 16, y: 16, width: 8, }, + 't': { x: 24, y: 16, width: 8, }, + 'u': { x: 32, y: 16, width: 8, }, + 'v': { x: 40, y: 16, width: 8, }, + 'w': { x: 48, y: 16, width: 8, }, + 'x': { x: 56, y: 16, width: 8, }, + 'y': { x: 0, y: 24, width: 8, }, + 'z': { x: 8, y: 24, width: 8, }, + '0': { x: 16, y: 24, width: 8, }, + '1': { x: 24, y: 24, width: 8, }, + '2': { x: 32, y: 24, width: 8, }, + '3': { x: 40, y: 24, width: 8, }, + '4': { x: 48, y: 24, width: 8, }, + '5': { x: 56, y: 24, width: 8, }, + '6': { x: 0, y: 32, width: 8, }, + '7': { x: 8, y: 32, width: 8, }, + '8': { x: 16, y: 32, width: 8, }, + '9': { x: 24, y: 32, width: 8, }, + '-': { x: 32, y: 32, width: 8, }, + '*': { x: 40, y: 32, width: 8, }, + '!': { x: 48, y: 32, width: 8, }, + '?': { x: 56, y: 32, width: 8, }, + }, + }; + + return new LabelFactory(gl, '/static/res/assets/font.png', fontInfo, shader); +} diff --git a/web/pw-frontend/src/lib/visualizer/webgl/texture.ts b/web/pw-frontend/src/lib/visualizer/webgl/texture.ts new file mode 100644 index 0000000..9d6adcf --- /dev/null +++ b/web/pw-frontend/src/lib/visualizer/webgl/texture.ts @@ -0,0 +1,106 @@ +import type { Renderer } from "./renderer"; + +export class Texture { + texture: WebGLTexture; + width: number; + height: number; + loaded: boolean; + name: string; + + static fromImage( + gl: WebGLRenderingContext, + path: string, + name: string, + ): Texture { + const out = new Texture(gl, name); + + const image = new Image(); + image.onload = out.setImage.bind(out, gl, image); + image.onerror = error; + image.src = path; + + return out; + } + + static fromRenderer( + gl: WebGLRenderingContext, + name: string, + width: number, + height: number, + renderer: Renderer + ): Texture { + const out = new Texture(gl, name); + out.width = width; + out.height = height; + + gl.texImage2D( + gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, + gl.RGBA, gl.UNSIGNED_BYTE, null); + + const fb = gl.createFramebuffer(); + gl.bindFramebuffer(gl.FRAMEBUFFER, fb); + + const attachmentPoint = gl.COLOR_ATTACHMENT0; + gl.framebufferTexture2D(gl.FRAMEBUFFER, attachmentPoint, gl.TEXTURE_2D, out.texture, 0); + + renderer.render(gl, fb, width, height); + + out.loaded = true; + + return out; + } + + constructor( + gl: WebGLRenderingContext, + name: string, + ) { + this.loaded = false; + this.name = name; + + this.texture = gl.createTexture(); + this.bind(gl); + + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); + + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, + gl.UNSIGNED_BYTE, new Uint8Array([255, 0, 0, 255])); + } + + setImage(gl: WebGLRenderingContext, image: HTMLImageElement) { + this.bind(gl); + this.width = image.width; + this.height = image.height; + + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image); + + this.unbind(gl); + + this.loaded = true; + } + + bind(gl: WebGLRenderingContext, location=0) { + gl.activeTexture(gl.TEXTURE0 + location); + gl.bindTexture(gl.TEXTURE_2D, this.texture); + } + + unbind(gl: WebGLRenderingContext) { + gl.bindTexture(gl.TEXTURE_2D, null); + } + + + getWidth(): number { + return this.width; + } + + getHeight(): number { + return this.height; + } +} + +function error(e: any) { + console.error("IMAGE LOAD ERROR"); + console.error(e); +} diff --git a/web/pw-frontend/src/lib/visualizer/webgl/util.ts b/web/pw-frontend/src/lib/visualizer/webgl/util.ts new file mode 100644 index 0000000..3ed2b4d --- /dev/null +++ b/web/pw-frontend/src/lib/visualizer/webgl/util.ts @@ -0,0 +1,229 @@ +import { parse as parsePath } from 'extract-svg-path'; +import svgMesh3d from 'svg-mesh-3d'; + +export interface Dictionary { + [Key: string]: T; +} + + +interface OnLoadable { + onload: any; +} + +export function onload2promise(obj: T): Promise { + return new Promise(resolve => { + obj.onload = () => resolve(obj); + }); +} + +export function resizeCanvasToDisplaySize( + canvas: HTMLCanvasElement, + multiplier?: number, +): boolean { + multiplier = multiplier || 1; + var width = canvas.clientWidth * multiplier | 0; + var height = canvas.clientHeight * multiplier | 0; + if (canvas.width !== width || canvas.height !== height) { + canvas.width = width; + canvas.height = height; + return true; + } + return false; +} + +export class FPSCounter { + last: number; + count: number; + _delta: number; + _prev: number; + + _frame_start: number; + _total_frametime: number; + + constructor() { + this.last = 0; + this.count = 0; + this._delta = 0; + this._prev = 0; + } + + frame(now: number) { + this._frame_start = performance.now(); + this.count += 1; + this._delta = now - this._prev; + this._prev = now; + + if (now - this.last > 1000) { + this.last = now; + console.log(`${this.count} fps, ${(this._total_frametime / this.count).toFixed(2)}ms avg per frame`); + this.count = 0; + this._total_frametime = 0; + } + } + + frame_end() { + this._total_frametime += (performance.now() - this._frame_start); + } + + delta(now: number): number { + return this._delta; + } +} + +export class Resizer { + hoovering = false; + dragging = false; + + mouse_pos = [0, 0]; + last_drag = [0, 0]; + + viewbox: number[]; + orig_viewbox: number[]; + + el_box: number[]; + + scaleX = 1; + scaleY = 1; + + constructor(el: HTMLCanvasElement, viewbox: number[], keep_aspect_ratio=false) { + viewbox = [-viewbox[0] - viewbox[2], - viewbox[1] - viewbox[3], viewbox[2], viewbox[3]]; + this.viewbox = [...viewbox]; + this.el_box = [el.width, el.height]; + + if (keep_aspect_ratio) { + const or_width = this.viewbox[2]; + const or_height = this.viewbox[3]; + + const width_percentage = this.viewbox[2] / el.width; + const height_percentage = this.viewbox[3] / el.height; + + if (width_percentage < height_percentage) { + // width should be larger + this.viewbox[2] = height_percentage * el.width; + } else { + // height should be larger + this.viewbox[3] = width_percentage * el.height; + } + + this.viewbox[0] -= (this.viewbox[2] - or_width) / 2; + this.viewbox[1] -= (this.viewbox[3] - or_height) / 2; + + this.scaleX = this.viewbox[2] / this.viewbox[3]; + } + + this.orig_viewbox = [...this.viewbox]; + + el.addEventListener("mouseenter", this.mouseenter.bind(this), { capture: false, passive: true}); + el.addEventListener("mouseleave", this.mouseleave.bind(this), { capture: false, passive: true}); + el.addEventListener("mousemove", this.mousemove.bind(this), { capture: false, passive: true}); + el.addEventListener("mousedown", this.mousedown.bind(this), { capture: false, passive: true}); + el.addEventListener("mouseup", this.mouseup.bind(this), { capture: false, passive: true}); + + window.addEventListener('wheel', this.wheel.bind(this), { capture: false, passive: true}); + } + + _clip_viewbox() { + this.viewbox[0] = Math.max(this.viewbox[0], this.orig_viewbox[0]); + this.viewbox[1] = Math.max(this.viewbox[1], this.orig_viewbox[1]); + + this.viewbox[0] = Math.min(this.viewbox[0] + this.viewbox[2], this.orig_viewbox[0] + this.orig_viewbox[2]) - this.viewbox[2]; + this.viewbox[1] = Math.min(this.viewbox[1] + this.viewbox[3], this.orig_viewbox[1] + this.orig_viewbox[3]) - this.viewbox[3]; + } + + mouseenter() { + this.hoovering = true; + } + + mouseleave() { + this.hoovering = false; + } + + mousemove(e: MouseEvent) { + this.mouse_pos = [e.offsetX, this.el_box[1] - e.offsetY]; + + if (this.dragging) { + const scaleX = this.viewbox[2] / this.el_box[0]; + const scaleY = this.viewbox[3] / this.el_box[1]; + + this.viewbox[0] += (this.last_drag[0] - this.mouse_pos[0]) * scaleX; + this.viewbox[1] += (this.last_drag[1] - this.mouse_pos[1]) * scaleY; + + this.last_drag = [...this.mouse_pos]; + + this._clip_viewbox(); + } + } + + mousedown() { + this.dragging = true; + this.last_drag = [...this.mouse_pos]; + } + + mouseup() { + this.dragging = false; + } + + wheel(e: WheelEvent) { + if (this.hoovering) { + const delta = e.deltaY > 0 ? 0.1 * this.viewbox[2] : -0.1 * this.viewbox[2]; + const dx = delta * this.scaleX; + const dy = delta * this.scaleY; + + const mouse_dx = this.mouse_pos[0] / this.el_box[0]; + const mouse_dy = this.mouse_pos[1] / this.el_box[1]; + + this._zoom([dx, dy], [mouse_dx, mouse_dy]); + } + } + + _zoom(deltas: number[], center: number[]) { + this.viewbox[2] += deltas[0]; + this.viewbox[0] -= deltas[0] * center[0]; + this.viewbox[2] = Math.min(this.viewbox[2], this.orig_viewbox[2]); + + this.viewbox[3] += deltas[1]; + this.viewbox[1] -= deltas[1] * center[1]; + this.viewbox[3] = Math.min(this.viewbox[3], this.orig_viewbox[3]); + + this._clip_viewbox(); + } + + get_viewbox(): number[] { + return this.viewbox; + } + + get_mouse_pos(): number[] { + return this.mouse_pos; + } +} + +export class Mesh { + cells: number[]; + positions: number[]; + + constructor(mesh: any) { + this.cells = mesh.cells.flat(); + this.positions = mesh.positions.flat(); + } +} + +export async function url_to_mesh(url: string): Promise { + + return new Promise(function(resolve) { + fetch(url) + .then(resp => resp.text()) + .then(data => { + // var div = document.createElement('div'); + // div.innerHTML = data; + // var svg = div.querySelector('svg'); + + var svgPath = parsePath(data); + var mesh = svgMesh3d(svgPath, { + delaunay: false, + scale: 10, + }); + + resolve(new Mesh(mesh)); + }) + }); +} diff --git a/web/pw-frontend/src/lib/visualizer/webgl/vertexBufferLayout.ts b/web/pw-frontend/src/lib/visualizer/webgl/vertexBufferLayout.ts new file mode 100644 index 0000000..f44ed47 --- /dev/null +++ b/web/pw-frontend/src/lib/visualizer/webgl/vertexBufferLayout.ts @@ -0,0 +1,115 @@ +import type { VertexBuffer } from './buffer'; +import type { Shader } from './shader'; + +export class VertexBufferElement { + type: number; + amount: number; + type_size: number; + normalized: boolean; + index: string; + + constructor( + type: number, + amount: number, + type_size: number, + index: string, + normalized: boolean, + ) { + this.type = type; + this.amount = amount; + this.type_size = type_size; + this.normalized = normalized; + this.index = index; + } +} + +export class VertexBufferLayout { + elements: VertexBufferElement[]; + stride: number; + offset: number; + + constructor(offset = 0) { + this.elements = []; + this.stride = 0; + this.offset = offset; + } + + // Maybe wrong normalized type + push( + type: number, + amount: number, + type_size: number, + index: string, + normalized = false, + ) { + this.elements.push(new VertexBufferElement(type, amount, type_size, index, normalized)); + this.stride += amount * type_size; + } + + getElements(): VertexBufferElement[] { + return this.elements; + } + + getStride(): number { + return this.stride; + } +} + +// glEnableVertexAttribArray is to specify what location of the current program the follow data is needed +// glVertexAttribPointer tells gl that that data is at which location in the supplied data +export class VertexArray { + // There is no renderer ID, always at bind buffers and use glVertexAttribPointer + buffers: VertexBuffer[]; + layouts: VertexBufferLayout[]; + + constructor() { + this.buffers = []; + this.layouts = []; + } + + addBuffer(vb: VertexBuffer, layout: VertexBufferLayout) { + this.buffers.push(vb); + this.layouts.push(layout); + } + + updateBuffer(gl: WebGLRenderingContext, index: number, data: number[]) { + this.buffers[index].updateData(gl, data); + } + + /// Bind buffers providing program data + bind(gl: WebGLRenderingContext, shader: Shader) { + shader.bind(gl); + for(let i = 0; i < this.buffers.length; i ++) { + const buffer = this.buffers[i]; + const layout = this.layouts[i]; + + buffer.bind(gl); + const elements = layout.getElements(); + let offset = layout.offset; + + for (let j = 0; j < elements.length; j ++) { + const element = elements[j]; + const location = shader.getAttribLocation(gl, element.index); + + if (location >= 0) { + gl.enableVertexAttribArray(location); + gl.vertexAttribPointer( + location, element.amount, element.type, + element.normalized, layout.stride, offset + ); + } + + offset += element.amount * element.type_size; + } + } + } + + /// Undo bind operation + unbind(gl: WebGLRenderingContext) { + this.layouts.forEach((layout) => { + layout.getElements().forEach((_, index) => { + gl.disableVertexAttribArray(index); + }); + }) + } +} diff --git a/web/pw-frontend/src/main.ts b/web/pw-frontend/src/main.ts new file mode 100644 index 0000000..eb9e5a0 --- /dev/null +++ b/web/pw-frontend/src/main.ts @@ -0,0 +1,9 @@ +import App from './App.svelte' +import load_wasm_module from "planetwars-rs"; + +load_wasm_module().then(() => { + const app = new App({ + target: document.getElementById('app') + }) +}); + diff --git a/web/pw-frontend/src/vite-env.d.ts b/web/pw-frontend/src/vite-env.d.ts new file mode 100644 index 0000000..4078e74 --- /dev/null +++ b/web/pw-frontend/src/vite-env.d.ts @@ -0,0 +1,2 @@ +/// +/// diff --git a/web/pw-frontend/svelte.config.js b/web/pw-frontend/svelte.config.js new file mode 100644 index 0000000..3630bb3 --- /dev/null +++ b/web/pw-frontend/svelte.config.js @@ -0,0 +1,7 @@ +import sveltePreprocess from 'svelte-preprocess' + +export default { + // Consult https://github.com/sveltejs/svelte-preprocess + // for more information about preprocessors + preprocess: sveltePreprocess() +} diff --git a/web/pw-frontend/tsconfig.json b/web/pw-frontend/tsconfig.json new file mode 100644 index 0000000..2bfc2d3 --- /dev/null +++ b/web/pw-frontend/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "@tsconfig/svelte/tsconfig.json", + "compilerOptions": { + "target": "esnext", + "useDefineForClassFields": true, + "module": "esnext", + + "resolveJsonModule": true, + "baseUrl": ".", + /** + * Typecheck JS in `.svelte` and `.js` files by default. + * Disable checkJs if you'd like to use dynamic types in JS. + * Note that setting allowJs false does not prevent the use + * of JS in `.svelte` files. + */ + "allowJs": false, + "checkJs": false + }, + "include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"] +} diff --git a/web/pw-frontend/vite.config.js b/web/pw-frontend/vite.config.js new file mode 100644 index 0000000..7682f94 --- /dev/null +++ b/web/pw-frontend/vite.config.js @@ -0,0 +1,12 @@ +import { defineConfig } from 'vite' +import { svelte } from '@sveltejs/vite-plugin-svelte' +import nodePolyfills from 'rollup-plugin-polyfill-node' + + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [ + svelte(), + nodePolyfills() + ], +})