Skip to content

Commit

Permalink
webui: Embed visualizer in WebUI
Browse files Browse the repository at this point in the history
  • Loading branch information
mrozycki committed Dec 10, 2023
1 parent be59c8c commit f6d747e
Show file tree
Hide file tree
Showing 11 changed files with 193 additions and 15 deletions.
5 changes: 5 additions & 0 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ jobs:
uses: actions-rs/cargo@v1
with:
command: clippy
- name: Clippy WebUI with visualizer enabled
uses: actions-rs/cargo@v1
with:
command: clippy
args: -p rustmas-webui --features visualizer
- name: Test
uses: actions-rs/cargo@v1
with:
Expand Down
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions visualizer/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ pub fn run(frames_endpoint: Url, points: Vec<(f32, f32, f32)>) {
primary_window: Some(Window {
title: "Rustmas Visualizer".to_string(),
present_mode: PresentMode::AutoNoVsync,
canvas: Some("#visualizer".into()),
fit_canvas_to_parent: true,
..default()
}),
..default()
Expand Down
10 changes: 10 additions & 0 deletions webapi/DEPLOYMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,16 @@ rustup target add wasm32-unknown-unknown
trunk build --release
```

You can also include a visualizer embedded in the UI by using the `visualizer` feature:

```
trunk build --release --features visualizer
```

This will make the compiled WASM file significantly larger and will impact loading time,
so it is turned off by default. The visualizer will also only show up on large displays
(tablets, computer screens), and not on a phone.

Reverse proxy
-------------

Expand Down
6 changes: 6 additions & 0 deletions webapi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,12 @@ trunk serve --features local

The `local` feature will connect WebUI to a locally running WebAPI.

You can also include a visualizer embedded in the UI by using the `visualizer` feature:

```
trunk serve --features local,visualizer
```

Deployment
----------

Expand Down
4 changes: 4 additions & 0 deletions webui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ edition = "2021"
[dependencies]
lightfx = { path = "../lightfx" }
animation-api = { path = "../animation-api" }
rustmas-visualizer = { path = "../visualizer", optional = true }

gloo-net = "0.2"
gloo-utils = "0.1"
Expand All @@ -20,9 +21,12 @@ web-sys = { version = "0.3.60", features = [
"FormData",
"HtmlFormElement",
"HtmlSelectElement",
"Screen",
] }
yew = "0.19"
url = "2.5.0"

[features]
default = []
local = []
visualizer = ["rustmas-visualizer"]
43 changes: 36 additions & 7 deletions webui/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ use animation_api::parameter_schema::ParametersSchema;
use gloo_net::http::Request;
use serde::Deserialize;
use serde_json::json;
use url::Url;

#[derive(Clone, PartialEq)]
pub struct Gateway {
endpoint: String,
endpoint: Url,
}

#[derive(Debug)]
Expand Down Expand Up @@ -41,6 +42,12 @@ fn extract_response<T>(res: ApiResponse<T>) -> Result<T> {
}
}

#[cfg(feature = "visualizer")]
#[derive(Deserialize)]
pub struct GetPointsResponse {
points: Vec<(f32, f32, f32)>,
}

#[derive(Clone, Deserialize)]
pub struct AnimationEntry {
pub id: String,
Expand All @@ -65,14 +72,36 @@ struct GetParamsResponse {
}

impl Gateway {
pub fn new(endpoint: &str) -> Self {
Self {
endpoint: endpoint.to_owned(),
}
pub fn new(endpoint: Url) -> Self {
Self { endpoint }
}

fn url(&self, path: &str) -> String {
format!("{}/{}", self.endpoint, path)
self.endpoint.join(path).unwrap().to_string()
}

#[cfg(feature = "visualizer")]
pub fn frames(&self) -> Url {
let mut endpoint = self.endpoint.clone();
endpoint.set_scheme("ws").unwrap();
endpoint.join("frames").unwrap()
}

#[cfg(feature = "visualizer")]
pub async fn get_points(&self) -> Result<Vec<(f32, f32, f32)>> {
Ok(Request::get(&self.url("points"))
.send()
.await
.map_err(|e| GatewayError::RequestError {
reason: e.to_string(),
})?
.json::<ApiResponse<GetPointsResponse>>()
.await
.map_err(|e| GatewayError::InvalidResponse {
reason: e.to_string(),
})
.and_then(extract_response)?
.points)
}

pub async fn restart_events(&self) -> Result<()> {
Expand Down Expand Up @@ -215,7 +244,7 @@ impl Gateway {
impl Default for Gateway {
fn default() -> Self {
Self {
endpoint: "http://localhost/".to_owned(),
endpoint: Url::parse("http://localhost/").unwrap(),
}
}
}
21 changes: 21 additions & 0 deletions webui/src/dummy.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
use yew::{html, prelude::Html, Component, Context};

#[derive(Default)]
pub struct Dummy {}

impl Component for Dummy {
type Message = ();
type Properties = ();

fn create(_ctx: &Context<Self>) -> Self {
Default::default()
}

fn update(&mut self, _ctx: &Context<Self>, _msg: Self::Message) -> bool {
false
}

fn view(&self, _ctx: &Context<Self>) -> Html {
html!()
}
}
34 changes: 30 additions & 4 deletions webui/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
mod api;
mod controls;

#[cfg(feature = "visualizer")]
mod visualizer;

#[cfg(not(feature = "visualizer"))]
mod dummy;

use std::error::Error;

use api::Gateway;
use log::error;
use url::Url;
use yew::prelude::*;

use crate::controls::ParameterControlList;
#[cfg(not(feature = "visualizer"))]
use crate::dummy::Dummy as Visualizer;
#[cfg(feature = "visualizer")]
use crate::visualizer::Visualizer;

enum Msg {
LoadedAnimations(Vec<api::AnimationEntry>),
Expand All @@ -32,11 +43,14 @@ impl Component for AnimationSelector {
type Properties = ();

fn create(ctx: &Context<Self>) -> Self {
let api = if cfg!(feature = "local") {
api::Gateway::new("http://127.0.0.1:8081")
let api_url = if cfg!(feature = "local") {
Url::parse("http://127.0.0.1:8081").unwrap()
} else if let Some(url) = web_sys::window().and_then(|w| w.location().href().ok()) {
Url::parse(&url).and_then(|u| u.join("api/")).unwrap()
} else {
api::Gateway::new("/api")
Url::parse("http://127.0.0.1:8081").unwrap()
};
let api = api::Gateway::new(api_url);

{
let api = api.clone();
Expand Down Expand Up @@ -140,9 +154,14 @@ impl Component for AnimationSelector {
}
}

#[allow(clippy::let_unit_value)]
fn view(&self, ctx: &Context<Self>) -> Html {
let link = ctx.link();
let animations = self.animations.clone();
let width = web_sys::window()
.and_then(|w| w.screen().ok())
.and_then(|s| s.avail_width().ok())
.unwrap_or_default();
html! {
<ContextProvider<Gateway> context={self.api.clone()}>
<>
Expand All @@ -161,6 +180,13 @@ impl Component for AnimationSelector {
}
</ul>
</nav>
{
if width > 640 {
html!(<Visualizer />)
} else {
html!()
}
}
{if let Some(parameters) = &self.parameters {
html! {
<ParameterControlList
Expand All @@ -186,7 +212,7 @@ impl Component for AnimationSelector {
}

fn main() -> Result<(), Box<dyn Error>> {
wasm_logger::init(wasm_logger::Config::default());
wasm_logger::init(wasm_logger::Config::new(log::Level::Error));
yew::start_app::<AnimationSelector>();

Ok(())
Expand Down
60 changes: 60 additions & 0 deletions webui/src/visualizer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
use log::error;
use yew::{html, prelude::Html, Callback, Component, Context};

use crate::api;

#[derive(Default)]
pub struct Visualizer {}

pub enum Msg {
PointsLoaded(Vec<(f32, f32, f32)>),
}

fn get_api(ctx: &Context<Visualizer>) -> api::Gateway {
ctx.link()
.context::<api::Gateway>(Callback::noop())
.expect("gateway to be created")
.0
}

impl Component for Visualizer {
type Message = Msg;
type Properties = ();

fn create(_ctx: &Context<Self>) -> Self {
Default::default()
}

fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
Msg::PointsLoaded(points) => {
let api = get_api(ctx);
wasm_bindgen_futures::spawn_local(async move {
rustmas_visualizer::run(api.frames(), points);
});
false
}
}
}

fn rendered(&mut self, ctx: &Context<Self>, first_render: bool) {
if first_render {
let api = get_api(ctx);
let points_loaded = ctx.link().callback(Msg::PointsLoaded);
wasm_bindgen_futures::spawn_local(async move {
match api.get_points().await {
Ok(points) => points_loaded.emit(points),
Err(e) => error!("Failed to load points for visualizer, reason: {}", e),
}
})
}
}

fn view(&self, _ctx: &Context<Self>) -> Html {
html! {
<section class="visualizer-container">
<canvas id="visualizer"></canvas>
</section>
}
}
}
21 changes: 17 additions & 4 deletions webui/style_dark.css
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,13 @@ header h1 {
display: flex;
flex-grow: 1;
background-color: black;
max-height: calc(100vh - 3rem);
max-width: 100vw;
}

nav {
width: 20vw;
min-width: 240px;
flex: 0 1 360px;
background-color: black;
position: relative;
}

nav::after {
Expand All @@ -62,7 +62,9 @@ nav::after {
}

nav ul {
height: 100%;
list-style-type: none;
overflow-y: auto;
}

nav a {
Expand All @@ -87,9 +89,20 @@ nav hr {
}

.parameter-control-list {
width: 50vw;
flex: 1 0 400px;
padding: 0 3rem;
margin-bottom: 2rem;
max-height: 100%;
overflow-y: auto;
}

.visualizer-container {
flex: 6 6;
}

#visualizer {
width: 100% !important;
height: 100% !important;
}

.parameter-control-list p {
Expand Down

0 comments on commit f6d747e

Please sign in to comment.