port to egui (WIP)

This commit is contained in:
Simon Gardling 2022-02-23 15:10:08 -05:00
parent eaf858b097
commit 59e4793a1b
15 changed files with 383 additions and 1072 deletions

View File

@ -13,11 +13,14 @@ opt-level = 3
lto = true lto = true
[dependencies] [dependencies]
wasm-bindgen = "0.2.79" meval = { git = "https://github.com/Titaniumtown/meval-rs.git" }
plotters = "0.3.1" egui = "0.17.0"
plotters-canvas = "0.3.0" eframe = "0.17.0"
emath = "0.17.0"
epaint = "0.17.0"
[target.'cfg(target_arch = "wasm32")'.dependencies]
console_error_panic_hook = "0.1.7"
tracing-wasm = "0.2.1"
wee_alloc = "0.4.5" wee_alloc = "0.4.5"
web-sys = { version = "0.3.56", features = ["HtmlCanvasElement"] } web-sys = { version = "0.3.56", features = ["HtmlCanvasElement"] }
meval = { git = "https://github.com/Titaniumtown/meval-rs.git" } wasm-bindgen = "0.2.79"
console_error_panic_hook = "0.1.7"
tracing-wasm = "0.2.1"

4
TODO.md Normal file
View File

@ -0,0 +1,4 @@
## TODO:
1. Port to [egui](https://github.com/emilk/egui)
- Reimplement testing the validity of functions.
- Proper support for dynamic chart display size.

125
src/chart_manager.rs Normal file
View File

@ -0,0 +1,125 @@
use crate::misc::{add_asterisks, Cache, Function};
// Manages Chart generation and caching of values
pub struct ChartManager {
function: Function,
min_x: f64,
max_x: f64,
num_interval: usize,
resolution: usize,
back_cache: Cache<Vec<(f64, f64)>>,
front_cache: Cache<(Vec<(f64, f64, f64)>, f64)>,
}
impl ChartManager {
pub fn new(
func_str: String, min_x: f64, max_x: f64, num_interval: usize,
resolution: usize,
) -> Self {
Self {
function: Function::from_string(func_str),
min_x,
max_x,
num_interval,
resolution,
back_cache: Cache::new_empty(),
front_cache: Cache::new_empty(),
}
}
#[inline]
fn draw(
&mut self
) -> (Vec<(f64, f64)>, Vec<(f64, f64, f64)>, f64) {
let absrange = (self.max_x - self.min_x).abs();
let data: Vec<(f64, f64)> = match self.back_cache.is_valid() {
true => self.back_cache.get().clone(),
false => {
let output: Vec<(f64, f64)> = (1..=self.resolution)
.map(|x| ((x as f64 / self.resolution as f64) * absrange) + self.min_x)
.map(|x| (x, self.function.run(x)))
.collect();
self.back_cache.set(output.clone());
output
}
};
let filtered_data: Vec<(f64, f64)> = data
.iter()
.map(|(x, y)| (*x, *y))
.collect();
let (rect_data, area): (Vec<(f64, f64, f64)>, f64) = match self.front_cache.is_valid() {
true => self.front_cache.get().clone(),
false => {
let step = absrange / (self.num_interval as f64);
let output: (Vec<(f64, f64, f64)>, f64) = self.integral_rectangles(step);
self.front_cache.set(output.clone());
output
}
};
(filtered_data, rect_data, area)
}
#[allow(clippy::too_many_arguments)]
pub fn update(
&mut self, func_str_new: String, min_x: f64, max_x: f64, num_interval: usize, resolution: usize,
) -> (Vec<(f64, f64)>, Vec<(f64, f64, f64)>, f64) {
let func_str: String = add_asterisks(func_str_new);
let update_func: bool = !self.function.str_compare(func_str.clone());
let underlying_update = update_func
| (min_x != self.min_x)
| (max_x != self.max_x);
if underlying_update | (self.resolution != resolution) {
self.back_cache.invalidate();
}
if underlying_update | (num_interval != self.num_interval) {
self.front_cache.invalidate();
}
if update_func {
self.function = Function::from_string(func_str);
}
self.min_x = min_x;
self.max_x = max_x;
self.num_interval = num_interval;
self.resolution = resolution;
self.draw()
}
// Creates and does the math for creating all the rectangles under the graph
#[inline]
fn integral_rectangles(&self, step: f64) -> (Vec<(f64, f64, f64)>, f64) {
let data2: Vec<(f64, f64, f64)> = (0..self.num_interval)
.map(|e| {
let x: f64 = ((e as f64) * step) + self.min_x;
// Makes sure rectangles are properly handled on x values below 0
let x2: f64 = match x > 0.0 {
true => x + step,
false => x - step,
};
let tmp1: f64 = self.function.run(x);
let tmp2: f64 = self.function.run(x2);
// Chooses the y value who's absolute value is the smallest
let y: f64 = match tmp2.abs() > tmp1.abs() {
true => tmp1,
false => tmp2,
};
(x, x2, y)
})
.filter(|(_, _, y)| !y.is_nan())
.collect();
let area: f64 = data2.iter().map(|(_, _, y)| y * step).sum(); // sum of all rectangles' areas
(data2, area)
}
}

94
src/egui_app.rs Normal file
View File

@ -0,0 +1,94 @@
use eframe::{egui, epi};
use egui::{plot::{HLine, Line, Plot, Value, Values, Text}, Pos2};
use crate::chart_manager::ChartManager;
use meval::Expr;
use crate::misc::{add_asterisks, Cache, Function};
use egui::{Color32, ColorImage, Ui};
use emath::Rect;
use epaint::{Rounding, RectShape, Stroke};
use egui::widgets::plot::{Bar, BarChart};
pub struct MathApp {
func_str: String,
min_x: f64,
max_x: f64,
num_interval: usize,
resolution: usize,
chart_manager: ChartManager,
}
impl Default for MathApp {
fn default() -> Self {
Self {
func_str: "x^2".to_string(),
min_x: -10.0,
max_x: 10.0,
num_interval: 100,
resolution: 10000,
chart_manager: ChartManager::new("x^2".to_string(), -10.0, 10.0, 100, 10000)
}
}
}
impl epi::App for MathApp {
fn name(&self) -> &str {
"eframe template"
}
/// Called once before the first frame.
fn setup(
&mut self,
_ctx: &egui::Context,
_frame: &epi::Frame,
_storage: Option<&dyn epi::Storage>,
) { }
/// Called each time the UI needs repainting, which may be many times per second.
/// Put your widgets into a `SidePanel`, `TopPanel`, `CentralPanel`, `Window` or `Area`.
fn update(&mut self, ctx: &egui::Context, frame: &epi::Frame) {
let Self {
func_str,
min_x,
max_x,
num_interval,
resolution,
chart_manager
} = self;
egui::SidePanel::left("side_panel").show(ctx, |ui| {
ui.heading("Side Panel");
ui.horizontal(|ui| {
ui.label("Function: ");
ui.text_edit_singleline(func_str);
});
ui.add(egui::Slider::new(min_x, -1000.0..=1000.0).text("Min X"));
ui.add(egui::Slider::new(max_x, *min_x..=1000.0).text("Max X"));
ui.add(egui::Slider::new(num_interval, 0..=usize::MAX).text("Interval"));
});
egui::CentralPanel::default().show(ctx, |ui| {
let (filtered_data, rect_data, area) = chart_manager.update(self.func_str.clone(), self.min_x, self.max_x, self.num_interval, self.resolution);
let filtered_data_values = filtered_data.iter().map(|(x, y)| Value::new(*x, *y)).collect();
let curve = Line::new(Values::from_values(filtered_data_values)).color(Color32::RED);
let bars = rect_data.iter().map(|(_, x2, y)| Bar::new(*x2, *y)).collect();
let barchart = BarChart::new(bars).color(Color32::BLUE);
// ui.label("Graph:");
ui.label(format!("Area: {:.10}", area));
Plot::new("plot")
.view_aspect(1.0)
.include_y(0)
.show(ui, |plot_ui| {
plot_ui.line(curve);
plot_ui.bar_chart(barchart);
});
});
}
}

View File

@ -1,18 +1,22 @@
#![allow(clippy::unused_unit)] // Fixes clippy keep complaining about wasm_bindgen #![allow(clippy::unused_unit)] // Fixes clippy keep complaining about wasm_bindgen
#![allow(clippy::type_complexity)] // Clippy, my types are fine. #![allow(clippy::type_complexity)] // Clippy, my types are fine.
use meval::Expr;
use plotters::prelude::*;
use plotters_canvas::CanvasBackend;
use std::panic;
use wasm_bindgen::prelude::*;
use web_sys::HtmlCanvasElement;
mod misc; mod misc;
use crate::misc::{add_asterisks, Cache, ChartOutput, DrawResult, Function}; mod egui_app;
mod chart_manager;
use std::panic;
#[cfg(target_arch = "wasm32")]
use eframe::{epi, egui};
#[cfg(target_arch = "wasm32")]
use wasm_bindgen::prelude::*;
#[cfg(target_arch = "wasm32")]
#[global_allocator] #[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen] #[wasm_bindgen]
extern "C" { extern "C" {
// Use `js_namespace` here to bind `console.log(..)` instead of just // Use `js_namespace` here to bind `console.log(..)` instead of just
@ -21,8 +25,9 @@ extern "C" {
fn log(s: &str); fn log(s: &str);
} }
#[wasm_bindgen(start)] #[cfg(target_arch = "wasm32")]
pub fn init() { #[wasm_bindgen]
pub fn start(canvas_id: &str) -> Result<(), wasm_bindgen::JsValue> {
log("Initializing..."); log("Initializing...");
// See performance in browser profiler! // See performance in browser profiler!
@ -36,244 +41,8 @@ pub fn init() {
log("Initialized console_error_panic_hook!"); log("Initialized console_error_panic_hook!");
log("Finished initializing!"); log("Finished initializing!");
}
log("Starting App...");
// Manages Chart generation and caching of values let app = egui_app::MathApp::default();
#[wasm_bindgen] eframe::start_web(canvas_id, Box::new(app))
pub struct ChartManager {
function: Function,
min_x: f32,
max_x: f32,
min_y: f32,
max_y: f32,
num_interval: usize,
resolution: usize,
back_cache: Cache<Vec<(f32, f32)>>,
front_cache: Cache<(Vec<(f32, f32, f32)>, f32)>,
}
#[wasm_bindgen]
impl ChartManager {
pub fn new(
func_str: String, min_x: f32, max_x: f32, min_y: f32, max_y: f32, num_interval: usize,
resolution: usize,
) -> Self {
Self {
function: Function::from_string(func_str),
min_x,
max_x,
min_y,
max_y,
num_interval,
resolution,
back_cache: Cache::new_empty(),
front_cache: Cache::new_empty(),
}
}
// Tests function to make sure it's able to be parsed. Returns the string of the Error produced, or an empty string if it runs successfully.
pub fn test_func(function_string: String) -> String {
// Factorials do not work, and it would be really difficult to make them work
if function_string.contains('!') {
return "Factorials are unsupported".to_string();
}
let new_func_str: String = add_asterisks(function_string);
let expr_result = new_func_str.parse();
let expr_error = match &expr_result {
Ok(_) => "".to_string(),
Err(error) => format!("{}", error),
};
if !expr_error.is_empty() {
return expr_error;
}
let expr: Expr = expr_result.unwrap();
let func_result = expr.bind("x");
let func_error = match &func_result {
Ok(_) => "".to_string(),
Err(error) => format!("{}", error),
};
if !func_error.is_empty() {
return func_error;
}
"".to_string()
}
#[inline]
fn draw(
&mut self, element: HtmlCanvasElement, dark_mode: bool,
) -> DrawResult<(impl Fn((i32, i32)) -> Option<(f32, f32)>, f32)> {
log("Drawing...");
let backend = CanvasBackend::with_canvas_object(element).unwrap();
let root = backend.into_drawing_area();
let font: FontDesc = ("sans-serif", 20.0).into();
if dark_mode {
root.fill(&RGBColor(28, 28, 28))?;
} else {
root.fill(&WHITE)?;
}
let mut chart = ChartBuilder::on(&root)
.margin(20.0)
.caption(format!("y={}", self.function.get_string()), font)
.x_label_area_size(30.0)
.y_label_area_size(30.0)
.build_cartesian_2d(self.min_x..self.max_x, self.min_y..self.max_y)?;
if dark_mode {
chart
.configure_mesh()
.x_labels(3)
.y_labels(3)
.light_line_style(&RGBColor(254, 254, 254))
.draw()?;
} else {
chart.configure_mesh().x_labels(3).y_labels(3).draw()?;
}
let absrange = (self.max_x - self.min_x).abs();
let data: Vec<(f32, f32)> = match self.back_cache.is_valid() {
true => self.back_cache.get().clone(),
false => {
log("Updating back_cache");
let output: Vec<(f32, f32)> = (1..=self.resolution)
.map(|x| ((x as f32 / self.resolution as f32) * absrange) + self.min_x)
.map(|x| (x, self.function.run(x)))
.collect();
self.back_cache.set(output.clone());
output
}
};
let filtered_data: Vec<(f32, f32)> = data
.iter()
.filter(|(_, y)| &self.min_y <= y && y <= &self.max_y)
.map(|(x, y)| (*x, *y))
.collect();
chart.draw_series(LineSeries::new(filtered_data, &RED))?;
let (rect_data, area): (Vec<(f32, f32, f32)>, f32) = match self.front_cache.is_valid() {
true => self.front_cache.get().clone(),
false => {
log("Updating front_cache");
let step = absrange / (self.num_interval as f32);
let output: (Vec<(f32, f32, f32)>, f32) = self.integral_rectangles(step);
self.front_cache.set(output.clone());
output
}
};
if self.num_interval <= 200 {
// Draw rectangles
chart.draw_series(
rect_data
.iter()
.map(|(x1, x2, y)| Rectangle::new([(*x2, *y), (*x1, 0.0)], &BLUE)),
)?;
} else {
// Save resources by not graphing rectangles and using an AreaSeries when you can no longer see the rectangles
let capped_data: Vec<(f32, f32)> = data
.iter()
.map(|(x, y)| {
if y.is_nan() {
return (*x, 0.0);
}
let new_y: &f32 = if y > &self.max_y {
&self.max_y
} else if &self.min_y > y {
&self.min_y
} else {
y
};
(*x, *new_y)
})
.collect();
chart.draw_series(AreaSeries::new(capped_data, 0.0, &BLUE))?;
}
root.present()?;
log("Finished Drawing!");
Ok((chart.into_coord_trans(), area))
}
#[allow(clippy::too_many_arguments)]
pub fn update(
&mut self, canvas: HtmlCanvasElement, func_str_new: String, min_x: f32, max_x: f32,
min_y: f32, max_y: f32, num_interval: usize, resolution: usize, dark_mode: bool,
) -> Result<ChartOutput, JsValue> {
let func_str: String = add_asterisks(func_str_new);
let update_func: bool = !self.function.str_compare(func_str.clone());
let underlying_update = update_func
| (min_x != self.min_x)
| (max_x != self.max_x)
| (min_y != self.min_y)
| (max_y != self.max_y);
if underlying_update | (self.resolution != resolution) {
self.back_cache.invalidate();
}
if underlying_update | (num_interval != self.num_interval) {
self.front_cache.invalidate();
}
if update_func {
self.function = Function::from_string(func_str);
}
self.min_x = min_x;
self.max_x = max_x;
self.min_y = min_y;
self.max_y = max_y;
self.num_interval = num_interval;
self.resolution = resolution;
let draw_output = self
.draw(canvas, dark_mode)
.map_err(|err| err.to_string())?;
let map_coord = draw_output.0;
let chart_output = ChartOutput {
convert: Box::new(move |coord| map_coord(coord).map(|(x, y)| (x, y))),
area: draw_output.1,
};
Ok(chart_output)
}
// Creates and does the math for creating all the rectangles under the graph
#[inline]
fn integral_rectangles(&self, step: f32) -> (Vec<(f32, f32, f32)>, f32) {
let data2: Vec<(f32, f32, f32)> = (0..self.num_interval)
.map(|e| {
let x: f32 = ((e as f32) * step) + self.min_x;
// Makes sure rectangles are properly handled on x values below 0
let x2: f32 = match x > 0.0 {
true => x + step,
false => x - step,
};
let tmp1: f32 = self.function.run(x);
let tmp2: f32 = self.function.run(x2);
// Chooses the y value who's absolute value is the smallest
let y: f32 = match tmp2.abs() > tmp1.abs() {
true => tmp1,
false => tmp2,
};
(x, x2, y)
})
.filter(|(_, _, y)| !y.is_nan())
.collect();
let area: f32 = data2.iter().map(|(_, _, y)| y * step).sum(); // sum of all rectangles' areas
(data2, area)
}
} }

16
src/main.rs Normal file
View File

@ -0,0 +1,16 @@
use eframe::{epi, egui};
mod egui_app;
mod misc;
mod chart_manager;
#[cfg(not(target_arch = "wasm32"))]
fn main() {
let app = egui_app::MathApp::default();
let options = eframe::NativeOptions {
// Let's show off that we support transparent windows
transparent: true,
drag_and_drop_support: true,
..Default::default()
};
eframe::run_native(Box::new(app), options);
}

View File

@ -1,7 +1,4 @@
use meval::Expr; use meval::Expr;
use wasm_bindgen::prelude::*;
pub type DrawResult<T> = Result<T, Box<dyn std::error::Error>>;
/* /*
EXTREMELY Janky function that tries to put asterisks in the proper places to be parsed. This is so cursed. But it works, and I hopefully won't ever have to touch it again. EXTREMELY Janky function that tries to put asterisks in the proper places to be parsed. This is so cursed. But it works, and I hopefully won't ever have to touch it again.
@ -80,6 +77,36 @@ pub fn add_asterisks(function_in: String) -> String {
output_string.replace('π', "pi") // π -> pi output_string.replace('π', "pi") // π -> pi
} }
// Tests function to make sure it's able to be parsed. Returns the string of the Error produced, or an empty string if it runs successfully.
pub fn test_func(function_string: String) -> String {
// Factorials do not work, and it would be really difficult to make them work
if function_string.contains('!') {
return "Factorials are unsupported".to_string();
}
let new_func_str: String = add_asterisks(function_string);
let expr_result = new_func_str.parse();
let expr_error = match &expr_result {
Ok(_) => "".to_string(),
Err(error) => format!("{}", error),
};
if !expr_error.is_empty() {
return expr_error;
}
let expr: Expr = expr_result.unwrap();
let func_result = expr.bind("x");
let func_error = match &func_result {
Ok(_) => "".to_string(),
Err(error) => format!("{}", error),
};
if !func_error.is_empty() {
return func_error;
}
"".to_string()
}
pub struct Function { pub struct Function {
function: Box<dyn Fn(f64) -> f64>, function: Box<dyn Fn(f64) -> f64>,
func_str: String, func_str: String,
@ -96,41 +123,13 @@ impl Function {
} }
#[inline] #[inline]
pub fn run(&self, x: f32) -> f32 { (self.function)(x as f64) as f32 } pub fn run(&self, x: f64) -> f64 { (self.function)(x) }
pub fn str_compare(&self, other_string: String) -> bool { self.func_str == other_string } pub fn str_compare(&self, other_string: String) -> bool { self.func_str == other_string }
pub fn get_string(&self) -> String { self.func_str.clone() } pub fn get_string(&self) -> String { self.func_str.clone() }
} }
/// Result of screen to chart coordinates conversion.
#[wasm_bindgen]
pub struct Point {
pub x: f32,
pub y: f32,
}
#[wasm_bindgen]
impl Point {
#[inline]
pub fn new(x: f32, y: f32) -> Self { Self { x, y } }
}
#[wasm_bindgen]
pub struct ChartOutput {
pub(crate) convert: Box<dyn Fn((i32, i32)) -> Option<(f32, f32)>>,
pub(crate) area: f32,
}
#[wasm_bindgen]
impl ChartOutput {
pub fn get_area(&self) -> f32 { self.area }
pub fn coord(&self, x: i32, y: i32) -> Option<Point> {
(self.convert)((x, y)).map(|(x, y)| Point::new(x, y))
}
}
pub struct Cache<T> { pub struct Cache<T> {
backing_data: Option<T>, backing_data: Option<T>,
} }

View File

@ -11,8 +11,6 @@ then
cargo install wasm-pack cargo install wasm-pack
fi fi
wasm-pack build --release wasm-pack build --release --target web
cd www basic-http-server
npm install
npm start

20
www/bootstrap.js vendored
View File

@ -1,20 +0,0 @@
init();
async function init() {
if (typeof process == "object") {
const [{ChartManager}, {main, setup}] = await Promise.all([
import("integral_site"),
import("./index.js"),
]);
setup(ChartManager);
main();
} else {
const [{ChartManager, default: init}, {main, setup}] = await Promise.all([
import("../pkg/integral_site.js"),
import("./index.js"),
]);
await init();
setup(ChartManager);
main();
}
}

View File

@ -1,5 +1,8 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<!-- Disable zooming: -->
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
@ -8,34 +11,25 @@
</head> </head>
<body> <body>
<noscript>Please enable Javascript, this page uses both WebAssembly and Javascript to run.</noscript> <noscript>Please enable Javascript, this page uses both WebAssembly and Javascript to run.</noscript>
<script src="bootstrap.js"></script>
<main> <canvas id="canvas"></canvas>
<h1>Integral Demonstration</h1>
<div id="coord"></div> <script type="module">
<canvas id="canvas" width="600" height="400"></canvas> import init, { start } from '../pkg/integral_site.js';
<div id="status">Loading WebAssembly...</div>
<div id="control"> async function run() {
<label for="math_function">y=</label> <input type="string" id="math_function" value="x^2"> await init();
<p></p>
<label for="minX">MinX: </label> <input type="number" id="minX" value="-10"> start("canvas");
<p></p> }
<label for="maxX">MaxX: </label> <input type="number" id="maxX" value="10"> run();
<p></p> </script>
<label for="minY">MinY: </label> <input type="number" id="minY" value="-10">
<p></p>
<label for="maxY">MaxY: </label> <input type="number" id="maxY" value="10">
<p></p>
<label for="num_interval">Interval: </label> <input type="number" id="num_interval" value="100" min="0" step="1">
<p></p>
<label for="resolution">Number of Points </label> <input type="number" id="resolution" value="10000" min="0" step="1">
<p></p>
</div>
<div id="area-msg">Area:</div>
</main>
</body> </body>
</html> </html>
<h4><a href="https://github.com/Titaniumtown/meval-rs#supported-expressions">Supported Expressions</a></h4>
<!-- Todo: port this to egui -->
<!-- <h4><a href="https://github.com/Titaniumtown/meval-rs#supported-expressions">Supported Expressions</a></h4>
<h3 id="%3Ca%20href=%22https://github.com/Titaniumtown/integral_site%22%3EI&amp;#8217;m%20Open%20Source!%3C/a%3E%20(and%20licensed%20under%20AGPLv3)"><a href="https://github.com/Titaniumtown/integral_site">I&#8217;m Open Source!</a> (and licensed under AGPLv3)</h3> <h3 id="%3Ca%20href=%22https://github.com/Titaniumtown/integral_site%22%3EI&amp;#8217;m%20Open%20Source!%3C/a%3E%20(and%20licensed%20under%20AGPLv3)"><a href="https://github.com/Titaniumtown/integral_site">I&#8217;m Open Source!</a> (and licensed under AGPLv3)</h3> -->

View File

@ -1,167 +0,0 @@
class ChartManager {}
const canvas = document.getElementById("canvas");
const coord = document.getElementById("coord");
const math_function = document.getElementById("math_function");
const status = document.getElementById("status");
const minX = document.getElementById("minX");
const maxX = document.getElementById("maxX");
const minY = document.getElementById("minY");
const maxY = document.getElementById("maxY");
const num_interval = document.getElementById("num_interval");
const area_msg = document.getElementById("area-msg");
const resolution = document.getElementById("resolution");
let darkMode = false;
let chart = null;
let chart_manager = null;
/** Main entry point */
export function main() {
setupUI();
setupCanvas();
}
/** This function is used in `bootstrap.js` to setup imports. */
export function setup(WasmChart) {
ChartManager = WasmChart;
}
/** Add event listeners. */
function setupUI() {
status.innerText = "WebAssembly loaded!";
// Handles browser color preferences
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
darkMode = true;
}
// Watches for changes in color preferences
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', event => {
darkMode = event.matches;
updatePlot();
});
math_function.addEventListener("change", updatePlot);
minX.addEventListener("input", updatePlot);
maxX.addEventListener("input", updatePlot);
minY.addEventListener("input", updatePlot);
maxY.addEventListener("input", updatePlot);
num_interval.addEventListener("input", updatePlot);
resolution.addEventListener("input", updatePlot);
window.addEventListener("resize", setupCanvas);
window.addEventListener("mousemove", onMouseMove);
}
function setupCanvas() {
const aspectRatio = canvas.width / canvas.height;
const size = canvas.parentNode.offsetWidth * 0.8;
canvas.style.width = size + "px";
canvas.style.height = size / aspectRatio + "px";
canvas.width = size;
canvas.height = size / aspectRatio;
updatePlot();
}
function onMouseMove(event) {
if (chart) {
var text = "Mouse is outside Chart.";
if (event.target == canvas) {
let actualRect = canvas.getBoundingClientRect();
let logicX = event.offsetX * canvas.width / actualRect.width;
let logicY = event.offsetY * canvas.height / actualRect.height;
const point = chart.coord(logicX, logicY);
text = (point)
? `(${point.x.toFixed(3)}, ${point.y.toFixed(3)})`
: text;
}
coord.innerText = text;
}
}
function postErrorStatus(string) {
status.style.color = "red";
status.innerText = string;
}
function postNormalStatus(string) {
status.style.color = "grey";
status.innerText = string;
}
// Checks variables put in input fields
function checkVariables() {
if (minX.value >= maxX.value) {
postErrorStatus("minX must be smaller than maxX!");
return;
}
if (minY.value >= maxY.value) {
postErrorStatus("minY must be smaller than maxY!");
return;
}
if (0 > num_interval.value) {
postErrorStatus("Interval is smaller than 0!");
return;
}
if (0 > resolution.value) {
postErrorStatus("Number of Points is smaller than 0!");
return;
}
}
// Generates a possible "tip" to assist the user when an error occurs.
function errorRecommend(error_string) {
if (error_string.includes("Evaluation error: unknown variable ")) {
return "This variable is not considered valid. Make sure you used a valid variable.";
} else if (error_string == "Factorials are unsupported") {
return "";
} else {
return "Make sure you're using proper syntax! Check console log (press F12) as well for more details.";
}
}
function updatePlot() {
checkVariables();
if (chart_manager == null) {
try {
chart_manager = ChartManager.new(math_function.value, Number(minX.value), Number(maxX.value), Number(minY.value), Number(maxY.value), Number(num_interval.value), Number(resolution.value));
} catch(err) {
postErrorStatus("Error during ChartManager creation! Check logs for details.");
return;
}
}
const test_result = ChartManager.test_func(math_function.value);
if (test_result != "") {
const error_recommendation = errorRecommend(test_result);
let error_status_str = test_result;
if (error_recommendation != "") {
error_status_str += `\nTip: ${error_recommendation}`;
}
postErrorStatus(error_status_str);
return;
}
try {
postNormalStatus(`Rendering y=${math_function.value}...`);
const start = performance.now();
chart = chart_manager.update(canvas, math_function.value, Number(minX.value), Number(maxX.value), Number(minY.value), Number(maxY.value), Number(num_interval.value), Number(resolution.value), false); // TODO: improve darkmode support
const end = performance.now();
area_msg.innerText = `Estimated Area: ${chart.get_area()}`;
postNormalStatus(`Rendered ${math_function.innerText} in ${Math.ceil(end - start)}ms`);
} catch(err) {
postErrorStatus(`Error! check console logs for more detail`);
}
}

