refactored

This commit is contained in:
trimill 2024-09-07 18:59:55 -04:00
parent 8702cddf44
commit e254edabf7
20 changed files with 1158 additions and 772 deletions

1133
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -2,7 +2,6 @@
members = [
"libcxgraph",
"cxgraph-desktop",
"cxgraph-web",
]
resolver = "2"

View file

@ -1,11 +0,0 @@
[package]
name = "cxgraph-desktop"
version = "0.1.0"
edition = "2021"
[dependencies]
libcxgraph = { path = "../libcxgraph" }
log = "0.4"
env_logger = "0.10"
winit = "0.28"
pollster = "0.3"

View file

@ -1,47 +0,0 @@
use std::collections::HashMap;
use libcxgraph::{renderer::WgpuState, language::compile};
use winit::{event_loop::EventLoop, window::Window, event::{Event, WindowEvent}};
fn main() {
env_logger::builder()
.filter_level(log::LevelFilter::Warn)
.init();
let src = "plot(z) = 27^z - 9^z - 3^z";
let wgsl = compile(src, &HashMap::new()).unwrap();
println!("{wgsl}");
let event_loop = EventLoop::new();
let window = Window::new(&event_loop).unwrap();
window.set_title("window");
pollster::block_on(run(event_loop, window, &wgsl));
}
async fn run(event_loop: EventLoop<()>, window: Window, code: &str) {
let size = window.inner_size();
let mut state = WgpuState::new(&window, size.into()).await;
state.load_shaders(code);
state.uniforms.bounds_min = (-5.0, -5.0).into();
state.uniforms.bounds_max = ( 5.0, 5.0).into();
state.uniforms.shading_intensity = 0.3;
event_loop.run(move |event, _, control_flow| {
control_flow.set_wait();
match event {
Event::WindowEvent { event: WindowEvent::CloseRequested, .. }
=> control_flow.set_exit(),
Event::RedrawRequested(_)
=> state.redraw(),
Event::WindowEvent { event: WindowEvent::Resized(size), .. } => {
state.resize(size.into());
window.request_redraw();
}
_ => (),
}
});
}

View file

@ -9,7 +9,7 @@ crate-type = ["cdylib", "rlib"]
[dependencies]
libcxgraph = { path = "../libcxgraph", features = ["webgl"] }
log = "0.4"
winit = "0.28"
winit = "0.29"
console_error_panic_hook = "0.1"
console_log = "1.0"
wasm-bindgen = "0.2"

161
cxgraph-web/editor.js Normal file
View file

@ -0,0 +1,161 @@
//
// Keyboard handling
//
const TAB_WIDTH = 4;
let sourceFocused = false;
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;
}
if (event.key == "Enter" && event.shiftKey) {
event.preventDefault();
button_graph.click();
} else if (event.key == "Escape") {
sourceFocused = false;
} else if (event.key == "Backspace" && !event.ctrlKey) {
let selStart = source_text.selectionStart;
let selEnd = source_text.selectionEnd;
if (selStart == selEnd) {
let pre = source_text.value.slice(0, selStart);
let lineStart = pre.lastIndexOf("\n") + 1;
let preLine = pre.slice(lineStart);
if (preLine.length > 2 && preLine.trim() == "") {
let count = (selStart - lineStart - 1)%TAB_WIDTH + 1;
for (let i = 0; i < count; i++) {
if (pre[pre.length - i - 1] != " ") {
count = i;
}
}
let post = source_text.value.slice(selStart);
source_text.value = pre.slice(0, selStart - count) + post;
source_text.selectionStart = selStart - count;
source_text.selectionEnd = selEnd - count;
event.preventDefault();
}
}
} else if (event.key == "Tab" && sourceFocused) {
event.preventDefault();
let selStart = source_text.selectionStart;
let selEnd = source_text.selectionEnd;
let pre = source_text.value.slice(0, selStart);
let post = source_text.value.slice(selStart);
let lineStart = pre.lastIndexOf("\n") + 1;
if (event.shiftKey) {
let count = (selStart - lineStart - 1)%TAB_WIDTH + 1;
for (let i = 0; i < count; i++) {
if (pre[pre.length - i - 1] != " ") {
count = i;
}
}
if (count > 0) {
source_text.value = pre.slice(0, selStart - count) + post;
source_text.selectionStart = selStart - count;
source_text.selectionEnd = selEnd - count;
}
} else {
let count = TAB_WIDTH - (selStart - lineStart)%TAB_WIDTH;
source_text.value = pre + " ".repeat(count) + post;
source_text.selectionStart = selStart + count;
source_text.selectionEnd = selEnd + count;
}
}
});
//
// Special characters
//
export let charMap = {
"alpha": "\u03b1",
"beta": "\u03b2",
"gamma": "\u03b3",
"delta": "\u03b4",
"epsilon": "\u03b5",
"zeta": "\u03b6",
"eta": "\u03b7",
"theta": "\u03b8",
"iota": "\u03b9",
"kappa": "\u03ba",
"lambda": "\u03bb",
"mu": "\u03bc",
"nu": "\u03bd",
"xi": "\u03be",
"omicron": "\u03bf",
"pi": "\u03c0",
"rho": "\u03c1",
"fsigma": "\u03c2",
"sigma": "\u03c3",
"tau": "\u03c4",
"upsilon": "\u03c5",
"phi": "\u03c6",
"chi": "\u03c7",
"psi": "\u03c8",
"omega": "\u03c9",
"Alpha": "\u0391",
"Beta": "\u0392",
"Gamma": "\u0393",
"Delta": "\u0394",
"Epsilon": "\u0395",
"Zeta": "\u0396",
"Eta": "\u0397",
"Theta": "\u0398",
"Iota": "\u0399",
"Kappa": "\u039a",
"Lambda": "\u039b",
"Mu": "\u039c",
"Nu": "\u039d",
"Xi": "\u039e",
"Omicron": "\u039f",
"Pi": "\u03a0",
"Rho": "\u03a1",
"Sigma": "\u03a3",
"Tau": "\u03a4",
"Upsilon": "\u03a5",
"Phi": "\u03a6",
"Chi": "\u03a7",
"Psi": "\u03a8",
"Omega": "\u03a9",
"vartheta": "\u03d1",
"0": "\u2080",
"1": "\u2081",
"2": "\u2082",
"3": "\u2083",
"4": "\u2084",
"5": "\u2085",
"6": "\u2086",
"7": "\u2087",
"8": "\u2088",
"9": "\u2089",
};
let specialChars = new RegExp(
`\\\\(${Object.keys(charMap).join("|")})`
);
source_text.addEventListener("input", (event) => {
if(event.isComposing) return;
let e = source_text.selectionEnd;
let amnt = 0;
source_text.value = source_text.value.replace(
specialChars,
(m, p) => {
amnt += m.length - charMap[p].length;
return charMap[p];
}
);
source_text.selectionEnd = e - amnt;
});

