changed a few things

This commit is contained in:
trimill 2024-09-17 17:45:56 -04:00
parent e254edabf7
commit a4845f2102
30 changed files with 529 additions and 261 deletions

4
Cargo.lock generated
View file

@ -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",

View file

@ -5,3 +5,7 @@ members = [
"cxgraph-web",
]
resolver = "2"
[profile.release]
lto = true
opt-level = 's'

View file

@ -1,6 +1,6 @@
[package]
name = "cxgraph-web"
version = "0.1.0"
version = "0.2.0"
edition = "2021"
[lib]

BIN
cxgraph-web/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View file

@ -10,16 +10,21 @@
<meta property="og:title" content="CXGraph">
<meta property="og:site_name" content="cx.trimill.xyz">
<title>CXGraph</title>
<link rel="stylesheet" href="style.css">
<link rel="icon" type="image/png" sizes="16x16" href="/static/icon/favicon16.png">
<link rel="icon" type="image/png" sizes="32x32" href="/static/icon/favicon32.png">
<link rel="icon" type="image/png" sizes="48x48" href="/static/icon/favicon48.png">
<link rel="icon" type="image/png" sizes="64x64" href="/static/icon/favicon64.png">
<link rel="icon" type="image/png" sizes="160x160" href="/static/icon/favicon160.png">
<link rel="icon" type="image/png" sizes="320x320" href="/static/icon/favicon320.png">
<link rel="icon" type="image/png" sizes="640x640" href="/static/icon/favicon640.png">
<title>cxgraph</title>
<link rel="stylesheet" href="static/style.css">
</head>
<body>
<body id="body" class="theme_dark">
<div class="canvas-container">
<canvas id="canvas"></canvas>
<svg id="overlay">
<line id="svg_axis_x" x1="0" y1="0" x2="0" y2="0" stroke="#0006" stroke-width="1.5" visibility="hidden" />
<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="#3337" />
<circle cx="0" cy="0" r="5" stroke="none" fill="#dddc" />
@ -41,7 +46,12 @@
</div>
<div id="div_error_msg" hidden></div>
<div><textarea id="source_text">f(z) = 6z^2 - 2i - 1&#10;plot(z) = f(1 + sin(z)) / 8</textarea></div>
<div><textarea id="source_text" spellcheck="false" title="Source text"></textarea></div>
<div>
<input type="checkbox" id="checkbox_theme" checked>
<label for="checkbox_theme">Dark theme</label>
</div>
</details>
<div>
@ -51,107 +61,106 @@
<input type="button" id="button_reset_view" value="Reset view">
<input type="button" id="button_help" value="Help" onclick="window.open('https://g.trimill.xyz/trimill/cxgraph')">
</div>
<div>
<div><label for="range_resolution">Resolution</label></div>
<div><fieldset>
<legend><label for="range_resolution">Resolution</label></legend>
<input type="range" id="range_resolution" name="resolution" min="-2" max="2" step="1" value="0">
</div>
</fieldset></div>
<div>
<div><label for="range_shading">Shading intensity</label></div>
<div><fieldset>
<legend><label for="range_shading">Shading intensity</label></legend>
<input type="range" id="range_shading" min="0" max="1" step="0.01" value="0.3">
</div>
</fieldset></div>
<div>
<div><fieldset>
<legend>Contours</legend>
<div>
<input type="range" id="range_contour" min="0" max="1" step="0.01" value="0.0" title="Contour intensity">
</div>
<div>
<input type="checkbox" class="decor" id="checkbox_decor_1" data-value="1">
<label for="checkbox_decor_1">Real contours</label>
<label for="checkbox_decor_1">Real</label>
</div>
<div>
<input type="checkbox" class="decor" id="checkbox_decor_2" data-value="2">
<label for="checkbox_decor_2">Imaginary contours</label>
<label for="checkbox_decor_2">Imaginary</label>
</div>
<div>
<input type="checkbox" class="decor" id="checkbox_decor_4" data-value="4">
<label for="checkbox_decor_4">Argument contours</label>
<label for="checkbox_decor_4">Argument</label>
</div>
<div>
<input type="checkbox" class="decor" id="checkbox_decor_8" data-value="8">
<label for="checkbox_decor_8">Magnitude contours</label>
<label for="checkbox_decor_8">Magnitude</label>
</div>
</fieldset></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>
<div><fieldset>
<legend>Coloring</legend>
<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>
<input type="radio" name="color_mode" id="radio_color_2" data-value="2">
<label for="radio_color_2">None</label>
</div>
</fieldset></div>
<div>
Overlay
<div>
<input type="checkbox" id="overlay_axes">
<label for="overlay_axes">Draw axes</label>
</div>
<div>
<input type="checkbox" id="overlay_unitcircle">
<label for="overlay_unitcircle">Draw unit circle</label>
</div>
</div>
<div><fieldset>
<legend>Grid</legend>
<input type="radio" name="grid_mode" id="radio_grid_0" data-value="0">
<label for="radio_grid_0">None</label><br>
<input type="radio" name="grid_mode" id="radio_grid_1" data-value="1">
<label for="radio_grid_1">Axes</label><br>
<input type="radio" name="grid_mode" id="radio_grid_2" data-value="2">
<label for="radio_grid_2">Cartesian</label>
</fieldset></div>
</details>
<details class="menu" open>
<summary>Variables</summary>
<div id="slider_template" hidden>
<div>
<input type="text" class="var-name" style="width: 5ch;" placeholder="a">
<input type="button" class="var-delete" value="X" title="Delete">
<input type="text" class="var-name" style="width: 5ch;" placeholder="a" title="Variable name">
=
<input type="number" class="var-value" style="width: 10ch;" value="0" required>
<input type="button" class="var-delete" value="X">
<input type="number" class="var-value" style="width: 10ch;" value="0" required title="Value">
</div>
<div>
<input type="range" class="var-slider" min="-1" max="1" step="0.01" value="0">
<input type="range" class="var-slider" min="-1" max="1" step="0.01" value="0" title="Slider">
</div>
<div style="display: flex; flex-direction: row; justify-content: space-between;">
<input type="number" class="var-min" style="width: 6ch;" required value="-1">
<input type="number" class="var-step" style="width: 6ch;" required value="0.01">
<input type="number" class="var-max" style="width: 6ch;" required value="1">
<input type="number" class="var-bounds var-min" style="width: 6ch;" required value="-1" title="Minimum">
<input type="number" class="var-bounds var-max" style="width: 6ch;" required value="1" title="Maximum">
<input type="number" class="var-bounds var-step" style="width: 6ch;" required value="0.01" title="Step">
</div>
<hr>
</div>
<div id="point_template" hidden>
<input type="text" class="var-name" style="width: 5ch;" placeholder="b">
<input type="button" class="var-delete" value="X" title="Delete">
<input type="text" class="var-name" style="width: 5ch;" placeholder="b" title="Variable name">
=
<input type="number" class="var-value-re" style="width: 8ch;" required value="0">
<input type="number" class="var-value-re" style="width: 8ch;" required value="0" title="Real part">
+
<input type="number" class="var-value-im" style="width: 8ch;" required value="0">i
<input type="button" class="var-delete" value="X">
<input type="number" class="var-value-im" style="width: 8ch;" required value="0" title="Imaginary part">i
<hr>
</div>
<div id="div_variables"></div>
<div id="buttons_var_new">
<input type="button" id="button_slider_new" value="+slider">
<input type="button" id="button_point_new" value="+point">
<input type="button" id="button_slider_new" value="+slider" title="Add slider">
<input type="button" id="button_point_new" value="+point" title="Add point">
</div>
</details>
</div>
</div>
<div class="info_overlay">
<span id="mouse_pos"></span>
</div>
<script>
window.modules = {};
import('./index.js').then(m => window.modules.index = m);
import('./editor.js').then(m => window.modules.editor = m);
import('./static/index.js').then(m => window.modules.index = m);
import('./static/editor.js').then(m => window.modules.editor = m);
import('./static/themes.js').then(m => window.modules.themes = m);
</script>
</body>
</html>

