feat: make web build work

This commit is contained in:
Tomáš Mládek 2026-01-25 02:13:29 +01:00
parent 96274de3ed
commit 03579ecc25
8 changed files with 184 additions and 36 deletions

View file

@ -35,7 +35,7 @@ env_logger = "0.11.8"
# web: # web:
[target.'cfg(target_arch = "wasm32")'.dependencies] [target.'cfg(target_arch = "wasm32")'.dependencies]
wasm-bindgen-futures = "0.4.50" wasm-bindgen-futures = "0.4.50"
web-sys = "0.3.70" # to access the DOM (to hide the loading text) web-sys = { version = "0.3.70", features = ["Performance", "Response", "Window"] } # to access the DOM (to hide the loading text)
[profile.release] [profile.release]
opt-level = 2 # fast and small wasm opt-level = 2 # fast and small wasm

View file

@ -23,6 +23,7 @@
<link data-trunk rel="copy-file" href="assets/icon-256.png" data-target-path="assets"/> <link data-trunk rel="copy-file" href="assets/icon-256.png" data-target-path="assets"/>
<link data-trunk rel="copy-file" href="assets/icon_ios_touch_192.png" data-target-path="assets"/> <link data-trunk rel="copy-file" href="assets/icon_ios_touch_192.png" data-target-path="assets"/>
<link data-trunk rel="copy-file" href="assets/maskable_icon_x512.png" data-target-path="assets"/> <link data-trunk rel="copy-file" href="assets/maskable_icon_x512.png" data-target-path="assets"/>
<link data-trunk rel="copy-file" href="content/intro.svg" data-target-path="content"/>
<link rel="manifest" href="manifest.json"> <link rel="manifest" href="manifest.json">

View file