View file

@ -1,7 +1,16 @@
<!DOCTYPE html>
<html>
<html lang="en">
<head>
<title>cxgraph</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="author" content="trimill">
<meta name="description" content="Plot complex functions">
<meta property="og:description" content="Plot complex functions">
<meta property="og:title" content="CXGraph">
<meta property="og:site_name" content="cx.trimill.xyz">
<title>CXGraph</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
@ -12,8 +21,8 @@
<line id="svg_axis_y" x1="0" y1="0" x2="0" y2="0" stroke="#0006" stroke-width="1.5" visibility="hidden" />
<circle id="svg_unitcircle" cx="0" cy="0" r="0" stroke="#0006" fill="none" stroke-width="1.5" visibility="hidden" />
<g id="svg_point_template" visibility="hidden">
<circle cx="0" cy="0" r="15" stroke="none" fill="#6664" />
<circle cx="0" cy="0" r="5" stroke="none" fill="#6666" />
<circle cx="0" cy="0" r="15" stroke="none" fill="#3337" />
<circle cx="0" cy="0" r="5" stroke="none" fill="#dddc" />
</g>
<g id="overlay_points">
</g>
@ -32,7 +41,7 @@
</div>
<div id="div_error_msg" hidden></div>
<div><textarea id="source_text">f(z) = z^2 + 3i&#10;plot(z) = 5z^2 + f(1/z) - 1</textarea></div>
<div><textarea id="source_text">f(z) = 6z^2 - 2i - 1&#10;plot(z) = f(1 + sin(z)) / 8</textarea></div>
</details>
<div>
@ -52,11 +61,6 @@
<input type="range" id="range_shading" min="0" max="1" step="0.01" value="0.3">
</div>
<div>
<div><label for="range_contour">Contour intensity</label></div>
<input type="range" id="range_contour" min="0" max="1" step="0.01" value="0.0">
</div>
<div>
<div>
<input type="checkbox" class="decor" id="checkbox_decor_1" data-value="1">
@ -77,11 +81,16 @@
<input type="checkbox" class="decor" id="checkbox_decor_8" data-value="8">
<label for="checkbox_decor_8">Magnitude contours</label>
</div>
<div>
<div><label for="range_contour">Contour intensity</label></div>
<input type="range" id="range_contour" min="0" max="1" step="0.01" value="0.0">
</div>
</div>
<div>
<div>Coloring</div>
<input type="radio" name="color_mode" id="radio_color_0" data-value="0" checked>
<input type="radio" name="color_mode" id="radio_color_0" data-value="0">
<label for="radio_color_0">Standard</label><br>
<input type="radio" name="color_mode" id="radio_color_1" data-value="1">
<label for="radio_color_1">Uniform</label><br>
@ -106,7 +115,7 @@
<summary>Variables</summary>
<div id="slider_template" hidden>
<div>
<input type="text" class="var-name" style="width: 5ch;"">
<input type="text" class="var-name" style="width: 5ch;" placeholder="a">
=
<input type="number" class="var-value" style="width: 10ch;" value="0" required>
<input type="button" class="var-delete" value="X">
@ -122,7 +131,7 @@
<hr>
</div>
<div id="point_template" hidden>
<input type="text" class="var-name" style="width: 5ch;">
<input type="text" class="var-name" style="width: 5ch;" placeholder="b">
=
<input type="number" class="var-value-re" style="width: 8ch;" required value="0">
+
@ -140,7 +149,9 @@
</div>
<script>
import('./index.js').then(m => window.module = m)
window.modules = {};
import('./index.js').then(m => window.modules.index = m);
import('./editor.js').then(m => window.modules.editor = m);
</script>
</body>
</html>

View file

