anim.rs (9714B)
1 use egui::{vec2, Pos2, Rect, Response, Sense}; 2 3 pub fn hover_expand( 4 ui: &mut egui::Ui, 5 id: egui::Id, 6 size: f32, 7 expand_size: f32, 8 anim_speed: f32, 9 ) -> (egui::Rect, f32, egui::Response) { 10 // Allocate space for the profile picture with a fixed size 11 let default_size = size + expand_size; 12 let (rect, response) = 13 ui.allocate_exact_size(egui::vec2(default_size, default_size), egui::Sense::click()); 14 15 let val = ui 16 .ctx() 17 .animate_bool_with_time(id, response.hovered(), anim_speed); 18 19 let size = size + val * expand_size; 20 (rect, size, response) 21 } 22 23 #[inline] 24 pub fn hover_small_size() -> f32 { 25 14.0 26 } 27 28 pub fn hover_expand_small(ui: &mut egui::Ui, id: egui::Id) -> (egui::Rect, f32, egui::Response) { 29 let size = hover_small_size(); 30 let expand_size = 5.0; 31 let anim_speed = 0.05; 32 33 hover_expand(ui, id, size, expand_size, anim_speed) 34 } 35 36 pub static ICON_EXPANSION_MULTIPLE: f32 = 1.2; 37 pub static ANIM_SPEED: f32 = 0.05; 38 pub struct AnimationHelper { 39 rect: Rect, 40 center: Pos2, 41 response: Response, 42 animation_progress: f32, 43 expansion_multiple: f32, 44 } 45 46 impl AnimationHelper { 47 pub fn new( 48 ui: &mut egui::Ui, 49 animation_name: impl std::hash::Hash, 50 max_size: egui::Vec2, 51 ) -> Self { 52 let id = ui.id().with(animation_name); 53 let (rect, response) = ui.allocate_exact_size(max_size, Sense::click()); 54 55 let animation_progress = 56 ui.ctx() 57 .animate_bool_with_time(id, response.hovered(), ANIM_SPEED); 58 59 Self { 60 rect, 61 center: rect.center(), 62 response, 63 animation_progress, 64 expansion_multiple: ICON_EXPANSION_MULTIPLE, 65 } 66 } 67 68 pub fn no_animation(ui: &mut egui::Ui, size: egui::Vec2, sense: Sense) -> Self { 69 let (rect, response) = ui.allocate_exact_size(size, sense); 70 71 Self { 72 rect, 73 center: rect.center(), 74 response, 75 animation_progress: 0.0, 76 expansion_multiple: ICON_EXPANSION_MULTIPLE, 77 } 78 } 79 80 pub fn new_from_rect( 81 ui: &mut egui::Ui, 82 animation_name: impl std::hash::Hash, 83 animation_rect: egui::Rect, 84 ) -> Self { 85 let id = ui.id().with(animation_name); 86 let response = ui.allocate_rect(animation_rect, Sense::click()); 87 88 let animation_progress = 89 ui.ctx() 90 .animate_bool_with_time(id, response.hovered(), ANIM_SPEED); 91 92 Self { 93 rect: animation_rect, 94 center: animation_rect.center(), 95 response, 96 animation_progress, 97 expansion_multiple: ICON_EXPANSION_MULTIPLE, 98 } 99 } 100 101 pub fn scale_1d_pos(&self, min_object_size: f32) -> f32 { 102 let max_object_size = min_object_size * self.expansion_multiple; 103 104 if self.response.is_pointer_button_down_on() { 105 min_object_size 106 } else { 107 min_object_size + ((max_object_size - min_object_size) * self.animation_progress) 108 } 109 } 110 111 pub fn scale_radius(&self, min_diameter: f32) -> f32 { 112 self.scale_1d_pos((min_diameter - 1.0) / 2.0) 113 } 114 115 pub fn get_animation_rect(&self) -> egui::Rect { 116 self.rect 117 } 118 119 pub fn scaled_rect(&self) -> egui::Rect { 120 let min_height = self.rect.height() * (1.0 / self.expansion_multiple); 121 let min_width = self.rect.width() * (1.0 / self.expansion_multiple); 122 123 egui::Rect::from_center_size( 124 self.center, 125 vec2(self.scale_1d_pos(min_width), self.scale_1d_pos(min_height)), 126 ) 127 } 128 129 pub fn center(&self) -> Pos2 { 130 self.rect.center() 131 } 132 133 pub fn take_animation_response(self) -> egui::Response { 134 self.response 135 } 136 137 // Scale a minimum position from center to the current animation position 138 pub fn scale_from_center(&self, x_min: f32, y_min: f32) -> Pos2 { 139 Pos2::new( 140 self.center.x + self.scale_1d_pos(x_min), 141 self.center.y + self.scale_1d_pos(y_min), 142 ) 143 } 144 145 pub fn scale_pos_from_center(&self, min_pos: Pos2) -> Pos2 { 146 self.scale_from_center(min_pos.x, min_pos.y) 147 } 148 149 /// New method for min/max scaling when needed 150 pub fn scale_1d_pos_min_max(&self, min_object_size: f32, max_object_size: f32) -> f32 { 151 min_object_size + ((max_object_size - min_object_size) * self.animation_progress) 152 } 153 } 154 155 pub struct PulseAlpha<'a> { 156 ctx: &'a egui::Context, 157 id: egui::Id, 158 alpha_min: u8, 159 alpha_max: u8, 160 animation_speed: f32, 161 start_max_alpha: bool, 162 } 163 164 impl<'a> PulseAlpha<'a> { 165 pub fn new(ctx: &'a egui::Context, id: egui::Id, alpha_min: u8, alpha_max: u8) -> Self { 166 Self { 167 ctx, 168 id, 169 alpha_min, 170 alpha_max, 171 animation_speed: ANIM_SPEED, 172 start_max_alpha: false, 173 } 174 } 175 176 pub fn with_speed(mut self, speed: f32) -> Self { 177 self.animation_speed = speed; 178 self 179 } 180 181 pub fn start_max_alpha(mut self) -> Self { 182 self.start_max_alpha = true; 183 self 184 } 185 186 // returns the current alpha value for the frame 187 pub fn animate(self) -> u8 { 188 let pulse_direction = if let Some(pulse_dir) = self.ctx.data(|d| d.get_temp(self.id)) { 189 pulse_dir 190 } else { 191 self.ctx 192 .data_mut(|d| d.insert_temp(self.id, self.start_max_alpha)); 193 self.start_max_alpha 194 }; 195 196 let alpha_min_f32 = self.alpha_min as f32; 197 let target = if pulse_direction { 198 self.alpha_max as f32 - alpha_min_f32 199 } else { 200 0.0 201 }; 202 203 let cur_val = self 204 .ctx 205 .animate_value_with_time(self.id, target, self.animation_speed); 206 207 if (target - cur_val).abs() < 0.5 { 208 self.ctx 209 .data_mut(|d| d.insert_temp(self.id, !pulse_direction)); 210 } 211 212 (cur_val + alpha_min_f32).clamp(self.alpha_min as f32, self.alpha_max as f32) as u8 213 } 214 } 215 216 /// Stateless rolling number using egui's internal animation memory. 217 /// Each digit has a different "speed" / easing. 218 pub fn rolling_number(ui: &mut egui::Ui, id_source: impl std::hash::Hash, value: u32) -> Response { 219 let ctx = ui.ctx(); 220 let id = ui.make_persistent_id(id_source); 221 222 // Global animated value (one float in egui's memory): 223 let anim = ctx.animate_value_with_time(id, value as f32, 0.35); 224 225 let anim_floor = anim.floor().max(0.0); 226 let base = anim_floor as u32; 227 let t_global = anim - anim_floor; // base step phase: 0..1 228 let next = if t_global == 0.0 { 229 base 230 } else { 231 base.saturating_add(1) 232 }; 233 234 // Choose how many digits we want to show. 235 let max_show = value.max(next); 236 let num_digits = max_show.to_string().len().max(1); 237 238 let font_size = 12.0; 239 let font_id = egui::FontId::proportional(font_size); 240 let color = ui.visuals().text_color(); 241 242 let response = ui.allocate_response(egui::Vec2::ZERO, Sense::hover()); 243 244 let prev_spacing = ui.spacing().item_spacing.x; 245 ui.spacing_mut().item_spacing.x = 0.0; 246 //let pos = ui.available_rect_before_wrap().min; 247 let digit_size = egui::vec2(7.0, font_size); 248 249 for i in 0..num_digits { 250 // Leftmost digit = index 0, rightmost = num_digits - 1 251 let place = 10_u32.pow((num_digits - 1 - i) as u32); 252 let from = (base / place) % 10; 253 let to = (next / place) % 10; 254 255 // Per-digit "speed": rightmost digits move more / earlier. 256 let idx_from_right = (num_digits - 1 - i) as f32; 257 // tweak these constants to taste: 258 let speed_factor = 0.8 + 0.25 * idx_from_right; // higher place → slightly faster 259 260 // Local phase for this digit: 261 let mut t_digit = (t_global * speed_factor).clamp(0.0, 1.0); 262 263 // Add a nice easing curve so some digits ease in/out: 264 t_digit = ease_in_out_cubic(t_digit); 265 266 draw_rolling_digit( 267 ui, from as u8, to as u8, t_digit, &font_id, color, digit_size, 268 ); 269 } 270 271 ui.spacing_mut().item_spacing.x = prev_spacing; 272 273 response 274 } 275 276 // Basic cubic ease-in-out 277 fn ease_in_out_cubic(t: f32) -> f32 { 278 if t < 0.5 { 279 4.0 * t * t * t 280 } else { 281 let t = 2.0 * t - 2.0; 282 0.5 * t * t * t + 1.0 283 } 284 } 285 286 fn draw_rolling_digit( 287 ui: &mut egui::Ui, 288 from: u8, 289 to: u8, 290 t: f32, // 0..1, already "warped" per digit 291 font_id: &egui::FontId, 292 color: egui::Color32, 293 desired_size: egui::Vec2, 294 ) -> egui::Response { 295 let (rect, response) = ui.allocate_exact_size(desired_size, egui::Sense::hover()); 296 297 let painter = ui.painter().with_clip_rect(rect); 298 299 let current_str = format!("{from}"); 300 let next_str = format!("{to}"); 301 302 let current_galley = painter.layout_no_wrap(current_str, font_id.clone(), color); 303 let next_galley = painter.layout_no_wrap(next_str, font_id.clone(), color); 304 305 let h = current_galley.rect.height().max(next_galley.rect.height()); 306 let center_x = rect.center().x; 307 let center_y = rect.center().y; 308 309 let current_y = egui::lerp(center_y..=center_y - h, t); 310 let next_y = egui::lerp(center_y + h..=center_y, t); 311 312 painter.galley( 313 egui::pos2( 314 center_x - current_galley.rect.width() * 0.5, 315 current_y - current_galley.rect.height() * 0.5, 316 ), 317 current_galley, 318 egui::Color32::RED, 319 ); 320 321 painter.galley( 322 egui::pos2( 323 center_x - next_galley.rect.width() * 0.5, 324 next_y - next_galley.rect.height() * 0.5, 325 ), 326 next_galley, 327 egui::Color32::RED, 328 ); 329 330 response 331 }