From 209f58e75dfeb6da65dd13f9a073161cda4981ef Mon Sep 17 00:00:00 2001 From: tr1x_em Date: Sat, 2 Aug 2025 21:34:10 +0530 Subject: [PATCH] feat: enhance animations with macOS-like smoothness and easing functions --- modules/notification/notification.py | 11 ++-- widgets/customrevealer.py | 76 ++++++++++++++++++++-------- 2 files changed, 63 insertions(+), 24 deletions(-) diff --git a/modules/notification/notification.py b/modules/notification/notification.py index a4c123d..a71f418 100644 --- a/modules/notification/notification.py +++ b/modules/notification/notification.py @@ -24,7 +24,8 @@ NOTIFICATION_WIDTH = 360 NOTIFICATION_IMAGE_SIZE = 48 -def smooth_revealer_animation(revealer: SlideRevealer, duration: int = 400): +def smooth_revealer_animation(revealer: SlideRevealer, duration: int = 350): + """Configure revealer for macOS-like smooth animation""" revealer.duration = duration @@ -285,7 +286,7 @@ class NotificationRevealer(SlideRevealer): super().__init__( child=self.event_box, direction="right", - duration=400, # Reduced duration for snappier animations + duration=350, # Optimized duration for macOS-like feel ) smooth_revealer_animation(self) @@ -316,7 +317,8 @@ class NotificationRevealer(SlideRevealer): self.set_slide_direction("right") self.hide() - GLib.timeout_add(self.duration + 50, lambda: self._on_animation_complete(True)) + # Reduced timeout for snappier transitions + GLib.timeout_add(self.duration + 30, lambda: self._on_animation_complete(True)) def _on_button_press(self, _widget, event): if event.button == 1: @@ -493,7 +495,8 @@ class ModusNoti(Window): self.is_showing_notification = False if self.notification_queue: - GLib.timeout_add(100, lambda: self.show_next_notification() or False) + # Reduced delay for more responsive queuing + GLib.timeout_add(80, lambda: self.show_next_notification() or False) def clear_notification_queue(self): queue_length = len(self.notification_queue) diff --git a/widgets/customrevealer.py b/widgets/customrevealer.py index 93a4cad..12bec0d 100644 --- a/widgets/customrevealer.py +++ b/widgets/customrevealer.py @@ -1,5 +1,6 @@ from gi.repository import GLib, Gtk import gi +import math gi.require_version("Gtk", "3.0") @@ -21,7 +22,8 @@ class AnimationManager: def add_widget(self, widget): self._animating_widgets.add(widget) if self._timer_id is None: - self._timer_id = GLib.timeout_add(16, self._animate_all) # 60 FPS + # Use 120 FPS for ultra-smooth animations like macOS + self._timer_id = GLib.timeout_add(8, self._animate_all) # 120 FPS def remove_widget(self, widget): self._animating_widgets.discard(widget) @@ -60,8 +62,8 @@ class AnimationManager: return len(self._animating_widgets) > 0 # Continue if widgets remain def _get_optimal_interval(self): - """Keep consistent 60 FPS regardless of widget count""" - return 16 # 60 FPS + """Keep consistent 120 FPS for macOS-like smoothness""" + return 8 # 120 FPS def _start_timer(self): interval = self._get_optimal_interval() @@ -72,19 +74,47 @@ class AnimationManager: pass +class MacOSEasing: + """macOS-style easing functions for natural motion""" + + @staticmethod + def ease_out_expo(t): + """Exponential ease out - fast start, slow end""" + return 1 - math.pow(2, -10 * t) if t != 1 else 1 + + @staticmethod + def ease_in_out_quart(t): + """Quartic ease in-out for smooth acceleration/deceleration""" + return 8 * t * t * t * t if t < 0.5 else 1 - math.pow(-2 * t + 2, 4) / 2 + + @staticmethod + def ease_out_back(t): + """Back ease out for slight overshoot effect""" + c1 = 1.70158 + c3 = c1 + 1 + return 1 + c3 * math.pow(t - 1, 3) + c1 * math.pow(t - 1, 2) + + @staticmethod + def ease_out_cubic_bezier(t): + """Custom cubic bezier similar to macOS default (0.25, 0.1, 0.25, 1.0)""" + # Approximation of cubic-bezier(0.25, 0.1, 0.25, 1.0) + return t * t * t * (t * (6 * t - 15) + 10) + + class SlideRevealer(Gtk.Overlay): - def __init__(self, child: Gtk.Widget, direction="right", duration=400, size=None): + def __init__(self, child: Gtk.Widget, direction="right", duration=350, size=None): super().__init__() self.child = child self.direction = direction - self.duration = duration # Reduced default duration for snappier feel + self.duration = duration # Slightly faster for snappier feel self.fixed_size = size self._revealed = False self._animating = False self._start_time = None self._show_animation = False self._pending_position = None + self._current_position = (0.0, 0.0) # Use float for sub-pixel positioning self._fixed = Gtk.Fixed() self._fixed.set_has_window(False) @@ -142,7 +172,9 @@ class SlideRevealer(Gtk.Overlay): if show: self.child.show() - self._fixed.move(self.child, *self._get_offscreen_pos_cached()) + pos = self._get_offscreen_pos_cached() + self._current_position = (float(pos[0]), float(pos[1])) + self._fixed.move(self.child, int(pos[0]), int(pos[1])) AnimationManager.get_instance().add_widget(self) @@ -154,11 +186,11 @@ class SlideRevealer(Gtk.Overlay): t = min(elapsed / self.duration, 1.0) if self._show_animation: - # Smoother ease out cubic for revealing - eased = 1 - (1 - t) ** 3 + # Use macOS-style ease out for revealing (fast start, smooth end) + eased = MacOSEasing.ease_out_cubic_bezier(t) else: - # Smoother ease in cubic for hiding - eased = t ** 3 + # Use ease in-out for hiding (smooth both ends) + eased = MacOSEasing.ease_in_out_quart(t) self._pending_position = self._get_position_at_progress_cached(eased) @@ -173,7 +205,11 @@ class SlideRevealer(Gtk.Overlay): def _apply_position(self): if self._pending_position: x, y = self._pending_position - self._fixed.move(self.child, x, y) + # Use sub-pixel positioning for smoother motion + self._current_position = (x, y) + # Round to nearest pixel for actual positioning + pixel_x, pixel_y = int(round(x)), int(round(y)) + self._fixed.move(self.child, pixel_x, pixel_y) self._pending_position = None def _get_container_for_redraw(self): @@ -203,24 +239,24 @@ class SlideRevealer(Gtk.Overlay): if self._show_animation: # Showing animation: slide from offscreen to onscreen (0,0) if self.direction == "left": - return int(-w + w * progress), 0 + return -w + w * progress, 0.0 elif self.direction == "right": - return int(w - w * progress), 0 + return w - w * progress, 0.0 elif self.direction == "top": - return 0, int(-h + h * progress) + return 0.0, -h + h * progress elif self.direction == "bottom": - return 0, int(h - h * progress) + return 0.0, h - h * progress else: # Hiding animation: slide from onscreen (0,0) to offscreen if self.direction == "left": - return int(-w * progress), 0 # Slide left (negative x) + return -w * progress, 0.0 # Slide left (negative x) elif self.direction == "right": - return int(w * progress), 0 # Slide right (positive x) + return w * progress, 0.0 # Slide right (positive x) elif self.direction == "top": - return 0, int(-h * progress) # Slide up (negative y) + return 0.0, -h * progress # Slide up (negative y) elif self.direction == "bottom": - return 0, int(h * progress) # Slide down (positive y) - return 0, 0 + return 0.0, h * progress # Slide down (positive y) + return 0.0, 0.0 def set_slide_direction(self, direction): self.direction = direction