diff --git a/Cargo.toml b/Cargo.toml index bb73b8a..5ef0a66 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,11 +13,14 @@ opt-level = 3 lto = true [dependencies] -wasm-bindgen = "0.2.79" -plotters = "0.3.1" -plotters-canvas = "0.3.0" +meval = { git = "https://github.com/Titaniumtown/meval-rs.git" } +egui = "0.17.0" +eframe = "0.17.0" +emath = "0.17.0" +epaint = "0.17.0" +[target.'cfg(target_arch = "wasm32")'.dependencies] +console_error_panic_hook = "0.1.7" +tracing-wasm = "0.2.1" wee_alloc = "0.4.5" web-sys = { version = "0.3.56", features = ["HtmlCanvasElement"] } -meval = { git = "https://github.com/Titaniumtown/meval-rs.git" } -console_error_panic_hook = "0.1.7" -tracing-wasm = "0.2.1" \ No newline at end of file +wasm-bindgen = "0.2.79" \ No newline at end of file diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..c5f3875 --- /dev/null +++ b/TODO.md @@ -0,0 +1,4 @@ +## TODO: +1. Port to [egui](https://github.com/emilk/egui) + - Reimplement testing the validity of functions. + - Proper support for dynamic chart display size. \ No newline at end of file diff --git a/src/chart_manager.rs b/src/chart_manager.rs new file mode 100644 index 0000000..826eaa1 --- /dev/null +++ b/src/chart_manager.rs @@ -0,0 +1,125 @@ +use crate::misc::{add_asterisks, Cache, Function}; + +// Manages Chart generation and caching of values +pub struct ChartManager { + function: Function, + min_x: f64, + max_x: f64, + num_interval: usize, + resolution: usize, + back_cache: Cache>, + front_cache: Cache<(Vec<(f64, f64, f64)>, f64)>, +} + +impl ChartManager { + pub fn new( + func_str: String, min_x: f64, max_x: f64, num_interval: usize, + resolution: usize, + ) -> Self { + Self { + function: Function::from_string(func_str), + min_x, + max_x, + num_interval, + resolution, + back_cache: Cache::new_empty(), + front_cache: Cache::new_empty(), + } + } + + #[inline] + fn draw( + &mut self + ) -> (Vec<(f64, f64)>, Vec<(f64, f64, f64)>, f64) { + let absrange = (self.max_x - self.min_x).abs(); + let data: Vec<(f64, f64)> = match self.back_cache.is_valid() { + true => self.back_cache.get().clone(), + false => { + let output: Vec<(f64, f64)> = (1..=self.resolution) + .map(|x| ((x as f64 / self.resolution as f64) * absrange) + self.min_x) + .map(|x| (x, self.function.run(x))) + .collect(); + self.back_cache.set(output.clone()); + output + } + }; + + let filtered_data: Vec<(f64, f64)> = data + .iter() + .map(|(x, y)| (*x, *y)) + .collect(); + + let (rect_data, area): (Vec<(f64, f64, f64)>, f64) = match self.front_cache.is_valid() { + true => self.front_cache.get().clone(), + false => { + let step = absrange / (self.num_interval as f64); + let output: (Vec<(f64, f64, f64)>, f64) = self.integral_rectangles(step); + self.front_cache.set(output.clone()); + output + } + }; + + (filtered_data, rect_data, area) + } + + #[allow(clippy::too_many_arguments)] + pub fn update( + &mut self, func_str_new: String, min_x: f64, max_x: f64, num_interval: usize, resolution: usize, + ) -> (Vec<(f64, f64)>, Vec<(f64, f64, f64)>, f64) { + let func_str: String = add_asterisks(func_str_new); + let update_func: bool = !self.function.str_compare(func_str.clone()); + + let underlying_update = update_func + | (min_x != self.min_x) + | (max_x != self.max_x); + + if underlying_update | (self.resolution != resolution) { + self.back_cache.invalidate(); + } + + if underlying_update | (num_interval != self.num_interval) { + self.front_cache.invalidate(); + } + + if update_func { + self.function = Function::from_string(func_str); + } + + self.min_x = min_x; + self.max_x = max_x; + self.num_interval = num_interval; + self.resolution = resolution; + + self.draw() + } + + // Creates and does the math for creating all the rectangles under the graph + #[inline] + fn integral_rectangles(&self, step: f64) -> (Vec<(f64, f64, f64)>, f64) { + let data2: Vec<(f64, f64, f64)> = (0..self.num_interval) + .map(|e| { + let x: f64 = ((e as f64) * step) + self.min_x; + + // Makes sure rectangles are properly handled on x values below 0 + let x2: f64 = match x > 0.0 { + true => x + step, + false => x - step, + }; + + let tmp1: f64 = self.function.run(x); + let tmp2: f64 = self.function.run(x2); + + // Chooses the y value who's absolute value is the smallest + let y: f64 = match tmp2.abs() > tmp1.abs() { + true => tmp1, + false => tmp2, + }; + + (x, x2, y) + }) + .filter(|(_, _, y)| !y.is_nan()) + .collect(); + let area: f64 = data2.iter().map(|(_, _, y)| y * step).sum(); // sum of all rectangles' areas + (data2, area) + } +} \ No newline at end of file diff --git a/src/egui_app.rs b/src/egui_app.rs new file mode 100644 index 0000000..468eb34 --- /dev/null +++ b/src/egui_app.rs @@ -0,0 +1,94 @@ +use eframe::{egui, epi}; +use egui::{plot::{HLine, Line, Plot, Value, Values, Text}, Pos2}; +use crate::chart_manager::ChartManager; +use meval::Expr; +use crate::misc::{add_asterisks, Cache, Function}; +use egui::{Color32, ColorImage, Ui}; +use emath::Rect; +use epaint::{Rounding, RectShape, Stroke}; +use egui::widgets::plot::{Bar, BarChart}; + +pub struct MathApp { + func_str: String, + min_x: f64, + max_x: f64, + num_interval: usize, + resolution: usize, + chart_manager: ChartManager, +} + +impl Default for MathApp { + fn default() -> Self { + Self { + func_str: "x^2".to_string(), + min_x: -10.0, + max_x: 10.0, + num_interval: 100, + resolution: 10000, + chart_manager: ChartManager::new("x^2".to_string(), -10.0, 10.0, 100, 10000) + } + } +} + +impl epi::App for MathApp { + fn name(&self) -> &str { + "eframe template" + } + + /// Called once before the first frame. + fn setup( + &mut self, + _ctx: &egui::Context, + _frame: &epi::Frame, + _storage: Option<&dyn epi::Storage>, + ) { } + + + /// Called each time the UI needs repainting, which may be many times per second. + /// Put your widgets into a `SidePanel`, `TopPanel`, `CentralPanel`, `Window` or `Area`. + fn update(&mut self, ctx: &egui::Context, frame: &epi::Frame) { + let Self { + func_str, + min_x, + max_x, + num_interval, + resolution, + chart_manager + } = self; + + egui::SidePanel::left("side_panel").show(ctx, |ui| { + ui.heading("Side Panel"); + + ui.horizontal(|ui| { + ui.label("Function: "); + ui.text_edit_singleline(func_str); + }); + + ui.add(egui::Slider::new(min_x, -1000.0..=1000.0).text("Min X")); + ui.add(egui::Slider::new(max_x, *min_x..=1000.0).text("Max X")); + + ui.add(egui::Slider::new(num_interval, 0..=usize::MAX).text("Interval")); + }); + + egui::CentralPanel::default().show(ctx, |ui| { + let (filtered_data, rect_data, area) = chart_manager.update(self.func_str.clone(), self.min_x, self.max_x, self.num_interval, self.resolution); + + let filtered_data_values = filtered_data.iter().map(|(x, y)| Value::new(*x, *y)).collect(); + + let curve = Line::new(Values::from_values(filtered_data_values)).color(Color32::RED); + let bars = rect_data.iter().map(|(_, x2, y)| Bar::new(*x2, *y)).collect(); + let barchart = BarChart::new(bars).color(Color32::BLUE); + + // ui.label("Graph:"); + ui.label(format!("Area: {:.10}", area)); + Plot::new("plot") + .view_aspect(1.0) + .include_y(0) + .show(ui, |plot_ui| { + plot_ui.line(curve); + plot_ui.bar_chart(barchart); + }); + }); + + } +} diff --git a/src/lib.rs b/src/lib.rs index c97ba46..8fb03af 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,18 +1,22 @@ #![allow(clippy::unused_unit)] // Fixes clippy keep complaining about wasm_bindgen #![allow(clippy::type_complexity)] // Clippy, my types are fine. -use meval::Expr; -use plotters::prelude::*; -use plotters_canvas::CanvasBackend; -use std::panic; -use wasm_bindgen::prelude::*; -use web_sys::HtmlCanvasElement; mod misc; -use crate::misc::{add_asterisks, Cache, ChartOutput, DrawResult, Function}; +mod egui_app; +mod chart_manager; +use std::panic; +#[cfg(target_arch = "wasm32")] +use eframe::{epi, egui}; + +#[cfg(target_arch = "wasm32")] +use wasm_bindgen::prelude::*; + +#[cfg(target_arch = "wasm32")] #[global_allocator] static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; +#[cfg(target_arch = "wasm32")] #[wasm_bindgen] extern "C" { // Use `js_namespace` here to bind `console.log(..)` instead of just @@ -21,8 +25,9 @@ extern "C" { fn log(s: &str); } -#[wasm_bindgen(start)] -pub fn init() { +#[cfg(target_arch = "wasm32")] +#[wasm_bindgen] +pub fn start(canvas_id: &str) -> Result<(), wasm_bindgen::JsValue> { log("Initializing..."); // See performance in browser profiler! @@ -36,244 +41,8 @@ pub fn init() { log("Initialized console_error_panic_hook!"); log("Finished initializing!"); -} - -// Manages Chart generation and caching of values -#[wasm_bindgen] -pub struct ChartManager { - function: Function, - min_x: f32, - max_x: f32, - min_y: f32, - max_y: f32, - num_interval: usize, - resolution: usize, - back_cache: Cache>, - front_cache: Cache<(Vec<(f32, f32, f32)>, f32)>, -} - -#[wasm_bindgen] -impl ChartManager { - pub fn new( - func_str: String, min_x: f32, max_x: f32, min_y: f32, max_y: f32, num_interval: usize, - resolution: usize, - ) -> Self { - Self { - function: Function::from_string(func_str), - min_x, - max_x, - min_y, - max_y, - num_interval, - resolution, - back_cache: Cache::new_empty(), - front_cache: Cache::new_empty(), - } - } - - // Tests function to make sure it's able to be parsed. Returns the string of the Error produced, or an empty string if it runs successfully. - pub fn test_func(function_string: String) -> String { - // Factorials do not work, and it would be really difficult to make them work - if function_string.contains('!') { - return "Factorials are unsupported".to_string(); - } - - let new_func_str: String = add_asterisks(function_string); - let expr_result = new_func_str.parse(); - let expr_error = match &expr_result { - Ok(_) => "".to_string(), - Err(error) => format!("{}", error), - }; - if !expr_error.is_empty() { - return expr_error; - } - - let expr: Expr = expr_result.unwrap(); - let func_result = expr.bind("x"); - let func_error = match &func_result { - Ok(_) => "".to_string(), - Err(error) => format!("{}", error), - }; - if !func_error.is_empty() { - return func_error; - } - - "".to_string() - } - - #[inline] - fn draw( - &mut self, element: HtmlCanvasElement, dark_mode: bool, - ) -> DrawResult<(impl Fn((i32, i32)) -> Option<(f32, f32)>, f32)> { - log("Drawing..."); - let backend = CanvasBackend::with_canvas_object(element).unwrap(); - let root = backend.into_drawing_area(); - let font: FontDesc = ("sans-serif", 20.0).into(); - - if dark_mode { - root.fill(&RGBColor(28, 28, 28))?; - } else { - root.fill(&WHITE)?; - } - - let mut chart = ChartBuilder::on(&root) - .margin(20.0) - .caption(format!("y={}", self.function.get_string()), font) - .x_label_area_size(30.0) - .y_label_area_size(30.0) - .build_cartesian_2d(self.min_x..self.max_x, self.min_y..self.max_y)?; - - if dark_mode { - chart - .configure_mesh() - .x_labels(3) - .y_labels(3) - .light_line_style(&RGBColor(254, 254, 254)) - .draw()?; - } else { - chart.configure_mesh().x_labels(3).y_labels(3).draw()?; - } - - let absrange = (self.max_x - self.min_x).abs(); - let data: Vec<(f32, f32)> = match self.back_cache.is_valid() { - true => self.back_cache.get().clone(), - false => { - log("Updating back_cache"); - let output: Vec<(f32, f32)> = (1..=self.resolution) - .map(|x| ((x as f32 / self.resolution as f32) * absrange) + self.min_x) - .map(|x| (x, self.function.run(x))) - .collect(); - self.back_cache.set(output.clone()); - output - } - }; - - let filtered_data: Vec<(f32, f32)> = data - .iter() - .filter(|(_, y)| &self.min_y <= y && y <= &self.max_y) - .map(|(x, y)| (*x, *y)) - .collect(); - chart.draw_series(LineSeries::new(filtered_data, &RED))?; - - let (rect_data, area): (Vec<(f32, f32, f32)>, f32) = match self.front_cache.is_valid() { - true => self.front_cache.get().clone(), - false => { - log("Updating front_cache"); - let step = absrange / (self.num_interval as f32); - let output: (Vec<(f32, f32, f32)>, f32) = self.integral_rectangles(step); - self.front_cache.set(output.clone()); - output - } - }; - - if self.num_interval <= 200 { - // Draw rectangles - chart.draw_series( - rect_data - .iter() - .map(|(x1, x2, y)| Rectangle::new([(*x2, *y), (*x1, 0.0)], &BLUE)), - )?; - } else { - // Save resources by not graphing rectangles and using an AreaSeries when you can no longer see the rectangles - let capped_data: Vec<(f32, f32)> = data - .iter() - .map(|(x, y)| { - if y.is_nan() { - return (*x, 0.0); - } - - let new_y: &f32 = if y > &self.max_y { - &self.max_y - } else if &self.min_y > y { - &self.min_y - } else { - y - }; - - (*x, *new_y) - }) - .collect(); - chart.draw_series(AreaSeries::new(capped_data, 0.0, &BLUE))?; - } - - root.present()?; - log("Finished Drawing!"); - Ok((chart.into_coord_trans(), area)) - } - - #[allow(clippy::too_many_arguments)] - pub fn update( - &mut self, canvas: HtmlCanvasElement, func_str_new: String, min_x: f32, max_x: f32, - min_y: f32, max_y: f32, num_interval: usize, resolution: usize, dark_mode: bool, - ) -> Result { - let func_str: String = add_asterisks(func_str_new); - let update_func: bool = !self.function.str_compare(func_str.clone()); - - let underlying_update = update_func - | (min_x != self.min_x) - | (max_x != self.max_x) - | (min_y != self.min_y) - | (max_y != self.max_y); - - if underlying_update | (self.resolution != resolution) { - self.back_cache.invalidate(); - } - - if underlying_update | (num_interval != self.num_interval) { - self.front_cache.invalidate(); - } - - if update_func { - self.function = Function::from_string(func_str); - } - - self.min_x = min_x; - self.max_x = max_x; - self.min_y = min_y; - self.max_y = max_y; - self.num_interval = num_interval; - self.resolution = resolution; - - let draw_output = self - .draw(canvas, dark_mode) - .map_err(|err| err.to_string())?; - let map_coord = draw_output.0; - - let chart_output = ChartOutput { - convert: Box::new(move |coord| map_coord(coord).map(|(x, y)| (x, y))), - area: draw_output.1, - }; - - Ok(chart_output) - } - - // Creates and does the math for creating all the rectangles under the graph - #[inline] - fn integral_rectangles(&self, step: f32) -> (Vec<(f32, f32, f32)>, f32) { - let data2: Vec<(f32, f32, f32)> = (0..self.num_interval) - .map(|e| { - let x: f32 = ((e as f32) * step) + self.min_x; - - // Makes sure rectangles are properly handled on x values below 0 - let x2: f32 = match x > 0.0 { - true => x + step, - false => x - step, - }; - - let tmp1: f32 = self.function.run(x); - let tmp2: f32 = self.function.run(x2); - - // Chooses the y value who's absolute value is the smallest - let y: f32 = match tmp2.abs() > tmp1.abs() { - true => tmp1, - false => tmp2, - }; - - (x, x2, y) - }) - .filter(|(_, _, y)| !y.is_nan()) - .collect(); - let area: f32 = data2.iter().map(|(_, _, y)| y * step).sum(); // sum of all rectangles' areas - (data2, area) - } + + log("Starting App..."); + let app = egui_app::MathApp::default(); + eframe::start_web(canvas_id, Box::new(app)) } diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..5aa526e --- /dev/null +++ b/src/main.rs @@ -0,0 +1,16 @@ +use eframe::{epi, egui}; +mod egui_app; +mod misc; +mod chart_manager; + +#[cfg(not(target_arch = "wasm32"))] +fn main() { + let app = egui_app::MathApp::default(); + let options = eframe::NativeOptions { + // Let's show off that we support transparent windows + transparent: true, + drag_and_drop_support: true, + ..Default::default() + }; + eframe::run_native(Box::new(app), options); +} \ No newline at end of file diff --git a/src/misc.rs b/src/misc.rs index 3c425bf..7f6a6ec 100644 --- a/src/misc.rs +++ b/src/misc.rs @@ -1,7 +1,4 @@ use meval::Expr; -use wasm_bindgen::prelude::*; - -pub type DrawResult = Result>; /* EXTREMELY Janky function that tries to put asterisks in the proper places to be parsed. This is so cursed. But it works, and I hopefully won't ever have to touch it again. @@ -80,6 +77,36 @@ pub fn add_asterisks(function_in: String) -> String { output_string.replace('π', "pi") // π -> pi } +// Tests function to make sure it's able to be parsed. Returns the string of the Error produced, or an empty string if it runs successfully. +pub fn test_func(function_string: String) -> String { + // Factorials do not work, and it would be really difficult to make them work + if function_string.contains('!') { + return "Factorials are unsupported".to_string(); + } + + let new_func_str: String = add_asterisks(function_string); + let expr_result = new_func_str.parse(); + let expr_error = match &expr_result { + Ok(_) => "".to_string(), + Err(error) => format!("{}", error), + }; + if !expr_error.is_empty() { + return expr_error; + } + + let expr: Expr = expr_result.unwrap(); + let func_result = expr.bind("x"); + let func_error = match &func_result { + Ok(_) => "".to_string(), + Err(error) => format!("{}", error), + }; + if !func_error.is_empty() { + return func_error; + } + + "".to_string() +} + pub struct Function { function: Box f64>, func_str: String, @@ -96,41 +123,13 @@ impl Function { } #[inline] - pub fn run(&self, x: f32) -> f32 { (self.function)(x as f64) as f32 } + pub fn run(&self, x: f64) -> f64 { (self.function)(x) } pub fn str_compare(&self, other_string: String) -> bool { self.func_str == other_string } pub fn get_string(&self) -> String { self.func_str.clone() } } -/// Result of screen to chart coordinates conversion. -#[wasm_bindgen] -pub struct Point { - pub x: f32, - pub y: f32, -} - -#[wasm_bindgen] -impl Point { - #[inline] - pub fn new(x: f32, y: f32) -> Self { Self { x, y } } -} - -#[wasm_bindgen] -pub struct ChartOutput { - pub(crate) convert: Box Option<(f32, f32)>>, - pub(crate) area: f32, -} - -#[wasm_bindgen] -impl ChartOutput { - pub fn get_area(&self) -> f32 { self.area } - - pub fn coord(&self, x: i32, y: i32) -> Option { - (self.convert)((x, y)).map(|(x, y)| Point::new(x, y)) - } -} - pub struct Cache { backing_data: Option, } diff --git a/start-server.sh b/start-server.sh index f60bd47..4478e99 100755 --- a/start-server.sh +++ b/start-server.sh @@ -11,8 +11,6 @@ then cargo install wasm-pack fi -wasm-pack build --release +wasm-pack build --release --target web -cd www -npm install -npm start +basic-http-server \ No newline at end of file diff --git a/www/bootstrap.js b/www/bootstrap.js deleted file mode 100644 index bf0f88a..0000000 --- a/www/bootstrap.js +++ /dev/null @@ -1,20 +0,0 @@ -init(); - -async function init() { - if (typeof process == "object") { - const [{ChartManager}, {main, setup}] = await Promise.all([ - import("integral_site"), - import("./index.js"), - ]); - setup(ChartManager); - main(); - } else { - const [{ChartManager, default: init}, {main, setup}] = await Promise.all([ - import("../pkg/integral_site.js"), - import("./index.js"), - ]); - await init(); - setup(ChartManager); - main(); - } -} diff --git a/www/index.html b/www/index.html index 2fbaf96..aa8cc84 100644 --- a/www/index.html +++ b/www/index.html @@ -1,5 +1,8 @@ + + + @@ -8,34 +11,25 @@ - -
-