View File

@ -1,18 +0,0 @@
{
"name": "integral_site",
"version": "0.1.0",
"main": "index.js",
"scripts": {
"build": "webpack --config webpack.config.js",
"start": "webpack-dev-server"
},
"dependencies": {
"integral_site": "file:../pkg"
},
"devDependencies": {
"webpack": "^4.43.0",
"webpack-cli": "^3.3.11",
"webpack-dev-server": "^3.10.3",
"copy-webpack-plugin": "^5.0.0"
}
}

View File

@ -1,55 +1,81 @@
html, body, main { html {
width: 100%; /* Remove touch delay: */
margin: 0; touch-action: manipulation;
padding: 0;
} }
body { body {
margin: auto; /* Light mode background color for what is not covered by the egui canvas,
max-width: 800px; or where the egui canvas is translucent. */
display: flex; background: #909090;
flex-direction: column;
} }
@media (max-width: 800px) { @media (prefers-color-scheme: dark) {
body { body {
padding: 10px; /* Dark mode background color for what is not covered by the egui canvas,
box-sizing: border-box; or where the egui canvas is translucent. */
background: #404040;
} }
} }
main { /* Allow canvas to fill entire web page: */
display: flex; html,
flex-direction: column; body {
align-items: center; overflow: hidden;
margin: 0 !important;
padding: 0 !important;
} }
#coord, #status { /* Position canvas in center-top: */
color: grey; canvas {
font-size: 10px; margin-right: auto;
height: 15px margin-left: auto;
display: block;
position: absolute;
top: 0%;
left: 50%;
transform: translate(-50%, 0%);
} }
#control { .centered {
margin-top: 1em; margin-right: auto;
} margin-left: auto;
display: block;
#control label { position: absolute;
font-weight: bold; top: 50%;
margin-right: 1em; left: 50%;
} transform: translate(-50%, -50%);
color: #f0f0f0;
#control select { font-size: 24px;
padding: 0.25em 0.5em; font-family: Ubuntu-Light, Helvetica, sans-serif;
}
footer {
margin-top: 2em;
font-size: 12px;
text-align: center; text-align: center;
} }
.hide { /* ---------------------------------------------- */
visibility: hidden; /* Loading animation from https://loading.io/css/ */
height: 0px; .lds-dual-ring {
display: inline-block;
width: 24px;
height: 24px;
}
.lds-dual-ring:after {
content: " ";
display: block;
width: 24px;
height: 24px;
margin: 0px;
border-radius: 50%;
border: 3px solid #fff;
border-color: #fff transparent #fff transparent;
animation: lds-dual-ring 1.2s linear infinite;
}
@keyframes lds-dual-ring {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
} }

