//! Avatar thumbnail component for rendering avatar previews. //! //! A simplified canvas-based component for rendering avatars in the //! avatar store grid. Uses the same layer compositing as the main //! avatar renderer but at thumbnail size. use leptos::prelude::*; use leptos::web_sys; /// Avatar thumbnail component for the avatar store. /// /// Renders a small preview of an avatar using canvas compositing. /// Takes layer paths directly as props. /// /// Props: /// - `skin_layer`: Asset paths for skin layer positions 0-8 /// - `clothes_layer`: Asset paths for clothes layer positions 0-8 /// - `accessories_layer`: Asset paths for accessories layer positions 0-8 /// - `emotion_layer`: Asset paths for emotion layer positions 0-8 /// - `size`: Optional canvas size in pixels (default: 80) #[component] pub fn AvatarThumbnail( #[prop(into)] skin_layer: Signal<[Option; 9]>, #[prop(into)] clothes_layer: Signal<[Option; 9]>, #[prop(into)] accessories_layer: Signal<[Option; 9]>, #[prop(into)] emotion_layer: Signal<[Option; 9]>, #[prop(default = 80)] size: u32, ) -> impl IntoView { let canvas_ref = NodeRef::::new(); let cell_size = size / 3; #[cfg(feature = "hydrate")] { use std::cell::RefCell; use std::collections::HashMap; use std::rc::Rc; use wasm_bindgen::JsCast; use wasm_bindgen::closure::Closure; use crate::utils::normalize_asset_path; // Image cache for this thumbnail let image_cache: Rc>> = Rc::new(RefCell::new(HashMap::new())); // Redraw trigger - incremented when images load let (redraw_trigger, set_redraw_trigger) = signal(0u32); Effect::new(move |_| { // Subscribe to redraw trigger let _ = redraw_trigger.get(); let Some(canvas) = canvas_ref.get() else { return; }; let skin = skin_layer.get(); let clothes = clothes_layer.get(); let accessories = accessories_layer.get(); let emotion = emotion_layer.get(); let canvas_el: &web_sys::HtmlCanvasElement = &canvas; canvas_el.set_width(size); canvas_el.set_height(size); let Ok(Some(ctx)) = canvas_el.get_context("2d") else { return; }; let ctx: web_sys::CanvasRenderingContext2d = ctx.dyn_into().unwrap(); // Clear canvas ctx.clear_rect(0.0, 0.0, size as f64, size as f64); // Draw background ctx.set_fill_style_str("#374151"); ctx.fill_rect(0.0, 0.0, size as f64, size as f64); // Helper to load and draw an image at a grid position let draw_at_position = |path: &str, pos: usize, cache: &Rc>>, ctx: &web_sys::CanvasRenderingContext2d| { let normalized_path = normalize_asset_path(path); let mut cache_borrow = cache.borrow_mut(); let row = pos / 3; let col = pos % 3; let x = (col * cell_size as usize) as f64; let y = (row * cell_size as usize) as f64; let sz = cell_size as f64; if let Some(img) = cache_borrow.get(&normalized_path) { if img.complete() && img.natural_width() > 0 { let _ = ctx.draw_image_with_html_image_element_and_dw_and_dh( img, x, y, sz, sz, ); } } else { let img = web_sys::HtmlImageElement::new().unwrap(); let trigger = set_redraw_trigger; let onload = Closure::once(Box::new(move || { trigger.update(|v| *v += 1); }) as Box); img.set_onload(Some(onload.as_ref().unchecked_ref())); onload.forget(); img.set_src(&normalized_path); cache_borrow.insert(normalized_path, img); } }; // Draw layers in order: skin -> clothes -> accessories -> emotion for (pos, path) in skin.iter().enumerate() { if let Some(p) = path { draw_at_position(p, pos, &image_cache, &ctx); } } for (pos, path) in clothes.iter().enumerate() { if let Some(p) = path { draw_at_position(p, pos, &image_cache, &ctx); } } for (pos, path) in accessories.iter().enumerate() { if let Some(p) = path { draw_at_position(p, pos, &image_cache, &ctx); } } for (pos, path) in emotion.iter().enumerate() { if let Some(p) = path { draw_at_position(p, pos, &image_cache, &ctx); } } }); } view! { } }