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
|
||||
|
||||
[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
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::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
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 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>,
|
||||
}
|
||||
|
||||
@ -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
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>
|
||||
<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&#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 {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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