From 42bdfabd91ad9679a7df469de0d2f60b150be94a Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 29 Jul 2024 05:30:16 -0700 Subject: [PATCH] Rust trial: port interface to Sycamore Now we have a reactive web app written entirely in Rust. The Trunk build tool compiles it to WebAssembly and generates a little JavaScript glue. --- lang-trials/rust/.gitignore | 2 + lang-trials/rust/Cargo.toml | 15 ++- lang-trials/rust/Makefile | 6 - lang-trials/rust/app.civet | 113 ------------------ lang-trials/rust/index.html | 17 +-- lang-trials/rust/{app.css => main.css} | 12 ++ lang-trials/rust/src/lib.rs | 55 --------- lang-trials/rust/src/main.rs | 153 +++++++++++++++++++++++++ lang-trials/rust/src/utils.rs | 10 -- lang-trials/rust/tsconfig.json | 15 --- 10 files changed, 178 insertions(+), 220 deletions(-) create mode 100644 lang-trials/rust/.gitignore delete mode 100644 lang-trials/rust/Makefile delete mode 100644 lang-trials/rust/app.civet rename lang-trials/rust/{app.css => main.css} (80%) delete mode 100644 lang-trials/rust/src/lib.rs create mode 100644 lang-trials/rust/src/main.rs delete mode 100644 lang-trials/rust/src/utils.rs delete mode 100644 lang-trials/rust/tsconfig.json diff --git a/lang-trials/rust/.gitignore b/lang-trials/rust/.gitignore new file mode 100644 index 0000000..1d4e644 --- /dev/null +++ b/lang-trials/rust/.gitignore @@ -0,0 +1,2 @@ +target/* +dist/* \ No newline at end of file diff --git a/lang-trials/rust/Cargo.toml b/lang-trials/rust/Cargo.toml index 01c0b86..278a7c3 100644 --- a/lang-trials/rust/Cargo.toml +++ b/lang-trials/rust/Cargo.toml @@ -1,19 +1,15 @@ [package] -name = "engine" +name = "sycamore-trial" version = "0.1.0" authors = ["Aaron"] edition = "2021" -[lib] -crate-type = ["cdylib", "rlib"] - [features] default = ["console_error_panic_hook"] [dependencies] nalgebra = "0.33.0" -js-sys = "0.3.69" -wasm-bindgen = "0.2.84" +sycamore = "0.9.0-beta.2" # The `console_error_panic_hook` crate provides better debugging of panics by # logging them with `console.error`. This is great for development, but requires @@ -21,6 +17,13 @@ wasm-bindgen = "0.2.84" # code size when deploying. console_error_panic_hook = { version = "0.1.7", optional = true } +[dependencies.web-sys] +version = "0.3.69" +features = [ + 'CanvasRenderingContext2d', + 'HtmlCanvasElement', +] + [dev-dependencies] wasm-bindgen-test = "0.3.34" diff --git a/lang-trials/rust/Makefile b/lang-trials/rust/Makefile deleted file mode 100644 index b94935b..0000000 --- a/lang-trials/rust/Makefile +++ /dev/null @@ -1,6 +0,0 @@ -app.js: app.civet pkg/engine_bg.wasm - civet --typecheck app.civet - civet --js -c app.civet -o .js - -pkg/engine_bg.wasm: src/lib.rs - wasm-pack build --target web diff --git a/lang-trials/rust/app.civet b/lang-trials/rust/app.civet deleted file mode 100644 index ab35044..0000000 --- a/lang-trials/rust/app.civet +++ /dev/null @@ -1,113 +0,0 @@ -// engine functions -{default: init, Circle, circThru} from "./pkg/engine.js" - -// === elements and state === - -// input -dataInputs .= new Array(6) -data .= new Float64Array(6) - -// output and display -let circ: Circle | null -let display: HTMLCanvasElement -let ctx: CanvasRenderingContext2D - -// === display === - -// display style -const rView = 5 -const highlightStyle = "#fcfcfc" -const gridStyle = "#404040" -const dataFillStyles = ["#ba5d09", "#0e8a06", "#8951fb"] -const dataStrokeStyles = ["#f89142", "#58c145", "#c396fc"] - -function render: void - // update resolution - res .= display.width / (2*rView) - - // set transformation - ctx.setTransform 1, 0, 0, -1, 0.5*display.width, 0.5*display.height - - // clear previous frame - ctx.clearRect -0.5*display.width, -0.5*display.height, display.width, display.height - - // draw grid - gridRange .= [Math.ceil(-rView + 0.01) .. Math.floor(rView - 0.01)] - edgeScr .= res*rView - ctx.strokeStyle = gridStyle - for t of gridRange - tScr .= res*t - - // draw horizontal grid line - ctx.beginPath() - ctx.moveTo -edgeScr, tScr - ctx.lineTo edgeScr, tScr - ctx.stroke() - - // draw vertical grid line - ctx.beginPath() - ctx.moveTo tScr, -edgeScr - ctx.lineTo tScr, edgeScr - ctx.stroke() - - // draw circle - if circ - ctx.beginPath() - ctx.strokeStyle = highlightStyle - ctx.arc res*circ.center_x, res*circ.center_y, res*circ.radius, 0, 2*Math.PI - ctx.stroke() - - // draw data points - for n of [0..2] - const ind_x = 2*n - const ind_y = ind_x + 1 - if dataInputs[ind_x].validity.valid and dataInputs[ind_y].validity.valid - ctx.beginPath() - ctx.fillStyle = dataFillStyles[n] - ctx.strokeStyle = dataStrokeStyles[n] - ctx.arc res*data[ind_x], res*data[ind_y], 3, 0, 2*Math.PI - ctx.fill() - ctx.stroke() - -// === interaction === - -// --- inputs --- - -function inputData: void - allDataValid .= true - for n of [0 .. dataInputs.length-1] - if dataInputs[n].validity.valid - data[n] = dataInputs[n].valueAsNumber - else - allDataValid = false - - if allDataValid - try - circ = circThru data - catch ex - circ = null - console.error ex - else - circ = null - - // update the display - requestAnimationFrame(render) - -// --- binding --- - -function bindDoc: Promise - // wait for the engine to load - await init() - - // set up the data inputs - for n of [0..5] - dataInputs[n] = document.querySelector(`#data-input-${n}`) as HTMLInputElement - dataInputs[n].addEventListener "input", inputData - dataInputs[n].style.borderColor = dataFillStyles[Math.floor(0.5*n)] - - // initialize the display - display = document.querySelector("#display") as HTMLCanvasElement - ctx = display.getContext("2d") as CanvasRenderingContext2D - inputData() - -document.addEventListener "DOMContentLoaded", bindDoc diff --git a/lang-trials/rust/index.html b/lang-trials/rust/index.html index 34e6d97..04a39a0 100644 --- a/lang-trials/rust/index.html +++ b/lang-trials/rust/index.html @@ -2,20 +2,7 @@ Lattice circle - - + - - -
-
x
-
y
- - - - - - -
- + diff --git a/lang-trials/rust/app.css b/lang-trials/rust/main.css similarity index 80% rename from lang-trials/rust/app.css rename to lang-trials/rust/main.css index 5486be3..7045d73 100644 --- a/lang-trials/rust/app.css +++ b/lang-trials/rust/main.css @@ -13,6 +13,18 @@ input { border-radius: 4px; } +input.point-1 { + border-color: #ba5d09; +} + +input.point-2 { + border-color: #0e8a06; +} + +input.point-3 { + border-color: #8951fb; +} + #data-panel { float: left; margin-left: 20px; diff --git a/lang-trials/rust/src/lib.rs b/lang-trials/rust/src/lib.rs deleted file mode 100644 index 22c20bc..0000000 --- a/lang-trials/rust/src/lib.rs +++ /dev/null @@ -1,55 +0,0 @@ -mod utils; - -extern crate js_sys; - -use nalgebra::*; -use wasm_bindgen::prelude::*; - -#[wasm_bindgen] -pub struct Circle { - pub center_x: f64, - pub center_y: f64, - pub radius: f64, -} - -// construct the circle through -// -// (x1, y1), (x2, y2), (x3, y3) -// -// from the array -// -// [x1, y1, x2, y2, x3, y3] -// -#[wasm_bindgen] -pub fn circThru(data_raw: js_sys::Float64Array) -> Result { - // represent the given points as the columns of a matrix - let data = Matrix2x3::from_vec(data_raw.to_vec()); - - // build the matrix that maps the circle's coefficient vector to the - // negative of the linear part of the circle's equation, evaluated at the - // given points - let neg_lin_part = stack![2.0*data.transpose(), Vector3::repeat(1.0)]; - - // find the quadrdatic part of the circle's equation, evaluated at the given - // points - let quad_part = Vector3::from_iterator( - data.column_iter().map(|v| v.dot(&v)) - ); - - // find the circle's coefficient vector, and from there its center and - // radius - match neg_lin_part.lu().solve(&quad_part) { - None => Err(JsValue::from("Couldn't solve system")), - Some(coeffs) => { - let center_x = coeffs[0]; - let center_y = coeffs[1]; - Ok(Circle { - center_x: center_x, - center_y: center_y, - radius: ( - coeffs[2] + center_x*center_x + center_y*center_y - ).sqrt(), - }) - } - } -} diff --git a/lang-trials/rust/src/main.rs b/lang-trials/rust/src/main.rs new file mode 100644 index 0000000..f6b97c9 --- /dev/null +++ b/lang-trials/rust/src/main.rs @@ -0,0 +1,153 @@ +use nalgebra::*; +use std::f64::consts::PI as PI; +use sycamore::{prelude::*, rt::{JsCast, JsValue}}; + +// --- interface --- + +fn main() { + // set up a config option that forwards panic messages to `console.error` + #[cfg(feature = "console_error_panic_hook")] + console_error_panic_hook::set_once(); + + sycamore::render(|| { + let data = [-1.0, 0.0, 0.0, -1.0, 1.0, 0.0].map(|n| create_signal(n)); + let display = create_node_ref(); + + on_mount(move || { + let canvas = display + .get::() + .unchecked_into::(); + let ctx = canvas + .get_context("2d") + .unwrap() + .unwrap() + .dyn_into::() + .unwrap(); + + create_effect(move || { + // center and normalize the coordinate system + let width = canvas.width() as f64; + let height = canvas.height() as f64; + ctx.set_transform(1.0, 0.0, 0.0, -1.0, 0.5*width, 0.5*height).unwrap(); + + // clear the previous frame + ctx.clear_rect(-0.5*width, -0.5*width, width, height); + + // find the resolution + const R_DISP: f64 = 5.0; + let res = width / (2.0*R_DISP); + + // set colors + let highlight_style = JsValue::from("white"); + let grid_style = JsValue::from("#404040"); + let point_fill_styles = ["#ba5d09", "#0e8a06", "#8951fb"]; + let point_stroke_styles = ["#f89142", "#58c145", "#c396fc"]; + + // draw the grid + let r_grid = (R_DISP - 0.01).floor() as i32; + let edge_scr = res * R_DISP; + ctx.set_stroke_style(&grid_style); + for t in -r_grid ..= r_grid { + let t_scr = res * (t as f64); + + // draw horizontal grid line + ctx.begin_path(); + ctx.move_to(-edge_scr, t_scr); + ctx.line_to(edge_scr, t_scr); + ctx.stroke(); + + // draw vertical grid line + ctx.begin_path(); + ctx.move_to(t_scr, -edge_scr); + ctx.line_to(t_scr, edge_scr); + ctx.stroke(); + } + + // find and draw the circle through the given points + let data_vals = data.map(|sig| sig.get()).to_vec(); + let points = Matrix2x3::from_vec(data_vals); + if let Some(circ) = circ_thru(points) { + ctx.begin_path(); + ctx.set_stroke_style(&highlight_style); + ctx.arc( + res * circ.center_x, + res * circ.center_y, + res * circ.radius, + 0.0, 2.0*PI + ).unwrap(); + ctx.stroke(); + } + + // draw the data points + for n in 0..3 { + ctx.begin_path(); + ctx.set_fill_style(&JsValue::from(point_fill_styles[n])); + ctx.set_stroke_style(&JsValue::from(point_stroke_styles[n])); + let ind_x = 2*n; + let ind_y = ind_x + 1; + ctx.arc( + res * data[ind_x].get(), + res * data[ind_y].get(), + 3.0, + 0.0, 2.0*PI + ).unwrap(); + ctx.fill(); + ctx.stroke(); + } + }); + }); + + view! { + canvas(ref=display, width="600", height="600") + div(id="data-panel") { + div { "x" } + div { "y" } + input(type="number", class="point-1", bind:valueAsNumber=data[0]) + input(type="number", class="point-1", bind:valueAsNumber=data[1]) + input(type="number", class="point-2", bind:valueAsNumber=data[2]) + input(type="number", class="point-2", bind:valueAsNumber=data[3]) + input(type="number", class="point-3", bind:valueAsNumber=data[4]) + input(type="number", class="point-3", bind:valueAsNumber=data[5]) + } + } + }); +} + +// --- engine --- + +struct Circle { + center_x: f64, + center_y: f64, + radius: f64, +} + +// construct the circle through the points given by the columns of `points` +fn circ_thru(points: Matrix2x3) -> Option { + // build the matrix that maps the circle's coefficient vector to the + // negative of the linear part of the circle's equation, evaluated at the + // given points + let neg_lin_part = stack![2.0*points.transpose(), Vector3::repeat(1.0)]; + + // find the quadrdatic part of the circle's equation, evaluated at the given + // points + let quad_part = Vector3::from_iterator( + points.column_iter().map(|v| v.dot(&v)) + ); + + // find the circle's coefficient vector, and from there its center and + // radius + match neg_lin_part.lu().solve(&quad_part) { + None => None, + Some(coeffs) => { + let center_x = coeffs[0]; + let center_y = coeffs[1]; + Some(Circle { + center_x: center_x, + center_y: center_y, + radius: ( + coeffs[2] + center_x*center_x + center_y*center_y + ).sqrt(), + }) + } + } +} \ No newline at end of file diff --git a/lang-trials/rust/src/utils.rs b/lang-trials/rust/src/utils.rs deleted file mode 100644 index b1d7929..0000000 --- a/lang-trials/rust/src/utils.rs +++ /dev/null @@ -1,10 +0,0 @@ -pub fn set_panic_hook() { - // When the `console_error_panic_hook` feature is enabled, we can call the - // `set_panic_hook` function at least once during initialization, and then - // we will get better error messages if our code ever panics. - // - // For more details see - // https://github.com/rustwasm/console_error_panic_hook#readme - #[cfg(feature = "console_error_panic_hook")] - console_error_panic_hook::set_once(); -} diff --git a/lang-trials/rust/tsconfig.json b/lang-trials/rust/tsconfig.json deleted file mode 100644 index a6c05ac..0000000 --- a/lang-trials/rust/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "compilerOptions": { - "strict": true, - "jsx": "preserve", - "lib": ["es2021", "dom"], - "forceConsistentCasingInFileNames": true, - "esModuleInterop": true - }, - "ts-node": { - "transpileOnly": true, - "compilerOptions": { - "module": "ES2020" - } - } -}