Add animation effects for smooth user experience

Implemented comprehensive animation effects throughout the extension:

1. Zone Overlay Animations:
   - Fade-in animation (200ms) when showing zones
   - Fade-out animation (150ms) when hiding zones
   - Uses Clutter.AnimationMode.EASE_OUT_QUAD for smooth easing

2. Window Snapping Animations:
   - Smooth animated window movement (250ms) when snapping to zones
   - Animates window actor position and size
   - Falls back to instant move if actor not available
   - Final position set precisely after animation completes

3. Visual Snap Feedback:
   - Green flash effect when window snaps to zone
   - Flash shows the exact zone boundaries
   - Fades out over 400ms
   - Works for both Shift+drag and keyboard snapping

Implementation details:
- Used Clutter's ease() method for all animations
- Overlay fade animations prevent jarring appearance/disappearance
- Window animations use compositor actor for smooth transitions
- Flash feedback uses temporary widget with auto-cleanup
- All animations use EASE_OUT_QUAD for natural feel

User experience improvements:
- Zones appear and disappear smoothly instead of instantly
- Windows glide into position rather than jumping
- Clear visual confirmation when snap occurs
- Professional, polished feel throughout

Fixes TODO item #11.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-15 21:54:35 -07:00
parent 7816d41571
commit eb7a57617e
2 changed files with 91 additions and 16 deletions

View File

@@ -111,11 +111,11 @@
- [ ] 1/3 - 2/3 layouts
- [ ] Picture-in-picture style layouts
### 11. Animation Effects
### 11. Animation Effects ✅ COMPLETED
**Priority: Low**
- [ ] Add smooth transitions when snapping windows
- [ ] Animate zone overlay appearance/disappearance
- [ ] Visual feedback when window snaps to zone
- [x] Add smooth transitions when snapping windows
- [x] Animate zone overlay appearance/disappearance
- [x] Visual feedback when window snaps to zone
### 12. Settings Panel ✅ COMPLETED
**Priority: Medium**

View File

@@ -1071,6 +1071,14 @@ ZoneManager.prototype = {
Main.layoutManager.addChrome(this.overlay);
this.isShowing = true;
// Fade in animation
this.overlay.opacity = 0;
this.overlay.ease({
opacity: 255,
duration: 200,
mode: Clutter.AnimationMode.EASE_OUT_QUAD
});
},
_createZoneActor: function(zone, monitor, index) {
@@ -1126,13 +1134,23 @@ ZoneManager.prototype = {
_hideZonesOverlay: function() {
if (!this.isShowing) return;
if (this.overlay) {
Main.layoutManager.removeChrome(this.overlay);
this.overlay.destroy();
this.overlay = null;
}
this.isShowing = false;
if (this.overlay) {
// Fade out animation
this.overlay.ease({
opacity: 0,
duration: 150,
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
onComplete: () => {
if (this.overlay) {
Main.layoutManager.removeChrome(this.overlay);
this.overlay.destroy();
this.overlay = null;
}
}
});
}
},
_snapWindowToZone: function(window) {
@@ -1159,6 +1177,7 @@ ZoneManager.prototype = {
if (targetZone) {
this._moveWindowToZone(window, targetZone, monitor);
this._showSnapFeedback(targetZone, monitor);
// Show notification if enabled
if (this.settings.getValue('notification-on-snap')) {
@@ -1181,7 +1200,9 @@ ZoneManager.prototype = {
if (zoneIndex >= layout.zones.length) return;
let monitor = Main.layoutManager.primaryMonitor;
this._moveWindowToZone(window, layout.zones[zoneIndex], monitor);
let targetZone = layout.zones[zoneIndex];
this._moveWindowToZone(window, targetZone, monitor);
this._showSnapFeedback(targetZone, monitor);
// Show notification if enabled
if (this.settings.getValue('notification-on-snap')) {
@@ -1189,6 +1210,42 @@ ZoneManager.prototype = {
}
},
_showSnapFeedback: function(zone, monitor) {
// Create a flash effect on the zone to show snap feedback
let x = monitor.x + Math.round(monitor.width * zone.x);
let y = monitor.y + Math.round(monitor.height * zone.y);
let width = Math.round(monitor.width * zone.width);
let height = Math.round(monitor.height * zone.height);
let flashActor = new St.Widget({
style_class: 'gridsnap-flash',
x: x,
y: y,
width: width,
height: height
});
flashActor.set_style(
'border: 4px solid rgba(100, 255, 100, 1.0);' +
'background-color: rgba(100, 255, 100, 0.3);' +
'border-radius: 4px;'
);
Main.layoutManager.addChrome(flashActor);
// Animate flash effect
flashActor.opacity = 255;
flashActor.ease({
opacity: 0,
duration: 400,
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
onComplete: () => {
Main.layoutManager.removeChrome(flashActor);
flashActor.destroy();
}
});
},
_moveWindowToZone: function(window, zone, monitor) {
if (!window || !zone || !monitor) return;
@@ -1206,8 +1263,26 @@ ZoneManager.prototype = {
// Use Mainloop.idle_add to defer the operation
// This ensures the unmaximize completes before move/resize
Mainloop.idle_add(Lang.bind(this, function() {
// Move and resize window (false = programmatic operation, not user-initiated)
window.move_resize_frame(false, x, y, width, height);
// Get window actor for animation
let actor = window.get_compositor_private();
if (actor) {
// Animate window movement
actor.ease({
x: x,
y: y,
width: width,
height: height,
duration: 250,
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
onComplete: () => {
// Set final position precisely after animation
window.move_resize_frame(false, x, y, width, height);
}
});
} else {
// Fallback to instant move if no actor
window.move_resize_frame(false, x, y, width, height);
}
return false; // Don't repeat
}));
},