fix: Move to use HTML for text
Previously we had the bubbles drawn on the avatar canvas. Now it's actually text, so is the label.
This commit is contained in:
parent
23374ee024
commit
66368fe274
6 changed files with 650 additions and 403 deletions
|
|
@ -22,12 +22,14 @@ use uuid::Uuid;
|
|||
|
||||
use chattyness_db::models::{ChannelMemberWithAvatar, LooseProp, Scene};
|
||||
|
||||
use super::avatar_canvas::{AvatarCanvas, member_key};
|
||||
use super::avatar_canvas::{AvatarBoundsStore, AvatarCanvas, ScreenBounds, member_key};
|
||||
#[cfg(feature = "hydrate")]
|
||||
use super::canvas_utils::hit_test_canvas;
|
||||
use super::chat_types::ActiveBubble;
|
||||
use super::context_menu::{ContextMenu, ContextMenuItem};
|
||||
use super::loose_prop_canvas::LoosePropCanvas;
|
||||
use super::speech_bubble::SpeechBubble;
|
||||
use super::username_label::UsernameLabel;
|
||||
use super::settings::{
|
||||
BASE_AVATAR_SCALE, BASE_PROP_SIZE, REFERENCE_HEIGHT, REFERENCE_WIDTH, ViewerSettings,
|
||||
calculate_min_zoom,
|
||||
|
|
@ -460,6 +462,34 @@ pub fn RealmSceneViewer(
|
|||
sorted_members.get().iter().map(member_key).collect::<Vec<_>>()
|
||||
});
|
||||
|
||||
// Shared store for avatar bounds - AvatarCanvas writes, SpeechBubble reads
|
||||
let avatar_bounds_store: AvatarBoundsStore = RwSignal::new(HashMap::new());
|
||||
|
||||
// Clean up bounds store when members change (prevent memory leak)
|
||||
Effect::new(move |_| {
|
||||
let current_member_ids: std::collections::HashSet<_> = members
|
||||
.get()
|
||||
.iter()
|
||||
.map(|m| m.member.user_id)
|
||||
.collect();
|
||||
|
||||
avatar_bounds_store.update(|map| {
|
||||
map.retain(|id, _| current_member_ids.contains(id));
|
||||
});
|
||||
});
|
||||
|
||||
// Scene bounds for clamping bubbles - computed once outside render loop
|
||||
let scene_bounds_signal = Signal::derive(move || {
|
||||
ScreenBounds::from_transform(
|
||||
scene_width_signal.get(),
|
||||
scene_height_signal.get(),
|
||||
scale_x_signal.get(),
|
||||
scale_y_signal.get(),
|
||||
offset_x_signal.get(),
|
||||
offset_y_signal.get(),
|
||||
)
|
||||
});
|
||||
|
||||
let scene_name = scene.name.clone();
|
||||
|
||||
view! {
|
||||
|
|
@ -504,7 +534,6 @@ pub fn RealmSceneViewer(
|
|||
let z_index_signal = Signal::derive(move || {
|
||||
members_by_key.get().get(&key).map(|(idx, _)| (*idx as i32) + 10).unwrap_or(10)
|
||||
});
|
||||
let bubble_signal = Signal::derive(move || active_bubbles.get().get(&key).cloned());
|
||||
let z = z_index_signal.get_untracked();
|
||||
|
||||
view! {
|
||||
|
|
@ -516,10 +545,10 @@ pub fn RealmSceneViewer(
|
|||
offset_y=offset_y_signal
|
||||
prop_size=prop_size
|
||||
z_index=z
|
||||
active_bubble=bubble_signal
|
||||
text_em_size=text_em_size
|
||||
scene_width=scene_width_signal
|
||||
scene_height=scene_height_signal
|
||||
bounds_store=avatar_bounds_store
|
||||
/>
|
||||
}
|
||||
}).collect_view()
|
||||
|
|
@ -538,7 +567,7 @@ pub fn RealmSceneViewer(
|
|||
if elapsed < fading.fade_duration {
|
||||
let opacity = (1.0 - (elapsed as f64 / fading.fade_duration as f64)).max(0.0).min(1.0);
|
||||
let member_signal = Signal::derive({ let m = fading.member.clone(); move || m.clone() });
|
||||
let bubble_signal: Signal<Option<ActiveBubble>> = Signal::derive(|| None);
|
||||
// Note: fading members don't need to update bounds store
|
||||
Some(view! {
|
||||
<AvatarCanvas
|
||||
member=member_signal
|
||||
|
|
@ -548,7 +577,6 @@ pub fn RealmSceneViewer(
|
|||
offset_y=offset_y_signal
|
||||
prop_size=prop_size
|
||||
z_index=5
|
||||
active_bubble=bubble_signal
|
||||
text_em_size=text_em_size
|
||||
opacity=opacity
|
||||
scene_width=scene_width_signal
|
||||
|
|
@ -560,9 +588,96 @@ pub fn RealmSceneViewer(
|
|||
}}
|
||||
</Show>
|
||||
</div>
|
||||
<div class="labels-container absolute inset-0" style="z-index: 3; pointer-events: none; overflow: visible;">
|
||||
<Show when=move || scales_ready.get() fallback=|| ()>
|
||||
// Debug dots at avatar bounds center
|
||||
{move || {
|
||||
members_by_key.get().into_iter().map(|(_, (_, m))| {
|
||||
let user_id = m.member.user_id;
|
||||
let dot_style = Memo::new(move |_| {
|
||||
let bounds = avatar_bounds_store.get();
|
||||
if let Some(ab) = bounds.get(&user_id) {
|
||||
format!(
|
||||
"position: absolute; left: {}px; top: {}px; \
|
||||
width: 5px; height: 5px; background: red; \
|
||||
border-radius: 50%; transform: translate(-50%, -50%); \
|
||||
z-index: 99997;",
|
||||
ab.content_center_x, ab.content_center_y
|
||||
)
|
||||
} else {
|
||||
"display: none;".to_string()
|
||||
}
|
||||
});
|
||||
view! {
|
||||
<div style=dot_style data-debug="bounds-center" data-user-id=user_id.to_string() />
|
||||
}
|
||||
}).collect_view()
|
||||
}}
|
||||
// Active members
|
||||
{move || {
|
||||
members_by_key.get().into_iter().map(|(_, (_, m))| {
|
||||
let user_id = m.member.user_id;
|
||||
let display_name = m.member.display_name.clone();
|
||||
view! {
|
||||
<UsernameLabel
|
||||
user_id=user_id
|
||||
display_name=display_name
|
||||
avatar_bounds_store=avatar_bounds_store
|
||||
text_em_size=text_em_size
|
||||
/>
|
||||
}
|
||||
}).collect_view()
|
||||
}}
|
||||
// Fading members
|
||||
{move || {
|
||||
let Some(fading_signal) = fading_members else {
|
||||
return Vec::new().into_iter().collect_view();
|
||||
};
|
||||
#[cfg(feature = "hydrate")]
|
||||
let now = js_sys::Date::now() as i64;
|
||||
#[cfg(not(feature = "hydrate"))]
|
||||
let now = 0i64;
|
||||
|
||||
fading_signal.get().into_iter().filter_map(|fading| {
|
||||
let elapsed = now - fading.fade_start;
|
||||
if elapsed < fading.fade_duration {
|
||||
let opacity = (1.0 - (elapsed as f64 / fading.fade_duration as f64)).max(0.0).min(1.0);
|
||||
let user_id = fading.member.member.user_id;
|
||||
let display_name = fading.member.member.display_name.clone();
|
||||
Some(view! {
|
||||
<UsernameLabel
|
||||
user_id=user_id
|
||||
display_name=display_name
|
||||
avatar_bounds_store=avatar_bounds_store
|
||||
text_em_size=text_em_size
|
||||
opacity=opacity
|
||||
/>
|
||||
})
|
||||
} else { None }
|
||||
}).collect_view()
|
||||
}}
|
||||
</Show>
|
||||
</div>
|
||||
<div class="bubbles-container absolute inset-0" style="z-index: 4; pointer-events: none; overflow: visible;">
|
||||
<Show when=move || scales_ready.get() fallback=|| ()>
|
||||
{move || {
|
||||
active_bubbles.get().into_iter().map(|(user_id, bubble)| {
|
||||
view! {
|
||||
<SpeechBubble
|
||||
user_id=user_id
|
||||
bubble=bubble
|
||||
avatar_bounds_store=avatar_bounds_store
|
||||
bounds=scene_bounds_signal
|
||||
text_em_size=text_em_size
|
||||
/>
|
||||
}
|
||||
}).collect_view()
|
||||
}}
|
||||
</Show>
|
||||
</div>
|
||||
<div
|
||||
class="click-overlay absolute inset-0"
|
||||
style="z-index: 3; cursor: pointer;"
|
||||
style="z-index: 5; cursor: pointer;"
|
||||
aria-label=format!("Scene: {}", scene_name)
|
||||
role="img"
|
||||
on:click=move |ev| {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue