fix pan clamping
Hunter
Monday, October 20th, 2025 at 1:34 PM
199 return { maxPanX: 0, maxPanY: 0 };
200 }
201
- // Pan coordinates are in the image's original coordinate system (before rotation)
202 + // Pan coordinates are in the image's original coordinate system (before scale and rotation)
203 // So we need to calculate bounds based on the original image dimensions
204 const isRotated90or270 = imageData.rotation % 180 !== 0;
205
210 const effectiveContainerWidth = isRotated90or270 ? containerHeight : containerWidth;
211 const effectiveContainerHeight = isRotated90or270 ? containerWidth : containerHeight;
212
- // Calculate actual rendered size with both base scale and user scale
213 + // Pan values are in pre-scale image space, but the CSS transform scales them
214 + // So we need to calculate bounds in pre-scale space
215 + // The image's natural size minus the container size (in pre-scale space) gives us the overhang
216 const totalScale = imageData.baseScale * imageData.userScale;
- const renderedWidth = imageData.naturalWidth * totalScale;
- const renderedHeight = imageData.naturalHeight * totalScale;
217 + const containerWidthInImageSpace = effectiveContainerWidth / totalScale;
218 + const containerHeightInImageSpace = effectiveContainerHeight / totalScale;
219
- // Calculate maximum pan in each direction
- const maxPanX = Math.max(0, (renderedWidth - effectiveContainerWidth) / 2);
- const maxPanY = Math.max(0, (renderedHeight - effectiveContainerHeight) / 2);
220 + // Calculate maximum pan in each direction (in pre-scale image space)
221 + const maxPanX = Math.max(0, (imageData.naturalWidth - containerWidthInImageSpace) / 2);
222 + const maxPanY = Math.max(0, (imageData.naturalHeight - containerHeightInImageSpace) / 2);
223
224 return { maxPanX, maxPanY };
225 }
449 if (e.ctrlKey) {
450 // Zoom at cursor position
451 const rect = container.getBoundingClientRect();
- const cursorX = e.clientX - rect.left - rect.width / 2;
- const cursorY = e.clientY - rect.top - rect.height / 2;
452 + const cursorX = e.clientX - rect.left - rect.width / 2; // Screen pixels from center
453 + const cursorY = e.clientY - rect.top - rect.height / 2; // Screen pixels from center
454
455 const oldUserScale = imageData.userScale;
456 const oldTotalScale = imageData.baseScale * oldUserScale;
458 const newUserScale = Math.max(1, Math.min(5, oldUserScale * (1 + zoomDelta)));
459 const newTotalScale = imageData.baseScale * newUserScale;
460
461 + // Convert cursor position to pre-scale image space
462 + const cursorXInImageSpace = cursorX / oldTotalScale;
463 + const cursorYInImageSpace = cursorY / oldTotalScale;
464 +
465 // Adjust pan to keep the point under cursor fixed during zoom
466 + // Pan is in pre-scale image space, so we work entirely in that space
467 const scaleRatio = newTotalScale / oldTotalScale;
- imageData.panX = cursorX * (1 - scaleRatio) + imageData.panX * scaleRatio;
- imageData.panY = cursorY * (1 - scaleRatio) + imageData.panY * scaleRatio;
468 + imageData.panX = cursorXInImageSpace * (1 - scaleRatio) + imageData.panX * scaleRatio;
469 + imageData.panY = cursorYInImageSpace * (1 - scaleRatio) + imageData.panY * scaleRatio;
470 imageData.userScale = newUserScale;
471
472 // Clamp after zooming to prevent whitespace
482 const rotatedDeltaX = e.deltaX * cos - e.deltaY * sin;
483 const rotatedDeltaY = e.deltaX * sin + e.deltaY * cos;
484
- imageData.panX -= rotatedDeltaX;
- imageData.panY -= rotatedDeltaY;
485 + // Convert screen-space delta to pre-scale image space
486 + const totalScale = imageData.baseScale * imageData.userScale;
487 + imageData.panX -= rotatedDeltaX / totalScale;
488 + imageData.panY -= rotatedDeltaY / totalScale;
489
490 // Clamp pan to prevent whitespace
491 clampPan(imageData);