@ -12,7 +12,6 @@ let graphView = {
};
let graphPoints = [];
let graphSliders = [];
let mouseX = 0.0;
let mouseY = 0.0;
@ -111,20 +110,20 @@ function onWheel(e) {
onViewChange();
}
function onMouseDown(e) {
function onPointerDown(e) {
mousePressed = true;
mouseX = e.offsetX;
mouseY = e.offsetY;
}
function onMouseUp(e) {
function onPointerUp() {
mousePressed = false;
for(let point of graphPoints) {
point.mousePressed = false;
}
}
function onMouseMove(e) {
function onPointerMove(e) {
if(mousePressed) {
let dX = e.offsetX - mouseX;
let dY = e.offsetY - mouseY;
@ -135,16 +134,16 @@ function onMouseMove(e) {
onViewChange();
} else {
for(let point of graphPoints) {
point.onMouseMove(e);
point.onPointerMove(e);
}
}
}
window.addEventListener("resize", onResize);
canvas.addEventListener("wheel", onWheel);
canvas.addEventListener("mousedown", onMouseDown);
canvas.addEventListener("mouseup", onMouseUp);
canvas.addEventListener("mousemove", onMouseMove);
canvas.addEventListener("pointerdown", onPointerDown);
canvas.addEventListener("pointerup", onPointerUp);
canvas.addEventListener("pointermove", onPointerMove);
//
// Graph/redraw
@ -166,86 +165,6 @@ function onGraph() {
button_graph.addEventListener("click", onGraph);
button_redraw.addEventListener("click", redraw);
let charMap = {
"alpha": "\u03b1",
"beta": "\u03b2",
"gamma": "\u03b3",
"delta": "\u03b4",
"epsilon": "\u03b5",
"zeta": "\u03b6",
"eta": "\u03b7",
"theta": "\u03b8",
"iota": "\u03b9",
"kappa": "\u03ba",
"lambda": "\u03bb",
"mu": "\u03bc",
"nu": "\u03bd",
"xi": "\u03be",
"omicron": "\u03bf",
"pi": "\u03c0",
"rho": "\u03c1",
"fsigma": "\u03c2",
"sigma": "\u03c3",
"tau": "\u03c4",
"upsilon": "\u03c5",
"phi": "\u03c6",
"chi": "\u03c7",
"psi": "\u03c8",
"omega": "\u03c9",
"Alpha": "\u0391",
"Beta": "\u0392",
"Gamma": "\u0393",
"Delta": "\u0394",
"Epsilon": "\u0395",
"Zeta": "\u0396",
"Eta": "\u0397",
"Theta": "\u0398",
"Iota": "\u0399",
"Kappa": "\u039a",
"Lambda": "\u039b",
"Mu": "\u039c",
"Nu": "\u039d",
"Xi": "\u039e",
"Omicron": "\u039f",
"Pi": "\u03a0",
"Rho": "\u03a1",
"Sigma": "\u03a3",
"Tau": "\u03a4",
"Upsilon": "\u03a5",
"Phi": "\u03a6",
"Chi": "\u03a7",
"Psi": "\u03a8",
"Omega": "\u03a9",
"vartheta": "\u03d1",
"0": "\u2080",
"1": "\u2081",
"2": "\u2082",
"3": "\u2083",
"4": "\u2084",
"5": "\u2085",
"6": "\u2086",
"7": "\u2087",
"8": "\u2088",
"9": "\u2089",
};
let specialChars = new RegExp(
`\\\\(${Object.keys(charMap).join("|")})`
);
source_text.addEventListener("input", (event) => {
if(event.isComposing) return;
let e = source_text.selectionEnd;
let amnt = 0;
source_text.value = source_text.value.replace(
specialChars,
(m, p) => {
amnt += m.length - charMap[p].length;
return charMap[p];
}
);
source_text.selectionEnd = e - amnt;
});
//
// Options
//
@ -298,7 +217,8 @@ for(let e of nameColorMode) {
});
e.checked = false;
}
nameColorMode[0].checked = true;
nameColorMode[1].checked = true;
cxgraph.set_coloring(1);
overlay_axes.addEventListener("change", () => {
let vis = overlay_axes.checked ? "visible" : "hidden";
@ -429,24 +349,24 @@ class Point {
tryRedraw();
});
svgPoint.addEventListener("mousedown", (e) => {
svgPoint.addEventListener("pointerdown", (e) => {
this.mousePressed = true;
mouseX = e.offsetX;
mouseY = e.offsetY;
});
svgPoint.addEventListener("mouseup", () => {
svgPoint.addEventListener("pointerup", () => {
this.mousePressed = false;
mousePressed = false;
});
svgPoint.addEventListener("mousemove", (e) => this.onMouseMove(e));
svgPoint.addEventListener("pointermove", (e) => this.onPointerMove(e));
this.onViewChange();
genVarNames();
}
onMouseMove(e) {
onPointerMove(e) {
if(this.mousePressed) {
mouseX = e.offsetX;
mouseY = e.offsetY;
@ -496,3 +416,5 @@ onGraph();
export function show_ast() {
console.info(cxgraph.show_shader_ast(source_text.value));
}
export function get_cxgraph() { return cxgraph; }

View file

@ -20,7 +20,6 @@ where F: Fn(&mut WgpuState) {
#[wasm_bindgen(start)]
pub async fn start() {
use winit::dpi::PhysicalSize;
use winit::platform::web::WindowExtWebSys;
std::panic::set_hook(Box::new(console_error_panic_hook::hook));
console_log::init_with_level(log::Level::Info).expect("Couldn't initialize logger");
@ -34,16 +33,18 @@ pub async fn start() {
.dyn_into()
.expect("Canvas was not a canvas");
let event_loop = EventLoop::new();
let event_loop = EventLoop::new().unwrap();
let window = WindowBuilder::new()
.with_canvas(Some(canvas))
.with_prevent_default(false)
.with_inner_size(PhysicalSize::new(100, 100))
.with_title("window")
.build(&event_loop)
.expect("Failed to build window");
let size = window.inner_size();
let mut state = WgpuState::new(&window, size.into()).await;
let window_ref = Box::leak(Box::new(window));
let mut state = WgpuState::new(window_ref, (100, 100)).await;
state.uniforms.bounds_min = (-5.0, -5.0).into();
state.uniforms.bounds_max = ( 5.0, 5.0).into();
state.uniforms.shading_intensity = 0.3;

View file

@ -26,6 +26,7 @@ canvas, #overlay {
#canvas {
z-index: 0;
-webkit-transform: translate3d(0, 0, 0);
}
#overlay {

View file

@ -1,47 +1,123 @@
# cxgraph language
# CXGraph language
cxgraph uses a custom expression language that is compiled to WGSL.
CXGraph uses a custom expression language that is compiled to WGSL.
## names
## Names
names must begin with any alphabetic character (lowercase or capital letters, Greek letters,
etc.) and may contain alphanumeric chararcters as well as underscores (`_`) and apostrophes (`'`).
the words `sum`, `prod`, and `iter` may not be used for names. names may refer to either
functions or variables.
Names must begin with any alphabetic character (lowercase or capital letters,
Greek letters, etc.) and may contain alphanumeric chararcters as well as
underscores (`_`) and apostrophes (`'`). The words `sum`, `prod`, `iter`,
and `if` may not be used for names. Names may refer to either functions
or variables.
examples of names include:
Examples of names include:
```
a A aaa ω z_3 __5__ f' Кαl'けx焼__검
```
names may either be **built-in**, **global**, or **local**. global or local names may shadow
built-in names, and local names may shadow global ones.
Names may either be **built-in**, **global**, or **local**. global or local names
may shadow built-in names, and local names may shadow global ones.
## declarations
## Declarations
a **function declaration** declares a new function. functions may have zero or more arguments.
A **function declaration** declares a new function. Functions may have zero
or more arguments.
```
f(x) = 3
```
a **constant declaration** declares a new constant.
A **constant declaration** declares a new constant.
```
n = 5
```
declarations are separated by newlines. declarations may only reference functions and constants in
declarations that precede them. the name used in a declaration (`f` and `n` in the above examples)
is in the global scope
Declarations are separated by newlines. Declarations may only reference functions
and constants in declarations that precede them. The name used in a declaration
(`f` and `n` in the above examples) is in the global scope.
## built-ins
The `plot` function is special and serves as the entry point. It must exist and have exactly one argument.
## Operators
Below is a reference to all operators in the CXGraph language.
| operator | description | precedence |
|----------|----------------------------|------------|
| `,` | separate expressions | 0 |
| `->` | assign to | 1 |
| `==` | equal | 2 |
| `!=` | not equal | 2 |
| `>` | real part greater | 3 |
| `<` | real part less | 3 |
| `>=` | real part greater or equal | 3 |
| `<=` | real part less or equal | 3 |
| `+` | addition | 4 |
| `-` | subtraction | 4 |
| `*` | multiplication | 5 |
| `/` | division | 5 |
| `^` | power | 6 |
The comma `,` separates expressions in locations where multiple are allowed (ie.
in a definition or block).
The arrow `->` stores the value of the expression to its left to the local
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.
`+`, `-`, and `*` also function as the unary plus, minus, and conugation operators.
Multiplication can also be done via juxtaposition - `2x(x+1)` is equivalent to `2*x*(x+1)`.
Juxtaposition takes precedence over other operations, so `1/2x` is `1/(2*x)`, not `1/2*x`,
and `z^5(x+1)` is `z^(5*(x+1))`, not `z^5*(x+1)`.
## Grouping
Parentheses `( )` can be used to group within an expression. Braces `{ }` can be used to group multiple expressions, separated by commas. Newlines, which ordinarily separate declarations, are treated as whitespace between grouping symbols, allowing for multiline definitions.
## Repetition and conditionals
To allow for finite sums and products, the `sum` and `prod` expressions can be used.
```
plot(z) = sum(n: 0, 20) { z^n / n }
```
The first parameter to `sum` or `prod` is the name of the summation index, the next two
values are the lower and upper bounds (inclusive). These may be arbitrary expressions,
they are converted to integers by rounding down the real component. The value of the body
for each index value is summed or multiplied and the result is returned.
`iter` works similarly:
```
plot(c) = iter(20, 0 -> z) { z^2 + c }
```
The first parameter is the number of iterations, the second is an assignment to initialize
the iteration variable. For each iteration, the iteration variable will be updated to the
new value of the body.
`if` can be used to choose between two expressions based on a condition:
```
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.
## Built-in functions and constants
arithmetic functions:
| name | description |
|---------------|-------------------------------------------------------|
| `pos(z)` | equivalent to unary `+` |
| `pos(z)` | identity, equivalent to unary `+` |
| `neg(z)` | equivalent to unary `-` |
| `conj(z)` | complex conjugate, equivalent to unary `*` |
| `re(z)` | real part |
@ -58,62 +134,61 @@ arithmetic functions:
power/exponential functions:
| name | description |
|---------------|-------------------------------------------|
| `exp(z)` | exponential function, equivalent to `e^z` |
| `log(z)` | logarithm base `e` |
| `logbr(z,br)` | logarithm base `e` with specified branch |
| `pow(z)` | power, equivalent to `^` |
|----------------|-------------------------------------------|
| `exp(z)` | Exponential function, equivalent to `e^z` |
| `log(z)` | Natural logarithm |
| `logbr(z,br)` | Natural logarithm with specified branch |
| `pow(z)` | Power, equivalent to `^` |
| `powbr(z,br)` | `pow` with specified branch |
| `sqrt(z)` | square root, equivalent to `z^0.5` |
| `sqrtbr(z,br)`| square root with specified branch |
| `sqrtbr(z,br)` | square root with specified branch |
| `cbrt(z)` | cube root, equivalent to `z^0.5` |
| `cbrtbr(z,br)`| cube root with specified branch |
| `cbrtbr(z,br)` | cube root with specified branch |
trigonometric functions:
| name | description |
|------------|-------------------------------------|
| `sin(z)` | sine function |
| `cos(z)` | cosine function |
| `tan(z)` | tangent function |
| `sinh(z)` | hyperbolic sine function |
| `cosh(z)` | hyperbolic cosine function |
| `tanh(z)` | hyperbolic tangent function |
| `asin(z)` | inverse sine function |
| `acos(z)` | inverse cosine function |
| `atan(z)` | inverse tangent function |
| `asinh(z)` | inverse hyperbolic sine function |
| `acosh(z)` | inverse hyperbolic cosine function |
| `atanh(z)` | inverse hyperbolic tangent function |
| `sin(z)` | Sine function |
| `cos(z)` | Cosine function |
| `tan(z)` | Tangent function |
| `sinh(z)` | Hyperbolic sine function |
| `cosh(z)` | Hyperbolic cosine function |
| `tanh(z)` | Hyperbolic tangent function |
| `asin(z)` | Inverse sine function |
| `acos(z)` | Inverse cosine function |
| `atan(z)` | Inverse tangent function |
| `asinh(z)` | Inverse hyperbolic sine function |
| `acosh(z)` | Inverse hyperbolic cosine function |
| `atanh(z)` | Inverse hyperbolic tangent function |
special functions:
| function | description |
|--------------------------|--------------------------------------------------------------------|
|--------------------------|------------------------------------------------------------------------|
| `gamma(z)`, `Γ(z)` | [gamma function](https://en.wikipedia.org/wiki/Gamma_function) |
| `invgamma(z)`, `invΓ(z)` | reciprocal of the gamma function |
| `loggamma(z)`, `logΓ(z)` | logarithm of the gamma function |
| `digamma(z)`, `ψ(z)` | [digamma function](https://en.wikipedia.org/wiki/Digamma_function) |
| `lambertw(z)` | [Lambert W function](https://en.wikipedia.org/wiki/Lambert_W_function) |
| `lambertwbr(z,br)` | Lambert W function on specfied branch |
| `erf(z)` | The [Error function](https://en.wikipedia.org/wiki/Error_function) |
logic functions:
| function | description |
|-----------------|----------------------------------------------------------------------------|
| `signre(z)` | sign of real part (1 if `re(z) > 0`, -1 if `re(z) < 0`, 0 if `re(z) == 0`) |
| `signim(z)` | sign of imaginary part |
| `ifgt(p,q,z,w)` | evaluates to `z` if `re(p) > re(q)`, otherwise `w` |
| `iflt(p,q,z,w)` | evaluates to `z` if `re(p) < re(q)`, otherwise `w` |
| `ifge(p,q,z,w)` | evaluates to `z` if `re(p) ≥ re(q)`, otherwise `w` |
| `ifle(p,q,z,w)` | evaluates to `z` if `re(p) ≤ re(q)`, otherwise `w` |
| `ifeq(p,q,z,w)` | evaluates to `z` if `re(p) = re(q)`, otherwise `w` |
| `ifne(p,q,z,w)` | evaluates to `z` if `re(p) ≠ re(q)`, otherwise `w` |
| `ifnan(p,z,w)` | evaluates to `z` if `p` is `NaN`, otherwise `w` |
|-------------|----------------------------------------------------------------------------|
| `signre(z)` | Sign of real part (1 if `re(z) > 0`, -1 if `re(z) < 0`, 0 if `re(z) == 0`) |
| `signim(z)` | Sign of imaginary part |
| `absre(z)` | Absolute value of real part |
| `absim(z)` | Absolute value of imaginary part |
| `isnan(z)` | 1 if `z` is NaN, 0 otherwise |
constants:
| name | description |
|----------------|--------------------------------------------------------------------------------------------------------|
| `i` | the imaginary constant, equal to `sqrt(-1)` |
| `e` | the [exponential constant](https://en.wikipedia.org/wiki/E_(mathematical_constant)), equal to `exp(1)` |
| `tau`, `τ` | the [circle constant](https://tauday.com/tau-manifesto) |
| `emgamma`, `γ` | the [Euler-Mascheroni](https://en.wikipedia.org/wiki/Euler%27s_constant) constant, equal to `-ψ(1)` |
| `phi`, `φ` | the [golden ratio](https://en.wikipedia.org/wiki/Golden_ratio), equal to `1/2 + sqrt(5)/2` |
| `i` | The imaginary constant, equal to `sqrt(-1)` |
| `e` | The [exponential constant](https://en.wikipedia.org/wiki/E_(mathematical_constant)), equal to `exp(1)` |
| `tau`, `τ` | The [circle constant](https://tauday.com/tau-manifesto) |
| `emgamma`, `γ` | The [Euler-Mascheroni](https://en.wikipedia.org/wiki/Euler%27s_constant) constant, equal to `-ψ(1)` |
| `phi`, `φ` | The [golden ratio](https://en.wikipedia.org/wiki/Golden_ratio), equal to `1/2 + sqrt(5)/2` |
## ebnf grammar
@ -129,7 +204,17 @@ Exprs := (Expr ",")* Expr ","?
Expr := Store
Store := Store "->" NAME | Sum
Store := Equality "->" NAME | Equality
Equality := Compare "==" Compare
| Compare "!=" Compare
| Compare
Compare := Sum ">" Sum
| Sum "<" Sum
| Sum ">=" Sum
| Sum "<=" Sum
| Sum
Sum := Sum "+" Product
| Sum "-" Product
@ -151,15 +236,16 @@ Power := FnCall "^" Unary | FnCall
FnCall := NAME "(" Exprs ")" | Item
PreJuxtapose := Number | "(" <Expr> ")"
PreJuxtapose := NUMBER | "(" <Expr> ")"
Item := Number
Block := "{" Exprs "}"
Item := NUMBER
| NAME
| "(" Expr ")"
| "{" Exprs "}"
| "sum" "(" NAME ":" INT "," INT ")" "{" Exprs "}"
| "prod" "(" NAME ":" INT "," INT ")" "{" Exprs "}"
| "iter" "(" INT "," NAME ":" Expr ")" "{" Exprs "}"
Number = FLOAT | INT
| Block
| "sum" "(" NAME ":" Expr "," Expr ")" Block
| "prod" "(" NAME ":" Expr "," Expr ")" block
| "iter" "(" Expr "," Expr "->" NAME ")" Block
| "if" "(" Expr ")" Block Block
```

View file

@ -0,0 +1,33 @@
# CXGraph Web UI
## Source
Enter the program to plot into the text area. For more information, see [the language docs](language.md).
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.
The Redraw button redraws the screen. If Auto Redraw is enabled, the screen will be redrawn automatically after every change (eg. dragging, zooming, changing options or variables).
## 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.
## Options
Reset View resets the plot's position and scale. Help opens the documentation.
The Resolution slider controls the canvas's resolution scale on a range from x0.25 to x4. This is set to x1 by default. Higher values provide better visuals at the expense of performance.
Shading Intensity controls how intense the black and white shading near zero/infinity is. Setting this to zero disables shading.
Contours can be toggled with the contour checkboxes. Real and imaginary contours show the integer grid, argument contours show angles around the origin divided into 16 segments, and magnitude contours show magnitudes delineated by powers of two.
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.
## Variables
Sliders can be added with the "+slider" button. Once the slider has been named, the variable name can be used in the program and the plot will redraw automatically when the slider's value is changed. The slider's start, step, and end default to 1, 0.01, and -1, respectively. The slider's value can also be edited directly.
Points can be added with the "+point" button. They behave similarly to sliders, but add a draggable point to the plot. The position of the point can also be edited directly.

View file

@ -9,11 +9,11 @@ webgl = ["wgpu/webgl"]
[dependencies]
log = "0.4"
lalrpop-util = { version = "0.20.0", features = ["lexer", "unicode"] }
lalrpop-util = { version = "0.21.0", features = ["lexer", "unicode"] }
num-complex = "0.4"
wgpu = "0.16"
raw-window-handle = "0.5"
wgpu = "22.1"
raw-window-handle = "0.6"
unicode-xid = "0.2"
[build-dependencies]
lalrpop = "0.20.0"
lalrpop = "0.21.0"

View file

@ -23,9 +23,9 @@ pub enum ExpressionType<'a> {
FnCall(&'a str),
Store(&'a str),
If,
Sum { countvar: &'a str, min: i32, max: i32 },
Prod { countvar: &'a str, min: i32, max: i32 },
Iter { itervar: &'a str, count: i32 },
Sum { countvar: &'a str },
Prod { countvar: &'a str },
Iter { itervar: &'a str },
}
#[derive(Clone, Debug)]
@ -70,24 +70,24 @@ impl<'a> Expression<'a> {
}
}
pub fn new_sum(countvar: &'a str, min: i32, max: i32, body: Self) -> Self {
pub fn new_sum(countvar: &'a str, min: Self, max: Self, body: Self) -> Self {
Self {
ty: ExpressionType::Sum { countvar, min, max },
children: vec![body],
ty: ExpressionType::Sum { countvar },
children: vec![min, max, body],
}
}
pub fn new_prod(accvar: &'a str, min: i32, max: i32, body: Self) -> Self {
pub fn new_prod(countvar: &'a str, min: Self, max: Self, body: Self) -> Self {
Self {
ty: ExpressionType::Prod { countvar: accvar, min, max },
children: vec![body],
ty: ExpressionType::Prod { countvar },
children: vec![min, max, body],
}
}
pub fn new_iter(itervar: &'a str, count: i32, init: Self, body: Self) -> Self {
pub fn new_iter(itervar: &'a str, count: Self, init: Self, body: Self) -> Self {
Self {
ty: ExpressionType::Iter { itervar, count },
children: vec![init, body],
ty: ExpressionType::Iter { itervar },
children: vec![count, init, body],
}
}
}
@ -108,9 +108,9 @@ 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::Sum { countvar, min, max } => write!(w, "{:indent$}SUM {countvar} {min} {max}", "", indent=indent)?,
ExpressionType::Prod { countvar, min, max } => write!(w, "{:indent$}PROD {countvar} {min} {max}", "", indent=indent)?,
ExpressionType::Iter { itervar, count } => write!(w, "{:indent$}ITER {itervar} {count}", "", 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)?,
}
writeln!(w)?;
for child in &expr.children {

View file

@ -60,6 +60,7 @@ thread_local! {
m.insert("\u{03C8}", ("c_digamma", 1));
m.insert("lambertw", ("c_lambertw", 1));
m.insert("lambertwbr", ("c_lambertwbr", 2));
m.insert("erf", ("c_erf", 1));
m
};

View file

@ -250,8 +250,15 @@ impl<'w, 'i, W: fmt::Write> Compiler<'w, 'i, W> {
writeln!(self.buf, "}}")?;
Ok(result)
},
ExpressionType::Sum { countvar, min, max }
| ExpressionType::Prod { countvar, min, max } => {
ExpressionType::Sum { countvar }
| ExpressionType::Prod { countvar } => {
let min = local.next_tmp();
let max = local.next_tmp();
let v = self.compile_expr(local, &expr.children[0])?;
writeln!(self.buf, "var {min} = i32(floor({v}.x));")?;
let v = self.compile_expr(local, &expr.children[1])?;
writeln!(self.buf, "var {max} = i32(floor({v}.x));")?;
let acc = local.next_tmp();
let ivar = local.next_tmp();
if matches!(expr.ty, ExpressionType::Sum { .. }) {
@ -259,11 +266,11 @@ impl<'w, 'i, W: fmt::Write> Compiler<'w, 'i, W> {
} else {
writeln!(self.buf, "var {acc} = vec2f(1.0, 0.0);")?;
}
writeln!(self.buf, "for(var {ivar}: i32 = {min}; {ivar} <= {max}; {ivar}++) {{")?;
writeln!(self.buf, "for(var {ivar} = {min}; {ivar} <= {max}; {ivar}++) {{")?;
writeln!(self.buf, "var {} = vec2f(f32({ivar}), 0.0);", format_local(countvar))?;
let mut loop_local = local.clone();
loop_local.local_vars.insert(countvar);
let body = self.compile_expr(&mut loop_local, &expr.children[0])?;
let body = self.compile_expr(&mut loop_local, &expr.children[2])?;
if matches!(expr.ty, ExpressionType::Sum { .. }) {
writeln!(self.buf, "{acc} = {acc} + {body};")?;
} else {
@ -272,16 +279,21 @@ impl<'w, 'i, W: fmt::Write> Compiler<'w, 'i, W> {
writeln!(self.buf, "}}")?;
Ok(acc)
},
ExpressionType::Iter { itervar, count } => {
let init = &expr.children[0];
ExpressionType::Iter { itervar } => {
let countvar = local.next_tmp();
let v = self.compile_expr(local, &expr.children[0])?;
writeln!(self.buf, "var {countvar} = i32(floor({v}.x));")?;
let init = &expr.children[1];
let itervar_fmt = format_local(itervar);
let v = self.compile_expr(local, init)?;
writeln!(self.buf, "var {itervar_fmt} = {v};")?;
let ivar = local.next_tmp();
writeln!(self.buf, "for(var {ivar}: i32 = 0; {ivar} < {count}; {ivar}++) {{")?;
writeln!(self.buf, "for(var {ivar}: i32 = 0; {ivar} < {countvar}; {ivar}++) {{")?;
let mut loop_local = local.clone();
loop_local.local_vars.insert(itervar);
let body = self.compile_expr(&mut loop_local, &expr.children[1])?;
let body = self.compile_expr(&mut loop_local, &expr.children[2])?;
writeln!(self.buf, "{itervar_fmt} = {body};")?;
writeln!(self.buf, "}}")?;
Ok(itervar_fmt)

View file

@ -32,8 +32,7 @@ extern {
"prod" => Token::Prod,
"iter" => Token::Iter,
"if" => Token::If,
Float => Token::Float(<f64>),
Int => Token::Int(<i32>),
Number => Token::Number(<f64>),
Name => Token::Name(<&'input str>),
}
}
@ -122,7 +121,7 @@ FnCall: Expression<'input> = {
}
PreJuxtapose: Expression<'input> = {
Number,
<n:Number> => Expression::new_number(n),
"(" <Expr> ")",
}
@ -131,21 +130,16 @@ Block: Expression<'input> = {
}
Item: Expression<'input> = {
Number,
<n:Number> => Expression::new_number(n),
<n:Name> => Expression::new_name(n),
"(" <Expr> ")",
Block,
"sum" "(" <name:Name> ":" <min:Int> "," <max:Int> ")" <body:Block>
"sum" "(" <name:Name> ":" <min:Expr> "," <max:Expr> ")" <body:Block>
=> Expression::new_sum(name, min, max, body),
"prod" "(" <name:Name> ":" <min:Int> "," <max:Int> ")" <body:Block>
"prod" "(" <name:Name> ":" <min:Expr> "," <max:Expr> ")" <body:Block>
=> Expression::new_prod(name, min, max, body),
"iter" "(" <count:Int> "," <name:Name> ":" <init:Expr> ")" <body:Block>
"iter" "(" <count:Expr> "," <init:Equality> "->" <name:Name> ")" <body:Block>
=> Expression::new_iter(name, count, init, body),
"if" "(" <cond:Expr> ")" <t:Block> <f:Block>
=> Expression::new_if(cond, t, f),
}
Number: Expression<'input> = {
<n:Float> => Expression::new_number(n),
<n:Int> => Expression::new_number(n as f64),
}

View file

@ -5,8 +5,7 @@ use unicode_xid::UnicodeXID;
#[derive(Clone, Copy, Debug)]
pub enum Token<'i> {
Float(f64),
Int(i32),
Number(f64),
Name(&'i str),
Sum, Prod, Iter, If,
LParen, RParen,
@ -21,8 +20,7 @@ pub enum Token<'i> {
impl<'i> fmt::Display for Token<'i> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Token::Float(n) => write!(f, "{n}"),
Token::Int(n) => write!(f, "{n}"),
Token::Number(n) => write!(f, "{n}"),
Token::Name(n) => write!(f, "{n}"),
Token::Sum => f.write_str("sum"),
Token::Prod => f.write_str("prod"),
@ -94,7 +92,7 @@ impl<'i> Lexer<'i> {
}
}
fn next_number(&mut self, i: usize, mut has_dot: bool) -> Spanned<Token<'i>, usize, LexerError> {
fn next_number(&mut self, i: usize, has_dot: bool) -> Spanned<Token<'i>, usize, LexerError> {
let mut j = i;
while self.chars.peek().is_some_and(|(_, c)| c.is_ascii_digit()) {
@ -103,20 +101,14 @@ impl<'i> Lexer<'i> {
if !has_dot && matches!(self.chars.peek(), Some((_, '.'))) {
j = self.chars.next().unwrap().0;
has_dot = true;
while self.chars.peek().is_some_and(|(_, c)| c.is_ascii_digit()) {
j = self.chars.next().unwrap().0;
}
}
let s = &self.src[i..j+1];
if !has_dot {
if let Ok(n) = s.parse::<i32>() {
return Ok((i, Token::Int(n), j+1))
}
}
match s.parse::<f64>() {
Ok(n) => Ok((i, Token::Float(n), j+1)),
Ok(n) => Ok((i, Token::Number(n), j+1)),
Err(_) => Err(LexerError::InvalidNumber(i, j+1)),
}
}

View file

@ -364,6 +364,30 @@ fn c_lambertw_init(z: vec2f, br: f32) -> vec2f {
}
}
fn c_erf(z: vec2f) -> vec2f {
if z.x >= 0 {
return c_erf_plus(z);
} else {
return -c_erf_plus(-z);
}
}
const ERF_P = 0.3275911;
const ERF_A1 = 0.2548295922;
const ERF_A2 = -0.2844967358;
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 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);
}
/////////////////
// rendering //
/////////////////

View file

@ -1,6 +1,6 @@
use std::{num::NonZeroU64, io::Cursor};
use wgpu::util::DeviceExt;
use wgpu::{util::DeviceExt, MemoryHints};
#[derive(Debug)]
#[repr(C)]
@ -37,9 +37,9 @@ impl Uniforms {
}
}
pub struct WgpuState {
pub struct WgpuState<'a> {
pub uniforms: Uniforms,
surface: wgpu::Surface,
surface: wgpu::Surface<'a>,
device: wgpu::Device,
config: wgpu::SurfaceConfiguration,
render_pipeline: Option<wgpu::RenderPipeline>,
@ -49,12 +49,12 @@ pub struct WgpuState {
queue: wgpu::Queue
}
impl WgpuState {
pub async fn new<W>(window: &W, size: (u32, u32)) -> Self
where W: raw_window_handle::HasRawWindowHandle + raw_window_handle::HasRawDisplayHandle {
impl<'a> WgpuState<'a> {
pub async fn new<W>(window: &'a W, size: (u32, u32)) -> Self
where W: raw_window_handle::HasWindowHandle + raw_window_handle::HasDisplayHandle + Sync {
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::default());
let surface = unsafe { instance.create_surface(&window) }.unwrap();
let surface = instance.create_surface(window).unwrap();
let adapter = instance.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::default(),
@ -65,8 +65,12 @@ impl WgpuState {
let (device, queue) = adapter.request_device(
&wgpu::DeviceDescriptor {
label: None,
features: wgpu::Features::empty(),
limits: wgpu::Limits::downlevel_webgl2_defaults(),
required_features: wgpu::Features::empty(),
required_limits: wgpu::Limits {
max_texture_dimension_2d: 8192,
..wgpu::Limits::downlevel_webgl2_defaults()
},
memory_hints: MemoryHints::Performance,
},
None
).await.map_err(|e| e.to_string()).unwrap();
@ -81,6 +85,7 @@ impl WgpuState {
present_mode: wgpu::PresentMode::Fifo,
alpha_mode: wgpu::CompositeAlphaMode::Auto,
view_formats: vec![format],
desired_maximum_frame_latency: 2,
};
surface.configure(&device, &config);
@ -168,12 +173,14 @@ impl WgpuState {
let vertex = wgpu::VertexState {
module: &vertex_module,
entry_point: "main",
compilation_options: Default::default(),
buffers: &[]
};
let fragment = wgpu::FragmentState {
module: &fragment_module,
entry_point: "main",
compilation_options: Default::default(),
targets: &[Some(wgpu::ColorTargetState {
format: self.config.format,
blend: Some(wgpu::BlendState {
@ -201,6 +208,7 @@ impl WgpuState {
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview: None,
cache: None,
});
self.render_pipeline = Some(render_pipeline);
@ -216,7 +224,7 @@ impl WgpuState {
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
store: true,
store: wgpu::StoreOp::Store,
}
};
@ -224,6 +232,8 @@ impl WgpuState {
label: None,
color_attachments: &[Some(color_attachment)],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
});
if let Some(pipeline) = &self.render_pipeline {
rpass.set_pipeline(pipeline);