fix: avatar center to cursor, and cleanup.
Lots of cleanup, went in with this too
This commit is contained in:
parent
c3320ddcce
commit
fe65835f4a
14 changed files with 769 additions and 708 deletions
|
|
@ -14,6 +14,75 @@ use super::chat_types::{emotion_bubble_colors, ActiveBubble};
|
|||
/// Base text size multiplier. Text at 100% slider = base_sizes * 1.4
|
||||
const BASE_TEXT_SCALE: f64 = 1.4;
|
||||
|
||||
/// Content bounds for a 3x3 avatar grid.
|
||||
/// Tracks which rows/columns contain actual content for centering calculations.
|
||||
struct ContentBounds {
|
||||
min_col: usize,
|
||||
max_col: usize,
|
||||
min_row: usize,
|
||||
max_row: usize,
|
||||
}
|
||||
|
||||
impl ContentBounds {
|
||||
/// Calculate content bounds from 4 layers (9 positions each).
|
||||
fn from_layers(
|
||||
skin: &[Option<String>; 9],
|
||||
clothes: &[Option<String>; 9],
|
||||
accessories: &[Option<String>; 9],
|
||||
emotion: &[Option<String>; 9],
|
||||
) -> Self {
|
||||
let has_content_at = |pos: usize| -> bool {
|
||||
skin[pos].is_some()
|
||||
|| clothes[pos].is_some()
|
||||
|| accessories[pos].is_some()
|
||||
|| emotion[pos].is_some()
|
||||
};
|
||||
|
||||
// Columns: 0 (left), 1 (middle), 2 (right)
|
||||
let left_col = [0, 3, 6].iter().any(|&p| has_content_at(p));
|
||||
let mid_col = [1, 4, 7].iter().any(|&p| has_content_at(p));
|
||||
let right_col = [2, 5, 8].iter().any(|&p| has_content_at(p));
|
||||
|
||||
let min_col = if left_col { 0 } else if mid_col { 1 } else { 2 };
|
||||
let max_col = if right_col { 2 } else if mid_col { 1 } else { 0 };
|
||||
|
||||
// Rows: 0 (top), 1 (middle), 2 (bottom)
|
||||
let top_row = [0, 1, 2].iter().any(|&p| has_content_at(p));
|
||||
let mid_row = [3, 4, 5].iter().any(|&p| has_content_at(p));
|
||||
let bot_row = [6, 7, 8].iter().any(|&p| has_content_at(p));
|
||||
|
||||
let min_row = if top_row { 0 } else if mid_row { 1 } else { 2 };
|
||||
let max_row = if bot_row { 2 } else if mid_row { 1 } else { 0 };
|
||||
|
||||
Self { min_col, max_col, min_row, max_row }
|
||||
}
|
||||
|
||||
/// Content center column (0.0 to 2.0, grid center is 1.0).
|
||||
fn center_col(&self) -> f64 {
|
||||
(self.min_col + self.max_col) as f64 / 2.0
|
||||
}
|
||||
|
||||
/// Content center row (0.0 to 2.0, grid center is 1.0).
|
||||
fn center_row(&self) -> f64 {
|
||||
(self.min_row + self.max_row) as f64 / 2.0
|
||||
}
|
||||
|
||||
/// X offset from grid center to content center.
|
||||
fn x_offset(&self, cell_size: f64) -> f64 {
|
||||
(self.center_col() - 1.0) * cell_size
|
||||
}
|
||||
|
||||
/// Y offset from grid center to content center.
|
||||
fn y_offset(&self, cell_size: f64) -> f64 {
|
||||
(self.center_row() - 1.0) * cell_size
|
||||
}
|
||||
|
||||
/// Number of empty rows at the bottom (for name positioning).
|
||||
fn empty_bottom_rows(&self) -> usize {
|
||||
2 - self.max_row
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a unique key for a member (for Leptos For keying).
|
||||
pub fn member_key(m: &ChannelMemberWithAvatar) -> (Option<Uuid>, Option<Uuid>) {
|
||||
(m.member.user_id, m.member.guest_session_id)
|
||||
|
|
@ -57,39 +126,27 @@ pub fn AvatarCanvas(
|
|||
let display_name = member.member.display_name.clone();
|
||||
let current_emotion = member.member.current_emotion;
|
||||
|
||||
// Helper to check if any layer has content at a position
|
||||
let has_content_at = |pos: usize| -> bool {
|
||||
skin_layer[pos].is_some()
|
||||
|| clothes_layer[pos].is_some()
|
||||
|| accessories_layer[pos].is_some()
|
||||
|| emotion_layer[pos].is_some()
|
||||
};
|
||||
// Calculate content bounds for centering on actual content
|
||||
let content_bounds = ContentBounds::from_layers(
|
||||
&skin_layer,
|
||||
&clothes_layer,
|
||||
&accessories_layer,
|
||||
&emotion_layer,
|
||||
);
|
||||
|
||||
// Calculate content bounds for positioning
|
||||
// X-axis: which columns have content
|
||||
let left_col_has_content = [0, 3, 6].iter().any(|&p| has_content_at(p));
|
||||
let middle_col_has_content = [1, 4, 7].iter().any(|&p| has_content_at(p));
|
||||
let right_col_has_content = [2, 5, 8].iter().any(|&p| has_content_at(p));
|
||||
|
||||
let min_col = if left_col_has_content { 0 } else if middle_col_has_content { 1 } else { 2 };
|
||||
let max_col = if right_col_has_content { 2 } else if middle_col_has_content { 1 } else { 0 };
|
||||
let content_center_col = (min_col + max_col) as f64 / 2.0;
|
||||
let x_content_offset = (content_center_col - 1.0) * prop_size;
|
||||
|
||||
// Y-axis: which rows have content
|
||||
let bottom_row_has_content = [6, 7, 8].iter().any(|&p| has_content_at(p));
|
||||
let middle_row_has_content = [3, 4, 5].iter().any(|&p| has_content_at(p));
|
||||
|
||||
let max_row = if bottom_row_has_content { 2 } else if middle_row_has_content { 1 } else { 0 };
|
||||
let empty_bottom_rows = 2 - max_row;
|
||||
let y_content_offset = empty_bottom_rows as f64 * prop_size;
|
||||
// Get offsets from grid center to content center
|
||||
let x_content_offset = content_bounds.x_offset(prop_size);
|
||||
let y_content_offset = content_bounds.y_offset(prop_size);
|
||||
let empty_bottom_rows = content_bounds.empty_bottom_rows();
|
||||
|
||||
// Avatar is a 3x3 grid of props, each prop is prop_size
|
||||
let avatar_size = prop_size * 3.0;
|
||||
|
||||
// Calculate canvas position from scene coordinates, adjusted for content bounds
|
||||
let canvas_x = member.member.position_x * scale_x + offset_x - avatar_size / 2.0 - x_content_offset * scale_x;
|
||||
let canvas_y = member.member.position_y * scale_y + offset_y - avatar_size + y_content_offset * scale_y;
|
||||
// Both X and Y center the avatar content on the click point
|
||||
// Note: x_content_offset and y_content_offset are already in viewport pixels (prop_size includes scale)
|
||||
let canvas_x = member.member.position_x * scale_x + offset_x - avatar_size / 2.0 - x_content_offset;
|
||||
let canvas_y = member.member.position_y * scale_y + offset_y - avatar_size / 2.0 - y_content_offset;
|
||||
|
||||
// Fixed text dimensions (independent of prop_size/zoom)
|
||||
// Text stays readable regardless of zoom level - only affected by text_em_size slider
|
||||
|
|
@ -289,44 +346,15 @@ pub fn AvatarCanvas(
|
|||
let _ = ctx.fill_text(&format!("{}", current_emotion), badge_x, badge_y);
|
||||
}
|
||||
|
||||
// Helper to check if any layer has content at a position
|
||||
let has_content_at = |pos: usize| -> bool {
|
||||
skin_layer_clone[pos].is_some()
|
||||
|| clothes_layer_clone[pos].is_some()
|
||||
|| accessories_layer_clone[pos].is_some()
|
||||
|| emotion_layer_clone[pos].is_some()
|
||||
};
|
||||
|
||||
// Calculate empty bottom rows to adjust name Y position
|
||||
let mut empty_bottom_rows = 0;
|
||||
|
||||
// Check row 2 (positions 6, 7, 8)
|
||||
let row2_has_content = (6..=8).any(&has_content_at);
|
||||
if !row2_has_content {
|
||||
empty_bottom_rows += 1;
|
||||
// Check row 1 (positions 3, 4, 5)
|
||||
let row1_has_content = (3..=5).any(&has_content_at);
|
||||
if !row1_has_content {
|
||||
empty_bottom_rows += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate X offset to center name on columns with content
|
||||
// Column 0 (left): positions 0, 3, 6
|
||||
// Column 1 (middle): positions 1, 4, 7
|
||||
// Column 2 (right): positions 2, 5, 8
|
||||
let left_col_has_content = [0, 3, 6].iter().any(|&p| has_content_at(p));
|
||||
let middle_col_has_content = [1, 4, 7].iter().any(|&p| has_content_at(p));
|
||||
let right_col_has_content = [2, 5, 8].iter().any(|&p| has_content_at(p));
|
||||
|
||||
// Find leftmost and rightmost columns with content
|
||||
let min_col = if left_col_has_content { 0 } else if middle_col_has_content { 1 } else { 2 };
|
||||
let max_col = if right_col_has_content { 2 } else if middle_col_has_content { 1 } else { 0 };
|
||||
|
||||
// Calculate center of content columns (grid center is column 1)
|
||||
let content_center_col = (min_col + max_col) as f64 / 2.0;
|
||||
let x_offset = (content_center_col - 1.0) * cell_size;
|
||||
let name_x = avatar_cx + x_offset;
|
||||
// Calculate content bounds for name positioning
|
||||
let content_bounds = ContentBounds::from_layers(
|
||||
&skin_layer_clone,
|
||||
&clothes_layer_clone,
|
||||
&accessories_layer_clone,
|
||||
&emotion_layer_clone,
|
||||
);
|
||||
let name_x = avatar_cx + content_bounds.x_offset(cell_size);
|
||||
let empty_bottom_rows = content_bounds.empty_bottom_rows();
|
||||
|
||||
// Draw display name below avatar (with black outline for readability)
|
||||
ctx.set_font(&format!("{}px sans-serif", 12.0 * text_scale));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue