refactor/clean up
Hunter
Wednesday, October 22nd, 2025 at 2:45 PM
253 imageData.panY = Math.max(-maxPanY, Math.min(maxPanY, imageData.panY));
254 }
255
256 +// Helper to transform screen-space coordinates to image coordinate space (accounting for rotation)
257 +function rotatePoint(x, y, angleDegrees) {
258 + const angle = -angleDegrees * Math.PI / 180;
259 + const cos = Math.cos(angle);
260 + const sin = Math.sin(angle);
261 + return {
262 + x: x * cos - y * sin,
263 + y: x * sin + y * cos
264 + };
265 +}
266 +
267 +// Helper to apply pan adjustment based on screen delta
268 +function applyPanDelta(imageData, screenDeltaX, screenDeltaY) {
269 + const rotated = rotatePoint(screenDeltaX, screenDeltaY, imageData.rotation);
270 + const totalScale = imageData.baseScale * imageData.userScale;
271 + imageData.panX += rotated.x / totalScale;
272 + imageData.panY += rotated.y / totalScale;
273 +}
274 +
275 +// Helper to calculate zoom-centered pan adjustment
276 +function adjustPanForZoom(imageData, cursorX, cursorY, oldTotalScale, newTotalScale) {
277 + const rotated = rotatePoint(cursorX, cursorY, imageData.rotation);
278 + const scaleDiff = 1/newTotalScale - 1/oldTotalScale;
279 + imageData.panX += rotated.x * scaleDiff;
280 + imageData.panY += rotated.y * scaleDiff;
281 +}
282 +
283 function updateImagePosition(img) {
284 const cellSize = getCellSize();
285 img.container.style.left = img.xCell * cellSize.width + 'px';
355 // Unified pointer start handler
356 function handlePointerStart(clientX, clientY, isTouch = false) {
357 // Clear any existing timers from other images
- if (longPressTimer) {
- clearTimeout(longPressTimer);
- longPressTimer = null;
- }
358 + clearDragState();
359
360 // Lock cursor to move during drag operation
361 document.body.style.cursor = 'move';
404 // Two fingers - prepare for pinch/pan
405 e.preventDefault();
406
- // Cancel any ongoing drag and long press timer
- if (longPressTimer) {
- clearTimeout(longPressTimer);
- longPressTimer = null;
- }
- if (dragState) {
- dragState.image.container.classList.remove('dragging');
- dragState = null;
- document.body.style.cursor = '';
- document.body.classList.remove('dragging');
- }
407 + // Cancel any ongoing drag
408 + clearDragState();
409
410 const touch1 = e.touches[0];
411 const touch2 = e.touches[1];
468 const newUserScale = Math.max(1, Math.min(5, touchState.initialScale * scaleFactor));
469 const newTotalScale = imageData.baseScale * newUserScale;
470
- // Transform cursor position to account for rotation
- const angle = -imageData.rotation * Math.PI / 180;
- const cos = Math.cos(angle);
- const sin = Math.sin(angle);
- const rotatedCursorX = cursorX * cos - cursorY * sin;
- const rotatedCursorY = cursorX * sin + cursorY * cos;
-
- // Calculate new pan to keep zoom centered on pinch point
- imageData.panX = rotatedCursorX * (1/newTotalScale - 1/oldTotalScale) + touchState.lastPanX;
- imageData.panY = rotatedCursorY * (1/newTotalScale - 1/oldTotalScale) + touchState.lastPanY;
471 + // Restore previous pan state and apply zoom adjustment
472 + imageData.panX = touchState.lastPanX;
473 + imageData.panY = touchState.lastPanY;
474 + adjustPanForZoom(imageData, cursorX, cursorY, oldTotalScale, newTotalScale);
475 imageData.userScale = newUserScale;
476
477 clampPan(imageData);
479 touchState.lastPanY = imageData.panY;
480 } else {
481 // Two-finger pan
- const deltaX = centerMoveX;
- const deltaY = centerMoveY;
-
- // Transform deltas to account for image rotation
- const angle = -imageData.rotation * Math.PI / 180;
- const cos = Math.cos(angle);
- const sin = Math.sin(angle);
- const rotatedDeltaX = deltaX * cos - deltaY * sin;
- const rotatedDeltaY = deltaX * sin + deltaY * cos;
-
- // Convert screen-space delta to pre-scale image space
- // Note: positive delta means moving fingers right/down, which should move image right/down
- const totalScale = imageData.baseScale * imageData.userScale;
- imageData.panX = touchState.lastPanX + rotatedDeltaX / totalScale;
- imageData.panY = touchState.lastPanY + rotatedDeltaY / totalScale;
482 + imageData.panX = touchState.lastPanX;
483 + imageData.panY = touchState.lastPanY;
484 + applyPanDelta(imageData, centerMoveX, centerMoveY);
485
486 clampPan(imageData);
487 touchState.lastPanX = imageData.panX;
619 handlePointerStart(e.clientX, e.clientY);
620 });
621
- // Resizing
622 + // Resizing - unified handler for mouse and touch
623 + function startResize(clientX, clientY, direction, cursorStyle = null) {
624 + if (cursorStyle) {
625 + document.body.style.cursor = cursorStyle;
626 + }
627 + document.body.classList.add('resizing');
628 +
629 + resizeState = {
630 + image: imageData,
631 + direction: direction,
632 + startX: clientX,
633 + startY: clientY,
634 + startXCell: imageData.xCell,
635 + startYCell: imageData.yCell,
636 + startWidthCells: imageData.widthCells,
637 + startHeightCells: imageData.heightCells
638 + };
639 + container.classList.add('resizing');
640 + }
641 +
642 container.querySelectorAll('.resize-handle').forEach(handle => {
643 handle.addEventListener('mousedown', (e) => {
644 e.preventDefault();
645 e.stopPropagation();
-
- // Get the cursor style from the handle and apply it to the body
646 const cursorStyle = window.getComputedStyle(handle).cursor;
- document.body.style.cursor = cursorStyle;
- document.body.classList.add('resizing');
-
- resizeState = {
- image: imageData,
- direction: handle.dataset.direction,
- startX: e.clientX,
- startY: e.clientY,
- startXCell: imageData.xCell,
- startYCell: imageData.yCell,
- startWidthCells: imageData.widthCells,
- startHeightCells: imageData.heightCells
- };
- container.classList.add('resizing');
647 + startResize(e.clientX, e.clientY, handle.dataset.direction, cursorStyle);
648 });
649
- // Touch support for resize handles
650 handle.addEventListener('touchstart', (e) => {
651 e.preventDefault();
652 e.stopPropagation();
-
653 const touch = e.touches[0];
- document.body.classList.add('resizing');
-
- resizeState = {
- image: imageData,
- direction: handle.dataset.direction,
- startX: touch.clientX,
- startY: touch.clientY,
- startXCell: imageData.xCell,
- startYCell: imageData.yCell,
- startWidthCells: imageData.widthCells,
- startHeightCells: imageData.heightCells
- };
- container.classList.add('resizing');
654 + startResize(touch.clientX, touch.clientY, handle.dataset.direction);
655 }, { passive: false });
656 });
657
667 if (e.ctrlKey) {
668 // Zoom at cursor position
669 const rect = container.getBoundingClientRect();
- const cursorX = e.clientX - rect.left - rect.width / 2; // Screen pixels from center
- const cursorY = e.clientY - rect.top - rect.height / 2; // Screen pixels from center
670 + const cursorX = e.clientX - rect.left - rect.width / 2;
671 + const cursorY = e.clientY - rect.top - rect.height / 2;
672
673 const oldUserScale = imageData.userScale;
674 const oldTotalScale = imageData.baseScale * oldUserScale;
676 const newUserScale = Math.max(1, Math.min(5, oldUserScale * (1 + zoomDelta)));
677 const newTotalScale = imageData.baseScale * newUserScale;
678
- // Transform cursor position to account for rotation
- // The CSS transform applies rotation before pan, so we need to rotate the cursor position
- // into the image's coordinate system (inverse rotation)
- const angle = -imageData.rotation * Math.PI / 180; // Negative for inverse rotation
- const cos = Math.cos(angle);
- const sin = Math.sin(angle);
- const rotatedCursorX = cursorX * cos - cursorY * sin;
- const rotatedCursorY = cursorX * sin + cursorY * cos;
-
- // The point under the cursor in pre-scale image space is:
- // imagePoint = (cursorScreen / oldTotalScale) - panX
- // We want: (imagePoint + newPanX) * newTotalScale = cursorScreen
- // So: newPanX = (cursorScreen / newTotalScale) - imagePoint
- // = (cursorScreen / newTotalScale) - (cursorScreen / oldTotalScale - panX)
- // = cursorScreen * (1/newTotalScale - 1/oldTotalScale) + panX
- imageData.panX = rotatedCursorX * (1/newTotalScale - 1/oldTotalScale) + imageData.panX;
- imageData.panY = rotatedCursorY * (1/newTotalScale - 1/oldTotalScale) + imageData.panY;
679 + adjustPanForZoom(imageData, cursorX, cursorY, oldTotalScale, newTotalScale);
680 imageData.userScale = newUserScale;
681
- // Clamp after zooming to prevent whitespace
682 clampPan(imageData);
683 } else {
684 // Pan (two-finger scroll on macOS trackpad)
- // Transform deltas to account for image rotation
- const angle = -imageData.rotation * Math.PI / 180; // Negative because we want screen-to-image transform
- const cos = Math.cos(angle);
- const sin = Math.sin(angle);
-
- // Rotate the delta vector by the negative of the image rotation
- const rotatedDeltaX = e.deltaX * cos - e.deltaY * sin;
- const rotatedDeltaY = e.deltaX * sin + e.deltaY * cos;
-
- // Convert screen-space delta to pre-scale image space
- const totalScale = imageData.baseScale * imageData.userScale;
- imageData.panX -= rotatedDeltaX / totalScale;
- imageData.panY -= rotatedDeltaY / totalScale;
-
- // Clamp pan to prevent whitespace
685 + applyPanDelta(imageData, -e.deltaX, -e.deltaY);
686 clampPan(imageData);
687 }
688
690 }, { passive: false });
691 }
692
693 +// Helper to clear drag state and timers
694 +function clearDragState() {
695 + if (longPressTimer) {
696 + clearTimeout(longPressTimer);
697 + longPressTimer = null;
698 + }
699 + if (dragState) {
700 + dragState.image.container.classList.remove('dragging');
701 + dragState = null;
702 + document.body.style.cursor = '';
703 + document.body.classList.remove('dragging');
704 + }
705 +}
706 +
707 function handleMove(clientX, clientY) {
708 if (dragState) {
709 const dx = clientX - dragState.startX;
712 if (dragState.isPanMode) {
713 // Pan mode - move the image within its container
714 const imageData = dragState.image;
-
- // Transform deltas to account for image rotation
- const angle = -imageData.rotation * Math.PI / 180;
- const cos = Math.cos(angle);
- const sin = Math.sin(angle);
- const rotatedDeltaX = dx * cos - dy * sin;
- const rotatedDeltaY = dx * sin + dy * cos;
-
- // Convert screen-space delta to pre-scale image space
- const totalScale = imageData.baseScale * imageData.userScale;
- imageData.panX = dragState.initialPanX + rotatedDeltaX / totalScale;
- imageData.panY = dragState.initialPanY + rotatedDeltaY / totalScale;
-
715 + imageData.panX = dragState.initialPanX;
716 + imageData.panY = dragState.initialPanY;
717 + applyPanDelta(imageData, dx, dy);
718 clampPan(imageData);
719 updateImagePosition(imageData);
720 } else {
791 }
792
793 function handleEnd() {
- if (longPressTimer) {
- clearTimeout(longPressTimer);
- longPressTimer = null;
- }
- if (dragState) {
- dragState.image.container.classList.remove('dragging');
- dragState.image.container.style.opacity = '';
- dragState = null;
- // Restore the default cursor
- document.body.style.cursor = '';
- document.body.classList.remove('dragging');
- }
794 + clearDragState();
795 if (resizeState) {
796 resizeState.image.container.classList.remove('resizing');
797 resizeState = null;
- // Restore the default cursor
798 document.body.style.cursor = '';
799 document.body.classList.remove('resizing');
800 }