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

This commit is contained in:
tr1x_em committed 2025-08-02 21:34:10 +05:30
1 parent 84d0f14526
commit 209f58e75d
2 files changed
+63 -24

No files matched your search

+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