View file

@ -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| {

View file

@ -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";
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

View file

@ -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
//

View file

@ -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);
}

View file

@ -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();

View file

@ -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;
}

View file

@ -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 |
|----------------|--------------------------------------------------------------------------------------------------------|

View file

@ -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

View file

@ -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]

View file

@ -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)?,

View file

@ -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
};

View file

@ -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();

View file

@ -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<String, usize>) -> Result<String, Box<d
Ok(wgsl)
}
pub fn tokens(src: &str) -> Result<Vec<(usize, Token, usize)>, LexerError> {
Lexer::new(src).collect()
}
pub fn show_ast(src: &str) -> Result<String, Box<dyn std::error::Error>> {
let lexer = Lexer::new(src);
let result = syntax::ProgramParser::new()

View file

@ -32,6 +32,7 @@ extern {
"prod" => Token::Prod,
"iter" => Token::Iter,
"if" => Token::If,
"while" => Token::While,
Number => Token::Number(<f64>),
Name => Token::Name(<&'input str>),
}
@ -142,4 +143,6 @@ Item: Expression<'input> = {
=> Expression::new_iter(name, count, init, body),
"if" "(" <cond:Expr> ")" <t:Block> <f:Block>
=> Expression::new_if(cond, t, f),
"while" "(" <cond:Expr> ")" <body:Block>
=> Expression::new_while(cond, body),
}

View file

@ -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)),
}
}

View file

@ -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<uniform> 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);
}

View file

@ -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::<Uniforms>();
@ -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]);

View file

@ -1,10 +1,9 @@
@vertex
fn main(@builtin(vertex_index) in_vertex_index: u32) -> @builtin(position) vec4f {
var pos = array<vec2f, 4>(
var pos = array<vec2f, 3>(
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);