diff --git a/Cargo.lock b/Cargo.lock index 91694c0..9102d56 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -417,7 +417,7 @@ checksum = "96a6ac251f4a2aca6b3f91340350eab87ae57c3f127ffeb585e92bd336717991" [[package]] name = "cxgraph-web" -version = "0.1.0" +version = "0.2.0" dependencies = [ "console_error_panic_hook", "console_log", @@ -852,7 +852,7 @@ checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" [[package]] name = "libcxgraph" -version = "0.1.0" +version = "0.2.0" dependencies = [ "lalrpop", "lalrpop-util", diff --git a/Cargo.toml b/Cargo.toml index 019a058..769c1a6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,3 +5,7 @@ members = [ "cxgraph-web", ] resolver = "2" + +[profile.release] +lto = true +opt-level = 's' diff --git a/cxgraph-web/Cargo.toml b/cxgraph-web/Cargo.toml index e6a4380..cfde0d1 100644 --- a/cxgraph-web/Cargo.toml +++ b/cxgraph-web/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cxgraph-web" -version = "0.1.0" +version = "0.2.0" edition = "2021" [lib] diff --git a/cxgraph-web/favicon.ico b/cxgraph-web/favicon.ico new file mode 100644 index 0000000..fff7a23 Binary files /dev/null and b/cxgraph-web/favicon.ico differ diff --git a/cxgraph-web/index.html b/cxgraph-web/index.html index 3d3675c..830e00a 100644 --- a/cxgraph-web/index.html +++ b/cxgraph-web/index.html @@ -10,16 +10,21 @@ - CXGraph - + + + + + + + + + cxgraph + - +
- - - @@ -41,7 +46,12 @@
-
+
+ +
+ + +
@@ -51,107 +61,106 @@
-
-
+
+ -
+
-
-
+
+ -
+
-
+
+ Contours +
+ +
- +
-
- +
-
- +
-
- +
+
-
-
- -
-
- -
-
Coloring
+
+ Coloring

-
+
-
- Overlay -
- - -
-
- - -
-
+
+ Grid + +
+ +
+ + +
+
+ +
+ diff --git a/cxgraph-web/src/lib.rs b/cxgraph-web/src/lib.rs index 2237d81..03881d5 100644 --- a/cxgraph-web/src/lib.rs +++ b/cxgraph-web/src/lib.rs @@ -80,6 +80,11 @@ pub fn resize(width: u32, height: u32) { with_state(|state| state.resize((width, height))); } +#[wasm_bindgen] +pub fn set_res_scale(scale: f32) { + with_state(|state| state.uniforms.res_scale = scale); +} + #[wasm_bindgen] pub fn set_bounds(min_x: f32, min_y: f32, max_x: f32, max_y: f32) { with_state(|state| { @@ -109,6 +114,11 @@ pub fn set_decorations(value: u32) { with_state(|state| state.uniforms.decorations = value); } +#[wasm_bindgen] +pub fn set_grid_mode(value: u32) { + with_state(|state| state.uniforms.grid_mode = value); +} + #[wasm_bindgen] pub fn set_variable(idx: usize, re: f32, im: f32) { with_state(|state| { diff --git a/cxgraph-web/editor.js b/cxgraph-web/static/editor.js similarity index 92% rename from cxgraph-web/editor.js rename to cxgraph-web/static/editor.js index 4a81f8b..03484de 100644 --- a/cxgraph-web/editor.js +++ b/cxgraph-web/static/editor.js @@ -10,7 +10,6 @@ source_text.addEventListener("mousedown", () => { sourceFocused = true; }); source_text.addEventListener("focusout", () => { sourceFocused = false; }); source_text.addEventListener("keydown", (event) => { - console.log(event); if (event.key != "Tab") { sourceFocused = true; } @@ -159,3 +158,12 @@ source_text.addEventListener("input", (event) => { source_text.selectionEnd = e - amnt; }); +source_text.addEventListener("change", () => { + localStorage.setItem("editor_content", source_text.value); +}); + +if (localStorage.getItem("editor_content") !== null) { + source_text.value = localStorage.getItem("editor_content"); +} else { + source_text.value = "f(z) = 6z^2 - 2i - 1\nplot(z) = f(1 + sin(z)) / 8"; +} diff --git a/cxgraph-web/static/icon/favicon16.png b/cxgraph-web/static/icon/favicon16.png new file mode 100644 index 0000000..1789075 Binary files /dev/null and b/cxgraph-web/static/icon/favicon16.png differ diff --git a/cxgraph-web/static/icon/favicon160.png b/cxgraph-web/static/icon/favicon160.png new file mode 100644 index 0000000..0659812 Binary files /dev/null and b/cxgraph-web/static/icon/favicon160.png differ diff --git a/cxgraph-web/static/icon/favicon32.png b/cxgraph-web/static/icon/favicon32.png new file mode 100644 index 0000000..b6c459d Binary files /dev/null and b/cxgraph-web/static/icon/favicon32.png differ diff --git a/cxgraph-web/static/icon/favicon320.png b/cxgraph-web/static/icon/favicon320.png new file mode 100644 index 0000000..f2325aa Binary files /dev/null and b/cxgraph-web/static/icon/favicon320.png differ diff --git a/cxgraph-web/static/icon/favicon48.png b/cxgraph-web/static/icon/favicon48.png new file mode 100644 index 0000000..824a54a Binary files /dev/null and b/cxgraph-web/static/icon/favicon48.png differ diff --git a/cxgraph-web/static/icon/favicon64.png b/cxgraph-web/static/icon/favicon64.png new file mode 100644 index 0000000..14d52bd Binary files /dev/null and b/cxgraph-web/static/icon/favicon64.png differ diff --git a/cxgraph-web/static/icon/favicon640.png b/cxgraph-web/static/icon/favicon640.png new file mode 100644 index 0000000..721446e Binary files /dev/null and b/cxgraph-web/static/icon/favicon640.png differ diff --git a/cxgraph-web/index.js b/cxgraph-web/static/index.js similarity index 85% rename from cxgraph-web/index.js rename to cxgraph-web/static/index.js index 8206c03..bced0f8 100644 --- a/cxgraph-web/index.js +++ b/cxgraph-web/static/index.js @@ -1,6 +1,6 @@ "use strict" -import init, * as cxgraph from "./pkg/cxgraph_web.js"; +import init, * as cxgraph from "../pkg/cxgraph_web.js"; await init(); let graphView = { @@ -38,6 +38,11 @@ function screenToCx(screen) { } } +function panView(dX, dY) { + graphView.xoff -= 2.0 * graphView.scale * dX / window.innerHeight; + graphView.yoff += 2.0 * graphView.scale * dY / window.innerHeight; +} + // // Canvas // @@ -65,29 +70,8 @@ function calcBounds() { } function onViewChange() { - let dim = { w: window.innerWidth, h: innerHeight }; let bounds = calcBounds(); cxgraph.set_bounds(bounds.x_min, bounds.y_min, bounds.x_max, bounds.y_max); - let origin = cxToScreen({ re: 0, im: 0 }); - let one = cxToScreen({ re: 1, im: 0 }); - - if(svg_axis_x.visibility != "hidden") { - svg_axis_x.setAttribute("x1", 0) - svg_axis_x.setAttribute("x2", dim.w); - svg_axis_x.setAttribute("y1", origin.y); - svg_axis_x.setAttribute("y2", origin.y); - } - if(svg_axis_y.visibility != "hidden") { - svg_axis_y.setAttribute("x1", origin.x); - svg_axis_y.setAttribute("x2", origin.x); - svg_axis_y.setAttribute("y1", 0); - svg_axis_y.setAttribute("y2", dim.h); - } - if(svg_unitcircle.visibility != "hidden") { - svg_unitcircle.setAttribute("cx", origin.x); - svg_unitcircle.setAttribute("cy", origin.y); - svg_unitcircle.setAttribute("r", one.x - origin.x); - } for(let point of graphPoints) { point.onViewChange(); @@ -96,18 +80,34 @@ function onViewChange() { tryRedraw(); } +function updateCoordinates() { + let cx = screenToCx({ x: mouseX, y: mouseY }); + let scale = -Math.floor(Math.log10(graphView.scale * 0.001)); + if (scale < 0) scale = 0; + let re = cx.re.toFixed(scale); + let im = (-cx.im).toFixed(scale); + mouse_pos.textContent = `${re} + ${im}i`; +} + function onResize() { let width = window.innerWidth; let height = window.innerHeight; cxgraph.resize(width*graphView.res_mult, height*graphView.res_mult); + cxgraph.set_res_scale(graphView.res_mult); canvas.style.width = "100vw"; canvas.style.height = "100vh"; onViewChange(); + updateCoordinates(); } function onWheel(e) { - graphView.scale *= Math.exp(e.deltaY * 0.0007); + let factor = Math.exp(e.deltaY * 0.0007); + let dX = window.innerWidth/2 - e.x; + let dY = window.innerHeight/2 - e.y; + panView(dX * (1 - factor), dY * (1 - factor)); + graphView.scale *= factor onViewChange(); + updateCoordinates(); } function onPointerDown(e) { @@ -124,19 +124,25 @@ function onPointerUp() { } function onPointerMove(e) { + let dX = e.offsetX - mouseX; + let dY = e.offsetY - mouseY; + mouseX = e.offsetX; + mouseY = e.offsetY; if(mousePressed) { - let dX = e.offsetX - mouseX; - let dY = e.offsetY - mouseY; - mouseX = e.offsetX; - mouseY = e.offsetY; - graphView.xoff -= 2.0 * graphView.scale * dX / window.innerHeight; - graphView.yoff += 2.0 * graphView.scale * dY / window.innerHeight; + panView(dX, dY); onViewChange(); } else { for(let point of graphPoints) { point.onPointerMove(e); } } + updateCoordinates(); +} + +function onKeyDown(e) { + if (e.key == "c" && e.ctrlKey) { + navigator.clipboard.writeText(mouse_pos.textContent); + } } window.addEventListener("resize", onResize); @@ -144,6 +150,8 @@ canvas.addEventListener("wheel", onWheel); canvas.addEventListener("pointerdown", onPointerDown); canvas.addEventListener("pointerup", onPointerUp); canvas.addEventListener("pointermove", onPointerMove); +canvas.addEventListener("pointermove", onPointerMove); +canvas.addEventListener("keydown", onKeyDown); // // Graph/redraw @@ -220,16 +228,18 @@ for(let e of nameColorMode) { nameColorMode[1].checked = true; cxgraph.set_coloring(1); -overlay_axes.addEventListener("change", () => { - let vis = overlay_axes.checked ? "visible" : "hidden"; - svg_axis_x.setAttribute("visibility", vis); - svg_axis_y.setAttribute("visibility", vis); -}); +let nameGridMode = document.getElementsByName("grid_mode"); +for(let e of nameGridMode) { + e.addEventListener("change", () => { + let selected = document.querySelector("input[name=grid_mode]:checked"); + cxgraph.set_grid_mode(parseInt(selected.getAttribute("data-value"))); + tryRedraw(); + }); + e.checked = false; +} +nameGridMode[2].checked = true; +cxgraph.set_grid_mode(2); -overlay_unitcircle.addEventListener("change", () => { - let vis = overlay_unitcircle.checked ? "visible" : "hidden"; - svg_unitcircle.setAttribute("visibility", vis); -}); // // Variables @@ -404,6 +414,7 @@ function addPoint() { button_slider_new.addEventListener("click", addSlider); button_point_new.addEventListener("click", addPoint); + // // Init // diff --git a/cxgraph-web/static/style.css b/cxgraph-web/static/style.css new file mode 100644 index 0000000..b463411 --- /dev/null +++ b/cxgraph-web/static/style.css @@ -0,0 +1,164 @@ +* { + font-family: monospace; + font-size: 16px; +} + +body, html { + height: 100%; + margin: 0; + padding: 0; + overflow: hidden; +} + +body.theme_dark { + --col-bg: #334; + --col-fg: #fff; + --col-error: #f9a; + --col-shadow: #0004; + --col-input: #667; + --col-input-border: #def; + --col-input-hover: #889; + --col-editor-bg: #445; + --col-editor-border: #223; + --col-trans-bg: #3344; +} + +body.theme_light { + --col-bg: #fff; + --col-fg: #223; + --col-error: #c24; + --col-shadow: #8884; + --col-input: #fff; + --col-input-border: #888; + --col-input-hover: #ddd; + --col-editor-bg: #eee; + --col-editor-border: #fff; + --col-trans-bg: #fff4; +} + +.canvas-container { + position: absolute; + left: 0px; + top: 0px; +} + +canvas, #overlay { + position: absolute; + left: 0px; + top: 0px; + width: 100vw; + height: 100vh; +} + +#canvas { + z-index: 0; + -webkit-transform: translate3d(0, 0, 0); +} + +#overlay { + z-index: 2; + pointer-events: none; +} + +#overlay_points { + pointer-events: all; +} + +.menus { + position: absolute; + left: 0px; + top: 0px; + right: 0px; + pointer-events: none; + display: flex; + flex-direction: row; + justify-content: space-between; + z-index: 10; +} + +.menu { + pointer-events: all; + margin: 10px; + padding: 10px; + background: var(--col-bg); + color: var(--col-fg); + height: fit-content; + box-shadow: 0 0 5px 1px #00000088; +} + +details > *:not(summary) { + padding-top: 2px; + padding-bottom: 2px; + padding-left: 8px; + border-left: 1px solid var(--col-fg); +} + +summary { + padding-bottom: 5px; +} + +.info_overlay { + position: absolute; + left: 0px; + bottom: 0px; + pointer-events: none; + z-index: 20; + + margin: 10px; + padding: 5px; + border-radius: 5px; + + background: #0004; + color: #fff; + box-shadow: 0 0 5px 1px #0004; +} + +#source_text { + width: 400px; + height: 150px; + font-size: 15px; + background-color: var(--col-editor-bg); + color: var(--col-fg); + border: none; /*var(--col-editor-border);*/ + padding: 5px; +} + +#div_error_msg { + color: var(--col-error); + white-space: pre-line; +} + +input { + color: var(--col-fg); + background: var(--col-input); + border: none; + border-radius: 2px; + margin-top: 2px; + margin-bottom: 2px; + padding-left: 3px; + padding-right: 3px; +} + +input, textarea{ + box-shadow: 0 0 5px 1px var(--col-shadow); +} + +input[type=number], input[type=text] { + border-bottom: 2px solid var(--col-input-border); +} + +input:hover { + background: var(--col-input-hover); +} + +input[type=button]:active { + background: var(--col-input); +} + +hr { + border-color: var(--col-input); +} + +fieldset { + border: 1px solid var(--col-input-border); +} diff --git a/cxgraph-web/static/themes.js b/cxgraph-web/static/themes.js new file mode 100644 index 0000000..5ab7cf2 --- /dev/null +++ b/cxgraph-web/static/themes.js @@ -0,0 +1,21 @@ +function themeChange() { + if (checkbox_theme.checked) { + body.classList.remove("theme_light"); + body.classList.add("theme_dark"); + } else { + body.classList.remove("theme_dark"); + body.classList.add("theme_light"); + } + localStorage.setItem("theme", checkbox_theme.checked ? "dark" : "light"); +} + +checkbox_theme.addEventListener("change", themeChange); + +if (localStorage.getItem("theme") !== null) { + if (localStorage.getItem("theme") == "light") { + checkbox_theme.checked = false; + } else { + checkbox_theme.checked = true; + } +} +themeChange(); diff --git a/cxgraph-web/style.css b/cxgraph-web/style.css deleted file mode 100644 index ff9c706..0000000 --- a/cxgraph-web/style.css +++ /dev/null @@ -1,110 +0,0 @@ -* { - font-family: monospace; - font-size: 16px; -} - -body, html { - height: 100%; - margin: 0; - padding: 0; - overflow: hidden; -} - -.canvas-container { - position: absolute; - left: 0px; - top: 0px; -} - -canvas, #overlay { - position: absolute; - left: 0px; - top: 0px; - width: 100vw; - height: 100vh; -} - -#canvas { - z-index: 0; - -webkit-transform: translate3d(0, 0, 0); -} - -#overlay { - z-index: 1; - pointer-events: none; -} - -#overlay_points { - pointer-events: all; -} - -.menus { - position: absolute; - left: 0px; - top: 0px; - right: 0px; - pointer-events: none; - display: flex; - flex-direction: row; - justify-content: space-between; - z-index: 10; -} - -.menu { - pointer-events: all; - margin: 10px; - padding: 10px; - background: #334; - color: #fff; - height: fit-content; - box-shadow: 0 0 5px 1px #00000088; -} - -details > *:not(summary) { - padding-top: 2px; - padding-bottom: 2px; - padding-left: 8px; - border-left: 1px solid #fff; -} - -summary { - padding-bottom: 5px; -} - -#source_text { - width: 400px; - height: 150px; - font-size: 15px; -} - -#div_error_msg { - color: #f9a; - white-space: pre-line; -} - -input { - color: #fff; - background: #667; - border: none; - border-radius: 2px; - margin-top: 2px; - margin-bottom: 2px; - padding-left: 3px; - padding-right: 3px; -} - -input, textarea{ - box-shadow: 0 0 5px 1px #00000044; -} - -input[type=number], input[type=text] { - border-bottom: 2px solid #def; -} - -input:hover { - background: #889; -} - -input[type=button]:active { - background: #667; -} diff --git a/docs/language.md b/docs/language.md index 305f917..3d22139 100644 --- a/docs/language.md +++ b/docs/language.md @@ -67,7 +67,8 @@ variable on the right. The equality operators `==` and `!=` compare two values, considering both their real and imaginary components. The comparison operators `>`, `<`, `>=`, and `<=` -only consider the real component. +only consider the real component. These all produce `0` if the equality or comparison +is false and `1` if it is true. `+`, `-`, and `*` also function as the unary plus, minus, and conugation operators. @@ -111,6 +112,12 @@ if(z > 0) { z^2 } { 2z } If the argument's real part is positive, the first body will be evaluated, and otherwise the second will be. +`while` can be used to repeat while a condition is met. Care should be taken to ensure the loop will always end eventually. + +``` +0 -> n, while(n < 10) { n + 1 -> n } +``` + ## Built-in functions and constants @@ -137,6 +144,9 @@ power/exponential functions: |----------------|-------------------------------------------| | `exp(z)` | Exponential function, equivalent to `e^z` | | `log(z)` | Natural logarithm | +| `log2(z)` | Logarithm base 2 | +| `log10(z)` | Logarithm base 10 | +| `logb(b,z)` | Logarithm base b | | `logbr(z,br)` | Natural logarithm with specified branch | | `pow(z)` | Power, equivalent to `^` | | `powbr(z,br)` | `pow` with specified branch | @@ -181,6 +191,11 @@ logic functions: | `absim(z)` | Absolute value of imaginary part | | `isnan(z)` | 1 if `z` is NaN, 0 otherwise | +other functions: +| function | description | +|--------------|----------------------------------| +| `mix(u,v,a)` | `u*(1-a) + v*a` | + constants: | name | description | |----------------|--------------------------------------------------------------------------------------------------------| diff --git a/docs/web.md b/docs/web.md index 0ed579f..14191dc 100644 --- a/docs/web.md +++ b/docs/web.md @@ -2,7 +2,9 @@ ## Source -Enter the program to plot into the text area. For more information, see [the language docs](language.md). +Enter the program to plot into the text area. For more information, see [the language docs](language.md). The tab key can be used to add indentation - to use tab to navigate out of the text area, press escape first. Special characters can be inserted with a backslash followed by: +- a digit 0-9 for a subscript +- a lowercase or uppercase Greek letter name (eg. `alpha` or `Zeta`) for that letter in the respective case The Graph button compiles the program and redraws the screen. This must be pressed after changes are made to the program to see them. This can also be accomplished by pressing `Shift`+`Enter` in the text area. @@ -10,7 +12,7 @@ The Redraw button redraws the screen. If Auto Redraw is enabled, the screen will ## The plot -Most of the screen is occupied by the plot. Click and drag with the mouse to move around, and use the scroll wheel to zoom in and out. +Most of the screen is occupied by the plot. Click and drag with the mouse to move around, and use the scroll wheel to zoom in and out. Press Ctrl+C while focused on the plot to copy the cursor's position to the clipboard. ## Options @@ -24,7 +26,7 @@ Contours can be toggled with the contour checkboxes. Real and imaginary contours Standard coloring directly maps argument to hue in HSV while keeping saturation and value constant. Uniform coloring uses a modified mapping that tries to avoid variation in perceptual brightness. None disables coloring entirely. -Draw Axes and Unit Circle enable or disable the axes and circle overlays. +The grid can be toggled on and off, or set to show the axes only. ## Variables diff --git a/libcxgraph/Cargo.toml b/libcxgraph/Cargo.toml index 2ebc43f..f80d053 100644 --- a/libcxgraph/Cargo.toml +++ b/libcxgraph/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "libcxgraph" -version = "0.1.0" +version = "0.2.0" edition = "2021" build = "build.rs" @@ -12,7 +12,7 @@ log = "0.4" lalrpop-util = { version = "0.21.0", features = ["lexer", "unicode"] } num-complex = "0.4" wgpu = "22.1" -raw-window-handle = "0.6" +raw-window-handle = "0.6.2" unicode-xid = "0.2" [build-dependencies] diff --git a/libcxgraph/src/language/ast.rs b/libcxgraph/src/language/ast.rs index eaf1ca3..bf8d4e3 100644 --- a/libcxgraph/src/language/ast.rs +++ b/libcxgraph/src/language/ast.rs @@ -22,7 +22,7 @@ pub enum ExpressionType<'a> { Unary(UnaryOp), FnCall(&'a str), Store(&'a str), - If, + If, While, Sum { countvar: &'a str }, Prod { countvar: &'a str }, Iter { itervar: &'a str }, @@ -70,6 +70,13 @@ impl<'a> Expression<'a> { } } + pub fn new_while(cond: Self, body: Self) -> Self { + Self { + ty: ExpressionType::While, + children: vec![cond, body], + } + } + pub fn new_sum(countvar: &'a str, min: Self, max: Self, body: Self) -> Self { Self { ty: ExpressionType::Sum { countvar }, @@ -108,6 +115,7 @@ fn display_expr(w: &mut impl fmt::Write, expr: &Expression, depth: usize) -> fmt ExpressionType::FnCall(f) => write!(w, "{:indent$}CALL {f}", "", indent=indent)?, ExpressionType::Store(n) => write!(w, "{:indent$}STORE {n}", "", indent=indent)?, ExpressionType::If => write!(w, "{:indent$}IF", "", indent=indent)?, + ExpressionType::While => write!(w, "{:indent$}WHILE", "", indent=indent)?, ExpressionType::Sum { countvar } => write!(w, "{:indent$}SUM {countvar}", "", indent=indent)?, ExpressionType::Prod { countvar } => write!(w, "{:indent$}PROD {countvar}", "", indent=indent)?, ExpressionType::Iter { itervar } => write!(w, "{:indent$}ITER {itervar}", "", indent=indent)?, diff --git a/libcxgraph/src/language/builtins.rs b/libcxgraph/src/language/builtins.rs index 4af391e..82b9832 100644 --- a/libcxgraph/src/language/builtins.rs +++ b/libcxgraph/src/language/builtins.rs @@ -21,6 +21,11 @@ thread_local! { m.insert("abs", ("c_abs", 1)); m.insert("arg", ("c_arg", 1)); m.insert("argbr", ("c_argbr", 2)); + m.insert("diveu", ("c_diveu", 2)); + m.insert("mod", ("c_mod", 2)); + m.insert("floor", ("c_floor", 1)); + m.insert("ceil", ("c_ceil", 1)); + m.insert("round", ("c_round", 1)); m.insert("add", ("c_add", 2)); m.insert("sub", ("c_sub", 2)); @@ -31,6 +36,9 @@ thread_local! { m.insert("exp", ("c_exp", 1)); m.insert("log", ("c_log", 1)); + m.insert("log2", ("c_log2", 1)); + m.insert("log10", ("c_log10", 1)); + m.insert("logb", ("c_logb", 2)); m.insert("logbr", ("c_logbr", 2)); m.insert("sqrt", ("c_sqrt", 1)); m.insert("sqrtbr", ("c_sqrtbr", 2)); @@ -62,6 +70,8 @@ thread_local! { m.insert("lambertwbr", ("c_lambertwbr", 2)); m.insert("erf", ("c_erf", 1)); + m.insert("mix", ("c_mix", 3)); + m }; diff --git a/libcxgraph/src/language/compiler.rs b/libcxgraph/src/language/compiler.rs index 17c2bfb..f543fab 100644 --- a/libcxgraph/src/language/compiler.rs +++ b/libcxgraph/src/language/compiler.rs @@ -250,6 +250,20 @@ impl<'w, 'i, W: fmt::Write> Compiler<'w, 'i, W> { writeln!(self.buf, "}}")?; Ok(result) }, + ExpressionType::While => { + let res = local.next_tmp(); + writeln!(self.buf, "var {res}: vec2f;")?; + writeln!(self.buf, "loop {{")?; + + let cond = self.compile_expr(local, &expr.children[0])?; + writeln!(self.buf, "if {cond}.x <= 0.0 {{ break; }}")?; + + let mut loop_local = local.clone(); + let body = self.compile_expr(&mut loop_local, &expr.children[1])?; + writeln!(self.buf, "{res} = {body};")?; + writeln!(self.buf, "}}")?; + Ok(res) + } ExpressionType::Sum { countvar } | ExpressionType::Prod { countvar } => { let min = local.next_tmp(); diff --git a/libcxgraph/src/language/mod.rs b/libcxgraph/src/language/mod.rs index aaecb41..f9af07f 100644 --- a/libcxgraph/src/language/mod.rs +++ b/libcxgraph/src/language/mod.rs @@ -4,7 +4,7 @@ use lalrpop_util::lalrpop_mod; use crate::language::token::Lexer; -use self::{compiler::Compiler, ast::display_def}; +use self::{ast::display_def, compiler::Compiler, token::{LexerError, Token}}; mod token; mod ast; @@ -27,6 +27,10 @@ pub fn compile(src: &str, vars: &HashMap) -> Result Result, LexerError> { + Lexer::new(src).collect() +} + pub fn show_ast(src: &str) -> Result> { let lexer = Lexer::new(src); let result = syntax::ProgramParser::new() diff --git a/libcxgraph/src/language/syntax.lalrpop b/libcxgraph/src/language/syntax.lalrpop index 9417326..841d23b 100644 --- a/libcxgraph/src/language/syntax.lalrpop +++ b/libcxgraph/src/language/syntax.lalrpop @@ -32,6 +32,7 @@ extern { "prod" => Token::Prod, "iter" => Token::Iter, "if" => Token::If, + "while" => Token::While, Number => Token::Number(), Name => Token::Name(<&'input str>), } @@ -142,4 +143,6 @@ Item: Expression<'input> = { => Expression::new_iter(name, count, init, body), "if" "(" ")" => Expression::new_if(cond, t, f), + "while" "(" ")" + => Expression::new_while(cond, body), } diff --git a/libcxgraph/src/language/token.rs b/libcxgraph/src/language/token.rs index 4b82905..971b1d8 100644 --- a/libcxgraph/src/language/token.rs +++ b/libcxgraph/src/language/token.rs @@ -7,7 +7,7 @@ use unicode_xid::UnicodeXID; pub enum Token<'i> { Number(f64), Name(&'i str), - Sum, Prod, Iter, If, + Sum, Prod, Iter, If, While, LParen, RParen, LBrace, RBrace, Plus, Minus, Star, Slash, Caret, @@ -26,6 +26,7 @@ impl<'i> fmt::Display for Token<'i> { Token::Prod => f.write_str("prod"), Token::Iter => f.write_str("iter"), Token::If => f.write_str("if"), + Token::While => f.write_str("while"), Token::LParen => f.write_str("("), Token::RParen => f.write_str(")"), Token::LBrace => f.write_str("{"), @@ -124,6 +125,7 @@ impl<'i> Lexer<'i> { "prod" => Ok((i, Token::Prod, j)), "iter" => Ok((i, Token::Iter, j)), "if" => Ok((i, Token::If, j)), + "while" => Ok((i, Token::While, j)), _ => Ok((i, Token::Name(s), j)), } } diff --git a/libcxgraph/src/renderer/fragment.wgsl b/libcxgraph/src/renderer/fragment.wgsl index a3d4324..3ce636b 100644 --- a/libcxgraph/src/renderer/fragment.wgsl +++ b/libcxgraph/src/renderer/fragment.wgsl @@ -7,10 +7,12 @@ struct Uniforms { resolution: vec2u, bounds_min: vec2f, bounds_max: vec2f, + res_scale: f32, shading_intensity: f32, contour_intensity: f32, decoration: u32, coloring: u32, + grid_mode: u32, } @group(0) @binding(1) var uniforms: Uniforms; @@ -24,6 +26,7 @@ const E = 2.718281828459045; const RECIP_SQRT2 = 0.7071067811865475; const LOG_TAU = 1.8378770664093453; const LOG_2 = 0.6931471805599453; +const LOG_10 = 2.302585092994046; const RECIP_SQRT29 = 0.18569533817705186; const C_TAU = vec2f(TAU, 0.0); @@ -43,11 +46,19 @@ fn remap(val: vec2f, a1: vec2f, b1: vec2f, a2: vec2f, b2: vec2f) -> vec2f { return a2 + (b2 - a2) * ((val - a1) / (b1 - a1)); } -fn correct_mod(x: f32, y: f32) -> f32 { +fn screen2cx(pos: vec2f) -> vec2f { + return remap(pos, C_ZERO, vec2f(uniforms.resolution), uniforms.bounds_min, uniforms.bounds_max); +} + +fn cx2screen(z: vec2f) -> vec2f { + return remap(z, uniforms.bounds_min, uniforms.bounds_max, C_ZERO, vec2f(uniforms.resolution)); +} + +fn emod(x: f32, y: f32) -> f32 { return ((x % y) + y) % y; } -fn correct_mod2(x: vec2f, y: vec2f) -> vec2f { +fn emod2(x: vec2f, y: vec2f) -> vec2f { return ((x % y) + y) % y; } @@ -151,6 +162,20 @@ fn c_recip(v: vec2f) -> vec2f { return vec2(v.x, -v.y) / dot(v, v); } +fn c_diveu(u: vec2f, v: vec2f) -> vec2f { + let z = c_div(u, v); + return floor(z); +} + +fn c_mod(u: vec2f, v: vec2f) -> vec2f { + let z = c_diveu(u, v); + return u - c_mul(z, v); +} + +fn c_floor(z: vec2f) -> vec2f { return floor(z); } +fn c_ceil(z: vec2f) -> vec2f { return ceil(z); } +fn c_round(z: vec2f) -> vec2f { return round(z); } + fn c_exp(z: vec2f) -> vec2f { return exp(z.x) * vec2(cos(z.y), sin(z.y)); } @@ -159,6 +184,18 @@ fn c_log(z: vec2f) -> vec2f { return vec2(0.5 * log(dot(z, z)), c_arg(z).x); } +fn c_log2(z: vec2f) -> vec2f { + return c_log(z)/LOG_2; +} + +fn c_log10(z: vec2f) -> vec2f { + return c_log(z)/LOG_10; +} + +fn c_logb(b: vec2f, z: vec2f) -> vec2f { + return c_div(c_log(z), c_log(b)); +} + fn c_logbr(z: vec2f, br: vec2f) -> vec2f { return vec2(0.5 * log(dot(z, z)), c_argbr(z, br).x); } @@ -213,7 +250,7 @@ fn c_tanh(z: vec2f) -> vec2f { fn c_asin(z: vec2f) -> vec2f { let m = select(-1.0, 1.0, z.y < 0.0 || (z.y == 0.0 && z.x > 0.0)); - let u = c_sqrt(vec2(1.0, 0.0) - c_mul(z, z)); + let u = c_sqrt(C_ONE - c_mul(z, z)); let v = c_log(u + m*vec2(-z.y, z.x)); return m*vec2(v.y, -v.x); } @@ -221,27 +258,27 @@ fn c_asin(z: vec2f) -> vec2f { // TODO fix fn c_acos(z: vec2f) -> vec2f { let m = select(-1.0, 1.0, z.y < 0.0 || (z.y == 0.0 && z.x > 0.0)); - let u = c_sqrt(vec2(1.0, 0.0) - c_mul(z, z)); + let u = c_sqrt(C_ONE - c_mul(z, z)); let v = c_log(u + m*vec2(-z.y, z.x)); return C_TAU/4.0 + m*vec2(-v.y, v.x); } fn c_atan(z: vec2f) -> vec2f { - let u = vec2(1.0, 0.0) - vec2(-z.y, z.x); - let v = vec2(1.0, 0.0) + vec2(-z.y, z.x); + let u = C_ONE - vec2(-z.y, z.x); + let v = C_ONE + vec2(-z.y, z.x); let w = c_log(c_div(u, v)); return 0.5 * vec2(-w.y, w.x); } fn c_asinh(z: vec2f) -> vec2f { let m = select(-1.0, 1.0, z.x > 0.0 || (z.x == 0.0 && z.y > 0.0)); - let u = c_sqrt(vec2(1.0, 0.0) + c_mul(z, z)); + let u = c_sqrt(C_ONE + c_mul(z, z)); return c_log(u + z*m) * m; } fn c_acosh(z: vec2f) -> vec2f { let b = select(0.0, TAU, z.x < 0.0 || (z.x == 0.0 && z.y < 0.0)); - let u = c_sqrtbr(vec2(-1.0, 0.0) + c_mul(z, z), vec2(b, 0.0)); + let u = c_sqrtbr(-C_ONE + c_mul(z, z), vec2(b, 0.0)); return c_log(u + z); } @@ -255,7 +292,7 @@ fn c_loggamma(z: vec2f) -> vec2f { let reflect = z.x < 0.5 && abs(z.y) < 13.0; var zp = z; if reflect { - zp = vec2(1.0, 0.0) - z; + zp = C_ONE - z; } var w = c_loggamma_inner2(zp); if reflect { @@ -270,8 +307,8 @@ fn c_loggamma_inner(z: vec2f) -> vec2f { } fn c_loggamma_inner2(z: vec2f) -> vec2f { - let w = c_loggamma_inner(z + vec2(3.0, 0.0)); - let l = c_log(z) + c_log(z + vec2(1.0, 0.0)) + c_log(z + vec2(2.0, 0.0)); + let w = c_loggamma_inner(z + 3*C_ONE); + let l = c_log(z) + c_log(z + C_ONE) + c_log(z + 2*C_ONE); return w - l; } @@ -291,7 +328,7 @@ fn c_digamma(z: vec2f) -> vec2f { let reflect = z.x < 0.5 && abs(z.y) < 13.0; var zp = z; if reflect { - zp = vec2(1.0, 0.0) - z; + zp = C_ONE - z; } var w = c_digamma_inner2(zp); if reflect { @@ -309,8 +346,8 @@ fn c_digamma_inner(z: vec2f) -> vec2f { } fn c_digamma_inner2(z: vec2f) -> vec2f { - let w = c_digamma_inner(z + vec2(3.0, 0.0)); - let l = c_recip(z + vec2(2.0, 0.0)) + c_recip(z + vec2(1.0, 0.0)) + c_recip(z); + let w = c_digamma_inner(z + 3*C_ONE); + let l = c_recip(z) + c_recip(z + C_ONE) + c_recip(z + 2*C_ONE); return w - l; } @@ -379,13 +416,17 @@ const ERF_A3 = 1.4214137412; const ERF_A4 = -1.4531520268; const ERF_A5 = 1.0614054292; fn c_erf_plus(z: vec2f) -> vec2f { - let t = c_recip(vec2(1.0, 0.0) + ERF_P * z); + let t = c_recip(C_ONE + ERF_P * z); let m = c_exp(-c_mul(z, z)); let r = c_mul(t, vec2f(ERF_A1, 0.0) + c_mul(t, vec2f(ERF_A2, 0.0) + c_mul(t, vec2f(ERF_A3, 0.0) + c_mul(t, vec2f(ERF_A4, 0.0) + t * ERF_A5)))); - return vec2f(1.0, 0.0) - c_mul(m, r); + return C_ONE - c_mul(m, r); +} + +fn c_mix(u: vec2f, v: vec2f, a: vec2f) -> vec2f { + return c_mul(u, C_ONE - a) + c_mul(v, a); } ///////////////// @@ -463,43 +504,38 @@ fn coloring_none(z: vec2f) -> vec3f { } fn decoration_contour_re(z: vec2f) -> f32 { - return correct_mod(floor(z.x), 2.0) * 2.0 - 1.0; + return emod(floor(z.x), 2.0) * 2.0 - 1.0; } fn decoration_contour_im(z: vec2f) -> f32 { - return correct_mod(floor(z.y), 2.0) * 2.0 - 1.0; + return emod(floor(z.y), 2.0) * 2.0 - 1.0; } fn decoration_contour_arg(z: vec2f) -> f32 { let arg = c_arg(z).x; - return round(correct_mod(arg + TAU, TAU/8.0) * 8.0/TAU) * 2.0 - 1.0; + return round(emod(arg + TAU, TAU/8.0) * 8.0/TAU) * 2.0 - 1.0; } fn decoration_contour_mag(z: vec2f) -> f32 { let logmag = 0.5 * log2(z.x*z.x + z.y*z.y); - return round(correct_mod(0.5 * logmag, 1.0)) * 2.0 - 1.0; + return round(emod(0.5 * logmag, 1.0)) * 2.0 - 1.0; } -@fragment -fn main(@builtin(position) in: vec4f) -> @location(0) vec4f { - let pos = vec2(in.x, f32(uniforms.resolution.y) - in.y); - let w = remap(pos, vec2(0.0, 0.0), vec2f(uniforms.resolution), uniforms.bounds_min, uniforms.bounds_max); - - let z = func_plot(w); - - var col = vec3f(); +fn color_result(z: vec2f) -> vec3f { switch uniforms.coloring { - case 0u, default: { - col = coloring_standard(z); + case 0u: { + return coloring_standard(z); } - case 1u: { - col = coloring_uniform(z); + case 1u, default: { + return coloring_uniform(z); } case 2u: { - col = coloring_none(z); + return coloring_none(z); } } +} +fn contour_result(z: vec2f) -> f32 { var contours = 1.0; if (uniforms.decoration & 0x01u) != 0u { @@ -522,7 +558,62 @@ fn main(@builtin(position) in: vec4f) -> @location(0) vec4f { contours = 0.0; } - let final_col = mix(col, vec3f(contours * 0.5 + 0.5), uniforms.contour_intensity); + return contours; +} + +fn grid_ortho(pos: vec2f) -> f32 { + let gt = ceil(uniforms.res_scale); + let z0 = screen2cx(pos - vec2f(gt/2)); + let z1 = screen2cx(pos + vec2f(gt/2)); + + let p0 = cx2screen(vec2(0.0, 0.0)) / uniforms.res_scale; + let p1 = cx2screen(vec2(1.0, 1.0)) / uniforms.res_scale; + + let gs = 64.0/pow(4.0, floor(-0.5 + log2(p1.x - p0.x)/2)); + let gs2 = gs/4.0; + + if (emod(z0.x, gs) > emod(z1.x, gs) || emod(z0.y, gs) > emod(z1.y, gs)) { + return 0.7; + } + if (emod(z0.x, gs2) > emod(z1.x, gs2) || emod(z0.y, gs2) > emod(z1.y, gs2)) { + return 0.25; + } + return 0.0; +} + +fn grid_axes(pos: vec2f) -> f32 { + let gt = ceil(uniforms.res_scale); + let z0 = screen2cx(pos - vec2f(gt/2)); + let z1 = screen2cx(pos + vec2f(gt/2)); + + if ((sign(z0.x) <= 0 && sign(z1.x) > 0) || (sign(z0.y) <= 0 && sign(z1.y) > 0)) { + return 0.7; + } + return 0.0; +} + +@fragment +fn main(@builtin(position) in: vec4f) -> @location(0) vec4f { + let pos = vec2(in.x, f32(uniforms.resolution.y) - in.y); + let z = screen2cx(pos); + + let w = func_plot(z); + + let col = color_result(w); + let contours = contour_result(w); + let plot_col = mix(col, vec3f(contours * 0.5 + 0.5), uniforms.contour_intensity); + + var grid_val = 0.0; + switch (uniforms.grid_mode) { + case 0u, default {} + case 1u { + grid_val = grid_axes(pos); + } + case 2u { + grid_val = grid_ortho(pos); + } + } + let final_col = mix(plot_col, vec3(0.0), grid_val); return vec4f(pow(final_col, vec3(1.68)), 1.0); } diff --git a/libcxgraph/src/renderer/mod.rs b/libcxgraph/src/renderer/mod.rs index 181a1f5..a6b902c 100644 --- a/libcxgraph/src/renderer/mod.rs +++ b/libcxgraph/src/renderer/mod.rs @@ -1,6 +1,6 @@ use std::{num::NonZeroU64, io::Cursor}; -use wgpu::{util::DeviceExt, MemoryHints}; +use wgpu::util::DeviceExt; #[derive(Debug)] #[repr(C)] @@ -9,11 +9,12 @@ pub struct Uniforms { pub resolution: (u32, u32), pub bounds_min: (f32, f32), pub bounds_max: (f32, f32), + pub res_scale: f32, pub shading_intensity: f32, pub contour_intensity: f32, pub decorations: u32, pub coloring: u32, - _padding: [u8; 8], + pub grid_mode: u32, } const UNIFORM_SIZE: usize = std::mem::size_of::(); @@ -29,10 +30,12 @@ impl Uniforms { buf.write_all(&self.bounds_min.1.to_le_bytes())?; buf.write_all(&self.bounds_max.0.to_le_bytes())?; buf.write_all(&self.bounds_max.1.to_le_bytes())?; + buf.write_all(&self.res_scale.to_le_bytes())?; buf.write_all(&self.shading_intensity.to_le_bytes())?; buf.write_all(&self.contour_intensity.to_le_bytes())?; buf.write_all(&self.decorations.to_le_bytes())?; buf.write_all(&self.coloring.to_le_bytes())?; + buf.write_all(&self.grid_mode.to_le_bytes())?; Ok(()) } } @@ -70,11 +73,11 @@ impl<'a> WgpuState<'a> { max_texture_dimension_2d: 8192, ..wgpu::Limits::downlevel_webgl2_defaults() }, - memory_hints: MemoryHints::Performance, + memory_hints: wgpu::MemoryHints::default(), }, None ).await.map_err(|e| e.to_string()).unwrap(); - + let format = surface.get_capabilities(&adapter).formats[0]; let config = wgpu::SurfaceConfiguration { @@ -135,11 +138,12 @@ impl<'a> WgpuState<'a> { resolution: size.into(), bounds_min: (-0.0, -0.0), bounds_max: ( 0.0, 0.0), + res_scale: 1.0, shading_intensity: 0.0, contour_intensity: 0.0, decorations: 0, coloring: 0, - _padding: [0; 8], + grid_mode: 0, }; Self { @@ -239,7 +243,6 @@ impl<'a> WgpuState<'a> { rpass.set_pipeline(pipeline); rpass.set_bind_group(0, &self.uniform_bind_group, &[]); rpass.draw(0..3, 0..1); - rpass.draw(1..4, 0..1); } } let mut cursor = Cursor::new([0; UNIFORM_SIZE]); diff --git a/libcxgraph/src/renderer/vertex.wgsl b/libcxgraph/src/renderer/vertex.wgsl index 8fac842..70a5dda 100644 --- a/libcxgraph/src/renderer/vertex.wgsl +++ b/libcxgraph/src/renderer/vertex.wgsl @@ -1,10 +1,9 @@ @vertex fn main(@builtin(vertex_index) in_vertex_index: u32) -> @builtin(position) vec4f { - var pos = array( + var pos = array( vec2(-1.0,-1.0), - vec2( 1.0,-1.0), - vec2(-1.0, 1.0), - vec2( 1.0, 1.0), + vec2( 3.0,-1.0), + vec2(-1.0, 3.0), ); return vec4f(pos[in_vertex_index], 0.0, 1.0);