port to egui (WIP)
This commit is contained in:
parent
eaf858b097
commit
59e4793a1b
15
Cargo.toml
15
Cargo.toml
@ -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
4
TODO.md
Normal 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
125
src/chart_manager.rs
Normal 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
94
src/egui_app.rs
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
267
src/lib.rs
267
src/lib.rs
@ -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
16
src/main.rs
Normal 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);
|
||||||
|
}
|
||||||
63
src/misc.rs
63
src/misc.rs
@ -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>,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
20
www/bootstrap.js
vendored
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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&#8217;m%20Open%20Source!%3C/a%3E%20(and%20licensed%20under%20AGPLv3)"><a href="https://github.com/Titaniumtown/integral_site">I’m Open Source!</a> (and licensed under AGPLv3)</h3>
|
<h3 id="%3Ca%20href=%22https://github.com/Titaniumtown/integral_site%22%3EI&#8217;m%20Open%20Source!%3C/a%3E%20(and%20licensed%20under%20AGPLv3)"><a href="https://github.com/Titaniumtown/integral_site">I’m Open Source!</a> (and licensed under AGPLv3)</h3> -->
|
||||||
|
|||||||
167
www/index.js
167
www/index.js
@ -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`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
102
www/style.css
102
www/style.css
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
|
||||||
}
|
|
||||||
@ -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'])
|
|
||||||
],
|
|
||||||
};
|
|
||||||
Loading…
x
Reference in New Issue
Block a user