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.
This commit is contained in:
parent
12abef4076
commit
42bdfabd91
2
lang-trials/rust/.gitignore
vendored
Normal file
2
lang-trials/rust/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
target/*
|
||||||
|
dist/*
|
@ -1,19 +1,15 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "engine"
|
name = "sycamore-trial"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
authors = ["Aaron"]
|
authors = ["Aaron"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[lib]
|
|
||||||
crate-type = ["cdylib", "rlib"]
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["console_error_panic_hook"]
|
default = ["console_error_panic_hook"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
nalgebra = "0.33.0"
|
nalgebra = "0.33.0"
|
||||||
js-sys = "0.3.69"
|
sycamore = "0.9.0-beta.2"
|
||||||
wasm-bindgen = "0.2.84"
|
|
||||||
|
|
||||||
# The `console_error_panic_hook` crate provides better debugging of panics by
|
# The `console_error_panic_hook` crate provides better debugging of panics by
|
||||||
# logging them with `console.error`. This is great for development, but requires
|
# 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.
|
# code size when deploying.
|
||||||
console_error_panic_hook = { version = "0.1.7", optional = true }
|
console_error_panic_hook = { version = "0.1.7", optional = true }
|
||||||
|
|
||||||
|
[dependencies.web-sys]
|
||||||
|
version = "0.3.69"
|
||||||
|
features = [
|
||||||
|
'CanvasRenderingContext2d',
|
||||||
|
'HtmlCanvasElement',
|
||||||
|
]
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
wasm-bindgen-test = "0.3.34"
|
wasm-bindgen-test = "0.3.34"
|
||||||
|
|
||||||
|
@ -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
|
|
@ -1,113 +0,0 @@
|
|||||||
// engine functions
|
|
||||||
{default: init, Circle, circThru} from "./pkg/engine.js"
|
|
||||||
|
|
||||||
// === elements and state ===
|
|
||||||
|
|
||||||
// input
|
|
||||||
dataInputs .= new Array<HTMLInputElement>(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<void>
|
|
||||||
// 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
|
|
@ -2,20 +2,7 @@
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>Lattice circle</title>
|
<title>Lattice circle</title>
|
||||||
<script type="module" src="app.js"></script>
|
<link data-trunk rel="css" href="main.css"/>
|
||||||
<link rel="stylesheet" type="text/css" href="app.css"/>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body></body>
|
||||||
<canvas id="display" width=600, height=600></canvas>
|
|
||||||
<div id="data-panel">
|
|
||||||
<div>x</div>
|
|
||||||
<div>y</div>
|
|
||||||
<input type="number" id="data-input-0" value="-1"/>
|
|
||||||
<input type="number" id="data-input-1" value="0"/>
|
|
||||||
<input type="number" id="data-input-2" value="0"/>
|
|
||||||
<input type="number" id="data-input-3" value="-1"/>
|
|
||||||
<input type="number" id="data-input-4" value="1"/>
|
|
||||||
<input type="number" id="data-input-5" value="0"/>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
</html>
|
||||||
|
@ -13,6 +13,18 @@ input {
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input.point-1 {
|
||||||
|
border-color: #ba5d09;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.point-2 {
|
||||||
|
border-color: #0e8a06;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.point-3 {
|
||||||
|
border-color: #8951fb;
|
||||||
|
}
|
||||||
|
|
||||||
#data-panel {
|
#data-panel {
|
||||||
float: left;
|
float: left;
|
||||||
margin-left: 20px;
|
margin-left: 20px;
|
@ -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<Circle, JsValue> {
|
|
||||||
// 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(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
153
lang-trials/rust/src/main.rs
Normal file
153
lang-trials/rust/src/main.rs
Normal file
@ -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::<DomNode>()
|
||||||
|
.unchecked_into::<web_sys::HtmlCanvasElement>();
|
||||||
|
let ctx = canvas
|
||||||
|
.get_context("2d")
|
||||||
|
.unwrap()
|
||||||
|
.unwrap()
|
||||||
|
.dyn_into::<web_sys::CanvasRenderingContext2d>()
|
||||||
|
.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<f64>) -> Option<Circle> {
|
||||||
|
// 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(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
|
||||||
}
|
|
@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"strict": true,
|
|
||||||
"jsx": "preserve",
|
|
||||||
"lib": ["es2021", "dom"],
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"esModuleInterop": true
|
|
||||||
},
|
|
||||||
"ts-node": {
|
|
||||||
"transpileOnly": true,
|
|
||||||
"compilerOptions": {
|
|
||||||
"module": "ES2020"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user