make emotions named instead, add drop prop
This commit is contained in:
parent
989e20757b
commit
ea3b444d71
19 changed files with 1429 additions and 150 deletions
|
|
@ -9,7 +9,7 @@ use std::collections::HashMap;
|
|||
use leptos::prelude::*;
|
||||
use uuid::Uuid;
|
||||
|
||||
use chattyness_db::models::{ChannelMemberWithAvatar, Scene};
|
||||
use chattyness_db::models::{ChannelMemberWithAvatar, LooseProp, Scene};
|
||||
|
||||
use super::chat_types::{emotion_bubble_colors, ActiveBubble};
|
||||
|
||||
|
|
@ -53,9 +53,10 @@ fn parse_bounds_dimensions(bounds_wkt: &str) -> Option<(u32, u32)> {
|
|||
|
||||
/// Scene viewer component for displaying a realm scene with avatars.
|
||||
///
|
||||
/// Uses two layered canvases:
|
||||
/// Uses three layered canvases:
|
||||
/// - Background canvas (z-index 0): Static background, drawn once
|
||||
/// - Avatar canvas (z-index 1): Transparent, redrawn on member updates
|
||||
/// - Props canvas (z-index 1): Loose props, redrawn on drop/pickup
|
||||
/// - Avatar canvas (z-index 2): Transparent, redrawn on member updates
|
||||
#[component]
|
||||
pub fn RealmSceneViewer(
|
||||
scene: Scene,
|
||||
|
|
@ -66,7 +67,11 @@ pub fn RealmSceneViewer(
|
|||
#[prop(into)]
|
||||
active_bubbles: Signal<HashMap<(Option<Uuid>, Option<Uuid>), ActiveBubble>>,
|
||||
#[prop(into)]
|
||||
loose_props: Signal<Vec<LooseProp>>,
|
||||
#[prop(into)]
|
||||
on_move: Callback<(f64, f64)>,
|
||||
#[prop(into)]
|
||||
on_prop_click: Callback<Uuid>,
|
||||
) -> impl IntoView {
|
||||
let dimensions = parse_bounds_dimensions(&scene.bounds_wkt);
|
||||
let (scene_width, scene_height) = dimensions.unwrap_or((800, 600));
|
||||
|
|
@ -81,8 +86,9 @@ pub fn RealmSceneViewer(
|
|||
#[allow(unused_variables)]
|
||||
let image_path = scene.background_image_path.clone().unwrap_or_default();
|
||||
|
||||
// Two separate canvas refs for layered rendering
|
||||
// Three separate canvas refs for layered rendering
|
||||
let bg_canvas_ref = NodeRef::<leptos::html::Canvas>::new();
|
||||
let props_canvas_ref = NodeRef::<leptos::html::Canvas>::new();
|
||||
let avatar_canvas_ref = NodeRef::<leptos::html::Canvas>::new();
|
||||
|
||||
// Store scale factors for coordinate conversion (shared between both canvases)
|
||||
|
|
@ -91,10 +97,11 @@ pub fn RealmSceneViewer(
|
|||
let offset_x = StoredValue::new(0.0_f64);
|
||||
let offset_y = StoredValue::new(0.0_f64);
|
||||
|
||||
// Handle canvas click for movement (on avatar canvas - topmost layer)
|
||||
// Handle canvas click for movement or prop pickup (on avatar canvas - topmost layer)
|
||||
#[cfg(feature = "hydrate")]
|
||||
let on_canvas_click = {
|
||||
let on_move = on_move.clone();
|
||||
let on_prop_click = on_prop_click.clone();
|
||||
move |ev: web_sys::MouseEvent| {
|
||||
let Some(canvas) = avatar_canvas_ref.get() else {
|
||||
return;
|
||||
|
|
@ -118,7 +125,26 @@ pub fn RealmSceneViewer(
|
|||
let scene_x = scene_x.max(0.0).min(scene_width as f64);
|
||||
let scene_y = scene_y.max(0.0).min(scene_height as f64);
|
||||
|
||||
on_move.run((scene_x, scene_y));
|
||||
// Check if click is within 32px of any loose prop
|
||||
let current_props = loose_props.get();
|
||||
let prop_click_radius = 32.0;
|
||||
let mut clicked_prop: Option<Uuid> = None;
|
||||
|
||||
for prop in ¤t_props {
|
||||
let dx = scene_x - prop.position_x;
|
||||
let dy = scene_y - prop.position_y;
|
||||
let distance = (dx * dx + dy * dy).sqrt();
|
||||
if distance <= prop_click_radius {
|
||||
clicked_prop = Some(prop.id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(prop_id) = clicked_prop {
|
||||
on_prop_click.run(prop_id);
|
||||
} else {
|
||||
on_move.run((scene_x, scene_y));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -157,10 +183,12 @@ pub fn RealmSceneViewer(
|
|||
let image_path = image_path_clone.clone();
|
||||
let bg_drawn_inner = bg_drawn.clone();
|
||||
|
||||
// Use setTimeout to ensure DOM is ready before drawing
|
||||
let draw_bg = Closure::once(Box::new(move || {
|
||||
let display_width = canvas_el.client_width() as u32;
|
||||
let display_height = canvas_el.client_height() as u32;
|
||||
|
||||
// If still no dimensions, the canvas likely isn't visible - skip drawing
|
||||
if display_width == 0 || display_height == 0 {
|
||||
return;
|
||||
}
|
||||
|
|
@ -226,8 +254,12 @@ pub fn RealmSceneViewer(
|
|||
}
|
||||
}) as Box<dyn FnOnce()>);
|
||||
|
||||
// Use setTimeout with small delay to ensure canvas is in DOM and has dimensions
|
||||
let window = web_sys::window().unwrap();
|
||||
let _ = window.request_animation_frame(draw_bg.as_ref().unchecked_ref());
|
||||
let _ = window.set_timeout_with_callback_and_timeout_and_arguments_0(
|
||||
draw_bg.as_ref().unchecked_ref(),
|
||||
100, // 100ms delay to allow DOM to settle
|
||||
);
|
||||
draw_bg.forget();
|
||||
});
|
||||
|
||||
|
|
@ -295,6 +327,57 @@ pub fn RealmSceneViewer(
|
|||
let _ = window.request_animation_frame(draw_avatars_closure.as_ref().unchecked_ref());
|
||||
draw_avatars_closure.forget();
|
||||
});
|
||||
|
||||
// =========================================================
|
||||
// Props Effect - runs when loose_props changes
|
||||
// =========================================================
|
||||
Effect::new(move |_| {
|
||||
// Track loose_props signal
|
||||
let current_props = loose_props.get();
|
||||
|
||||
let Some(canvas) = props_canvas_ref.get() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let canvas_el: &web_sys::HtmlCanvasElement = &canvas;
|
||||
let canvas_el = canvas_el.clone();
|
||||
|
||||
let draw_props_closure = Closure::once(Box::new(move || {
|
||||
let display_width = canvas_el.client_width() as u32;
|
||||
let display_height = canvas_el.client_height() as u32;
|
||||
|
||||
if display_width == 0 || display_height == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
// Resize props canvas to match (if needed)
|
||||
if canvas_el.width() != display_width || canvas_el.height() != display_height {
|
||||
canvas_el.set_width(display_width);
|
||||
canvas_el.set_height(display_height);
|
||||
}
|
||||
|
||||
if let Ok(Some(ctx)) = canvas_el.get_context("2d") {
|
||||
let ctx: web_sys::CanvasRenderingContext2d =
|
||||
ctx.dyn_into::<web_sys::CanvasRenderingContext2d>().unwrap();
|
||||
|
||||
// Clear with transparency
|
||||
ctx.clear_rect(0.0, 0.0, display_width as f64, display_height as f64);
|
||||
|
||||
// Get stored scale factors
|
||||
let sx = scale_x.get_value();
|
||||
let sy = scale_y.get_value();
|
||||
let ox = offset_x.get_value();
|
||||
let oy = offset_y.get_value();
|
||||
|
||||
// Draw loose props
|
||||
draw_loose_props(&ctx, ¤t_props, sx, sy, ox, oy);
|
||||
}
|
||||
}) as Box<dyn FnOnce()>);
|
||||
|
||||
let window = web_sys::window().unwrap();
|
||||
let _ = window.request_animation_frame(draw_props_closure.as_ref().unchecked_ref());
|
||||
draw_props_closure.forget();
|
||||
});
|
||||
}
|
||||
|
||||
let aspect_ratio = scene_width as f64 / scene_height as f64;
|
||||
|
|
@ -315,11 +398,18 @@ pub fn RealmSceneViewer(
|
|||
style="z-index: 0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
// Props layer - loose props, redrawn on drop/pickup
|
||||
<canvas
|
||||
node_ref=props_canvas_ref
|
||||
class="absolute inset-0 w-full h-full"
|
||||
style="z-index: 1"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
// Avatar layer - dynamic, transparent background
|
||||
<canvas
|
||||
node_ref=avatar_canvas_ref
|
||||
class="absolute inset-0 w-full h-full"
|
||||
style="z-index: 1"
|
||||
style="z-index: 2"
|
||||
aria-label=format!("Scene: {}", scene.name)
|
||||
role="img"
|
||||
on:click=move |ev| {
|
||||
|
|
@ -477,7 +567,7 @@ fn draw_speech_bubbles(
|
|||
|
||||
// Get emotion colors
|
||||
let (bg_color, border_color, text_color) =
|
||||
emotion_bubble_colors(bubble.message.emotion);
|
||||
emotion_bubble_colors(&bubble.message.emotion);
|
||||
|
||||
// Measure and wrap text
|
||||
ctx.set_font(&format!("{}px sans-serif", font_size));
|
||||
|
|
@ -603,3 +693,57 @@ fn draw_rounded_rect(
|
|||
ctx.arc_to(x, y, x + radius, y, radius).ok();
|
||||
ctx.close_path();
|
||||
}
|
||||
|
||||
/// Draw loose props on the props canvas layer.
|
||||
#[cfg(feature = "hydrate")]
|
||||
fn draw_loose_props(
|
||||
ctx: &web_sys::CanvasRenderingContext2d,
|
||||
props: &[LooseProp],
|
||||
scale_x: f64,
|
||||
scale_y: f64,
|
||||
offset_x: f64,
|
||||
offset_y: f64,
|
||||
) {
|
||||
let prop_size = 48.0 * scale_x.min(scale_y);
|
||||
|
||||
for prop in props {
|
||||
let x = prop.position_x * scale_x + offset_x;
|
||||
let y = prop.position_y * scale_y + offset_y;
|
||||
|
||||
// Draw prop sprite if asset path available
|
||||
if !prop.prop_asset_path.is_empty() {
|
||||
let img = web_sys::HtmlImageElement::new().unwrap();
|
||||
let img_clone = img.clone();
|
||||
let ctx_clone = ctx.clone();
|
||||
let draw_x = x - prop_size / 2.0;
|
||||
let draw_y = y - prop_size / 2.0;
|
||||
let size = prop_size;
|
||||
|
||||
let onload = wasm_bindgen::closure::Closure::once(Box::new(move || {
|
||||
let _ = ctx_clone.draw_image_with_html_image_element_and_dw_and_dh(
|
||||
&img_clone, draw_x, draw_y, size, size,
|
||||
);
|
||||
}) as Box<dyn FnOnce()>);
|
||||
|
||||
img.set_onload(Some(onload.as_ref().unchecked_ref()));
|
||||
onload.forget();
|
||||
img.set_src(&normalize_asset_path(&prop.prop_asset_path));
|
||||
} else {
|
||||
// Fallback: draw a placeholder circle with prop name
|
||||
ctx.begin_path();
|
||||
let _ = ctx.arc(x, y, prop_size / 2.0, 0.0, std::f64::consts::PI * 2.0);
|
||||
ctx.set_fill_style_str("#f59e0b"); // Amber color
|
||||
ctx.fill();
|
||||
ctx.set_stroke_style_str("#d97706");
|
||||
ctx.set_line_width(2.0);
|
||||
ctx.stroke();
|
||||
|
||||
// Draw prop name below
|
||||
ctx.set_fill_style_str("#fff");
|
||||
ctx.set_font(&format!("{}px sans-serif", 10.0 * scale_x.min(scale_y)));
|
||||
ctx.set_text_align("center");
|
||||
ctx.set_text_baseline("top");
|
||||
let _ = ctx.fill_text(&prop.prop_name, x, y + prop_size / 2.0 + 2.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue