fix: eliminate redraws on position changes

This commit is contained in:
Evan Carroll 2026-01-18 00:47:46 -06:00
parent 5fcd49e847
commit b361460485
4 changed files with 235 additions and 176 deletions

View file

@ -737,6 +737,31 @@ pub fn RealmSceneViewer(
// Text size multiplier from settings
let text_em_size = Signal::derive(move || settings.get().text_em_size);
// Create signals for scale/offset values to pass to AvatarCanvas
let scale_x_signal = Signal::derive(move || scale_x.get_value());
let scale_y_signal = Signal::derive(move || scale_y.get_value());
let offset_x_signal = Signal::derive(move || offset_x.get_value());
let offset_y_signal = Signal::derive(move || offset_y.get_value());
// Create a map of members by key for efficient lookup
let members_by_key = Signal::derive(move || {
use std::collections::HashMap;
sorted_members.get()
.into_iter()
.enumerate()
.map(|(idx, m)| (member_key(&m), (idx, m)))
.collect::<HashMap<_, _>>()
});
// Get the list of member keys - use Memo so it only updates when keys actually change
// (not when member data like position changes)
let member_keys = Memo::new(move |_| {
sorted_members.get()
.iter()
.map(member_key)
.collect::<Vec<_>>()
});
let scene_name = scene.name.clone();
view! {
@ -764,76 +789,97 @@ pub fn RealmSceneViewer(
class="avatars-container absolute inset-0"
style="z-index: 2; pointer-events: none;"
>
{move || {
// Wait for scale factors to be calculated before rendering avatars
if !scales_ready.get() {
return Vec::new().into_iter().collect_view();
}
// Wait for scale factors before rendering
<Show
when=move || scales_ready.get()
fallback=|| ()
>
// Use stable keys - each AvatarCanvas gets its own derived signal
{move || {
member_keys.get().into_iter().map(|key| {
// Create a derived signal for this specific member
let member_signal = Signal::derive(move || {
members_by_key.get()
.get(&key)
.map(|(_, m)| m.clone())
.expect("member key should exist")
});
let current_bubbles = active_bubbles.get();
let sx = scale_x.get_value();
let sy = scale_y.get_value();
let ox = offset_x.get_value();
let oy = offset_y.get_value();
let ps = prop_size.get();
let te = text_em_size.get();
// Derive z-index from position in sorted list
let z_index_signal = Signal::derive(move || {
members_by_key.get()
.get(&key)
.map(|(idx, _)| (*idx as i32) + 10)
.unwrap_or(10)
});
// Derive bubble for this member
let bubble_signal = Signal::derive(move || {
active_bubbles.get().get(&key).cloned()
});
let z = z_index_signal.get_untracked();
// Render active members
let mut views: Vec<_> = sorted_members.get()
.into_iter()
.enumerate()
.map(|(idx, member)| {
let key = member_key(&member);
let bubble = current_bubbles.get(&key).cloned();
let z = (idx as i32) + 10;
view! {
<AvatarCanvas
member=member
scale_x=sx
scale_y=sy
offset_x=ox
offset_y=oy
prop_size=ps
member=member_signal
scale_x=scale_x_signal
scale_y=scale_y_signal
offset_x=offset_x_signal
offset_y=offset_y_signal
prop_size=prop_size
z_index=z
active_bubble=bubble
text_em_size=te
active_bubble=bubble_signal
text_em_size=text_em_size
/>
}
})
.collect();
}).collect_view()
}}
// Fading members use closure approach (temporary, per-frame updates)
{move || {
let Some(fading_signal) = fading_members else {
return Vec::new().into_iter().collect_view();
};
// Render fading members with calculated opacity
if let Some(fading_signal) = fading_members {
#[cfg(feature = "hydrate")]
let now = js_sys::Date::now() as i64;
#[cfg(not(feature = "hydrate"))]
let now = 0i64;
for fading in fading_signal.get() {
let elapsed = now - fading.fade_start;
if elapsed < fading.fade_duration {
let opacity = 1.0 - (elapsed as f64 / fading.fade_duration as f64);
let opacity = opacity.max(0.0).min(1.0);
views.push(view! {
<AvatarCanvas
member=fading.member
scale_x=sx
scale_y=sy
offset_x=ox
offset_y=oy
prop_size=ps
z_index=5
active_bubble=None
text_em_size=te
opacity=opacity
/>
});
}
}
}
views.into_iter().collect_view()
}}
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);
let opacity = opacity.max(0.0).min(1.0);
// Fading members get static signals (they're temporary)
let member_signal = Signal::derive({
let m = fading.member.clone();
move || m.clone()
});
let bubble_signal: Signal<Option<ActiveBubble>> = Signal::derive(|| None);
Some(view! {
<AvatarCanvas
member=member_signal
scale_x=scale_x_signal
scale_y=scale_y_signal
offset_x=offset_x_signal
offset_y=offset_y_signal
prop_size=prop_size
z_index=5
active_bubble=bubble_signal
text_em_size=text_em_size
opacity=opacity
/>
})
} else {
None
}
})
.collect_view()
}}
</Show>
</div>
// Click overlay - captures clicks for movement and hit-testing
<div