notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

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 }