@ -1,6 +1,11 @@
use crate::svg::SvgContent; use crate::svg::SvgContent;
use crate::text_cache::TextCache; use crate::text_cache::TextCache;
#[cfg(target_arch = "wasm32")]
use std::cell::RefCell;
#[cfg(target_arch = "wasm32")]
use std::rc::Rc;
mod animation; mod animation;
mod input; mod input;
mod render; mod render;
@ -61,6 +66,10 @@ pub struct LasApp {
/// Text rendering cache for smooth scaling. /// Text rendering cache for smooth scaling.
#[serde(skip)] #[serde(skip)]
text_cache: Option<TextCache>, text_cache: Option<TextCache>,
#[cfg(target_arch = "wasm32")]
#[serde(skip)]
svg_loader: Option<Rc<RefCell<Option<Result<SvgContent, String>>>>>,
} }
impl Default for LasApp { impl Default for LasApp {
@ -81,6 +90,8 @@ impl Default for LasApp {
last_cursor_pos: None, last_cursor_pos: None,
reset_view_requested: false, reset_view_requested: false,
view_animation: None, view_animation: None,
#[cfg(target_arch = "wasm32")]
svg_loader: None,
} }
} }
} }
@ -101,6 +112,8 @@ impl LasApp {
.and_then(|s| eframe::get_value(s, eframe::APP_KEY)) .and_then(|s| eframe::get_value(s, eframe::APP_KEY))
.unwrap_or_default(); .unwrap_or_default();
#[cfg(not(target_arch = "wasm32"))]
{
// Load SVG content // Load SVG content
let svg_path = "../line-and-surface/content/intro.svg"; let svg_path = "../line-and-surface/content/intro.svg";
log::info!("Loading SVG from: {svg_path}"); log::info!("Loading SVG from: {svg_path}");
@ -128,6 +141,32 @@ impl LasApp {
log::error!("Failed to load SVG: {e}"); log::error!("Failed to load SVG: {e}");
} }
} }
}
#[cfg(target_arch = "wasm32")]
{
let svg_path = "./content/intro.svg";
log::info!("Loading SVG from: {svg_path}");
let loader = Rc::new(RefCell::new(None));
let loader_handle = loader.clone();
wasm_bindgen_futures::spawn_local(async move {
let start = wasm_now();
let result = load_svg_from_url(svg_path).await;
if let Ok(ref content) = result {
let elapsed_ms = (wasm_now() - start).max(0.0);
log::info!(
"Loaded SVG in {:.2}ms: {} video scrolls, {} audio areas, {} anchors, {} texts",
elapsed_ms,
content.video_scrolls.len(),
content.audio_areas.len(),
content.anchors.len(),
content.texts.len()
);
}
*loader_handle.borrow_mut() = Some(result);
});
app.svg_loader = Some(loader);
}
log::info!("Application initialized"); log::info!("Application initialized");
app app
@ -139,6 +178,38 @@ impl LasApp {
self.fps_ema = ALPHA * (1.0 / frame_time) + (1.0 - ALPHA) * self.fps_ema; self.fps_ema = ALPHA * (1.0 / frame_time) + (1.0 - ALPHA) * self.fps_ema;
} }
} }
#[cfg(target_arch = "wasm32")]
fn poll_svg_loader(&mut self, ctx: &egui::Context) {
if self.svg_content.is_some() {
return;
}
let Some(loader) = self.svg_loader.as_ref() else {
return;
};
let Some(result) = loader.borrow_mut().take() else {
return;
};
match result {
Ok(content) => {
if let Some((vb_x, vb_y, vb_w, vb_h)) = content.viewbox {
log::debug!("SVG viewbox: ({vb_x}, {vb_y}, {vb_w}, {vb_h})");
self.pan_x = -vb_x;
self.pan_y = -vb_y;
}
self.svg_content = Some(content);
ctx.request_repaint();
}
Err(err) => {
log::error!("Failed to load SVG: {err}");
}
}
self.svg_loader = None;
}
} }
impl eframe::App for LasApp { impl eframe::App for LasApp {
@ -147,6 +218,9 @@ impl eframe::App for LasApp {
} }
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
#[cfg(target_arch = "wasm32")]
self.poll_svg_loader(ctx);
let input = InputSnapshot::collect(ctx); let input = InputSnapshot::collect(ctx);
if input.primary_pressed if input.primary_pressed
@ -184,3 +258,46 @@ impl eframe::App for LasApp {
} }
} }
} }
#[cfg(target_arch = "wasm32")]
async fn load_svg_from_url(url: &str) -> Result<SvgContent, String> {
use wasm_bindgen_futures::JsFuture;
use web_sys::Response;
use web_sys::wasm_bindgen::JsCast;
let window = web_sys::window().ok_or("missing window".to_string())?;
let resp_value = JsFuture::from(window.fetch_with_str(url))
.await
.map_err(|err| format!("fetch failed: {err:?}"))?;
let response: Response = resp_value
.dyn_into()
.map_err(|_| "fetch response cast failed".to_string())?;
if !response.ok() {
return Err(format!(
"fetch failed: {} {}",
response.status(),
response.status_text()
));
}
let text_promise = response
.text()
.map_err(|err| format!("read text failed: {err:?}"))?;
let text_value = JsFuture::from(text_promise)
.await
.map_err(|err| format!("read text failed: {err:?}"))?;
let text = text_value
.as_string()
.ok_or("response text missing".to_string())?;
SvgContent::parse(&text).map_err(|err| format!("parse SVG failed: {err}"))
}
#[cfg(target_arch = "wasm32")]
fn wasm_now() -> f64 {
web_sys::window()
.and_then(|window| window.performance())
.map(|performance| performance.now())
.unwrap_or(0.0)
}

View file

