feat(web): add smooth page transitions and WASM loading states

- Implement navigation state tracking with optimistic UI updates
- Add loading spinner and error handling for WASM initialization
- Insert browser yield points during game initialization to prevent freezing
- Redesign leaderboard with tabbed navigation and mock data structure
- Add utility CSS classes for consistent page layouts
This commit is contained in:
2025-12-29 03:33:43 -06:00
parent d3514b84e9
commit 3bb3908853
22 changed files with 602 additions and 328 deletions
+11
View File
@@ -32,11 +32,16 @@ impl App {
pub fn new() -> GameResult<Self> {
info!("Initializing SDL2 application");
let sdl_context = sdl2::init().map_err(|e| GameError::Sdl(e.to_string()))?;
trace!("Yielding after SDL init");
platform::yield_to_browser();
debug!("Initializing SDL2 subsystems");
let ttf_context = sdl2::ttf::init().map_err(|e| GameError::Sdl(e.to_string()))?;
let video_subsystem = sdl_context.video().map_err(|e| GameError::Sdl(e.to_string()))?;
let audio_subsystem = sdl_context.audio().map_err(|e| GameError::Sdl(e.to_string()))?;
let event_pump = sdl_context.event_pump().map_err(|e| GameError::Sdl(e.to_string()))?;
trace!("Yielding after subsystem init");
platform::yield_to_browser();
trace!(
width = (CANVAS_SIZE.x as f32 * SCALE).round() as u32,
@@ -96,6 +101,8 @@ impl App {
// .index(index)
.build()
.map_err(|e| GameError::Sdl(e.to_string()))?;
trace!("Yielding after canvas creation");
platform::yield_to_browser();
trace!(
logical_width = CANVAS_SIZE.x,
@@ -106,12 +113,16 @@ impl App {
.set_logical_size(CANVAS_SIZE.x, CANVAS_SIZE.y)
.map_err(|e| GameError::Sdl(e.to_string()))?;
debug!(renderer_info = ?canvas.info(), "Canvas renderer initialized");
trace!("Yielding after logical size");
platform::yield_to_browser();
trace!("Creating texture factory");
let texture_creator = canvas.texture_creator();
info!("Starting game initialization");
let game = Game::new(canvas, ttf_context, texture_creator, event_pump)?;
trace!("Yielding after game init");
platform::yield_to_browser();
info!("Application initialization completed successfully");
Ok(App {
+21
View File
@@ -48,6 +48,7 @@ use crate::{
asset::Asset,
events::GameCommand,
map::render::MapRenderer,
platform,
systems::{BatchedLinesResource, Bindings, CursorPosition, TtfAtlasResource},
texture::sprite::{AtlasMapper, SpriteAtlas},
};
@@ -116,18 +117,27 @@ impl Game {
debug!("Setting up textures and fonts");
let (backbuffer, mut map_texture, debug_texture, ttf_atlas) =
Self::setup_textures_and_fonts(&mut canvas, &texture_creator, ttf_context)?;
trace!("Yielding after texture setup");
platform::yield_to_browser();
debug!("Initializing audio subsystem");
let audio = crate::audio::Audio::new();
trace!("Yielding after audio init");
platform::yield_to_browser();
debug!("Loading sprite atlas and map tiles");
let (mut atlas, map_tiles) = Self::load_atlas_and_map_tiles(&texture_creator)?;
trace!("Yielding after atlas load");
platform::yield_to_browser();
debug!("Rendering static map to texture cache");
canvas
.with_texture_canvas(&mut map_texture, |map_canvas| {
MapRenderer::render_map(map_canvas, &mut atlas, &map_tiles);
})
.map_err(|e| GameError::Sdl(e.to_string()))?;
trace!("Yielding after map render");
platform::yield_to_browser();
debug!("Building navigation graph from map layout");
let map = Map::new(constants::RAW_BOARD)?;
@@ -235,30 +245,41 @@ impl Game {
sdl2::render::Texture,
crate::texture::ttf::TtfAtlas,
)> {
trace!("Creating backbuffer texture");
let mut backbuffer = texture_creator
.create_texture_target(None, CANVAS_SIZE.x, CANVAS_SIZE.y)
.map_err(|e| GameError::Sdl(e.to_string()))?;
backbuffer.set_scale_mode(ScaleMode::Nearest);
platform::yield_to_browser();
trace!("Creating map texture");
let mut map_texture = texture_creator
.create_texture_target(None, CANVAS_SIZE.x, CANVAS_SIZE.y)
.map_err(|e| GameError::Sdl(e.to_string()))?;
map_texture.set_scale_mode(ScaleMode::Nearest);
platform::yield_to_browser();
trace!("Creating debug texture");
let output_size = constants::LARGE_CANVAS_SIZE;
let mut debug_texture = texture_creator
.create_texture_target(Some(sdl2::pixels::PixelFormatEnum::ARGB8888), output_size.x, output_size.y)
.map_err(|e| GameError::Sdl(e.to_string()))?;
debug_texture.set_blend_mode(BlendMode::Blend);
debug_texture.set_scale_mode(ScaleMode::Nearest);
platform::yield_to_browser();
trace!("Loading font");
let font_data: &'static [u8] = Asset::Font.get_bytes()?.to_vec().leak();
let font_asset = RWops::from_bytes(font_data).map_err(|_| GameError::Sdl("Failed to load font".to_string()))?;
let debug_font = ttf_context
.load_font_from_rwops(font_asset, constants::ui::DEBUG_FONT_SIZE)
.map_err(|e| GameError::Sdl(e.to_string()))?;
trace!("Creating TTF atlas");
let mut ttf_atlas = crate::texture::ttf::TtfAtlas::new(texture_creator, &debug_font)?;
platform::yield_to_browser();
trace!("Populating TTF atlas");
ttf_atlas.populate_atlas(canvas, texture_creator, &debug_font)?;
Ok((backbuffer, map_texture, debug_texture, ttf_atlas))
+4
View File
@@ -20,6 +20,10 @@ pub fn sleep(duration: Duration, focused: bool) {
}
}
/// No-op on desktop - only needed for browser event loop yielding.
#[inline]
pub fn yield_to_browser() {}
#[allow(unused_variables)]
pub fn init_console(force_console: bool) -> Result<(), PlatformError> {
use crate::formatter::CustomFormatter;
+9
View File
@@ -49,6 +49,15 @@ pub fn sleep(duration: Duration, _focused: bool) {
}
}
/// Yields control to browser event loop without delay.
/// Allows page transitions, animations, and events to process during initialization.
/// Uses ASYNCIFY to pause/resume WASM execution.
pub fn yield_to_browser() {
unsafe {
emscripten_sleep(0);
}
}
pub fn init_console(_force_console: bool) -> Result<(), PlatformError> {
use tracing_subscriber::{fmt, layer::SubscriberExt, EnvFilter};