From dd6d8c80b8bca30ee7f18f830f6d08f2052920dd Mon Sep 17 00:00:00 2001 From: Simon Gardling Date: Thu, 10 Mar 2022 09:01:21 -0500 Subject: [PATCH] use tabs, not spaces --- TODO.md | 14 +- build.sh | 20 +- src/egui_app.rs | 1054 ++++++++++++++++++++-------------------- src/function.rs | 912 +++++++++++++++++----------------- src/function_output.rs | 156 +++--- src/lib.rs | 36 +- src/main.rs | 14 +- src/misc.rs | 144 +++--- src/parsing.rs | 338 ++++++------- www/index.html | 48 +- www/style.css | 98 ++-- 11 files changed, 1417 insertions(+), 1417 deletions(-) diff --git a/TODO.md b/TODO.md index 2fe0bfd..a9a2124 100644 --- a/TODO.md +++ b/TODO.md @@ -1,14 +1,14 @@ ## TODO: 1. Multiple functions in one graph. - - Backend support - - Integrals between functions (too hard to implement, maybe will shelve) - - UI - - Different colors (kinda) + - Backend support + - Integrals between functions (too hard to implement, maybe will shelve) + - UI + - Different colors (kinda) 2. Rerwite of function parsing code - - Non `y=` functions. + - Non `y=` functions. 3. Smart display of graph - - Display of intersections between functions - - Caching of roots and extrema + - Display of intersections between functions + - Caching of roots and extrema 4. Fix integral line 5. re-add euler's number (well it works if you use capital e like `E^x`) 6. allow constants in min/max integral input (like pi or euler's number) diff --git a/build.sh b/build.sh index 9843d19..e9f1fd7 100755 --- a/build.sh +++ b/build.sh @@ -8,21 +8,21 @@ cargo test #apply optimizations via wasm-opt wasm_opt() { - wasm-opt -Oz -o pkg/ytbn_graphing_software_bg_2.wasm pkg/ytbn_graphing_software_bg.wasm - mv pkg/ytbn_graphing_software_bg_2.wasm pkg/ytbn_graphing_software_bg.wasm + wasm-opt -Oz -o pkg/ytbn_graphing_software_bg_2.wasm pkg/ytbn_graphing_software_bg.wasm + mv pkg/ytbn_graphing_software_bg_2.wasm pkg/ytbn_graphing_software_bg.wasm } if test "$1" == "" || test "$1" == "release"; then - wasm-pack build --target web --release --no-typescript - echo "Binary size (pre-wasm_opt): $(du -sb pkg/ytbn_graphing_software_bg.wasm)" - wasm_opt #apply wasm optimizations - echo "Binary size (pre-strip): $(du -sb pkg/ytbn_graphing_software_bg.wasm)" - llvm-strip --strip-all pkg/ytbn_graphing_software_bg.wasm + wasm-pack build --target web --release --no-typescript + echo "Binary size (pre-wasm_opt): $(du -sb pkg/ytbn_graphing_software_bg.wasm)" + wasm_opt #apply wasm optimizations + echo "Binary size (pre-strip): $(du -sb pkg/ytbn_graphing_software_bg.wasm)" + llvm-strip --strip-all pkg/ytbn_graphing_software_bg.wasm elif test "$1" == "debug"; then - wasm-pack build --target web --debug --no-typescript + wasm-pack build --target web --debug --no-typescript else - echo "ERROR: build.sh, argument invalid" - exit 1 + echo "ERROR: build.sh, argument invalid" + exit 1 fi mkdir tmp diff --git a/src/egui_app.rs b/src/egui_app.rs index 0f7f765..181800c 100644 --- a/src/egui_app.rs +++ b/src/egui_app.rs @@ -6,29 +6,29 @@ use const_format::formatc; use eframe::{egui, epi}; use egui::plot::Plot; use egui::{ - Button, CentralPanel, Color32, ComboBox, Context, FontData, FontDefinitions, FontFamily, - RichText, SidePanel, Slider, TopBottomPanel, Vec2, Visuals, Window, + Button, CentralPanel, Color32, ComboBox, Context, FontData, FontDefinitions, FontFamily, + RichText, SidePanel, Slider, TopBottomPanel, Vec2, Visuals, Window, }; use epi::{Frame, Storage}; use instant::Duration; use shadow_rs::shadow; use std::{ - collections::BTreeMap, - io::Read, - ops::{BitXorAssign, RangeInclusive}, - str, + collections::BTreeMap, + io::Read, + ops::{BitXorAssign, RangeInclusive}, + str, }; shadow!(build); // Constant string that has a string containing information about the build. const BUILD_INFO: &str = formatc!( - "Commit: {} ({})\nBuild Date: {}\nRust Channel: {}\nRust Version: {}", - &build::SHORT_COMMIT, - &build::BRANCH, - &build::BUILD_TIME, - &build::RUST_CHANNEL, - &build::RUST_VERSION, + "Commit: {} ({})\nBuild Date: {}\nRust Channel: {}\nRust Version: {}", + &build::SHORT_COMMIT, + &build::BRANCH, + &build::BUILD_TIME, + &build::RUST_CHANNEL, + &build::RUST_VERSION, ); // Sets some hard-coded limits to the application @@ -46,192 +46,192 @@ const DEFAULT_INTEGRAL_NUM: usize = 100; // Stores data loaded from files struct FileData { - // Stores fonts - pub font_ubuntu_light: FontData, - pub font_notoemoji: FontData, - pub font_hack: FontData, + // Stores fonts + pub font_ubuntu_light: FontData, + pub font_notoemoji: FontData, + pub font_hack: FontData, - // Stores text - pub text_help_expr: String, - pub text_help_vars: String, - pub text_help_panel: String, - pub text_help_function: String, - pub text_help_other: String, + // Stores text + pub text_help_expr: String, + pub text_help_vars: String, + pub text_help_panel: String, + pub text_help_function: String, + pub text_help_other: String, } lazy_static::lazy_static! { - // Load all of the data from the compressed tarball - static ref FILE_DATA: FileData = { - let start = instant::Instant::now(); - log_helper("Loading assets..."); - let mut tar_file_raw = include_bytes!("../data.tar.zst").as_slice(); - log_helper("Decompressing..."); - let mut tar_file = ruzstd::StreamingDecoder::new(&mut tar_file_raw).unwrap(); - let mut tar_file_data = Vec::new(); - tar_file.read_to_end(&mut tar_file_data).unwrap(); + // Load all of the data from the compressed tarball + static ref FILE_DATA: FileData = { + let start = instant::Instant::now(); + log_helper("Loading assets..."); + let mut tar_file_raw = include_bytes!("../data.tar.zst").as_slice(); + log_helper("Decompressing..."); + let mut tar_file = ruzstd::StreamingDecoder::new(&mut tar_file_raw).unwrap(); + let mut tar_file_data = Vec::new(); + tar_file.read_to_end(&mut tar_file_data).unwrap(); - let mut tar_archive = tar::Archive::new(&*tar_file_data); + let mut tar_archive = tar::Archive::new(&*tar_file_data); - // Stores fonts - let mut font_ubuntu_light: Option = None; - let mut font_notoemoji: Option = None; - let mut font_hack: Option = None; + // Stores fonts + let mut font_ubuntu_light: Option = None; + let mut font_notoemoji: Option = None; + let mut font_hack: Option = None; - // Stores text - let mut text_help_expr: Option = None; - let mut text_help_vars: Option = None; - let mut text_help_panel: Option = None; - let mut text_help_function: Option = None; - let mut text_help_other: Option = None; + // Stores text + let mut text_help_expr: Option = None; + let mut text_help_vars: Option = None; + let mut text_help_panel: Option = None; + let mut text_help_function: Option = None; + let mut text_help_other: Option = None; - log_helper("Reading asset files..."); - // Iterate through all entries in the tarball - for file in tar_archive.entries().unwrap() { - let mut file = file.unwrap(); - let mut data: Vec = Vec::new(); - file.read_to_end(&mut data).unwrap(); - let path = file.header().path().unwrap(); - let path_string = path.to_string_lossy(); + log_helper("Reading asset files..."); + // Iterate through all entries in the tarball + for file in tar_archive.entries().unwrap() { + let mut file = file.unwrap(); + let mut data: Vec = Vec::new(); + file.read_to_end(&mut data).unwrap(); + let path = file.header().path().unwrap(); + let path_string = path.to_string_lossy(); - debug_log(&format!("Loading file: {}", path_string)); + debug_log(&format!("Loading file: {}", path_string)); - // Match the filename - if path_string.ends_with(".ttf") { - // Parse font files - let font_data = FontData::from_owned(data); - match path_string.as_ref() { - "Hack-Regular.ttf" => { - font_hack = Some(font_data); - }, - "NotoEmoji-Regular.ttf" => { - font_notoemoji = Some(font_data); - }, - "Ubuntu-Light.ttf" => { - font_ubuntu_light = Some(font_data); - }, - _ => { - panic!("Font File {} not expected!", path_string); - } - } - } else if path_string.ends_with(".txt") { - // Parse text files - let string_data = str::from_utf8(&data).unwrap().to_string(); - match path_string.as_ref() { - "text_help_expr.txt" => { - text_help_expr = Some(string_data); - }, - "text_help_vars.txt" => { - text_help_vars = Some(string_data); - }, - "text_help_panel.txt" => { - text_help_panel = Some(string_data); - }, - "text_help_function.txt" => { - text_help_function = Some(string_data); - }, - "text_help_other.txt" => { - text_help_other = Some(string_data); - }, - _ => { - panic!("Text file {} not expected!", path_string); - } - } - } else { - panic!("Other file {} not expected!", path_string); - } - } + // Match the filename + if path_string.ends_with(".ttf") { + // Parse font files + let font_data = FontData::from_owned(data); + match path_string.as_ref() { + "Hack-Regular.ttf" => { + font_hack = Some(font_data); + }, + "NotoEmoji-Regular.ttf" => { + font_notoemoji = Some(font_data); + }, + "Ubuntu-Light.ttf" => { + font_ubuntu_light = Some(font_data); + }, + _ => { + panic!("Font File {} not expected!", path_string); + } + } + } else if path_string.ends_with(".txt") { + // Parse text files + let string_data = str::from_utf8(&data).unwrap().to_string(); + match path_string.as_ref() { + "text_help_expr.txt" => { + text_help_expr = Some(string_data); + }, + "text_help_vars.txt" => { + text_help_vars = Some(string_data); + }, + "text_help_panel.txt" => { + text_help_panel = Some(string_data); + }, + "text_help_function.txt" => { + text_help_function = Some(string_data); + }, + "text_help_other.txt" => { + text_help_other = Some(string_data); + }, + _ => { + panic!("Text file {} not expected!", path_string); + } + } + } else { + panic!("Other file {} not expected!", path_string); + } + } - log_helper(&format!("Done loading assets! Took: {:?}", start.elapsed())); + log_helper(&format!("Done loading assets! Took: {:?}", start.elapsed())); - // Create and return FileData struct - FileData { - font_ubuntu_light: font_ubuntu_light.expect("Ubuntu Light font not found!"), - font_notoemoji: font_notoemoji.expect("Noto Emoji font not found!"), - font_hack: font_hack.expect("Hack font not found!"), - text_help_expr: text_help_expr.expect("HELP_EXPR not found"), - text_help_vars: text_help_vars.expect("HELP_VARS not found"), - text_help_panel: text_help_panel.expect("HELP_PANEL not found"), - text_help_function: text_help_function.expect("HELP_FUNCTION not found"), - text_help_other: text_help_other.expect("HELP_OTHER not found"), - } - }; + // Create and return FileData struct + FileData { + font_ubuntu_light: font_ubuntu_light.expect("Ubuntu Light font not found!"), + font_notoemoji: font_notoemoji.expect("Noto Emoji font not found!"), + font_hack: font_hack.expect("Hack font not found!"), + text_help_expr: text_help_expr.expect("HELP_EXPR not found"), + text_help_vars: text_help_vars.expect("HELP_VARS not found"), + text_help_panel: text_help_panel.expect("HELP_PANEL not found"), + text_help_function: text_help_function.expect("HELP_FUNCTION not found"), + text_help_other: text_help_other.expect("HELP_OTHER not found"), + } + }; - // Stores the FontDefinitions used by egui - static ref FONT_DEFINITIONS: FontDefinitions = { - let mut font_data: BTreeMap = BTreeMap::new(); - let mut families = BTreeMap::new(); + // Stores the FontDefinitions used by egui + static ref FONT_DEFINITIONS: FontDefinitions = { + let mut font_data: BTreeMap = BTreeMap::new(); + let mut families = BTreeMap::new(); - font_data.insert("Hack".to_owned(), FILE_DATA.font_hack.clone()); - font_data.insert("Ubuntu-Light".to_owned(), FILE_DATA.font_ubuntu_light.clone()); - font_data.insert("NotoEmoji-Regular".to_owned(), FILE_DATA.font_notoemoji.clone()); + font_data.insert("Hack".to_owned(), FILE_DATA.font_hack.clone()); + font_data.insert("Ubuntu-Light".to_owned(), FILE_DATA.font_ubuntu_light.clone()); + font_data.insert("NotoEmoji-Regular".to_owned(), FILE_DATA.font_notoemoji.clone()); - families.insert( - FontFamily::Monospace, - vec!["Hack".to_owned(), "Ubuntu-Light".to_owned(), "NotoEmoji-Regular".to_owned()], - ); + families.insert( + FontFamily::Monospace, + vec!["Hack".to_owned(), "Ubuntu-Light".to_owned(), "NotoEmoji-Regular".to_owned()], + ); - families.insert( - FontFamily::Proportional, - vec!["Ubuntu-Light".to_owned(), "NotoEmoji-Regular".to_owned()], - ); + families.insert( + FontFamily::Proportional, + vec!["Ubuntu-Light".to_owned(), "NotoEmoji-Regular".to_owned()], + ); - FontDefinitions { - font_data, - families, - } - }; + FontDefinitions { + font_data, + families, + } + }; } // Tests to make sure archived (and compressed) assets match expected data #[test] fn test_file_data() { - assert_eq!( - FILE_DATA.font_ubuntu_light, - FontData::from_owned(include_bytes!("../assets/Ubuntu-Light.ttf").to_vec()) - ); - assert_eq!( - FILE_DATA.font_notoemoji, - FontData::from_owned(include_bytes!("../assets/NotoEmoji-Regular.ttf").to_vec()) - ); - assert_eq!( - FILE_DATA.font_hack, - FontData::from_owned(include_bytes!("../assets/Hack-Regular.ttf").to_vec()) - ); + assert_eq!( + FILE_DATA.font_ubuntu_light, + FontData::from_owned(include_bytes!("../assets/Ubuntu-Light.ttf").to_vec()) + ); + assert_eq!( + FILE_DATA.font_notoemoji, + FontData::from_owned(include_bytes!("../assets/NotoEmoji-Regular.ttf").to_vec()) + ); + assert_eq!( + FILE_DATA.font_hack, + FontData::from_owned(include_bytes!("../assets/Hack-Regular.ttf").to_vec()) + ); - assert_eq!( - FILE_DATA.text_help_expr, - include_str!("../assets/text_help_expr.txt") - ); - assert_eq!( - FILE_DATA.text_help_vars, - include_str!("../assets/text_help_vars.txt") - ); - assert_eq!( - FILE_DATA.text_help_panel, - include_str!("../assets/text_help_panel.txt") - ); - assert_eq!( - FILE_DATA.text_help_function, - include_str!("../assets/text_help_function.txt") - ); - assert_eq!( - FILE_DATA.text_help_other, - include_str!("../assets/text_help_other.txt") - ); + assert_eq!( + FILE_DATA.text_help_expr, + include_str!("../assets/text_help_expr.txt") + ); + assert_eq!( + FILE_DATA.text_help_vars, + include_str!("../assets/text_help_vars.txt") + ); + assert_eq!( + FILE_DATA.text_help_panel, + include_str!("../assets/text_help_panel.txt") + ); + assert_eq!( + FILE_DATA.text_help_function, + include_str!("../assets/text_help_function.txt") + ); + assert_eq!( + FILE_DATA.text_help_other, + include_str!("../assets/text_help_other.txt") + ); } cfg_if::cfg_if! { - if #[cfg(target_arch = "wasm32")] { - use wasm_bindgen::JsCast; + if #[cfg(target_arch = "wasm32")] { + use wasm_bindgen::JsCast; - // removes the "loading" element on the web page that displays a loading indicator - fn stop_loading() { - let document = web_sys::window().unwrap().document().unwrap(); - let loading_element = document.get_element_by_id("loading").unwrap().dyn_into::().unwrap(); - loading_element.remove(); - } - } + // removes the "loading" element on the web page that displays a loading indicator + fn stop_loading() { + let document = web_sys::window().unwrap().document().unwrap(); + let loading_element = document.get_element_by_id("loading").unwrap().dyn_into::().unwrap(); + loading_element.remove(); + } + } } // Used to provide info on the Licensing of the project @@ -242,428 +242,428 @@ const PROJECT_URL: &str = "https://github.com/Titaniumtown/YTBN-Graphing-Softwar // Stores settings struct AppSettings { - // Stores whether or not the Help window is open - pub help_open: bool, + // Stores whether or not the Help window is open + pub help_open: bool, - // Stores whether or not the Info window is open - pub info_open: bool, + // Stores whether or not the Info window is open + pub info_open: bool, - // Stores whether or not the side panel is shown or not - pub show_side_panel: bool, + // Stores whether or not the side panel is shown or not + pub show_side_panel: bool, - // Stores the type of Rienmann sum that should be calculated - pub sum: RiemannSum, + // Stores the type of Rienmann sum that should be calculated + pub sum: RiemannSum, - // Min and Max range for calculating an integral - pub integral_min_x: f64, - pub integral_max_x: f64, + // Min and Max range for calculating an integral + pub integral_min_x: f64, + pub integral_max_x: f64, - // Number of rectangles used to calculate integral - pub integral_num: usize, + // Number of rectangles used to calculate integral + pub integral_num: usize, - // Stores whether or not dark mode is enabled - pub dark_mode: bool, + // Stores whether or not dark mode is enabled + pub dark_mode: bool, - pub extrema: bool, + pub extrema: bool, - pub roots: bool, + pub roots: bool, } impl Default for AppSettings { - fn default() -> Self { - Self { - help_open: true, - info_open: false, - show_side_panel: true, - sum: DEFAULT_RIEMANN, - integral_min_x: DEFAULT_MIN_X, - integral_max_x: DEFAULT_MAX_X, - integral_num: DEFAULT_INTEGRAL_NUM, - dark_mode: true, - extrema: true, - roots: true, - } - } + fn default() -> Self { + Self { + help_open: true, + info_open: false, + show_side_panel: true, + sum: DEFAULT_RIEMANN, + integral_min_x: DEFAULT_MIN_X, + integral_max_x: DEFAULT_MAX_X, + integral_num: DEFAULT_INTEGRAL_NUM, + dark_mode: true, + extrema: true, + roots: true, + } + } } pub struct MathApp { - // Stores vector of functions - functions: Vec, + // Stores vector of functions + functions: Vec, - // Stores vector containing the string representation of the functions. This is used because of hacky reasons - func_strs: Vec, + // Stores vector containing the string representation of the functions. This is used because of hacky reasons + func_strs: Vec, - // Stores last error from parsing functions (used to display the same error when side panel is minimized) - last_error: Vec<(usize, String)>, + // Stores last error from parsing functions (used to display the same error when side panel is minimized) + last_error: Vec<(usize, String)>, - // Contains the list of Areas calculated (the vector of f64) and time it took for the last frame (the Duration). Stored in a Tuple. - last_info: (Vec, Duration), + // Contains the list of Areas calculated (the vector of f64) and time it took for the last frame (the Duration). Stored in a Tuple. + last_info: (Vec, Duration), - // Stores Settings (pretty self explanatory) - settings: AppSettings, + // Stores Settings (pretty self explanatory) + settings: AppSettings, } impl Default for MathApp { - fn default() -> Self { - Self { - functions: vec![EMPTY_FUNCTION_ENTRY.clone().integral(true)], - func_strs: vec![String::from(DEFAULT_FUNCION)], - last_error: Vec::new(), - last_info: (vec![0.0], Duration::ZERO), - settings: AppSettings::default(), - } - } + fn default() -> Self { + Self { + functions: vec![EMPTY_FUNCTION_ENTRY.clone().integral(true)], + func_strs: vec![String::from(DEFAULT_FUNCION)], + last_error: Vec::new(), + last_info: (vec![0.0], Duration::ZERO), + settings: AppSettings::default(), + } + } } impl MathApp { - fn side_panel(&mut self, ctx: &Context) { - // Side Panel which contains vital options to the operation of the application (such as adding functions and other options) - SidePanel::left("side_panel") - .resizable(false) - .show(ctx, |ui| { - ComboBox::from_label("Riemann Sum Type") - .selected_text(self.settings.sum.to_string()) - .show_ui(ui, |ui| { - ui.selectable_value(&mut self.settings.sum, RiemannSum::Left, "Left"); - ui.selectable_value(&mut self.settings.sum, RiemannSum::Middle, "Middle"); - ui.selectable_value(&mut self.settings.sum, RiemannSum::Right, "Right"); - }); + fn side_panel(&mut self, ctx: &Context) { + // Side Panel which contains vital options to the operation of the application (such as adding functions and other options) + SidePanel::left("side_panel") + .resizable(false) + .show(ctx, |ui| { + ComboBox::from_label("Riemann Sum Type") + .selected_text(self.settings.sum.to_string()) + .show_ui(ui, |ui| { + ui.selectable_value(&mut self.settings.sum, RiemannSum::Left, "Left"); + ui.selectable_value(&mut self.settings.sum, RiemannSum::Middle, "Middle"); + ui.selectable_value(&mut self.settings.sum, RiemannSum::Right, "Right"); + }); - let mut extrema_toggled: bool = false; - let mut roots_toggled: bool = false; - ui.horizontal(|ui| { - extrema_toggled = ui - .add(Button::new("Extrema")) - .on_hover_text(match self.settings.extrema { - true => "Disable Displaying Extrema", - false => "Display Extrema", - }) - .clicked(); + let mut extrema_toggled: bool = false; + let mut roots_toggled: bool = false; + ui.horizontal(|ui| { + extrema_toggled = ui + .add(Button::new("Extrema")) + .on_hover_text(match self.settings.extrema { + true => "Disable Displaying Extrema", + false => "Display Extrema", + }) + .clicked(); - roots_toggled = ui - .add(Button::new("Roots")) - .on_hover_text(match self.settings.roots { - true => "Disable Displaying Roots", - false => "Display Roots", - }) - .clicked(); - }); + roots_toggled = ui + .add(Button::new("Roots")) + .on_hover_text(match self.settings.roots { + true => "Disable Displaying Roots", + false => "Display Roots", + }) + .clicked(); + }); - self.settings.extrema.bitxor_assign(extrema_toggled); - self.settings.roots.bitxor_assign(roots_toggled); + self.settings.extrema.bitxor_assign(extrema_toggled); + self.settings.roots.bitxor_assign(roots_toggled); - let min_x_old = self.settings.integral_min_x; - let min_x_changed = ui - .add( - Slider::new(&mut self.settings.integral_min_x, INTEGRAL_X_RANGE) - .text("Min X"), - ) - .changed(); + let min_x_old = self.settings.integral_min_x; + let min_x_changed = ui + .add( + Slider::new(&mut self.settings.integral_min_x, INTEGRAL_X_RANGE) + .text("Min X"), + ) + .changed(); - let max_x_old = self.settings.integral_max_x; - let max_x_changed = ui - .add( - Slider::new(&mut self.settings.integral_max_x, INTEGRAL_X_RANGE) - .text("Max X"), - ) - .changed(); + let max_x_old = self.settings.integral_max_x; + let max_x_changed = ui + .add( + Slider::new(&mut self.settings.integral_max_x, INTEGRAL_X_RANGE) + .text("Max X"), + ) + .changed(); - // Checks bounds, and if they are invalid, fix them - if self.settings.integral_min_x >= self.settings.integral_max_x { - if max_x_changed { - self.settings.integral_max_x = max_x_old; - } else if min_x_changed { - self.settings.integral_min_x = min_x_old; - } else { - // No clue how this would happen, but just in case - self.settings.integral_min_x = DEFAULT_MIN_X; - self.settings.integral_max_x = DEFAULT_MAX_X; - } - } + // Checks bounds, and if they are invalid, fix them + if self.settings.integral_min_x >= self.settings.integral_max_x { + if max_x_changed { + self.settings.integral_max_x = max_x_old; + } else if min_x_changed { + self.settings.integral_min_x = min_x_old; + } else { + // No clue how this would happen, but just in case + self.settings.integral_min_x = DEFAULT_MIN_X; + self.settings.integral_max_x = DEFAULT_MAX_X; + } + } - let integral_num_changed = ui - .add( - Slider::new(&mut self.settings.integral_num, INTEGRAL_NUM_RANGE) - .text("Interval"), - ) - .changed(); + let integral_num_changed = ui + .add( + Slider::new(&mut self.settings.integral_num, INTEGRAL_NUM_RANGE) + .text("Interval"), + ) + .changed(); - let configs_changed = max_x_changed - | min_x_changed - | integral_num_changed - | roots_toggled - | extrema_toggled; + let configs_changed = max_x_changed + | min_x_changed + | integral_num_changed + | roots_toggled + | extrema_toggled; - let functions_len = self.functions.len(); - let mut remove_i: Option = None; - for (i, function) in self.functions.iter_mut().enumerate() { - let integral_enabled = function.integral; - let derivative_enabled = function.derivative; - let mut derivative_toggle: bool = false; - let mut integral_toggle: bool = false; + let functions_len = self.functions.len(); + let mut remove_i: Option = None; + for (i, function) in self.functions.iter_mut().enumerate() { + let integral_enabled = function.integral; + let derivative_enabled = function.derivative; + let mut derivative_toggle: bool = false; + let mut integral_toggle: bool = false; - // Entry for a function - ui.horizontal(|ui| { - ui.label("Function:"); + // Entry for a function + ui.horizontal(|ui| { + ui.label("Function:"); - if functions_len > 1 { - // There's more than 1 function! Functions can now be deleted - if ui - .add(Button::new("X")) - .on_hover_text("Delete Function") - .clicked() - { - remove_i = Some(i); - } - } else { - // Display greyed out "X" button if there's only one function added - ui.add_enabled(false, Button::new("X")); - } + if functions_len > 1 { + // There's more than 1 function! Functions can now be deleted + if ui + .add(Button::new("X")) + .on_hover_text("Delete Function") + .clicked() + { + remove_i = Some(i); + } + } else { + // Display greyed out "X" button if there's only one function added + ui.add_enabled(false, Button::new("X")); + } - integral_toggle = ui - .add(Button::new("∫")) - .on_hover_text(match integral_enabled { - true => "Don't integrate", - false => "Integrate", - }) - .clicked(); + integral_toggle = ui + .add(Button::new("∫")) + .on_hover_text(match integral_enabled { + true => "Don't integrate", + false => "Integrate", + }) + .clicked(); - derivative_toggle = ui - .add(Button::new("d/dx")) - .on_hover_text(match derivative_enabled { - true => "Don't Differentiate", - false => "Differentiate", - }) - .clicked(); + derivative_toggle = ui + .add(Button::new("d/dx")) + .on_hover_text(match derivative_enabled { + true => "Don't Differentiate", + false => "Differentiate", + }) + .clicked(); - ui.text_edit_singleline(&mut self.func_strs[i]); - }); + ui.text_edit_singleline(&mut self.func_strs[i]); + }); - let proc_func_str = process_func_str(self.func_strs[i].clone()); - if configs_changed - | integral_toggle - | derivative_toggle - | (proc_func_str != function.get_func_str()) - | self.last_error.iter().any(|ele| ele.0 == i) - { - // let proc_func_str = self.func_strs[i].clone(); - let func_test_output = test_func(&proc_func_str); - if let Some(test_output_value) = func_test_output { - self.last_error.push((i, test_output_value)); - } else { - function.update( - proc_func_str, - if integral_toggle { - !integral_enabled - } else { - integral_enabled - }, - if derivative_toggle { - !derivative_enabled - } else { - derivative_enabled - }, - Some(self.settings.integral_min_x), - Some(self.settings.integral_max_x), - Some(self.settings.integral_num), - Some(self.settings.sum), - self.settings.extrema, - self.settings.roots, - ); - self.last_error = self - .last_error - .iter() - .filter(|(i_ele, _)| i_ele != &i) - .map(|(a, b)| (*a, b.clone())) - .collect(); - } - } - } + let proc_func_str = process_func_str(self.func_strs[i].clone()); + if configs_changed + | integral_toggle + | derivative_toggle + | (proc_func_str != function.get_func_str()) + | self.last_error.iter().any(|ele| ele.0 == i) + { + // let proc_func_str = self.func_strs[i].clone(); + let func_test_output = test_func(&proc_func_str); + if let Some(test_output_value) = func_test_output { + self.last_error.push((i, test_output_value)); + } else { + function.update( + proc_func_str, + if integral_toggle { + !integral_enabled + } else { + integral_enabled + }, + if derivative_toggle { + !derivative_enabled + } else { + derivative_enabled + }, + Some(self.settings.integral_min_x), + Some(self.settings.integral_max_x), + Some(self.settings.integral_num), + Some(self.settings.sum), + self.settings.extrema, + self.settings.roots, + ); + self.last_error = self + .last_error + .iter() + .filter(|(i_ele, _)| i_ele != &i) + .map(|(a, b)| (*a, b.clone())) + .collect(); + } + } + } - if self.functions.len() > 1 { - if let Some(remove_i_unwrap) = remove_i { - self.functions.remove(remove_i_unwrap); - self.func_strs.remove(remove_i_unwrap); - } - } + if self.functions.len() > 1 { + if let Some(remove_i_unwrap) = remove_i { + self.functions.remove(remove_i_unwrap); + self.func_strs.remove(remove_i_unwrap); + } + } - // Open Source and Licensing information - ui.hyperlink_to("I'm Opensource!", PROJECT_URL); + // Open Source and Licensing information + ui.hyperlink_to("I'm Opensource!", PROJECT_URL); - ui.label(RichText::new("(and licensed under AGPLv3)").color(Color32::LIGHT_GRAY)) - .on_hover_text(LICENSE_INFO); - }); - } + ui.label(RichText::new("(and licensed under AGPLv3)").color(Color32::LIGHT_GRAY)) + .on_hover_text(LICENSE_INFO); + }); + } } impl epi::App for MathApp { - // The name of the program (displayed when running natively as the window title) - fn name(&self) -> &str { "(Yet-to-be-named) Graphing Software" } + // The name of the program (displayed when running natively as the window title) + fn name(&self) -> &str { "(Yet-to-be-named) Graphing Software" } - // Called once before the first frame. - fn setup(&mut self, _ctx: &Context, _frame: &Frame, _storage: Option<&dyn Storage>) { - #[cfg(target_arch = "wasm32")] - stop_loading(); - log_helper("egui app initialized."); - } + // Called once before the first frame. + fn setup(&mut self, _ctx: &Context, _frame: &Frame, _storage: Option<&dyn Storage>) { + #[cfg(target_arch = "wasm32")] + stop_loading(); + log_helper("egui app initialized."); + } - // Called each time the UI needs repainting, which may be many times per second. - #[inline(always)] - fn update(&mut self, ctx: &Context, _frame: &Frame) { - let start = instant::Instant::now(); - ctx.set_visuals(match self.settings.dark_mode { - true => Visuals::dark(), - false => Visuals::light(), - }); + // Called each time the UI needs repainting, which may be many times per second. + #[inline(always)] + fn update(&mut self, ctx: &Context, _frame: &Frame) { + let start = instant::Instant::now(); + ctx.set_visuals(match self.settings.dark_mode { + true => Visuals::dark(), + false => Visuals::light(), + }); - ctx.set_fonts(FONT_DEFINITIONS.clone()); // Initialize fonts + ctx.set_fonts(FONT_DEFINITIONS.clone()); // Initialize fonts - // Creates Top bar that contains some general options - TopBottomPanel::top("top_bar").show(ctx, |ui| { - ui.horizontal(|ui| { - self.settings.show_side_panel.bitxor_assign( - ui.add(Button::new("Panel")) - .on_hover_text(match self.settings.show_side_panel { - true => "Hide Side Panel", - false => "Show Side Panel", - }) - .clicked(), - ); + // Creates Top bar that contains some general options + TopBottomPanel::top("top_bar").show(ctx, |ui| { + ui.horizontal(|ui| { + self.settings.show_side_panel.bitxor_assign( + ui.add(Button::new("Panel")) + .on_hover_text(match self.settings.show_side_panel { + true => "Hide Side Panel", + false => "Show Side Panel", + }) + .clicked(), + ); - if ui - .add(Button::new("Add Function")) - .on_hover_text("Create and graph new function") - .clicked() - { - self.functions.push( - EMPTY_FUNCTION_ENTRY - .clone() - .update_riemann(self.settings.sum), - ); - self.func_strs.push(String::new()); - } + if ui + .add(Button::new("Add Function")) + .on_hover_text("Create and graph new function") + .clicked() + { + self.functions.push( + EMPTY_FUNCTION_ENTRY + .clone() + .update_riemann(self.settings.sum), + ); + self.func_strs.push(String::new()); + } - self.settings.help_open.bitxor_assign( - ui.add(Button::new("Help")) - .on_hover_text(match self.settings.help_open { - true => "Close Help Window", - false => "Open Help Window", - }) - .clicked(), - ); + self.settings.help_open.bitxor_assign( + ui.add(Button::new("Help")) + .on_hover_text(match self.settings.help_open { + true => "Close Help Window", + false => "Open Help Window", + }) + .clicked(), + ); - self.settings.info_open.bitxor_assign( - ui.add(Button::new("Info")) - .on_hover_text(match self.settings.info_open { - true => "Close Info Window", - false => "Open Info Window", - }) - .clicked(), - ); + self.settings.info_open.bitxor_assign( + ui.add(Button::new("Info")) + .on_hover_text(match self.settings.info_open { + true => "Close Info Window", + false => "Open Info Window", + }) + .clicked(), + ); - self.settings.dark_mode.bitxor_assign( - ui.add(Button::new(match self.settings.dark_mode { - true => "🌞", - false => "🌙", - })) - .on_hover_text(match self.settings.dark_mode { - true => "Turn the Lights on!", - false => "Turn the Lights off.", - }) - .clicked(), - ); + self.settings.dark_mode.bitxor_assign( + ui.add(Button::new(match self.settings.dark_mode { + true => "🌞", + false => "🌙", + })) + .on_hover_text(match self.settings.dark_mode { + true => "Turn the Lights on!", + false => "Turn the Lights off.", + }) + .clicked(), + ); - ui.label(format!( - "Area: {:?} Took: {:?}", - self.last_info.0, self.last_info.1 - )); - }); - }); + ui.label(format!( + "Area: {:?} Took: {:?}", + self.last_info.0, self.last_info.1 + )); + }); + }); - // Help window with information for users - Window::new("Help") - .default_pos([200.0, 200.0]) - .open(&mut self.settings.help_open) - .resizable(false) - .collapsible(false) - .show(ctx, |ui| { - ui.heading("Help With..."); + // Help window with information for users + Window::new("Help") + .default_pos([200.0, 200.0]) + .open(&mut self.settings.help_open) + .resizable(false) + .collapsible(false) + .show(ctx, |ui| { + ui.heading("Help With..."); - ui.collapsing("Supported Expressions", |ui| { - ui.label(&FILE_DATA.text_help_expr); - }); + ui.collapsing("Supported Expressions", |ui| { + ui.label(&FILE_DATA.text_help_expr); + }); - ui.collapsing("Supported Constants", |ui| { - ui.label(&FILE_DATA.text_help_vars); - }); + ui.collapsing("Supported Constants", |ui| { + ui.label(&FILE_DATA.text_help_vars); + }); - ui.collapsing("Panel", |ui| { - ui.label(&FILE_DATA.text_help_panel); - }); + ui.collapsing("Panel", |ui| { + ui.label(&FILE_DATA.text_help_panel); + }); - ui.collapsing("Functions", |ui| { - ui.label(&FILE_DATA.text_help_function); - }); + ui.collapsing("Functions", |ui| { + ui.label(&FILE_DATA.text_help_function); + }); - ui.collapsing("Other", |ui| { - ui.label(&FILE_DATA.text_help_other); - }); - }); + ui.collapsing("Other", |ui| { + ui.label(&FILE_DATA.text_help_other); + }); + }); - // Window with Misc Information - Window::new("Info") - .default_pos([200.0, 200.0]) - .open(&mut self.settings.info_open) - .resizable(false) - .collapsible(false) - .show(ctx, |ui| { - ui.label(&*BUILD_INFO); - }); + // Window with Misc Information + Window::new("Info") + .default_pos([200.0, 200.0]) + .open(&mut self.settings.info_open) + .resizable(false) + .collapsible(false) + .show(ctx, |ui| { + ui.label(&*BUILD_INFO); + }); - // If side panel is enabled, show it. - if self.settings.show_side_panel { - self.side_panel(ctx); - } + // If side panel is enabled, show it. + if self.settings.show_side_panel { + self.side_panel(ctx); + } - // Central panel which contains the central plot (or an error created when parsing) - CentralPanel::default().show(ctx, |ui| { - if !self.last_error.is_empty() { - ui.centered_and_justified(|ui| { - self.last_error.iter().for_each(|ele| { - ui.heading(&(&format!("(Function #{}) {}\n", ele.0, ele.1)).to_string()); - }) - }); - return; - } + // Central panel which contains the central plot (or an error created when parsing) + CentralPanel::default().show(ctx, |ui| { + if !self.last_error.is_empty() { + ui.centered_and_justified(|ui| { + self.last_error.iter().for_each(|ele| { + ui.heading(&(&format!("(Function #{}) {}\n", ele.0, ele.1)).to_string()); + }) + }); + return; + } - let available_width: usize = ui.available_width() as usize; - Plot::new("plot") - .set_margin_fraction(Vec2::ZERO) - .data_aspect(1.0) - .include_y(0) - .show(ui, |plot_ui| { - let bounds = plot_ui.plot_bounds(); - let minx_bounds: f64 = bounds.min()[0]; - let maxx_bounds: f64 = bounds.max()[0]; + let available_width: usize = ui.available_width() as usize; + Plot::new("plot") + .set_margin_fraction(Vec2::ZERO) + .data_aspect(1.0) + .include_y(0) + .show(ui, |plot_ui| { + let bounds = plot_ui.plot_bounds(); + let minx_bounds: f64 = bounds.min()[0]; + let maxx_bounds: f64 = bounds.max()[0]; - let area_list: Vec = self - .functions - .iter_mut() - .enumerate() - .map(|(i, function)| { - if self.func_strs[i].is_empty() { - return f64::NAN; - } + let area_list: Vec = self + .functions + .iter_mut() + .enumerate() + .map(|(i, function)| { + if self.func_strs[i].is_empty() { + return f64::NAN; + } - function.update_bounds(minx_bounds, maxx_bounds, available_width); - function.display(plot_ui) - }) - .collect(); - self.last_info = (area_list, start.elapsed()); - }); - }); - } + function.update_bounds(minx_bounds, maxx_bounds, available_width); + function.display(plot_ui) + }) + .collect(); + self.last_info = (area_list, start.elapsed()); + }); + }); + } - // Uncaps max canvas size. This was capped in egui due to a bug in Firefox. But it's fixed now. - fn max_size_points(&self) -> Vec2 { Vec2::new(f32::MAX, f32::MAX) } + // Uncaps max canvas size. This was capped in egui due to a bug in Firefox. But it's fixed now. + fn max_size_points(&self) -> Vec2 { Vec2::new(f32::MAX, f32::MAX) } } diff --git a/src/function.rs b/src/function.rs index 630ce8e..a278d1e 100644 --- a/src/function.rs +++ b/src/function.rs @@ -13,561 +13,561 @@ use std::fmt::{self, Debug}; #[derive(PartialEq, Debug, Copy, Clone)] pub enum RiemannSum { - Left, - Middle, - Right, + Left, + Middle, + Right, } impl fmt::Display for RiemannSum { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{:?}", self) } + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{:?}", self) } } lazy_static::lazy_static! { - pub static ref EMPTY_FUNCTION_ENTRY: FunctionEntry = FunctionEntry::empty(); + pub static ref EMPTY_FUNCTION_ENTRY: FunctionEntry = FunctionEntry::empty(); } #[derive(Clone)] pub struct FunctionEntry { - function: BackingFunction, - func_str: String, - min_x: f64, - max_x: f64, - pixel_width: usize, + function: BackingFunction, + func_str: String, + min_x: f64, + max_x: f64, + pixel_width: usize, - output: FunctionOutput, + output: FunctionOutput, - pub(crate) integral: bool, - pub(crate) derivative: bool, - integral_min_x: f64, - integral_max_x: f64, - integral_num: usize, - sum: RiemannSum, - roots: bool, - extrema: bool, + pub(crate) integral: bool, + pub(crate) derivative: bool, + integral_min_x: f64, + integral_max_x: f64, + integral_num: usize, + sum: RiemannSum, + roots: bool, + extrema: bool, } impl FunctionEntry { - // Creates Empty Function instance - pub fn empty() -> Self { - Self { - function: BackingFunction::new(DEFAULT_FUNCION), - func_str: String::new(), - min_x: -1.0, - max_x: 1.0, - pixel_width: 100, - output: FunctionOutput::new_empty(), - integral: false, - derivative: false, - integral_min_x: f64::NAN, - integral_max_x: f64::NAN, - integral_num: 0, - sum: DEFAULT_RIEMANN, - roots: true, - extrema: true, - } - } + // Creates Empty Function instance + pub fn empty() -> Self { + Self { + function: BackingFunction::new(DEFAULT_FUNCION), + func_str: String::new(), + min_x: -1.0, + max_x: 1.0, + pixel_width: 100, + output: FunctionOutput::new_empty(), + integral: false, + derivative: false, + integral_min_x: f64::NAN, + integral_max_x: f64::NAN, + integral_num: 0, + sum: DEFAULT_RIEMANN, + roots: true, + extrema: true, + } + } - pub fn update( - &mut self, func_str: String, integral: bool, derivative: bool, integral_min_x: Option, - integral_max_x: Option, integral_num: Option, sum: Option, - extrema: bool, roots: bool, - ) { - // If the function string changes, just wipe and restart from scratch - if func_str != self.func_str { - self.func_str = func_str.clone(); - self.function = BackingFunction::new(&func_str); - self.output.invalidate_whole(); - self.output.invalidate_points(); - } + pub fn update( + &mut self, func_str: String, integral: bool, derivative: bool, integral_min_x: Option, + integral_max_x: Option, integral_num: Option, sum: Option, + extrema: bool, roots: bool, + ) { + // If the function string changes, just wipe and restart from scratch + if func_str != self.func_str { + self.func_str = func_str.clone(); + self.function = BackingFunction::new(&func_str); + self.output.invalidate_whole(); + self.output.invalidate_points(); + } - self.derivative = derivative; - self.integral = integral; - self.extrema = extrema; - self.roots = roots; + self.derivative = derivative; + self.integral = integral; + self.extrema = extrema; + self.roots = roots; - // Makes sure proper arguments are passed when integral is enabled - if integral - && (integral_min_x != Some(self.integral_min_x)) - | (integral_max_x != Some(self.integral_max_x)) - | (integral_num != Some(self.integral_num)) - | (sum != Some(self.sum)) - { - self.output.invalidate_integral(); - self.integral_min_x = integral_min_x.expect("integral_min_x is None"); - self.integral_max_x = integral_max_x.expect("integral_max_x is None"); - self.integral_num = integral_num.expect("integral_num is None"); - self.sum = sum.expect("sum is None"); - } - } + // Makes sure proper arguments are passed when integral is enabled + if integral + && (integral_min_x != Some(self.integral_min_x)) + | (integral_max_x != Some(self.integral_max_x)) + | (integral_num != Some(self.integral_num)) + | (sum != Some(self.sum)) + { + self.output.invalidate_integral(); + self.integral_min_x = integral_min_x.expect("integral_min_x is None"); + self.integral_max_x = integral_max_x.expect("integral_max_x is None"); + self.integral_num = integral_num.expect("integral_num is None"); + self.sum = sum.expect("sum is None"); + } + } - pub fn update_bounds(&mut self, min_x: f64, max_x: f64, pixel_width: usize) { - if pixel_width != self.pixel_width { - self.output.invalidate_back(); - self.output.invalidate_derivative(); - self.min_x = min_x; - self.max_x = max_x; - self.pixel_width = pixel_width; - } else if ((min_x != self.min_x) | (max_x != self.max_x)) && self.output.back.is_some() { - let resolution: f64 = self.pixel_width as f64 / (max_x.abs() + min_x.abs()); - let back_cache = self.output.back.as_ref().unwrap(); + pub fn update_bounds(&mut self, min_x: f64, max_x: f64, pixel_width: usize) { + if pixel_width != self.pixel_width { + self.output.invalidate_back(); + self.output.invalidate_derivative(); + self.min_x = min_x; + self.max_x = max_x; + self.pixel_width = pixel_width; + } else if ((min_x != self.min_x) | (max_x != self.max_x)) && self.output.back.is_some() { + let resolution: f64 = self.pixel_width as f64 / (max_x.abs() + min_x.abs()); + let back_cache = self.output.back.as_ref().unwrap(); - let x_data: SteppedVector = back_cache - .iter() - .map(|ele| ele.x) - .collect::>() - .into(); + let x_data: SteppedVector = back_cache + .iter() + .map(|ele| ele.x) + .collect::>() + .into(); - self.output.back = Some( - (0..self.pixel_width) - .map(|x| (x as f64 / resolution as f64) + min_x) - .map(|x| { - if let Some(i) = x_data.get_index(x) { - back_cache[i] - } else { - Value::new(x, self.function.get(x)) - } - }) - .collect(), - ); - // assert_eq!(self.output.back.as_ref().unwrap().len(), self.pixel_width); + self.output.back = Some( + (0..self.pixel_width) + .map(|x| (x as f64 / resolution as f64) + min_x) + .map(|x| { + if let Some(i) = x_data.get_index(x) { + back_cache[i] + } else { + Value::new(x, self.function.get(x)) + } + }) + .collect(), + ); + // assert_eq!(self.output.back.as_ref().unwrap().len(), self.pixel_width); - let derivative_cache = self.output.derivative.as_ref().unwrap(); - let new_data = (0..self.pixel_width) - .map(|x| (x as f64 / resolution as f64) + min_x) - .map(|x| { - if let Some(i) = x_data.get_index(x) { - derivative_cache[i] - } else { - Value::new(x, self.function.get_derivative_1(x)) - } - }) - .collect(); + let derivative_cache = self.output.derivative.as_ref().unwrap(); + let new_data = (0..self.pixel_width) + .map(|x| (x as f64 / resolution as f64) + min_x) + .map(|x| { + if let Some(i) = x_data.get_index(x) { + derivative_cache[i] + } else { + Value::new(x, self.function.get_derivative_1(x)) + } + }) + .collect(); - self.output.derivative = Some(new_data); - } else { - self.output.invalidate_back(); - self.output.invalidate_derivative(); - self.pixel_width = pixel_width; - } + self.output.derivative = Some(new_data); + } else { + self.output.invalidate_back(); + self.output.invalidate_derivative(); + self.pixel_width = pixel_width; + } - self.min_x = min_x; - self.max_x = max_x; - self.output.invalidate_points(); - } + self.min_x = min_x; + self.max_x = max_x; + self.output.invalidate_points(); + } - pub fn run_back(&mut self) -> (Vec, Option<(Vec, f64)>, Option>) { - let resolution: f64 = (self.pixel_width as f64 / (self.max_x - self.min_x).abs()) as f64; - let back_values: Vec = { - if self.output.back.is_none() { - self.output.back = Some( - (0..self.pixel_width) - .map(|x| (x as f64 / resolution as f64) + self.min_x) - .map(|x| Value::new(x, self.function.get(x))) - .collect(), - ); - } + pub fn run_back(&mut self) -> (Vec, Option<(Vec, f64)>, Option>) { + let resolution: f64 = (self.pixel_width as f64 / (self.max_x - self.min_x).abs()) as f64; + let back_values: Vec = { + if self.output.back.is_none() { + self.output.back = Some( + (0..self.pixel_width) + .map(|x| (x as f64 / resolution as f64) + self.min_x) + .map(|x| Value::new(x, self.function.get(x))) + .collect(), + ); + } - self.output.back.as_ref().unwrap().clone() - }; + self.output.back.as_ref().unwrap().clone() + }; - let derivative_values: Option> = { - if self.output.derivative.is_none() { - self.output.derivative = Some( - (0..self.pixel_width) - .map(|x| (x as f64 / resolution as f64) + self.min_x) - .map(|x| Value::new(x, self.function.get_derivative_1(x))) - .collect(), - ); - } - Some(self.output.derivative.as_ref().unwrap().clone()) - }; + let derivative_values: Option> = { + if self.output.derivative.is_none() { + self.output.derivative = Some( + (0..self.pixel_width) + .map(|x| (x as f64 / resolution as f64) + self.min_x) + .map(|x| Value::new(x, self.function.get_derivative_1(x))) + .collect(), + ); + } + Some(self.output.derivative.as_ref().unwrap().clone()) + }; - let integral_data = match self.integral { - true => { - if self.output.integral.is_none() { - let (data, area) = self.integral_rectangles(); - self.output.integral = - Some((data.iter().map(|(x, y)| Bar::new(*x, *y)).collect(), area)); - } - let cache = self.output.integral.as_ref().unwrap(); - Some((cache.0.clone(), cache.1)) - } - false => None, - }; + let integral_data = match self.integral { + true => { + if self.output.integral.is_none() { + let (data, area) = self.integral_rectangles(); + self.output.integral = + Some((data.iter().map(|(x, y)| Bar::new(*x, *y)).collect(), area)); + } + let cache = self.output.integral.as_ref().unwrap(); + Some((cache.0.clone(), cache.1)) + } + false => None, + }; - (back_values, integral_data, derivative_values) - } + (back_values, integral_data, derivative_values) + } - // Creates and does the math for creating all the rectangles under the graph - fn integral_rectangles(&self) -> (Vec<(f64, f64)>, f64) { - if self.integral_min_x.is_nan() { - panic!("integral_min_x is NaN") - } else if self.integral_max_x.is_nan() { - panic!("integral_max_x is NaN") - } + // Creates and does the math for creating all the rectangles under the graph + fn integral_rectangles(&self) -> (Vec<(f64, f64)>, f64) { + if self.integral_min_x.is_nan() { + panic!("integral_min_x is NaN") + } else if self.integral_max_x.is_nan() { + panic!("integral_max_x is NaN") + } - let step = (self.integral_min_x - self.integral_max_x).abs() / (self.integral_num as f64); + let step = (self.integral_min_x - self.integral_max_x).abs() / (self.integral_num as f64); - let mut last_positive: Option = None; - let mut area: f64 = 0.0; - let data2: Vec<(f64, f64)> = (0..self.integral_num) - .map(|e| { - let x: f64 = ((e as f64) * step) + self.integral_min_x; - let step_offset = step * x.signum(); // store the offset here so it doesn't have to be calculated multiple times - let x2: f64 = x + step_offset; + let mut last_positive: Option = None; + let mut area: f64 = 0.0; + let data2: Vec<(f64, f64)> = (0..self.integral_num) + .map(|e| { + let x: f64 = ((e as f64) * step) + self.integral_min_x; + let step_offset = step * x.signum(); // store the offset here so it doesn't have to be calculated multiple times + let x2: f64 = x + step_offset; - let (left_x, right_x) = match x.is_sign_positive() { - true => (x, x2), - false => (x2, x), - }; + let (left_x, right_x) = match x.is_sign_positive() { + true => (x, x2), + false => (x2, x), + }; - let y = match self.sum { - RiemannSum::Left => self.function.get(left_x), - RiemannSum::Right => self.function.get(right_x), - RiemannSum::Middle => { - (self.function.get(left_x) + self.function.get(right_x)) / 2.0 - } - }; + let y = match self.sum { + RiemannSum::Left => self.function.get(left_x), + RiemannSum::Right => self.function.get(right_x), + RiemannSum::Middle => { + (self.function.get(left_x) + self.function.get(right_x)) / 2.0 + } + }; - if last_positive.is_none() { - last_positive = Some(x.is_sign_positive()); - } + if last_positive.is_none() { + last_positive = Some(x.is_sign_positive()); + } - if !y.is_nan() { - area += y * step; - } + if !y.is_nan() { + area += y * step; + } - (x + (step_offset / 2.0), y) - }) - .filter(|(_, y)| !y.is_nan()) - .collect(); - // assert_eq!(data2.len(), self.integral_num); + (x + (step_offset / 2.0), y) + }) + .filter(|(_, y)| !y.is_nan()) + .collect(); + // assert_eq!(data2.len(), self.integral_num); - (data2, area) - } + (data2, area) + } - pub fn get_func_str(&self) -> &str { &self.func_str } + pub fn get_func_str(&self) -> &str { &self.func_str } - // Updates riemann value and invalidates integral_cache if needed - pub fn update_riemann(mut self, riemann: RiemannSum) -> Self { - if self.sum != riemann { - self.sum = riemann; - self.output.invalidate_integral(); - } - self - } + // Updates riemann value and invalidates integral_cache if needed + pub fn update_riemann(mut self, riemann: RiemannSum) -> Self { + if self.sum != riemann { + self.sum = riemann; + self.output.invalidate_integral(); + } + self + } - // Toggles integral - pub fn integral(mut self, integral: bool) -> Self { - self.integral = integral; - self - } + // Toggles integral + pub fn integral(mut self, integral: bool) -> Self { + self.integral = integral; + self + } - #[allow(dead_code)] - pub fn integral_num(mut self, integral_num: usize) -> Self { - self.integral_num = integral_num; - self - } + #[allow(dead_code)] + pub fn integral_num(mut self, integral_num: usize) -> Self { + self.integral_num = integral_num; + self + } - #[allow(dead_code)] - pub fn pixel_width(mut self, pixel_width: usize) -> Self { - self.pixel_width = pixel_width; - self - } + #[allow(dead_code)] + pub fn pixel_width(mut self, pixel_width: usize) -> Self { + self.pixel_width = pixel_width; + self + } - #[allow(dead_code)] - pub fn integral_bounds(mut self, min_x: f64, max_x: f64) -> Self { - if min_x >= max_x { - panic!("integral_bounds: min_x is larger than max_x"); - } + #[allow(dead_code)] + pub fn integral_bounds(mut self, min_x: f64, max_x: f64) -> Self { + if min_x >= max_x { + panic!("integral_bounds: min_x is larger than max_x"); + } - self.integral_min_x = min_x; - self.integral_max_x = max_x; - self - } + self.integral_min_x = min_x; + self.integral_max_x = max_x; + self + } - // Finds roots - fn roots(&mut self) { - let resolution: f64 = (self.pixel_width as f64 / (self.max_x - self.min_x).abs()) as f64; - let mut root_list: Vec = Vec::new(); - let mut last_ele: Option = None; - for ele in self.output.back.as_ref().unwrap().iter() { - if last_ele.is_none() { - last_ele = Some(*ele); - continue; - } + // Finds roots + fn roots(&mut self) { + let resolution: f64 = (self.pixel_width as f64 / (self.max_x - self.min_x).abs()) as f64; + let mut root_list: Vec = Vec::new(); + let mut last_ele: Option = None; + for ele in self.output.back.as_ref().unwrap().iter() { + if last_ele.is_none() { + last_ele = Some(*ele); + continue; + } - let last_ele_signum = last_ele.unwrap().y.signum(); - let ele_signum = ele.y.signum(); + let last_ele_signum = last_ele.unwrap().y.signum(); + let ele_signum = ele.y.signum(); - if last_ele_signum.is_nan() | ele_signum.is_nan() { - continue; - } + if last_ele_signum.is_nan() | ele_signum.is_nan() { + continue; + } - if last_ele_signum != ele_signum { - // Do 50 iterations of newton's method, should be more than accurate - let x = { - let mut x1: f64 = last_ele.unwrap().x; - let mut x2: f64; - let mut fail: bool = false; - loop { - x2 = x1 - (self.function.get(x1) / self.function.get_derivative_1(x1)); - if !(self.min_x..self.max_x).contains(&x2) { - fail = true; - break; - } + if last_ele_signum != ele_signum { + // Do 50 iterations of newton's method, should be more than accurate + let x = { + let mut x1: f64 = last_ele.unwrap().x; + let mut x2: f64; + let mut fail: bool = false; + loop { + x2 = x1 - (self.function.get(x1) / self.function.get_derivative_1(x1)); + if !(self.min_x..self.max_x).contains(&x2) { + fail = true; + break; + } - if (x2 - x1).abs() < resolution { - break; - } + if (x2 - x1).abs() < resolution { + break; + } - x1 = x2; - } + x1 = x2; + } - match fail { - true => f64::NAN, - false => x1, - } - }; + match fail { + true => f64::NAN, + false => x1, + } + }; - if !x.is_nan() { - root_list.push(Value::new(x, self.function.get(x))); - } - } - last_ele = Some(*ele); - } - self.output.roots = Some(root_list); - } + if !x.is_nan() { + root_list.push(Value::new(x, self.function.get(x))); + } + } + last_ele = Some(*ele); + } + self.output.roots = Some(root_list); + } - // Finds extrema - fn extrema(&mut self) { - let resolution: f64 = (self.pixel_width as f64 / (self.max_x - self.min_x).abs()) as f64; - let mut extrama_list: Vec = Vec::new(); - let mut last_ele: Option = None; - for ele in self.output.derivative.as_ref().unwrap().iter() { - if last_ele.is_none() { - last_ele = Some(*ele); - continue; - } + // Finds extrema + fn extrema(&mut self) { + let resolution: f64 = (self.pixel_width as f64 / (self.max_x - self.min_x).abs()) as f64; + let mut extrama_list: Vec = Vec::new(); + let mut last_ele: Option = None; + for ele in self.output.derivative.as_ref().unwrap().iter() { + if last_ele.is_none() { + last_ele = Some(*ele); + continue; + } - let last_ele_signum = last_ele.unwrap().y.signum(); - let ele_signum = ele.y.signum(); + let last_ele_signum = last_ele.unwrap().y.signum(); + let ele_signum = ele.y.signum(); - if last_ele_signum.is_nan() | ele_signum.is_nan() { - continue; - } + if last_ele_signum.is_nan() | ele_signum.is_nan() { + continue; + } - if last_ele_signum != ele_signum { - // Do 50 iterations of newton's method, should be more than accurate - let x = { - let mut x1: f64 = last_ele.unwrap().x; - let mut x2: f64; - let mut fail: bool = false; - loop { - x2 = x1 - - (self.function.get_derivative_1(x1) - / self.function.get_derivative_2(x1)); - if !(self.min_x..self.max_x).contains(&x2) { - fail = true; - break; - } + if last_ele_signum != ele_signum { + // Do 50 iterations of newton's method, should be more than accurate + let x = { + let mut x1: f64 = last_ele.unwrap().x; + let mut x2: f64; + let mut fail: bool = false; + loop { + x2 = x1 + - (self.function.get_derivative_1(x1) + / self.function.get_derivative_2(x1)); + if !(self.min_x..self.max_x).contains(&x2) { + fail = true; + break; + } - if (x2 - x1).abs() < resolution { - break; - } + if (x2 - x1).abs() < resolution { + break; + } - x1 = x2; - } + x1 = x2; + } - match fail { - true => f64::NAN, - false => x1, - } - }; + match fail { + true => f64::NAN, + false => x1, + } + }; - if !x.is_nan() { - extrama_list.push(Value::new(x, self.function.get(x))); - } - } - last_ele = Some(*ele); - } - self.output.extrema = Some(extrama_list); - } + if !x.is_nan() { + extrama_list.push(Value::new(x, self.function.get(x))); + } + } + last_ele = Some(*ele); + } + self.output.extrema = Some(extrama_list); + } - pub fn display(&mut self, plot_ui: &mut PlotUi) -> f64 { - let (back_values, integral, derivative) = self.run_back(); - self.output.back = Some(back_values); - self.output.integral = integral; - self.output.derivative = derivative; + pub fn display(&mut self, plot_ui: &mut PlotUi) -> f64 { + let (back_values, integral, derivative) = self.run_back(); + self.output.back = Some(back_values); + self.output.integral = integral; + self.output.derivative = derivative; - if self.extrema { - self.extrema(); - } else { - self.output.extrema = None; - } + if self.extrema { + self.extrema(); + } else { + self.output.extrema = None; + } - if self.roots { - self.roots(); - } else { - self.output.roots = None; - } + if self.roots { + self.roots(); + } else { + self.output.roots = None; + } - self.output.display( - plot_ui, - self.get_func_str(), - &self.function.get_derivative_str(), - (self.integral_min_x - self.integral_max_x).abs() / (self.integral_num as f64), - self.derivative, - ) - } + self.output.display( + plot_ui, + self.get_func_str(), + &self.function.get_derivative_str(), + (self.integral_min_x - self.integral_max_x).abs() / (self.integral_num as f64), + self.derivative, + ) + } } #[cfg(test)] fn verify_function( - integral_num: usize, pixel_width: usize, function: &mut FunctionEntry, - back_values_target: Vec<(f64, f64)>, area_target: f64, + integral_num: usize, pixel_width: usize, function: &mut FunctionEntry, + back_values_target: Vec<(f64, f64)>, area_target: f64, ) { - { - let (back_values, bars, derivative) = function.run_back(); - assert!(derivative.is_some()); - assert!(bars.is_none()); - assert_eq!(back_values.len(), pixel_width); - let back_values_tuple: Vec<(f64, f64)> = - back_values.iter().map(|ele| (ele.x, ele.y)).collect(); - assert_eq!(back_values_tuple, back_values_target); - } + { + let (back_values, bars, derivative) = function.run_back(); + assert!(derivative.is_some()); + assert!(bars.is_none()); + assert_eq!(back_values.len(), pixel_width); + let back_values_tuple: Vec<(f64, f64)> = + back_values.iter().map(|ele| (ele.x, ele.y)).collect(); + assert_eq!(back_values_tuple, back_values_target); + } - { - *function = function.clone().integral(true); - let (back_values, bars, derivative) = function.run_back(); - assert!(derivative.is_some()); - assert!(bars.is_some()); - assert_eq!(back_values.len(), pixel_width); + { + *function = function.clone().integral(true); + let (back_values, bars, derivative) = function.run_back(); + assert!(derivative.is_some()); + assert!(bars.is_some()); + assert_eq!(back_values.len(), pixel_width); - assert_eq!(bars.clone().unwrap().1, area_target); + assert_eq!(bars.clone().unwrap().1, area_target); - let vec_bars = bars.unwrap().0; - assert_eq!(vec_bars.len(), integral_num); + let vec_bars = bars.unwrap().0; + assert_eq!(vec_bars.len(), integral_num); - let back_values_tuple: Vec<(f64, f64)> = - back_values.iter().map(|ele| (ele.x, ele.y)).collect(); - assert_eq!(back_values_tuple, back_values_target); - } + let back_values_tuple: Vec<(f64, f64)> = + back_values.iter().map(|ele| (ele.x, ele.y)).collect(); + assert_eq!(back_values_tuple, back_values_target); + } - { - let (back_values, bars, derivative) = function.run_back(); - assert!(derivative.is_some()); + { + let (back_values, bars, derivative) = function.run_back(); + assert!(derivative.is_some()); - assert!(bars.is_some()); - assert_eq!(back_values.len(), pixel_width); - assert_eq!(bars.clone().unwrap().1, area_target); - let bars_unwrapped = bars.unwrap(); + assert!(bars.is_some()); + assert_eq!(back_values.len(), pixel_width); + assert_eq!(bars.clone().unwrap().1, area_target); + let bars_unwrapped = bars.unwrap(); - assert_eq!(bars_unwrapped.0.iter().len(), integral_num); - } + assert_eq!(bars_unwrapped.0.iter().len(), integral_num); + } } #[test] fn left_function_test() { - let integral_num = 10; - let pixel_width = 10; + let integral_num = 10; + let pixel_width = 10; - let mut function = FunctionEntry::empty() - .update_riemann(RiemannSum::Left) - .pixel_width(pixel_width) - .integral_num(integral_num) - .integral_bounds(-1.0, 1.0); + let mut function = FunctionEntry::empty() + .update_riemann(RiemannSum::Left) + .pixel_width(pixel_width) + .integral_num(integral_num) + .integral_bounds(-1.0, 1.0); - let back_values_target = vec![ - (-1.0, 1.0), - (-0.8, 0.6400000000000001), - (-0.6, 0.36), - (-0.4, 0.16000000000000003), - (-0.19999999999999996, 0.03999999999999998), - (0.0, 0.0), - (0.19999999999999996, 0.03999999999999998), - (0.3999999999999999, 0.15999999999999992), - (0.6000000000000001, 0.3600000000000001), - (0.8, 0.6400000000000001), - ]; + let back_values_target = vec![ + (-1.0, 1.0), + (-0.8, 0.6400000000000001), + (-0.6, 0.36), + (-0.4, 0.16000000000000003), + (-0.19999999999999996, 0.03999999999999998), + (0.0, 0.0), + (0.19999999999999996, 0.03999999999999998), + (0.3999999999999999, 0.15999999999999992), + (0.6000000000000001, 0.3600000000000001), + (0.8, 0.6400000000000001), + ]; - let area_target = 0.9600000000000001; + let area_target = 0.9600000000000001; - verify_function( - integral_num, - pixel_width, - &mut function, - back_values_target, - area_target, - ); + verify_function( + integral_num, + pixel_width, + &mut function, + back_values_target, + area_target, + ); } #[test] fn middle_function_test() { - let integral_num = 10; - let pixel_width = 10; + let integral_num = 10; + let pixel_width = 10; - let mut function = FunctionEntry::empty() - .update_riemann(RiemannSum::Middle) - .pixel_width(pixel_width) - .integral_num(integral_num) - .integral_bounds(-1.0, 1.0); + let mut function = FunctionEntry::empty() + .update_riemann(RiemannSum::Middle) + .pixel_width(pixel_width) + .integral_num(integral_num) + .integral_bounds(-1.0, 1.0); - let back_values_target = vec![ - (-1.0, 1.0), - (-0.8, 0.6400000000000001), - (-0.6, 0.36), - (-0.4, 0.16000000000000003), - (-0.19999999999999996, 0.03999999999999998), - (0.0, 0.0), - (0.19999999999999996, 0.03999999999999998), - (0.3999999999999999, 0.15999999999999992), - (0.6000000000000001, 0.3600000000000001), - (0.8, 0.6400000000000001), - ]; + let back_values_target = vec![ + (-1.0, 1.0), + (-0.8, 0.6400000000000001), + (-0.6, 0.36), + (-0.4, 0.16000000000000003), + (-0.19999999999999996, 0.03999999999999998), + (0.0, 0.0), + (0.19999999999999996, 0.03999999999999998), + (0.3999999999999999, 0.15999999999999992), + (0.6000000000000001, 0.3600000000000001), + (0.8, 0.6400000000000001), + ]; - let area_target = 0.92; + let area_target = 0.92; - verify_function( - integral_num, - pixel_width, - &mut function, - back_values_target, - area_target, - ); + verify_function( + integral_num, + pixel_width, + &mut function, + back_values_target, + area_target, + ); } #[test] fn right_function_test() { - let integral_num = 10; - let pixel_width = 10; + let integral_num = 10; + let pixel_width = 10; - let mut function = FunctionEntry::empty() - .update_riemann(RiemannSum::Right) - .pixel_width(pixel_width) - .integral_num(integral_num) - .integral_bounds(-1.0, 1.0); + let mut function = FunctionEntry::empty() + .update_riemann(RiemannSum::Right) + .pixel_width(pixel_width) + .integral_num(integral_num) + .integral_bounds(-1.0, 1.0); - let back_values_target = vec![ - (-1.0, 1.0), - (-0.8, 0.6400000000000001), - (-0.6, 0.36), - (-0.4, 0.16000000000000003), - (-0.19999999999999996, 0.03999999999999998), - (0.0, 0.0), - (0.19999999999999996, 0.03999999999999998), - (0.3999999999999999, 0.15999999999999992), - (0.6000000000000001, 0.3600000000000001), - (0.8, 0.6400000000000001), - ]; + let back_values_target = vec![ + (-1.0, 1.0), + (-0.8, 0.6400000000000001), + (-0.6, 0.36), + (-0.4, 0.16000000000000003), + (-0.19999999999999996, 0.03999999999999998), + (0.0, 0.0), + (0.19999999999999996, 0.03999999999999998), + (0.3999999999999999, 0.15999999999999992), + (0.6000000000000001, 0.3600000000000001), + (0.8, 0.6400000000000001), + ]; - let area_target = 0.8800000000000001; + let area_target = 0.8800000000000001; - verify_function( - integral_num, - pixel_width, - &mut function, - back_values_target, - area_target, - ); + verify_function( + integral_num, + pixel_width, + &mut function, + back_values_target, + area_target, + ); } diff --git a/src/function_output.rs b/src/function_output.rs index f77c499..0605989 100644 --- a/src/function_output.rs +++ b/src/function_output.rs @@ -1,100 +1,100 @@ use eframe::{ - egui::{ - plot::{BarChart, Line, PlotUi, Points, Value, Values}, - widgets::plot::Bar, - }, - epaint::Color32, + egui::{ + plot::{BarChart, Line, PlotUi, Points, Value, Values}, + widgets::plot::Bar, + }, + epaint::Color32, }; use crate::misc::digits_precision; #[derive(Clone)] pub struct FunctionOutput { - pub(crate) back: Option>, - pub(crate) integral: Option<(Vec, f64)>, - pub(crate) derivative: Option>, - pub(crate) extrema: Option>, - pub(crate) roots: Option>, + pub(crate) back: Option>, + pub(crate) integral: Option<(Vec, f64)>, + pub(crate) derivative: Option>, + pub(crate) extrema: Option>, + pub(crate) roots: Option>, } impl FunctionOutput { - pub fn new_empty() -> Self { - Self { - back: None, - integral: None, - derivative: None, - extrema: None, - roots: None, - } - } + pub fn new_empty() -> Self { + Self { + back: None, + integral: None, + derivative: None, + extrema: None, + roots: None, + } + } - pub fn invalidate_whole(&mut self) { - self.back = None; - self.integral = None; - self.derivative = None; - self.extrema = None; - self.roots = None; - } + pub fn invalidate_whole(&mut self) { + self.back = None; + self.integral = None; + self.derivative = None; + self.extrema = None; + self.roots = None; + } - pub fn invalidate_back(&mut self) { self.back = None; } + pub fn invalidate_back(&mut self) { self.back = None; } - pub fn invalidate_integral(&mut self) { self.integral = None; } + pub fn invalidate_integral(&mut self) { self.integral = None; } - pub fn invalidate_derivative(&mut self) { self.derivative = None; } + pub fn invalidate_derivative(&mut self) { self.derivative = None; } - pub fn invalidate_points(&mut self) { - self.extrema = None; - self.roots = None; - } + pub fn invalidate_points(&mut self) { + self.extrema = None; + self.roots = None; + } - pub fn display( - &self, plot_ui: &mut PlotUi, func_str: &str, derivative_str: &str, step: f64, - derivative_enabled: bool, - ) -> f64 { - plot_ui.line( - Line::new(Values::from_values(self.back.clone().unwrap())) - .color(Color32::RED) - .name(func_str), - ); + pub fn display( + &self, plot_ui: &mut PlotUi, func_str: &str, derivative_str: &str, step: f64, + derivative_enabled: bool, + ) -> f64 { + plot_ui.line( + Line::new(Values::from_values(self.back.clone().unwrap())) + .color(Color32::RED) + .name(func_str), + ); - if derivative_enabled { - if let Some(derivative_data) = self.derivative.clone() { - plot_ui.line( - Line::new(Values::from_values(derivative_data)) - .color(Color32::GREEN) - .name(derivative_str), - ); - } - } + if derivative_enabled { + if let Some(derivative_data) = self.derivative.clone() { + plot_ui.line( + Line::new(Values::from_values(derivative_data)) + .color(Color32::GREEN) + .name(derivative_str), + ); + } + } - if let Some(extrema_data) = self.extrema.clone() { - plot_ui.points( - Points::new(Values::from_values(extrema_data)) - .color(Color32::YELLOW) - .name("Extrema") - .radius(5.0), - ); - } + if let Some(extrema_data) = self.extrema.clone() { + plot_ui.points( + Points::new(Values::from_values(extrema_data)) + .color(Color32::YELLOW) + .name("Extrema") + .radius(5.0), + ); + } - if let Some(roots_data) = self.roots.clone() { - plot_ui.points( - Points::new(Values::from_values(roots_data)) - .color(Color32::LIGHT_BLUE) - .name("Root") - .radius(5.0), - ); - } + if let Some(roots_data) = self.roots.clone() { + plot_ui.points( + Points::new(Values::from_values(roots_data)) + .color(Color32::LIGHT_BLUE) + .name("Root") + .radius(5.0), + ); + } - if let Some(integral_data) = self.integral.clone() { - plot_ui.bar_chart( - BarChart::new(integral_data.0) - .color(Color32::BLUE) - .width(step), - ); + if let Some(integral_data) = self.integral.clone() { + plot_ui.bar_chart( + BarChart::new(integral_data.0) + .color(Color32::BLUE) + .width(step), + ); - digits_precision(integral_data.1, 8) - } else { - f64::NAN - } - } + digits_precision(integral_data.1, 8) + } else { + f64::NAN + } + } } diff --git a/src/lib.rs b/src/lib.rs index 2314eb0..3ad0ece 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,27 +8,27 @@ mod misc; mod parsing; cfg_if::cfg_if! { - if #[cfg(target_arch = "wasm32")] { - use misc::log_helper; - use wasm_bindgen::prelude::*; + if #[cfg(target_arch = "wasm32")] { + use misc::log_helper; + use wasm_bindgen::prelude::*; - #[global_allocator] - static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; + #[global_allocator] + static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; - #[wasm_bindgen(start)] - pub fn start() -> Result<(), wasm_bindgen::JsValue> { - log_helper("Initializing..."); + #[wasm_bindgen(start)] + pub fn start() -> Result<(), wasm_bindgen::JsValue> { + log_helper("Initializing..."); - // Used in order to hook into `panic!()` to log in the browser's console - log_helper("Initializing panic hooks..."); - std::panic::set_hook(Box::new(console_error_panic_hook::hook)); - log_helper("Initialized panic hooks!"); + // Used in order to hook into `panic!()` to log in the browser's console + log_helper("Initializing panic hooks..."); + std::panic::set_hook(Box::new(console_error_panic_hook::hook)); + log_helper("Initialized panic hooks!"); - log_helper("Finished initializing!"); + log_helper("Finished initializing!"); - log_helper("Starting App..."); - let app = egui_app::MathApp::default(); - eframe::start_web("canvas", Box::new(app)) - } - } + log_helper("Starting App..."); + let app = egui_app::MathApp::default(); + eframe::start_web("canvas", Box::new(app)) + } + } } diff --git a/src/main.rs b/src/main.rs index 2e1bf6d..1d2281c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,11 +9,11 @@ mod parsing; // For running the program natively! (Because why not?) #[cfg(not(target_arch = "wasm32"))] fn main() { - let app = egui_app::MathApp::default(); - let options = eframe::NativeOptions { - transparent: true, - drag_and_drop_support: true, - ..Default::default() - }; - eframe::run_native(Box::new(app), options); + let app = egui_app::MathApp::default(); + let options = eframe::NativeOptions { + transparent: true, + drag_and_drop_support: true, + ..Default::default() + }; + eframe::run_native(Box::new(app), options); } diff --git a/src/misc.rs b/src/misc.rs index 3d94673..bb3cd1d 100644 --- a/src/misc.rs +++ b/src/misc.rs @@ -1,92 +1,92 @@ cfg_if::cfg_if! { - if #[cfg(target_arch = "wasm32")] { - use wasm_bindgen::prelude::*; - #[wasm_bindgen] - extern "C" { - // Use `js_namespace` here to bind `console.log(..)` instead of just - // `log(..)` - #[wasm_bindgen(js_namespace = console)] - fn log(s: &str); - } + if #[cfg(target_arch = "wasm32")] { + use wasm_bindgen::prelude::*; + #[wasm_bindgen] + extern "C" { + // Use `js_namespace` here to bind `console.log(..)` instead of just + // `log(..)` + #[wasm_bindgen(js_namespace = console)] + fn log(s: &str); + } - #[allow(dead_code)] - pub fn log_helper(s: &str) { - log(s); - } + #[allow(dead_code)] + pub fn log_helper(s: &str) { + log(s); + } - #[allow(dead_code)] - #[allow(unused_variables)] - pub fn debug_log(s: &str) { - #[cfg(debug_assertions)] - log(s); - } - } else { - #[allow(dead_code)] - pub fn log_helper(s: &str) { - println!("{}", s); - } + #[allow(dead_code)] + #[allow(unused_variables)] + pub fn debug_log(s: &str) { + #[cfg(debug_assertions)] + log(s); + } + } else { + #[allow(dead_code)] + pub fn log_helper(s: &str) { + println!("{}", s); + } - #[allow(dead_code)] - #[allow(unused_variables)] - pub fn debug_log(s: &str) { - #[cfg(debug_assertions)] - println!("{}", s); - } - } + #[allow(dead_code)] + #[allow(unused_variables)] + pub fn debug_log(s: &str) { + #[cfg(debug_assertions)] + println!("{}", s); + } + } } pub struct SteppedVector { - data: Vec, - min: f64, - max: f64, - step: f64, + data: Vec, + min: f64, + max: f64, + step: f64, } impl SteppedVector { - pub fn get_index(&self, x: f64) -> Option { - if (x > self.max) | (self.min > x) { - return None; - } + pub fn get_index(&self, x: f64) -> Option { + if (x > self.max) | (self.min > x) { + return None; + } - // Should work.... - let possible_i = ((x + self.min) / self.step) as usize; - if self.data[possible_i] == x { - Some(possible_i) - } else { - None - } + // Should work.... + let possible_i = ((x + self.min) / self.step) as usize; + if self.data[possible_i] == x { + Some(possible_i) + } else { + None + } - // Not really needed as the above code should handle everything - /* - for (i, ele) in self.data.iter().enumerate() { - if ele > &x { - return None; - } else if &x == ele { - return Some(i); - } - } - None - */ - } + // Not really needed as the above code should handle everything + /* + for (i, ele) in self.data.iter().enumerate() { + if ele > &x { + return None; + } else if &x == ele { + return Some(i); + } + } + None + */ + } } impl From> for SteppedVector { - // Note: input `data` is assumed to be sorted from min to max - fn from(data: Vec) -> SteppedVector { - let max = data[0]; - let min = data[data.len() - 1]; - let step = (max - min).abs() / ((data.len() - 1) as f64); - SteppedVector { - data, - min, - max, - step, - } - } + // Note: input `data` is assumed to be sorted from min to max + fn from(data: Vec) -> SteppedVector { + let max = data[0]; + let min = data[data.len() - 1]; + let step = (max - min).abs() / ((data.len() - 1) as f64); + SteppedVector { + data, + min, + max, + step, + } + } } // Rounds f64 to specific number of digits pub fn digits_precision(x: f64, digits: usize) -> f64 { - let large_number: f64 = 10.0_f64.powf(digits as f64); - (x * large_number).round() / large_number + let large_number: f64 = 10.0_f64.powf(digits as f64); + (x * large_number).round() / large_number } diff --git a/src/parsing.rs b/src/parsing.rs index a5fea21..5ce6b92 100644 --- a/src/parsing.rs +++ b/src/parsing.rs @@ -1,54 +1,54 @@ use exmex::prelude::*; lazy_static::lazy_static! { - static ref EMPTY_FUNCTION: FlatEx = exmex::parse::("0/0").unwrap(); + static ref EMPTY_FUNCTION: FlatEx = exmex::parse::("0/0").unwrap(); } #[derive(Clone)] pub struct BackingFunction { - function: FlatEx, - derivative_1: FlatEx, - derivative_2: FlatEx, + function: FlatEx, + derivative_1: FlatEx, + derivative_2: FlatEx, } impl BackingFunction { - pub fn new(func_str: &str) -> Self { - let function = exmex::parse::(func_str).unwrap(); - let derivative_1 = function - .partial(0) - .unwrap_or_else(|_| EMPTY_FUNCTION.clone()); - let derivative_2 = function - .partial_iter([0, 0].iter()) - .unwrap_or_else(|_| EMPTY_FUNCTION.clone()); + pub fn new(func_str: &str) -> Self { + let function = exmex::parse::(func_str).unwrap(); + let derivative_1 = function + .partial(0) + .unwrap_or_else(|_| EMPTY_FUNCTION.clone()); + let derivative_2 = function + .partial_iter([0, 0].iter()) + .unwrap_or_else(|_| EMPTY_FUNCTION.clone()); - Self { - function, - derivative_1, - derivative_2, - } - } + Self { + function, + derivative_1, + derivative_2, + } + } - pub fn get_derivative_str(&self) -> String { - String::from(self.derivative_1.unparse()).replace("{x}", "x") - } + pub fn get_derivative_str(&self) -> String { + String::from(self.derivative_1.unparse()).replace("{x}", "x") + } - pub fn get(&self, x: f64) -> f64 { self.function.eval(&[x]).unwrap_or(f64::NAN) } + pub fn get(&self, x: f64) -> f64 { self.function.eval(&[x]).unwrap_or(f64::NAN) } - pub fn get_derivative_1(&self, x: f64) -> f64 { - self.derivative_1.eval(&[x]).unwrap_or(f64::NAN) - } + pub fn get_derivative_1(&self, x: f64) -> f64 { + self.derivative_1.eval(&[x]).unwrap_or(f64::NAN) + } - pub fn get_derivative_2(&self, x: f64) -> f64 { - self.derivative_2.eval(&[x]).unwrap_or(f64::NAN) - } + pub fn get_derivative_2(&self, x: f64) -> f64 { + self.derivative_2.eval(&[x]).unwrap_or(f64::NAN) + } } lazy_static::lazy_static! { - static ref VALID_VARIABLES: Vec = "xXeEπ".chars().collect(); - static ref LETTERS: Vec = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" - .chars() - .collect(); - static ref NUMBERS: Vec = "0123456789".chars().collect(); + static ref VALID_VARIABLES: Vec = "xXeEπ".chars().collect(); + static ref LETTERS: Vec = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + .chars() + .collect(); + static ref NUMBERS: Vec = "0123456789".chars().collect(); } /* @@ -57,173 +57,173 @@ One limitation though, variables with multiple characters like `pi` cannot be mu In the future I may want to completely rewrite this or implement this natively in exmex. */ pub fn process_func_str(function_in: String) -> String { - let function = function_in.replace("log10(", "log(").replace("pi", "π"); // pi -> π and log10 -> log - let function_chars: Vec = function.chars().collect(); - let mut output_string: String = String::new(); - let mut prev_chars: Vec = Vec::new(); - for c in function_chars { - let mut add_asterisk: bool = false; - let prev_chars_len = prev_chars.len(); + let function = function_in.replace("log10(", "log(").replace("pi", "π"); // pi -> π and log10 -> log + let function_chars: Vec = function.chars().collect(); + let mut output_string: String = String::new(); + let mut prev_chars: Vec = Vec::new(); + for c in function_chars { + let mut add_asterisk: bool = false; + let prev_chars_len = prev_chars.len(); - let prev_prev_prev_char = if prev_chars_len >= 3 { - *prev_chars.get(prev_chars_len - 3).unwrap() - } else { - ' ' - }; + let prev_prev_prev_char = if prev_chars_len >= 3 { + *prev_chars.get(prev_chars_len - 3).unwrap() + } else { + ' ' + }; - let prev_prev_char = if prev_chars_len >= 2 { - *prev_chars.get(prev_chars_len - 2).unwrap() - } else { - ' ' - }; + let prev_prev_char = if prev_chars_len >= 2 { + *prev_chars.get(prev_chars_len - 2).unwrap() + } else { + ' ' + }; - let prev_char = if prev_chars_len >= 1 { - *prev_chars.get(prev_chars_len - 1).unwrap() - } else { - ' ' - }; + let prev_char = if prev_chars_len >= 1 { + *prev_chars.get(prev_chars_len - 1).unwrap() + } else { + ' ' + }; - if (prev_prev_prev_char == 'l') - && (prev_prev_char == 'o') - && (prev_char == 'g') - && (NUMBERS.contains(&c)) - { - prev_chars.push(c); - output_string += &c.to_string(); - continue; - } + if (prev_prev_prev_char == 'l') + && (prev_prev_char == 'o') + && (prev_char == 'g') + && (NUMBERS.contains(&c)) + { + prev_chars.push(c); + output_string += &c.to_string(); + continue; + } - let c_letters_var = LETTERS.contains(&c) | VALID_VARIABLES.contains(&c); - let prev_letters_var = VALID_VARIABLES.contains(&prev_char) | LETTERS.contains(&prev_char); + let c_letters_var = LETTERS.contains(&c) | VALID_VARIABLES.contains(&c); + let prev_letters_var = VALID_VARIABLES.contains(&prev_char) | LETTERS.contains(&prev_char); - if prev_char == ')' { - if (c == '(') | NUMBERS.contains(&c) | c_letters_var { - add_asterisk = true; - } - } else if c == '(' { - if (VALID_VARIABLES.contains(&prev_char) - | (')' == prev_char) - | NUMBERS.contains(&prev_char)) - && !LETTERS.contains(&prev_prev_char) - { - add_asterisk = true; - } - } else if NUMBERS.contains(&prev_char) { - if (c == '(') | c_letters_var { - add_asterisk = true; - } - } else if LETTERS.contains(&c) { - if NUMBERS.contains(&prev_char) - | (VALID_VARIABLES.contains(&prev_char) && VALID_VARIABLES.contains(&c)) - { - add_asterisk = true; - } - } else if (NUMBERS.contains(&c) | c_letters_var) && prev_letters_var { - add_asterisk = true; - } + if prev_char == ')' { + if (c == '(') | NUMBERS.contains(&c) | c_letters_var { + add_asterisk = true; + } + } else if c == '(' { + if (VALID_VARIABLES.contains(&prev_char) + | (')' == prev_char) + | NUMBERS.contains(&prev_char)) + && !LETTERS.contains(&prev_prev_char) + { + add_asterisk = true; + } + } else if NUMBERS.contains(&prev_char) { + if (c == '(') | c_letters_var { + add_asterisk = true; + } + } else if LETTERS.contains(&c) { + if NUMBERS.contains(&prev_char) + | (VALID_VARIABLES.contains(&prev_char) && VALID_VARIABLES.contains(&c)) + { + add_asterisk = true; + } + } else if (NUMBERS.contains(&c) | c_letters_var) && prev_letters_var { + add_asterisk = true; + } - if add_asterisk { - output_string += "*"; - } + if add_asterisk { + output_string += "*"; + } - prev_chars.push(c); - output_string += &c.to_string(); - } + prev_chars.push(c); + output_string += &c.to_string(); + } - output_string.replace("log(", "log10(") + output_string.replace("log(", "log10(") } // 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: &str) -> Option { - let parse_result = exmex::parse::(function_string); + let parse_result = exmex::parse::(function_string); - match parse_result { - Err(e) => Some(e.to_string()), - Ok(_) => { - let var_names = parse_result.unwrap().var_names().to_vec(); + match parse_result { + Err(e) => Some(e.to_string()), + Ok(_) => { + let var_names = parse_result.unwrap().var_names().to_vec(); - if var_names != ["x"] { - let var_names_not_x: Vec = var_names - .iter() - .filter(|ele| *ele != &"x".to_owned()) - .cloned() - .collect::>(); + if var_names != ["x"] { + let var_names_not_x: Vec = var_names + .iter() + .filter(|ele| *ele != &"x".to_owned()) + .cloned() + .collect::>(); - return match var_names_not_x.len() { - 1 => { - let var_name = &var_names_not_x[0]; - if var_name == "e" { - Some(String::from( - "If trying to use Euler's number, please use an uppercase E", - )) - } else { - Some(format!("Error: invalid variable: {}", var_name)) - } - } - _ => Some(format!("Error: invalid variables: {:?}", var_names_not_x)), - }; - } + return match var_names_not_x.len() { + 1 => { + let var_name = &var_names_not_x[0]; + if var_name == "e" { + Some(String::from( + "If trying to use Euler's number, please use an uppercase E", + )) + } else { + Some(format!("Error: invalid variable: {}", var_name)) + } + } + _ => Some(format!("Error: invalid variables: {:?}", var_names_not_x)), + }; + } - None - } - } + None + } + } } // Used for testing: passes function to `add_asterisks` before running `test_func` #[cfg(test)] fn test_func_helper(function_string: &str) -> Option { - test_func(&process_func_str(function_string.to_string())) + test_func(&process_func_str(function_string.to_string())) } #[test] fn test_func_test() { - // These shouldn't fail - assert!(test_func_helper("x^2").is_none()); - assert!(test_func_helper("2x").is_none()); - // assert!(test_func_helper("e^x").is_none()); // need to fix!!! PR to exmex - assert!(test_func_helper("E^x").is_none()); - assert!(test_func_helper("log10(x)").is_none()); - assert!(test_func_helper("xxxxx").is_none()); + // These shouldn't fail + assert!(test_func_helper("x^2").is_none()); + assert!(test_func_helper("2x").is_none()); + // assert!(test_func_helper("e^x").is_none()); // need to fix!!! PR to exmex + assert!(test_func_helper("E^x").is_none()); + assert!(test_func_helper("log10(x)").is_none()); + assert!(test_func_helper("xxxxx").is_none()); - // Expect these to fail - assert!(test_func_helper("a").is_some()); - assert!(test_func_helper("l^2").is_some()); - assert!(test_func_helper("log222(x)").is_some()); - assert!(test_func_helper("abcdef").is_some()); + // Expect these to fail + assert!(test_func_helper("a").is_some()); + assert!(test_func_helper("l^2").is_some()); + assert!(test_func_helper("log222(x)").is_some()); + assert!(test_func_helper("abcdef").is_some()); } // Tests to make sure my cursed function works as intended #[test] fn func_process_test() { - assert_eq!(&process_func_str("2x".to_string()), "2*x"); - assert_eq!(&process_func_str("x2".to_string()), "x*2"); - assert_eq!(&process_func_str("x(1+3)".to_string()), "x*(1+3)"); - assert_eq!(&process_func_str("(1+3)x".to_string()), "(1+3)*x"); - assert_eq!(&process_func_str("sin(x)".to_string()), "sin(x)"); - assert_eq!(&process_func_str("2sin(x)".to_string()), "2*sin(x)"); - assert_eq!(&process_func_str("max(x)".to_string()), "max(x)"); - assert_eq!(&process_func_str("2e^x".to_string()), "2*e^x"); - assert_eq!(&process_func_str("2max(x)".to_string()), "2*max(x)"); - assert_eq!(&process_func_str("cos(sin(x))".to_string()), "cos(sin(x))"); - assert_eq!(&process_func_str("x^(1+2x)".to_string()), "x^(1+2*x)"); - assert_eq!( - &process_func_str("(x+2)x(1+3)".to_string()), - "(x+2)*x*(1+3)" - ); - assert_eq!(&process_func_str("(x+2)(1+3)".to_string()), "(x+2)*(1+3)"); - assert_eq!(&process_func_str("xxx".to_string()), "x*x*x"); - assert_eq!(&process_func_str("eee".to_string()), "e*e*e"); - assert_eq!(&process_func_str("pi(x+2)".to_string()), "π*(x+2)"); - assert_eq!(&process_func_str("(x)pi".to_string()), "(x)*π"); - assert_eq!(&process_func_str("2e".to_string()), "2*e"); - assert_eq!(&process_func_str("2log10(x)".to_string()), "2*log10(x)"); - assert_eq!(&process_func_str("2log(x)".to_string()), "2*log10(x)"); - assert_eq!(&process_func_str("x!".to_string()), "x!"); - assert_eq!(&process_func_str("pipipipipipi".to_string()), "π*π*π*π*π*π"); - assert_eq!(&process_func_str("10pi".to_string()), "10*π"); - assert_eq!(&process_func_str("pi10".to_string()), "π*10"); + assert_eq!(&process_func_str("2x".to_string()), "2*x"); + assert_eq!(&process_func_str("x2".to_string()), "x*2"); + assert_eq!(&process_func_str("x(1+3)".to_string()), "x*(1+3)"); + assert_eq!(&process_func_str("(1+3)x".to_string()), "(1+3)*x"); + assert_eq!(&process_func_str("sin(x)".to_string()), "sin(x)"); + assert_eq!(&process_func_str("2sin(x)".to_string()), "2*sin(x)"); + assert_eq!(&process_func_str("max(x)".to_string()), "max(x)"); + assert_eq!(&process_func_str("2e^x".to_string()), "2*e^x"); + assert_eq!(&process_func_str("2max(x)".to_string()), "2*max(x)"); + assert_eq!(&process_func_str("cos(sin(x))".to_string()), "cos(sin(x))"); + assert_eq!(&process_func_str("x^(1+2x)".to_string()), "x^(1+2*x)"); + assert_eq!( + &process_func_str("(x+2)x(1+3)".to_string()), + "(x+2)*x*(1+3)" + ); + assert_eq!(&process_func_str("(x+2)(1+3)".to_string()), "(x+2)*(1+3)"); + assert_eq!(&process_func_str("xxx".to_string()), "x*x*x"); + assert_eq!(&process_func_str("eee".to_string()), "e*e*e"); + assert_eq!(&process_func_str("pi(x+2)".to_string()), "π*(x+2)"); + assert_eq!(&process_func_str("(x)pi".to_string()), "(x)*π"); + assert_eq!(&process_func_str("2e".to_string()), "2*e"); + assert_eq!(&process_func_str("2log10(x)".to_string()), "2*log10(x)"); + assert_eq!(&process_func_str("2log(x)".to_string()), "2*log10(x)"); + assert_eq!(&process_func_str("x!".to_string()), "x!"); + assert_eq!(&process_func_str("pipipipipipi".to_string()), "π*π*π*π*π*π"); + assert_eq!(&process_func_str("10pi".to_string()), "10*π"); + assert_eq!(&process_func_str("pi10".to_string()), "π*10"); - // Need to fix these checks, maybe I need to rewrite the whole asterisk adding system... (or just implement these changes into meval-rs, idk) - // assert_eq!(&add_asterisks("emax(x)".to_string()), "e*max(x)"); - // assert_eq!(&add_asterisks("pisin(x)".to_string()), "pi*sin(x)"); + // Need to fix these checks, maybe I need to rewrite the whole asterisk adding system... (or just implement these changes into meval-rs, idk) + // assert_eq!(&add_asterisks("emax(x)".to_string()), "e*max(x)"); + // assert_eq!(&add_asterisks("pisin(x)".to_string()), "pi*sin(x)"); } diff --git a/www/index.html b/www/index.html index ee62f5c..65647fb 100644 --- a/www/index.html +++ b/www/index.html @@ -1,31 +1,31 @@ - - + + - - - (Yet-to-be-named) Graphing Software - - - - + + + (Yet-to-be-named) Graphing Software + + + + - -
-