@ -122,11 +122,11 @@ impl LasApp {
self.pan_y = pan_y; self.pan_y = pan_y;
self.zoom = zoom; self.zoom = zoom;
} }
}
}
self.did_fit_start = true; self.did_fit_start = true;
ctx.request_repaint(); ctx.request_repaint();
} }
}
}
if input.space_pressed { if input.space_pressed {
self.reset_view_requested = true; self.reset_view_requested = true;

View file

@ -1,6 +1,7 @@
#![warn(clippy::all, rust_2018_idioms)] #![warn(clippy::all, rust_2018_idioms)]
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
#[cfg(not(target_arch = "wasm32"))]
use log::LevelFilter; use log::LevelFilter;
// When compiling natively: // When compiling natively:

View file

@ -3,6 +3,7 @@ use super::color::parse_background_color;
use super::types::{Anchor, AnchorLink, AudioArea, SvgContent, TextElement, TextLine, VideoScroll}; use super::types::{Anchor, AnchorLink, AudioArea, SvgContent, TextElement, TextLine, VideoScroll};
use quick_xml::Reader; use quick_xml::Reader;
use quick_xml::events::Event; use quick_xml::events::Event;
#[cfg(not(target_arch = "wasm32"))]
use std::fs; use std::fs;
/// State for tracking current element during parsing. /// State for tracking current element during parsing.
@ -79,6 +80,7 @@ impl LinkAccumulator {
impl SvgContent { impl SvgContent {
/// Parse an SVG file and extract special elements. /// Parse an SVG file and extract special elements.
#[cfg(not(target_arch = "wasm32"))]
pub fn from_file(path: &str) -> Result<Self, Box<dyn std::error::Error>> { pub fn from_file(path: &str) -> Result<Self, Box<dyn std::error::Error>> {
log::debug!("Reading SVG file: {path}"); log::debug!("Reading SVG file: {path}");
let content = fs::read_to_string(path)?; let content = fs::read_to_string(path)?;

View file

@ -1,4 +1,4 @@
use super::{Renderable, SvgContent}; use super::{Renderable as _, SvgContent};
#[test] #[test]
fn test_parse_video_scroll() { fn test_parse_video_scroll() {

View file

@ -193,7 +193,10 @@ impl TextCache {
size_key: u32, size_key: u32,
render_scale: f32, render_scale: f32,
) -> CachedText { ) -> CachedText {
#[cfg(not(target_arch = "wasm32"))]
let start_time = std::time::Instant::now(); let start_time = std::time::Instant::now();
#[cfg(target_arch = "wasm32")]
let start_time = wasm_now();
trace!( trace!(
"Rendering text '{}' at size {}px (scale {})...", "Rendering text '{}' at size {}px (scale {})...",
&text[..text.len().min(20)], &text[..text.len().min(20)],
@ -322,6 +325,8 @@ impl TextCache {
TEXTURE_OPTIONS, TEXTURE_OPTIONS,
); );
#[cfg(not(target_arch = "wasm32"))]
{
let duration = start_time.elapsed(); let duration = start_time.elapsed();
trace!( trace!(
"Rendered text '{}' ({}x{} @{}) in {:.2?}", "Rendered text '{}' ({}x{} @{}) in {:.2?}",
@ -331,6 +336,20 @@ impl TextCache {
chosen_scale, chosen_scale,
duration duration
); );
}
#[cfg(target_arch = "wasm32")]
{
let duration_ms = (wasm_now() - start_time).max(0.0);
trace!(
"Rendered text '{}' ({}x{} @{}) in {:.2}ms",
&text[..text.len().min(20)],
img_width,
img_height,
chosen_scale,
duration_ms
);
}
CachedText { CachedText {
texture, texture,
@ -468,6 +487,14 @@ impl TextCache {
} }
} }
#[cfg(target_arch = "wasm32")]
fn wasm_now() -> f64 {
web_sys::window()
.and_then(|window| window.performance())
.map(|performance| performance.now())
.unwrap_or(0.0)
}
fn texture_bytes(cached: &CachedText) -> usize { fn texture_bytes(cached: &CachedText) -> usize {
cached.width as usize * cached.height as usize * 4 cached.width as usize * cached.height as usize * 4
} }