Integral Demonstration

-
- -
Loading WebAssembly...
-
- -

- -

- -

- -

- -

- -

- -

-
-
Area:
-
+ + + + -

Supported Expressions

+ + + diff --git a/www/index.js b/www/index.js deleted file mode 100644 index 4edd38e..0000000 --- a/www/index.js +++ /dev/null @@ -1,167 +0,0 @@ -class ChartManager {} - -const canvas = document.getElementById("canvas"); -const coord = document.getElementById("coord"); -const math_function = document.getElementById("math_function"); -const status = document.getElementById("status"); - -const minX = document.getElementById("minX"); -const maxX = document.getElementById("maxX"); -const minY = document.getElementById("minY"); -const maxY = document.getElementById("maxY"); -const num_interval = document.getElementById("num_interval"); -const area_msg = document.getElementById("area-msg"); -const resolution = document.getElementById("resolution"); - -let darkMode = false; - -let chart = null; -let chart_manager = null; - -/** Main entry point */ -export function main() { - setupUI(); - setupCanvas(); -} - -/** This function is used in `bootstrap.js` to setup imports. */ -export function setup(WasmChart) { - ChartManager = WasmChart; -} - -/** Add event listeners. */ -function setupUI() { - status.innerText = "WebAssembly loaded!"; - - // Handles browser color preferences - if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { - darkMode = true; - } - - // Watches for changes in color preferences - window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', event => { - darkMode = event.matches; - updatePlot(); - }); - - math_function.addEventListener("change", updatePlot); - minX.addEventListener("input", updatePlot); - maxX.addEventListener("input", updatePlot); - minY.addEventListener("input", updatePlot); - maxY.addEventListener("input", updatePlot); - num_interval.addEventListener("input", updatePlot); - resolution.addEventListener("input", updatePlot); - - - window.addEventListener("resize", setupCanvas); - window.addEventListener("mousemove", onMouseMove); -} - -function setupCanvas() { - const aspectRatio = canvas.width / canvas.height; - const size = canvas.parentNode.offsetWidth * 0.8; - canvas.style.width = size + "px"; - canvas.style.height = size / aspectRatio + "px"; - canvas.width = size; - canvas.height = size / aspectRatio; - updatePlot(); -} - -function onMouseMove(event) { - if (chart) { - var text = "Mouse is outside Chart."; - - if (event.target == canvas) { - let actualRect = canvas.getBoundingClientRect(); - let logicX = event.offsetX * canvas.width / actualRect.width; - let logicY = event.offsetY * canvas.height / actualRect.height; - const point = chart.coord(logicX, logicY); - text = (point) - ? `(${point.x.toFixed(3)}, ${point.y.toFixed(3)})` - : text; - } - coord.innerText = text; - } -} - -function postErrorStatus(string) { - status.style.color = "red"; - status.innerText = string; -} - -function postNormalStatus(string) { - status.style.color = "grey"; - status.innerText = string; -} - -// Checks variables put in input fields -function checkVariables() { - if (minX.value >= maxX.value) { - postErrorStatus("minX must be smaller than maxX!"); - return; - } - - if (minY.value >= maxY.value) { - postErrorStatus("minY must be smaller than maxY!"); - return; - } - - if (0 > num_interval.value) { - postErrorStatus("Interval is smaller than 0!"); - return; - } - - if (0 > resolution.value) { - postErrorStatus("Number of Points is smaller than 0!"); - return; - } -} - -// Generates a possible "tip" to assist the user when an error occurs. -function errorRecommend(error_string) { - if (error_string.includes("Evaluation error: unknown variable ")) { - return "This variable is not considered valid. Make sure you used a valid variable."; - } else if (error_string == "Factorials are unsupported") { - return ""; - } else { - return "Make sure you're using proper syntax! Check console log (press F12) as well for more details."; - } -} - - -function updatePlot() { - checkVariables(); - - if (chart_manager == null) { - try { - chart_manager = ChartManager.new(math_function.value, Number(minX.value), Number(maxX.value), Number(minY.value), Number(maxY.value), Number(num_interval.value), Number(resolution.value)); - } catch(err) { - postErrorStatus("Error during ChartManager creation! Check logs for details."); - return; - } - } - - const test_result = ChartManager.test_func(math_function.value); - if (test_result != "") { - const error_recommendation = errorRecommend(test_result); - let error_status_str = test_result; - if (error_recommendation != "") { - error_status_str += `\nTip: ${error_recommendation}`; - } - postErrorStatus(error_status_str); - return; - } - - try { - postNormalStatus(`Rendering y=${math_function.value}...`); - const start = performance.now(); - chart = chart_manager.update(canvas, math_function.value, Number(minX.value), Number(maxX.value), Number(minY.value), Number(maxY.value), Number(num_interval.value), Number(resolution.value), false); // TODO: improve darkmode support - const end = performance.now(); - - area_msg.innerText = `Estimated Area: ${chart.get_area()}`; - - postNormalStatus(`Rendered ${math_function.innerText} in ${Math.ceil(end - start)}ms`); - } catch(err) { - postErrorStatus(`Error! check console logs for more detail`); - } -} diff --git a/www/package.json b/www/package.json deleted file mode 100644 index cc5e5cd..0000000 --- a/www/package.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "integral_site", - "version": "0.1.0", - "main": "index.js", - "scripts": { - "build": "webpack --config webpack.config.js", - "start": "webpack-dev-server" - }, - "dependencies": { - "integral_site": "file:../pkg" - }, - "devDependencies": { - "webpack": "^4.43.0", - "webpack-cli": "^3.3.11", - "webpack-dev-server": "^3.10.3", - "copy-webpack-plugin": "^5.0.0" - } -} diff --git a/www/style.css b/www/style.css index 404d385..ec19321 100644 --- a/www/style.css +++ b/www/style.css @@ -1,55 +1,81 @@ -html, body, main { - width: 100%; - margin: 0; - padding: 0; +html { + /* Remove touch delay: */ + touch-action: manipulation; } body { - margin: auto; - max-width: 800px; - display: flex; - flex-direction: column; + /* Light mode background color for what is not covered by the egui canvas, + or where the egui canvas is translucent. */ + background: #909090; } -@media (max-width: 800px) { +@media (prefers-color-scheme: dark) { body { - padding: 10px; - box-sizing: border-box; + /* Dark mode background color for what is not covered by the egui canvas, + or where the egui canvas is translucent. */ + background: #404040; } } -main { - display: flex; - flex-direction: column; - align-items: center; +/* Allow canvas to fill entire web page: */ +html, +body { + overflow: hidden; + margin: 0 !important; + padding: 0 !important; } -#coord, #status { - color: grey; - font-size: 10px; - height: 15px +/* Position canvas in center-top: */ +canvas { + margin-right: auto; + margin-left: auto; + display: block; + position: absolute; + top: 0%; + left: 50%; + transform: translate(-50%, 0%); } -#control { - margin-top: 1em; -} - -#control label { - font-weight: bold; - margin-right: 1em; -} - -#control select { - padding: 0.25em 0.5em; -} - -footer { - margin-top: 2em; - font-size: 12px; +.centered { + margin-right: auto; + margin-left: auto; + display: block; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + color: #f0f0f0; + font-size: 24px; + font-family: Ubuntu-Light, Helvetica, sans-serif; text-align: center; } -.hide { - visibility: hidden; - height: 0px; +/* ---------------------------------------------- */ +/* Loading animation from https://loading.io/css/ */ +.lds-dual-ring { + display: inline-block; + width: 24px; + height: 24px; +} + +.lds-dual-ring:after { + content: " "; + display: block; + width: 24px; + height: 24px; + margin: 0px; + border-radius: 50%; + border: 3px solid #fff; + border-color: #fff transparent #fff transparent; + animation: lds-dual-ring 1.2s linear infinite; +} + +@keyframes lds-dual-ring { + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } } diff --git a/www/style.css.new b/www/style.css.new deleted file mode 100644 index 7685c70..0000000 --- a/www/style.css.new +++ /dev/null @@ -1,498 +0,0 @@ -html { - font-size: 100%; - overflow-y: scroll; - -webkit-text-size-adjust: 100%; - -ms-text-size-adjust: 100%; -} - -@media (prefers-color-scheme: dark) { - input, - body { - color: #c9d1d9; - font-family: 'Open Sans', sans-serif; - font-size: 12px; - line-height: 1.5em; - padding: 1em; - margin: auto; - max-width: 52em; - /* github dark color: - background: #0d1117; */ - - /* duckduckgo dark color */ - background: #1c1c1c; - } -} - -@media (prefers-color-scheme: light) { - input, - body { - color: #444; - font-family: 'Open Sans', sans-serif; - font-size: 12px; - line-height: 1.5em; - padding: 1em; - margin: auto; - max-width: 52em; - background: #fefefe; - } -} - - -@media (prefers-color-scheme: dark) { - /* Links */ - a { - color: #58a6ff; - text-decoration: none; - } - - /* Visited Links */ - a:visited { - color: #58a6ff; - } - - /* Links that are being hovered over */ - a:hover { - color: #58a6ff; - cursor:pointer; - } -} - -@media (prefers-color-scheme: light) { - /* Links */ - a { - color: #0645ad; - text-decoration: none; - } - - /* Visited Links */ - a:visited { - color: #0b0080; - } - - /* Links that are being hovered over */ - a:hover { - color: #06e; - cursor:pointer; - } -} - - -a:active { - color: #faa700; -} - -a:focus { - outline: thin dotted; -} - -a:hover, -a:active { - outline: 0; -} - -/* Paragraph selected (Legacy firefox 61 and below) */ -::-moz-selection { - background: rgba(255, 255, 0, 0.3); - color: #000; -} - -/* Paragraph selected */ -::selection { - background: rgba(255, 255, 0, 0.3); - color: #000; -} - -/* Paragraph selected (Legacy firefox 61 and below) */ -a::-moz-selection { - background: rgba(255, 255, 0, 0.3); - color: #0645ad; -} - -/* Paragraph selected */ -a::selection { - background: rgba(255, 255, 0, 0.3); - color: #0645ad; -} - - -p { - margin: 1em 0; -} - -img { - max-width: 100%; -} - - -/* Inline code snippets */ -@media (prefers-color-scheme: dark) { - code { - border-radius: 5px; - -moz-border-radius: 5px; - -webkit-border-radius: 5px; - border: 1px solid #2f333a; - padding: 2px; - } -} - -/* Inline code snippets */ -@media (prefers-color-scheme: light) { - code { - border-radius: 5px; - -moz-border-radius: 5px; - -webkit-border-radius: 5px; - border: 1px solid #BCBEC0; - padding: 2px; - } -} - -/* Post-Inline code snippets */ -pre code { - border-radius: 0px; - -moz-border-radius: 0px; - -webkit-border-radius: 0px; - border: 0px; - padding: 2px; -} - - -/* Headers */ -@media (prefers-color-scheme: dark) { - h1, - h2, - h3, - h4, - h5, - h6 { - font-weight: 600; - color: #c9d1d9; - line-height: 1em; - } -} - -/* Headers */ -@media (prefers-color-scheme: light) { - h1, - h2, - h3, - h4, - h5, - h6 { - font-weight: 600; - color: #111; - line-height: 1em; - } -} - -h4, -h5, -h6 { - font-weight: bold; -} - -h1 { - font-size: 2em; -} - -h2 { - font-size: 1.3em; - padding-top: 30px; -} - -h3 { - font-size: 1.1em; - padding-top: 10px; -} - -h4 { - font-size: 0.9em; - padding-top: 5px; -} - -h5 { - font-size: 0.9em; -} - -h6 { - font-size: 0.9em; -} - - -blockquote { - color: #666666; - margin: 0; - padding-left: 3em; - border-left: 0.5em #eee solid; -} - -hr { - display: block; - border: 0; - border-top: 1px solid #aaa; - border-bottom: 1px solid #eee; - margin: 1em 0; - padding: 0; -} - -pre, -code, -kbd, -samp { - font-family: 'Fira Code', monospace; - font-size: 0.98em; -} - -/* Markdown code block */ -@media (prefers-color-scheme: dark) { - pre { - white-space: pre; - white-space: pre-wrap; - word-wrap: break-word; - /* Use with "github dark" background: */ - /* background-color: #1f2227; */ - /* Use with "duckduckgo dark" background: */ - background-color: #202325; - padding: 10px 15px; - } -} - -/* Markdown code block */ -@media (prefers-color-scheme: light) { - pre { - white-space: pre; - white-space: pre-wrap; - word-wrap: break-word; - background-color: #eee; - padding: 10px 15px; - } -} - -b, -strong { - font-weight: bold; -} - -dfn { - font-style: italic; -} - -ins { - background: #ff9; - color: #000; - text-decoration: none; -} - -mark { - background: #ff0; - color: #000; - font-style: italic; - font-weight: bold; -} - -sub, -sup { - font-size: 75%; - line-height: 0; - position: relative; - vertical-align: baseline; -} - -sup { - top: -0.5em; -} - -sub { - bottom: -0.25em; -} - -ul, -ol { - margin: 1em 0; - padding: 0 0 0 2em; -} - -li p:last-child { - margin: 0; -} - -dd { - margin: 0 0 0 2em; -} - -img { - border: 0; - -ms-interpolation-mode: bicubic; - vertical-align: middle; -} - -table { - border-collapse: collapse; - border-spacing: 0; -} - -td { - vertical-align: top; -} - -@media only screen and (min-width: 480px) { - body { - font-size: 14px; - } - .logo { - max-height: 30px - } -} - -@media only screen and (min-width: 768px) { - body { - font-size: 16px; - } - .logo { - max-height: 40px; - } - article { - margin: 50px 0; - } -} - -@media print { - * { - background: transparent !important; - color: black !important; - filter: none !important; - -ms-filter: none !important; - } - - body { - font-size: 12pt; - max-width: 100%; - } - - a, - a:visited { - text-decoration: underline; - } - - hr { - height: 1px; - border: 0; - border-bottom: 1px solid black; - } - - a[href]:after { - content: " (" attr(href) ")"; - } - - abbr[title]:after { - content: " (" attr(title) ")"; - } - - .ir a:after, - a[href^="javascript:"]:after, - a[href^="#"]:after { - content: ""; - } - - pre, - blockquote { - border: 1px solid #999; - padding-right: 1em; - page-break-inside: avoid; - } - - tr, - img { - page-break-inside: avoid; - } - - img { - max-width: 100% !important; - } - - @page :left { - margin: 15mm 20mm 15mm 10mm; - } - - @page :right { - margin: 15mm 10mm 15mm 20mm; - } - - p, - h2, - h3 { - orphans: 3; - widows: 3; - } - - h2, - h3 { - page-break-after: avoid; - } -} - -nav ul { - margin: 0; - padding: 0; - list-style-type: none; - overflow: hidden; -} - -nav ul li { - /* This allow us to arrange list items in a row, without using float */ - display: inline-block; - list-style-type: none; -} -/* Create a style for the first level items */ -nav > div > ul > li > a { - color: #333 !important; - display: block; - line-height: 2em; - padding: 0.5em 0em; - text-decoration: none; -} - -nav > div.nav-right > ul > li > a { - padding: 0.5em 0.5em; -} - -nav > div > ul > li > a:hover { - color: #aaa !important; -} - -.nav-left { - float: left; -} - -.nav-left ul li { - float: left; -} - -.nav-right ul li { - float: right; -} - -.logo { - margin-right: 0.5em -} - -article img { - margin: 1em 0; -} - -p.date { - font-size: 13px; - color: #666; -} - -ul.articles { - list-style: none; - padding: 0; -} - -#Articles { - margin-top: 2em; -} - -footer { - font-size: 13px; -} diff --git a/www/webpack.config.js b/www/webpack.config.js deleted file mode 100644 index 80ad814..0000000 --- a/www/webpack.config.js +++ /dev/null @@ -1,14 +0,0 @@ -const CopyWebpackPlugin = require("copy-webpack-plugin"); -const path = require('path'); - -module.exports = { - entry: "./bootstrap.js", - output: { - path: path.resolve(__dirname, "dist"), - filename: "bootstrap.js", - }, - mode: "development", - plugins: [ - new CopyWebpackPlugin(['index.html']) - ], -};