feat: enhance animations with macOS-like smoothness and easing functions

Co-authored-by: aider (openai/claude-sonnet-4) <aider@aider.chat>
This commit is contained in:
tr1x_emandaider committed 2025-08-02 21:34:10 +05:30
1 parent 15cf113d82
commit 305a105b22
2 files changed
+63 -24

No files matched your search

+7 -4
View File
@@ -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)
+56 -20
View File
@@ -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