From dd6d30ef37d7c078b685c59d22b8ce30f82e2f9c Mon Sep 17 00:00:00 2001 From: Simon Gardling Date: Mon, 14 Feb 2022 10:58:33 -0500 Subject: [PATCH] cache works! --- Cargo.toml | 3 +- src/func_plot.rs | 86 --------------------- src/lib.rs | 196 ++++++++++++++++++++++++++++++++++++++++++----- www/bootstrap.js | 8 +- www/index.js | 12 ++- 5 files changed, 192 insertions(+), 113 deletions(-) delete mode 100644 src/func_plot.rs diff --git a/Cargo.toml b/Cargo.toml index 1621111..9e4a82f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,4 +17,5 @@ plotters = "0.3.1" plotters-canvas = "0.3.0" wee_alloc = "0.4.5" web-sys = { version = "0.3.56", features = ["HtmlCanvasElement"] } -meval = { git = "https://github.com/rekka/meval-rs.git" } \ No newline at end of file +meval = { git = "https://github.com/rekka/meval-rs.git" } +console_error_panic_hook = { git = "https://github.com/rustwasm/console_error_panic_hook.git"} \ No newline at end of file diff --git a/src/func_plot.rs b/src/func_plot.rs deleted file mode 100644 index dd6d0a8..0000000 --- a/src/func_plot.rs +++ /dev/null @@ -1,86 +0,0 @@ -use crate::DrawResult; -use meval::Expr; -use plotters::prelude::*; -use plotters_canvas::CanvasBackend; -use web_sys::HtmlCanvasElement; - -pub fn draw( - element: HtmlCanvasElement, func_str: &str, min_x: f32, max_x: f32, min_y: f32, max_y: f32, - num_interval: usize, resolution: i32, -) -> DrawResult<(impl Fn((i32, i32)) -> Option<(f32, f32)>, f32)> { - let expr: Expr = func_str.parse().unwrap(); - let func = expr.bind("x").unwrap(); - - let absrange = (max_x - min_x).abs(); - let step = absrange / (num_interval as f32); - let backend = CanvasBackend::with_canvas_object(element).unwrap(); - - let root = backend.into_drawing_area(); - let font: FontDesc = ("sans-serif", 20.0).into(); - - root.fill(&WHITE)?; - - let mut chart = ChartBuilder::on(&root) - .margin(20.0) - .caption(format!("y={}", func_str), font) - .x_label_area_size(30.0) - .y_label_area_size(30.0) - .build_cartesian_2d(min_x..max_x, min_y..max_y)?; - - chart.configure_mesh().x_labels(3).y_labels(3).draw()?; - - let data: Vec<(f32, f32)> = (1..=resolution) - .map(|x| ((x as f32 / resolution as f32) * absrange) + min_x) - .map(|x| (x, func(x as f64) as f32)) - .filter(|(_, y)| &min_y <= y && y <= &max_y) - .collect(); - - chart.draw_series(LineSeries::new(data, &RED))?; - - let (data2, area): (Vec<(f32, f32, f32)>, f32) = - integral_rectangles(min_x, step, num_interval, &func); // Get rectangle coordinates and the total area - - // Draw rectangles - chart.draw_series( - data2 - .iter() - .map(|(x1, x2, y)| Rectangle::new([(*x2, *y), (*x1, 0.0)], &BLUE)), - )?; - - root.present()?; - Ok((chart.into_coord_trans(), area)) -} - -// Creates and does the math for creating all the rectangles under the graph -#[inline(always)] -fn integral_rectangles( - min_x: f32, step: f32, num_interval: usize, func: &dyn Fn(f64) -> f64, -) -> (Vec<(f32, f32, f32)>, f32) { - let data2: Vec<(f32, f32, f32)> = (0..num_interval) - .map(|e| { - let x: f32 = ((e as f32) * step) + min_x; - - let x2: f32 = match x > 0.0 { - true => x + step, - false => x - step, - }; - - let tmp1: f32 = func(x as f64) as f32; - let tmp2: f32 = func(x2 as f64) as f32; - - let y: f32 = match tmp2.abs() > tmp1.abs() { - true => tmp1, - false => tmp2, - }; - - if !y.is_nan() { - (x, x2, y) - } else { - (0.0, 0.0, 0.0) - } - }) - .filter(|ele| ele != &(0.0, 0.0, 0.0)) - .collect(); - let area: f32 = data2.iter().map(|(_, _, y)| y * step).sum(); // sum of all rectangles' areas - (data2, area) -} diff --git a/src/lib.rs b/src/lib.rs index 9f9c595..06d9dc1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,12 +1,51 @@ +use meval::Expr; +use plotters::prelude::*; +use plotters_canvas::CanvasBackend; use wasm_bindgen::prelude::*; use web_sys::HtmlCanvasElement; -mod func_plot; +extern crate console_error_panic_hook; +use console_error_panic_hook::hook; +use std::panic; #[global_allocator] static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; pub type DrawResult = Result>; +// Creates and does the math for creating all the rectangles under the graph +#[inline(always)] +fn integral_rectangles( + min_x: f32, step: f32, num_interval: usize, func: &dyn Fn(f64) -> f64, +) -> (Vec<(f32, f32, f32)>, f32) { + let data2: Vec<(f32, f32, f32)> = (0..num_interval) + .map(|e| { + let x: f32 = ((e as f32) * step) + min_x; + + let x2: f32 = match x > 0.0 { + true => x + step, + false => x - step, + }; + + let tmp1: f32 = func(x as f64) as f32; + let tmp2: f32 = func(x2 as f64) as f32; + + let y: f32 = match tmp2.abs() > tmp1.abs() { + true => tmp1, + false => tmp2, + }; + + if !y.is_nan() { + (x, x2, y) + } else { + (0.0, 0.0, 0.0) + } + }) + .filter(|ele| ele != &(0.0, 0.0, 0.0)) + .collect(); + let area: f32 = data2.iter().map(|(_, _, y)| y * step).sum(); // sum of all rectangles' areas + (data2, area) +} + /// Result of screen to chart coordinates conversion. #[wasm_bindgen] pub struct Point { @@ -20,19 +59,27 @@ impl Point { } #[wasm_bindgen] -pub struct Chart { - convert: Box Option<(f32, f32)>>, - area: f32, +pub struct ChartManager { + func_str: String, + min_x: f32, + max_x: f32, + min_y: f32, + max_y: f32, + num_interval: usize, + resolution: i32, + back_cache: Option>, + front_cache: Option<(Vec<(f32, f32, f32)>, f32)>, + use_back_cache: bool, + use_front_cache: bool, } #[wasm_bindgen] -impl Chart { - pub fn draw( - canvas: HtmlCanvasElement, func_str: &str, min_x: f32, max_x: f32, min_y: f32, max_y: f32, - num_interval: usize, resolution: i32, - ) -> Result { - let draw_output = func_plot::draw( - canvas, +impl ChartManager { + pub fn new( + func_str: String, min_x: f32, max_x: f32, min_y: f32, max_y: f32, num_interval: usize, + resolution: i32, + ) -> Self { + Self { func_str, min_x, max_x, @@ -40,16 +87,127 @@ impl Chart { max_y, num_interval, resolution, - ) - .map_err(|err| err.to_string())?; - let map_coord = draw_output.0; - - Ok(Chart { - convert: Box::new(move |coord| map_coord(coord).map(|(x, y)| (x, y))), - area: draw_output.1, - }) + back_cache: None, + front_cache: None, + use_back_cache: false, + use_front_cache: false, + } } + // Used in order to hook into `panic!()` to log in the browser's console + pub fn init_panic_hook() { panic::set_hook(Box::new(console_error_panic_hook::hook)); } + + fn draw( + &mut self, element: HtmlCanvasElement, + ) -> DrawResult<(impl Fn((i32, i32)) -> Option<(f32, f32)>, f32)> { + let expr: Expr = self.func_str.parse().unwrap(); + let func = expr.bind("x").unwrap(); + + let absrange = (self.max_x - self.min_x).abs(); + let step = absrange / (self.num_interval as f32); + let backend = CanvasBackend::with_canvas_object(element).unwrap(); + + let root = backend.into_drawing_area(); + let font: FontDesc = ("sans-serif", 20.0).into(); + + root.fill(&WHITE)?; + + let mut chart = ChartBuilder::on(&root) + .margin(20.0) + .caption(format!("y={}", self.func_str), 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)?; + + chart.configure_mesh().x_labels(3).y_labels(3).draw()?; + + let data: Vec<(f32, f32)> = match self.use_back_cache { + true => match &self.back_cache { + Some(x) => x.clone(), + None => panic!("use_back_cache is true, but back_cache is None!"), + }, + false => { + let output: Vec<(f32, f32)> = (1..=self.resolution) + .map(|x| ((x as f32 / self.resolution as f32) * absrange) + self.min_x) + .map(|x| (x, func(x as f64) as f32)) + .filter(|(_, y)| &self.min_y <= y && y <= &self.max_y) + .collect(); + self.back_cache = Some(output.clone()); + output + } + }; + + chart.draw_series(LineSeries::new(data, &RED))?; + + let (data2, area): (Vec<(f32, f32, f32)>, f32) = match self.use_front_cache { + true => match &self.front_cache { + Some(x) => x.clone(), + None => panic!("use_front_cache is true, but front_cache is None!"), + }, + false => { + let output: (Vec<(f32, f32, f32)>, f32) = + integral_rectangles(self.min_x, step, self.num_interval, &func); + self.front_cache = Some(output.clone()); + output + } + }; + + // Draw rectangles + chart.draw_series( + data2 + .iter() + .map(|(x1, x2, y)| Rectangle::new([(*x2, *y), (*x1, 0.0)], &BLUE)), + )?; + + root.present()?; + Ok((chart.into_coord_trans(), area)) + } + + pub fn update( + &mut self, canvas: HtmlCanvasElement, func_str: &str, min_x: f32, max_x: f32, min_y: f32, + max_y: f32, num_interval: usize, resolution: i32, + ) -> Result { + let underlying_update = (func_str.to_string() != self.func_str) + | (min_x != self.min_x) + | (max_x != self.max_x) + | (min_y != self.min_y) + | (max_y != self.max_y); + + self.use_back_cache = + !underlying_update && self.resolution == resolution && !self.back_cache.is_none(); + self.use_front_cache = match underlying_update { + true => false, + false => num_interval == self.num_interval && !self.front_cache.is_none(), + }; + + self.func_str = func_str.to_string(); + 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).map_err(|err| err.to_string())?; + let map_coord = draw_output.0; + + let chart = Chart { + convert: Box::new(move |coord| map_coord(coord).map(|(x, y)| (x, y))), + area: draw_output.1, + }; + + Ok(chart) + } +} + +#[wasm_bindgen] +pub struct Chart { + convert: Box Option<(f32, f32)>>, + area: f32, +} + +#[wasm_bindgen] +impl Chart { pub fn get_area(&self) -> f32 { self.area } pub fn coord(&self, x: i32, y: i32) -> Option { diff --git a/www/bootstrap.js b/www/bootstrap.js index 729a523..bf0f88a 100644 --- a/www/bootstrap.js +++ b/www/bootstrap.js @@ -2,19 +2,19 @@ init(); async function init() { if (typeof process == "object") { - const [{Chart}, {main, setup}] = await Promise.all([ + const [{ChartManager}, {main, setup}] = await Promise.all([ import("integral_site"), import("./index.js"), ]); - setup(Chart); + setup(ChartManager); main(); } else { - const [{Chart, default: init}, {main, setup}] = await Promise.all([ + const [{ChartManager, default: init}, {main, setup}] = await Promise.all([ import("../pkg/integral_site.js"), import("./index.js"), ]); await init(); - setup(Chart); + setup(ChartManager); main(); } } diff --git a/www/index.js b/www/index.js index 8a93e6b..fc75097 100644 --- a/www/index.js +++ b/www/index.js @@ -1,4 +1,4 @@ -class Chart {} +class ChartManager {} const canvas = document.getElementById("canvas"); const coord = document.getElementById("coord"); @@ -14,6 +14,7 @@ const area_msg = document.getElementById("area-msg"); const resolution = document.getElementById("resolution"); let chart = null; +let chart_manager = null; /** Main entry point */ export function main() { @@ -23,7 +24,8 @@ export function main() { /** This function is used in `bootstrap.js` to setup imports. */ export function setup(WasmChart) { - Chart = WasmChart; + ChartManager = WasmChart; + ChartManager.init_panic_hook(); } /** Add event listeners. */ @@ -71,8 +73,12 @@ function onMouseMove(event) { function updatePlot() { status.innerText = `Rendering y=${math_function.value}...`; + if (chart_manager == null) { + 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)); + } + const start = performance.now(); - chart = Chart.draw(canvas, math_function.value, Number(minX.value), Number(maxX.value), Number(minY.value), Number(maxY.value), Number(num_interval.value), Number(resolution.value)); + 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)); const end = performance.now(); area_msg.innerText = `Estimated Area: ${chart.get_area()}`;