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.
This commit is contained in:
Janus C. H. Knudsen 2025-10-08 18:30:03 +02:00
parent 3145752591
commit a8b9767524

View file

@ -64,6 +64,11 @@ export class DragDropManager {
private snapIntervalMinutes = 15; // Default 15 minutes private snapIntervalMinutes = 15; // Default 15 minutes
private hourHeightPx: number; // Will be set from config 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 { private get snapDistancePx(): number {
return (this.snapIntervalMinutes / 60) * this.hourHeightPx; return (this.snapIntervalMinutes / 60) * this.hourHeightPx;
@ -257,25 +262,18 @@ export class DragDropManager {
// Continue with normal drag behavior only if drag has started // Continue with normal drag behavior only if drag has started
if (this.isDragStarted && this.draggedElement && this.draggedClone) { if (this.isDragStarted && this.draggedElement && this.draggedClone) {
if (!this.draggedElement.hasAttribute("data-allday")) { 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 (positionData.column) {
if (deltaY >= this.snapDistancePx) { this.targetY = positionData.snappedY; // Store raw Y as target
this.lastLoggedPosition = currentPosition; this.targetColumn = positionData.column;
// Consolidated position calculations with snapping for normal drag // Start animation loop if not already running
const positionData = this.calculateDragPosition(currentPosition); if (this.dragAnimationId === null) {
this.currentY = parseFloat(this.draggedClone.style.top) || 0;
// Emit drag move event with snapped position (normal behavior) this.animateDrag();
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);
} }
// Check for auto-scroll // Check for auto-scroll
@ -308,6 +306,7 @@ export class DragDropManager {
*/ */
private handleMouseUp(event: MouseEvent): void { private handleMouseUp(event: MouseEvent): void {
this.stopAutoScroll(); this.stopAutoScroll();
this.stopDragAnimation();
if (this.draggedElement) { if (this.draggedElement) {
@ -391,6 +390,7 @@ export class DragDropManager {
// 4. Clean up state // 4. Clean up state
this.cleanupDragState(); this.cleanupDragState();
this.stopAutoScroll(); this.stopAutoScroll();
this.stopDragAnimation();
} }
/** /**
@ -420,6 +420,53 @@ export class DragDropManager {
return Math.max(0, snappedY); 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 * 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 * Clean up drag state
*/ */