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
[dependencies]
wasm-bindgen = "0.2.79"
plotters = "0.3.1"
plotters-canvas = "0.3.0"
meval = { git = "https://github.com/Titaniumtown/meval-rs.git" }
egui = "0.17.0"
eframe = "0.17.0"
emath = "0.17.0"
epaint = "0.17.0"
[target.'cfg(target_arch = "wasm32")'.dependencies]
console_error_panic_hook = "0.1.7"
tracing-wasm = "0.2.1"
wee_alloc = "0.4.5"
web-sys = { version = "0.3.56", features = ["HtmlCanvasElement"] }
meval = { git = "https://github.com/Titaniumtown/meval-rs.git" }
console_error_panic_hook = "0.1.7"
tracing-wasm = "0.2.1"
wasm-bindgen = "0.2.79"

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::type_complexity)] // Clippy, my types are fine.
use meval::Expr;
use plotters::prelude::*;
use plotters_canvas::CanvasBackend;
use std::panic;
use wasm_bindgen::prelude::*;
use web_sys::HtmlCanvasElement;
mod misc;
use crate::misc::{add_asterisks, Cache, ChartOutput, DrawResult, Function};
mod egui_app;
mod chart_manager;
use std::panic;
#[cfg(target_arch = "wasm32")]
use eframe::{epi, egui};
#[cfg(target_arch = "wasm32")]
use wasm_bindgen::prelude::*;
#[cfg(target_arch = "wasm32")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen]
extern "C" {
// Use `js_namespace` here to bind `console.log(..)` instead of just
@ -21,8 +25,9 @@ extern "C" {
fn log(s: &str);
}
#[wasm_bindgen(start)]
pub fn init() {
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen]
pub fn start(canvas_id: &str) -> Result<(), wasm_bindgen::JsValue> {
log("Initializing...");
// See performance in browser profiler!
@ -36,244 +41,8 @@ pub fn init() {
log("Initialized console_error_panic_hook!");
log("Finished initializing!");
}
// Manages Chart generation and caching of values
#[wasm_bindgen]
pub struct ChartManager {
function: Function,
min_x: f32,
max_x: f32,
min_y: f32,
max_y: f32,
num_interval: usize,
resolution: usize,
back_cache: Cache<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)
}
log("Starting App...");
let app = egui_app::MathApp::default();
eframe::start_web(canvas_id, Box::new(app))
}

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 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.
@ -80,6 +77,36 @@ pub fn add_asterisks(function_in: String) -> String {
output_string.replace('π', "pi") // π -> pi
}
// Tests function to make sure it's able to be parsed. Returns the string of the Error produced, or an empty string if it runs successfully.
pub fn test_func(function_string: String) -> String {
// Factorials do not work, and it would be really difficult to make them work
if function_string.contains('!') {
return "Factorials are unsupported".to_string();
}
let new_func_str: String = add_asterisks(function_string);
let expr_result = new_func_str.parse();
let expr_error = match &expr_result {
Ok(_) => "".to_string(),
Err(error) => format!("{}", error),
};
if !expr_error.is_empty() {
return expr_error;
}
let expr: Expr = expr_result.unwrap();
let func_result = expr.bind("x");
let func_error = match &func_result {
Ok(_) => "".to_string(),
Err(error) => format!("{}", error),
};
if !func_error.is_empty() {
return func_error;
}
"".to_string()
}
pub struct Function {
function: Box<dyn Fn(f64) -> f64>,
func_str: String,
@ -96,41 +123,13 @@ impl Function {
}
#[inline]
pub fn run(&self, x: f32) -> f32 { (self.function)(x as f64) as f32 }
pub fn run(&self, x: f64) -> f64 { (self.function)(x) }
pub fn str_compare(&self, other_string: String) -> bool { self.func_str == other_string }
pub fn get_string(&self) -> String { self.func_str.clone() }
}
/// Result of screen to chart coordinates conversion.
#[wasm_bindgen]
pub struct Point {
pub x: f32,
pub y: f32,
}
#[wasm_bindgen]
impl Point {
#[inline]
pub fn new(x: f32, y: f32) -> Self { Self { x, y } }
}
#[wasm_bindgen]
pub struct ChartOutput {
pub(crate) convert: Box<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> {
backing_data: Option<T>,
}

View File

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

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>
<html lang="en">
<!-- Disable zooming: -->
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
@ -8,34 +11,25 @@
</head>
<body>
<noscript>Please enable Javascript, this page uses both WebAssembly and Javascript to run.</noscript>
<script src="bootstrap.js"></script>
<main>
<h1>Integral Demonstration</h1>
<div id="coord"></div>
<canvas id="canvas" width="600" height="400"></canvas>
<div id="status">Loading WebAssembly...</div>
<div id="control">
<label for="math_function">y=</label> <input type="string" id="math_function" value="x^2">
<p></p>
<label for="minX">MinX: </label> <input type="number" id="minX" value="-10">
<p></p>
<label for="maxX">MaxX: </label> <input type="number" id="maxX" value="10">
<p></p>
<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>
<canvas id="canvas"></canvas>
<script type="module">
import init, { start } from '../pkg/integral_site.js';
async function run() {
await init();
start("canvas");
}
run();
</script>
</body>
</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 {
width: 100%;
margin: 0;
padding: 0;
html {
/* Remove touch delay: */
touch-action: manipulation;
}
body {
margin: auto;
max-width: 800px;
display: flex;
flex-direction: column;
/* Light mode background color for what is not covered by the egui canvas,
or where the egui canvas is translucent. */
background: #909090;
}
@media (max-width: 800px) {
@media (prefers-color-scheme: dark) {
body {
padding: 10px;
box-sizing: border-box;
/* Dark mode background color for what is not covered by the egui canvas,
or where the egui canvas is translucent. */
background: #404040;
}
}
main {
display: flex;
flex-direction: column;
align-items: center;
/* Allow canvas to fill entire web page: */
html,
body {
overflow: hidden;
margin: 0 !important;
padding: 0 !important;
}
#coord, #status {
color: grey;
font-size: 10px;
height: 15px
/* Position canvas in center-top: */
canvas {
margin-right: auto;
margin-left: auto;
display: block;
position: absolute;
top: 0%;
left: 50%;
transform: translate(-50%, 0%);
}
#control {
margin-top: 1em;
}
#control label {
font-weight: bold;
margin-right: 1em;
}
#control select {
padding: 0.25em 0.5em;
}
footer {
margin-top: 2em;
font-size: 12px;
.centered {
margin-right: auto;
margin-left: auto;
display: block;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #f0f0f0;
font-size: 24px;
font-family: Ubuntu-Light, Helvetica, sans-serif;
text-align: center;
}
.hide {
visibility: hidden;
height: 0px;
/* ---------------------------------------------- */
/* Loading animation from https://loading.io/css/ */
.lds-dual-ring {
display: inline-block;
width: 24px;
height: 24px;
}
.lds-dual-ring:after {
content: " ";
display: block;
width: 24px;
height: 24px;
margin: 0px;
border-radius: 50%;
border: 3px solid #fff;
border-color: #fff transparent #fff transparent;
animation: lds-dual-ring 1.2s linear infinite;
}
@keyframes lds-dual-ring {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}

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'])
],
};