From a8b9767524736a81995fa3dc2ac29ab6a5d89381 Mon Sep 17 00:00:00 2001 From: "Janus C. H. Knudsen" Date: Wed, 8 Oct 2025 18:30:03 +0200 Subject: [PATCH] Implements smooth drag animation Implements a smooth drag animation for a more fluid user experience. Instead of directly snapping to the mouse position, it interpolates the position over time using `requestAnimationFrame`. This provides a visually smoother movement during drag operations. --- src/managers/DragDropManager.ts | 91 +++++++++++++++++++++++++++------ 1 file changed, 74 insertions(+), 17 deletions(-) diff --git a/src/managers/DragDropManager.ts b/src/managers/DragDropManager.ts index d662bb2..b713971 100644 --- a/src/managers/DragDropManager.ts +++ b/src/managers/DragDropManager.ts @@ -64,6 +64,11 @@ export class DragDropManager { private snapIntervalMinutes = 15; // Default 15 minutes private hourHeightPx: number; // Will be set from config + // Smooth drag animation + private dragAnimationId: number | null = null; + private targetY = 0; + private currentY = 0; + private targetColumn: ColumnBounds | null = null; private get snapDistancePx(): number { return (this.snapIntervalMinutes / 60) * this.hourHeightPx; @@ -257,25 +262,18 @@ export class DragDropManager { // Continue with normal drag behavior only if drag has started if (this.isDragStarted && this.draggedElement && this.draggedClone) { if (!this.draggedElement.hasAttribute("data-allday")) { - const deltaY = Math.abs(currentPosition.y - this.lastLoggedPosition.y); + // Calculate raw target position from mouse (no snapping yet) + const positionData = this.calculateDragPosition(currentPosition); - // Check for snap interval vertical movement (normal drag behavior) - if (deltaY >= this.snapDistancePx) { - this.lastLoggedPosition = currentPosition; + if (positionData.column) { + this.targetY = positionData.snappedY; // Store raw Y as target + this.targetColumn = positionData.column; - // Consolidated position calculations with snapping for normal drag - const positionData = this.calculateDragPosition(currentPosition); - - // Emit drag move event with snapped position (normal behavior) - const dragMovePayload: DragMoveEventPayload = { - draggedElement: this.draggedElement, - draggedClone: this.draggedClone, - mousePosition: currentPosition, - snappedY: positionData.snappedY, - columnBounds: positionData.column, - mouseOffset: this.mouseOffset - }; - this.eventBus.emit('drag:move', dragMovePayload); + // Start animation loop if not already running + if (this.dragAnimationId === null) { + this.currentY = parseFloat(this.draggedClone.style.top) || 0; + this.animateDrag(); + } } // Check for auto-scroll @@ -308,6 +306,7 @@ export class DragDropManager { */ private handleMouseUp(event: MouseEvent): void { this.stopAutoScroll(); + this.stopDragAnimation(); if (this.draggedElement) { @@ -391,6 +390,7 @@ export class DragDropManager { // 4. Clean up state this.cleanupDragState(); this.stopAutoScroll(); + this.stopDragAnimation(); } /** @@ -420,6 +420,53 @@ export class DragDropManager { return Math.max(0, snappedY); } + /** + * Smooth drag animation using requestAnimationFrame + */ + private animateDrag(): void { + if (!this.isDragStarted || !this.draggedClone || !this.targetColumn) { + this.dragAnimationId = null; + return; + } + + // Smooth interpolation towards target + const diff = this.targetY - this.currentY; + const step = diff * 0.3; // 30% of distance per frame + + // Update if difference is significant + if (Math.abs(diff) > 0.5) { + this.currentY += step; + + // Emit drag move event with interpolated position + const dragMovePayload: DragMoveEventPayload = { + draggedElement: this.draggedElement!, + draggedClone: this.draggedClone, + mousePosition: this.lastMousePosition, + snappedY: this.currentY, + columnBounds: this.targetColumn, + mouseOffset: this.mouseOffset + }; + this.eventBus.emit('drag:move', dragMovePayload); + + this.dragAnimationId = requestAnimationFrame(() => this.animateDrag()); + } else { + // Close enough - snap to target + this.currentY = this.targetY; + + const dragMovePayload: DragMoveEventPayload = { + draggedElement: this.draggedElement!, + draggedClone: this.draggedClone, + mousePosition: this.lastMousePosition, + snappedY: this.currentY, + columnBounds: this.targetColumn, + mouseOffset: this.mouseOffset + }; + this.eventBus.emit('drag:move', dragMovePayload); + + this.dragAnimationId = null; + } + } + /** * Optimized auto-scroll check with cached container @@ -497,6 +544,16 @@ export class DragDropManager { } } + /** + * Stop drag animation + */ + private stopDragAnimation(): void { + if (this.dragAnimationId !== null) { + cancelAnimationFrame(this.dragAnimationId); + this.dragAnimationId = null; + } + } + /** * Clean up drag state */