View File

@ -1,498 +0,0 @@
html {
font-size: 100%;
overflow-y: scroll;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
@media (prefers-color-scheme: dark) {
input,
body {
color: #c9d1d9;
font-family: 'Open Sans', sans-serif;
font-size: 12px;
line-height: 1.5em;
padding: 1em;
margin: auto;
max-width: 52em;
/* github dark color:
background: #0d1117; */
/* duckduckgo dark color */
background: #1c1c1c;
}
}
@media (prefers-color-scheme: light) {
input,
body {
color: #444;
font-family: 'Open Sans', sans-serif;
font-size: 12px;
line-height: 1.5em;
padding: 1em;
margin: auto;
max-width: 52em;
background: #fefefe;
}
}
@media (prefers-color-scheme: dark) {
/* Links */
a {
color: #58a6ff;
text-decoration: none;
}
/* Visited Links */
a:visited {
color: #58a6ff;
}
/* Links that are being hovered over */
a:hover {
color: #58a6ff;
cursor:pointer;
}
}
@media (prefers-color-scheme: light) {
/* Links */
a {
color: #0645ad;
text-decoration: none;
}
/* Visited Links */
a:visited {
color: #0b0080;
}
/* Links that are being hovered over */
a:hover {
color: #06e;
cursor:pointer;
}
}
a:active {
color: #faa700;
}
a:focus {
outline: thin dotted;
}
a:hover,
a:active {
outline: 0;
}
/* Paragraph selected (Legacy firefox 61 and below) */
::-moz-selection {
background: rgba(255, 255, 0, 0.3);
color: #000;
}
/* Paragraph selected */
::selection {
background: rgba(255, 255, 0, 0.3);
color: #000;
}
/* Paragraph selected (Legacy firefox 61 and below) */
a::-moz-selection {
background: rgba(255, 255, 0, 0.3);
color: #0645ad;
}
/* Paragraph selected */
a::selection {
background: rgba(255, 255, 0, 0.3);
color: #0645ad;
}
p {
margin: 1em 0;
}
img {
max-width: 100%;
}
/* Inline code snippets */
@media (prefers-color-scheme: dark) {
code {
border-radius: 5px;
-moz-border-radius: 5px;
-webkit-border-radius: 5px;
border: 1px solid #2f333a;
padding: 2px;
}
}
/* Inline code snippets */
@media (prefers-color-scheme: light) {
code {
border-radius: 5px;
-moz-border-radius: 5px;
-webkit-border-radius: 5px;
border: 1px solid #BCBEC0;
padding: 2px;
}
}
/* Post-Inline code snippets */
pre code {
border-radius: 0px;
-moz-border-radius: 0px;
-webkit-border-radius: 0px;
border: 0px;
padding: 2px;
}
/* Headers */
@media (prefers-color-scheme: dark) {
h1,
h2,
h3,
h4,
h5,
h6 {
font-weight: 600;
color: #c9d1d9;
line-height: 1em;
}
}
/* Headers */
@media (prefers-color-scheme: light) {
h1,
h2,
h3,
h4,
h5,
h6 {
font-weight: 600;
color: #111;
line-height: 1em;
}
}
h4,
h5,
h6 {
font-weight: bold;
}
h1 {
font-size: 2em;
}
h2 {
font-size: 1.3em;
padding-top: 30px;
}
h3 {
font-size: 1.1em;
padding-top: 10px;
}
h4 {
font-size: 0.9em;
padding-top: 5px;
}
h5 {
font-size: 0.9em;
}
h6 {
font-size: 0.9em;
}
blockquote {
color: #666666;
margin: 0;
padding-left: 3em;
border-left: 0.5em #eee solid;
}
hr {
display: block;
border: 0;
border-top: 1px solid #aaa;
border-bottom: 1px solid #eee;
margin: 1em 0;
padding: 0;
}
pre,
code,
kbd,
samp {
font-family: 'Fira Code', monospace;
font-size: 0.98em;
}
/* Markdown code block */
@media (prefers-color-scheme: dark) {
pre {
white-space: pre;
white-space: pre-wrap;
word-wrap: break-word;
/* Use with "github dark" background: */
/* background-color: #1f2227; */
/* Use with "duckduckgo dark" background: */
background-color: #202325;
padding: 10px 15px;
}
}
/* Markdown code block */
@media (prefers-color-scheme: light) {
pre {
white-space: pre;
white-space: pre-wrap;
word-wrap: break-word;
background-color: #eee;
padding: 10px 15px;
}
}
b,
strong {
font-weight: bold;
}
dfn {
font-style: italic;
}
ins {
background: #ff9;
color: #000;
text-decoration: none;
}
mark {
background: #ff0;
color: #000;
font-style: italic;
font-weight: bold;
}
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sup {
top: -0.5em;
}
sub {
bottom: -0.25em;
}
ul,
ol {
margin: 1em 0;
padding: 0 0 0 2em;
}
li p:last-child {
margin: 0;
}
dd {
margin: 0 0 0 2em;
}
img {
border: 0;
-ms-interpolation-mode: bicubic;
vertical-align: middle;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
td {
vertical-align: top;
}
@media only screen and (min-width: 480px) {
body {
font-size: 14px;
}
.logo {
max-height: 30px
}
}
@media only screen and (min-width: 768px) {
body {
font-size: 16px;
}
.logo {
max-height: 40px;
}
article {
margin: 50px 0;
}
}
@media print {
* {
background: transparent !important;
color: black !important;
filter: none !important;
-ms-filter: none !important;
}
body {
font-size: 12pt;
max-width: 100%;
}
a,
a:visited {
text-decoration: underline;
}
hr {
height: 1px;
border: 0;
border-bottom: 1px solid black;
}
a[href]:after {
content: " (" attr(href) ")";
}
abbr[title]:after {
content: " (" attr(title) ")";
}
.ir a:after,
a[href^="javascript:"]:after,
a[href^="#"]:after {
content: "";
}
pre,
blockquote {
border: 1px solid #999;
padding-right: 1em;
page-break-inside: avoid;
}
tr,
img {
page-break-inside: avoid;
}
img {
max-width: 100% !important;
}
@page :left {
margin: 15mm 20mm 15mm 10mm;
}
@page :right {
margin: 15mm 10mm 15mm 20mm;
}
p,
h2,
h3 {
orphans: 3;
widows: 3;
}
h2,
h3 {
page-break-after: avoid;
}
}
nav ul {
margin: 0;
padding: 0;
list-style-type: none;
overflow: hidden;
}
nav ul li {
/* This allow us to arrange list items in a row, without using float */
display: inline-block;
list-style-type: none;
}
/* Create a style for the first level items */
nav > div > ul > li > a {
color: #333 !important;
display: block;
line-height: 2em;
padding: 0.5em 0em;
text-decoration: none;
}
nav > div.nav-right > ul > li > a {
padding: 0.5em 0.5em;
}
nav > div > ul > li > a:hover {
color: #aaa !important;
}
.nav-left {
float: left;
}
.nav-left ul li {
float: left;
}
.nav-right ul li {
float: right;
}
.logo {
margin-right: 0.5em
}
article img {
margin: 1em 0;
}
p.date {
font-size: 13px;
color: #666;
}
ul.articles {
list-style: none;
padding: 0;
}
#Articles {
margin-top: 2em;
}
footer {
font-size: 13px;
}

View File

@ -1,14 +0,0 @@
const CopyWebpackPlugin = require("copy-webpack-plugin");
const path = require('path');
module.exports = {
entry: "./bootstrap.js",
output: {
path: path.resolve(__dirname, "dist"),
filename: "bootstrap.js",
},
mode: "development",
plugins: [
new CopyWebpackPlugin(['index.html'])
],
};