your personal website on atproto - mirror blento.app
at switch-grid-layout 190 lines 5.8 kB view raw
1import { COLUMNS, margin, mobileMargin } from '$lib'; 2import { clamp } from '$lib/helper'; 3import type { Item } from '$lib/types'; 4 5export type GridPosition = { 6 x: number; 7 y: number; 8 swapWithId: string | null; 9 placement: 'above' | 'below' | null; 10}; 11 12export type DragState = { 13 item: Item; 14 mouseDeltaX: number; 15 mouseDeltaY: number; 16 originalPositions: Map<string, { x: number; y: number; mobileX: number; mobileY: number }>; 17 lastTargetId: string | null; 18 lastPlacement: 'above' | 'below' | null; 19}; 20 21/** 22 * Convert client coordinates to a grid position with swap detection and hysteresis. 23 * Returns undefined if container or dragState.item is missing. 24 * Mutates dragState.lastTargetId and dragState.lastPlacement for hysteresis tracking. 25 */ 26export function getGridPosition( 27 clientX: number, 28 clientY: number, 29 container: HTMLElement, 30 dragState: DragState, 31 items: Item[], 32 isMobile: boolean 33): GridPosition | undefined { 34 if (!dragState.item) return; 35 36 // x, y represent the top-left corner of the dragged card 37 const x = clientX + dragState.mouseDeltaX; 38 const y = clientY + dragState.mouseDeltaY; 39 40 const rect = container.getBoundingClientRect(); 41 const currentMargin = isMobile ? mobileMargin : margin; 42 const cellSize = (rect.width - currentMargin * 2) / COLUMNS; 43 44 // Get card dimensions based on current view mode 45 const cardW = isMobile ? dragState.item.mobileW : dragState.item.w; 46 const cardH = isMobile ? dragState.item.mobileH : dragState.item.h; 47 48 // Get dragged card's original position 49 const draggedOrigPos = dragState.originalPositions.get(dragState.item.id); 50 const draggedOrigY = draggedOrigPos ? (isMobile ? draggedOrigPos.mobileY : draggedOrigPos.y) : 0; 51 52 // Calculate raw grid position based on top-left of dragged card 53 let gridX = clamp(Math.round((x - rect.left - currentMargin) / cellSize), 0, COLUMNS - cardW); 54 gridX = Math.floor(gridX / 2) * 2; 55 56 let gridY = Math.max(Math.round((y - rect.top - currentMargin) / cellSize), 0); 57 58 if (isMobile) { 59 gridX = Math.floor(gridX / 2) * 2; 60 gridY = Math.floor(gridY / 2) * 2; 61 } 62 63 // Find if we're hovering over another card (using ORIGINAL positions) 64 const centerGridY = gridY + cardH / 2; 65 const centerGridX = gridX + cardW / 2; 66 67 let swapWithId: string | null = null; 68 let placement: 'above' | 'below' | null = null; 69 70 for (const other of items) { 71 if (other === dragState.item) continue; 72 73 // Use original positions for hit testing 74 const origPos = dragState.originalPositions.get(other.id); 75 if (!origPos) continue; 76 77 const otherX = isMobile ? origPos.mobileX : origPos.x; 78 const otherY = isMobile ? origPos.mobileY : origPos.y; 79 const otherW = isMobile ? other.mobileW : other.w; 80 const otherH = isMobile ? other.mobileH : other.h; 81 82 // Check if dragged card's center point is within this card's original bounds 83 if ( 84 centerGridX >= otherX && 85 centerGridX < otherX + otherW && 86 centerGridY >= otherY && 87 centerGridY < otherY + otherH 88 ) { 89 // Check if this is a swap situation: 90 // Cards have the same dimensions and are on the same row 91 const canSwap = cardW === otherW && cardH === otherH && draggedOrigY === otherY; 92 93 if (canSwap) { 94 // Swap positions 95 swapWithId = other.id; 96 gridX = otherX; 97 gridY = otherY; 98 placement = null; 99 100 dragState.lastTargetId = other.id; 101 dragState.lastPlacement = null; 102 } else { 103 // Vertical placement (above/below) 104 // Detect drag direction: if dragging up, always place above 105 const isDraggingUp = gridY < draggedOrigY; 106 107 if (isDraggingUp) { 108 // When dragging up, always place above 109 placement = 'above'; 110 } else { 111 // When dragging down, use top/bottom half logic 112 const midpointY = otherY + otherH / 2; 113 const hysteresis = 0.3; 114 115 if (dragState.lastTargetId === other.id && dragState.lastPlacement) { 116 if (dragState.lastPlacement === 'above') { 117 placement = centerGridY > midpointY + hysteresis ? 'below' : 'above'; 118 } else { 119 placement = centerGridY < midpointY - hysteresis ? 'above' : 'below'; 120 } 121 } else { 122 placement = centerGridY < midpointY ? 'above' : 'below'; 123 } 124 } 125 126 dragState.lastTargetId = other.id; 127 dragState.lastPlacement = placement; 128 129 if (placement === 'above') { 130 gridY = otherY; 131 } else { 132 gridY = otherY + otherH; 133 } 134 } 135 break; 136 } 137 } 138 139 // If we're not over any card, clear the tracking 140 if (!swapWithId && !placement) { 141 dragState.lastTargetId = null; 142 dragState.lastPlacement = null; 143 } 144 145 return { x: gridX, y: gridY, swapWithId, placement }; 146} 147 148/** 149 * Get the grid Y coordinate at the viewport center. 150 */ 151export function getViewportCenterGridY( 152 container: HTMLElement, 153 isMobile: boolean 154): { gridY: number; isMobile: boolean } { 155 const rect = container.getBoundingClientRect(); 156 const currentMargin = isMobile ? mobileMargin : margin; 157 const cellSize = (rect.width - currentMargin * 2) / COLUMNS; 158 const viewportCenterY = window.innerHeight / 2; 159 const gridY = (viewportCenterY - rect.top - currentMargin) / cellSize; 160 return { gridY, isMobile }; 161} 162 163/** 164 * Convert pixel drop coordinates to grid position. Used for file drops. 165 */ 166export function pixelToGrid( 167 clientX: number, 168 clientY: number, 169 container: HTMLElement, 170 isMobile: boolean, 171 cardW: number 172): { gridX: number; gridY: number } { 173 const rect = container.getBoundingClientRect(); 174 const currentMargin = isMobile ? mobileMargin : margin; 175 const cellSize = (rect.width - currentMargin * 2) / COLUMNS; 176 177 let gridX = clamp( 178 Math.round((clientX - rect.left - currentMargin) / cellSize), 179 0, 180 COLUMNS - cardW 181 ); 182 gridX = Math.floor(gridX / 2) * 2; 183 184 let gridY = Math.max(Math.round((clientY - rect.top - currentMargin) / cellSize), 0); 185 if (isMobile) { 186 gridY = Math.floor(gridY / 2) * 2; 187 } 188 189 return { gridX, gridY }; 190}