- Loading… -

-
-
+ +
+

+ Loading… +

+
+
- - + async function run() { + await init(); + } + run(); + + diff --git a/www/style.css b/www/style.css index ec19321..1159fdb 100644 --- a/www/style.css +++ b/www/style.css @@ -1,81 +1,81 @@ html { - /* Remove touch delay: */ - touch-action: manipulation; + /* Remove touch delay: */ + touch-action: manipulation; } body { - /* Light mode background color for what is not covered by the egui canvas, - or where the egui canvas is translucent. */ - background: #909090; + /* Light mode background color for what is not covered by the egui canvas, + or where the egui canvas is translucent. */ + background: #909090; } @media (prefers-color-scheme: dark) { - body { - /* Dark mode background color for what is not covered by the egui canvas, - or where the egui canvas is translucent. */ - background: #404040; - } + body { + /* Dark mode background color for what is not covered by the egui canvas, + or where the egui canvas is translucent. */ + background: #404040; + } } /* Allow canvas to fill entire web page: */ html, body { - overflow: hidden; - margin: 0 !important; - padding: 0 !important; + overflow: hidden; + margin: 0 !important; + padding: 0 !important; } /* Position canvas in center-top: */ canvas { - margin-right: auto; - margin-left: auto; - display: block; - position: absolute; - top: 0%; - left: 50%; - transform: translate(-50%, 0%); + margin-right: auto; + margin-left: auto; + display: block; + position: absolute; + top: 0%; + left: 50%; + transform: translate(-50%, 0%); } .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; + 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; } /* ---------------------------------------------- */ /* Loading animation from https://loading.io/css/ */ .lds-dual-ring { - display: inline-block; - width: 24px; - height: 24px; + 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; + 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); - } + 0% { + transform: rotate(0deg); + } - 100% { - transform: rotate(360deg); - } + 100% { + transform: rotate(360deg); + } }