more intuitive panning controls on rotated images
Hunter
Monday, October 20th, 2025 at 1:10 PM
191 }
192
193 function calculatePanBounds(imageData) {
194 + // Container dimensions in the current (possibly swapped) grid orientation
195 const containerWidth = imageData.widthCells * CELL_SIZE_PX;
196 const containerHeight = imageData.heightCells * CELL_SIZE_PX;
197
199 return { maxPanX: 0, maxPanY: 0 };
200 }
201
- // When rotated 90° or 270°, the image dimensions are effectively swapped
202 + // Pan coordinates are in the image's original coordinate system (before rotation)
203 + // So we need to calculate bounds based on the original image dimensions
204 const isRotated90or270 = imageData.rotation % 180 !== 0;
- const effectiveWidth = isRotated90or270 ? imageData.naturalHeight : imageData.naturalWidth;
- const effectiveHeight = isRotated90or270 ? imageData.naturalWidth : imageData.naturalHeight;
205 +
206 + // For pan bounds, we need to match image dimensions to container dimensions
207 + // in the image's coordinate space (not the screen's coordinate space)
208 + // When rotated 90/270, panX constrains vertical screen movement (maps to container height)
209 + // and panY constrains horizontal screen movement (maps to container width)
210 + const effectiveContainerWidth = isRotated90or270 ? containerHeight : containerWidth;
211 + const effectiveContainerHeight = isRotated90or270 ? containerWidth : containerHeight;
212
213 // Calculate actual rendered size with both base scale and user scale
214 const totalScale = imageData.baseScale * imageData.userScale;
- const renderedWidth = effectiveWidth * totalScale;
- const renderedHeight = effectiveHeight * totalScale;
215 + const renderedWidth = imageData.naturalWidth * totalScale;
216 + const renderedHeight = imageData.naturalHeight * totalScale;
217
218 // Calculate maximum pan in each direction
- const maxPanX = Math.max(0, (renderedWidth - containerWidth) / 2);
- const maxPanY = Math.max(0, (renderedHeight - containerHeight) / 2);
219 + const maxPanX = Math.max(0, (renderedWidth - effectiveContainerWidth) / 2);
220 + const maxPanY = Math.max(0, (renderedHeight - effectiveContainerHeight) / 2);
221
222 return { maxPanX, maxPanY };
223 }
456 clampPan(imageData);
457 } else {
458 // Pan (two-finger scroll on macOS trackpad)
- imageData.panX -= e.deltaX;
- imageData.panY -= e.deltaY;
459 + // Transform deltas to account for image rotation
460 + const angle = -imageData.rotation * Math.PI / 180; // Negative because we want screen-to-image transform
461 + const cos = Math.cos(angle);
462 + const sin = Math.sin(angle);
463 +
464 + // Rotate the delta vector by the negative of the image rotation
465 + const rotatedDeltaX = e.deltaX * cos - e.deltaY * sin;
466 + const rotatedDeltaY = e.deltaX * sin + e.deltaY * cos;
467 +
468 + imageData.panX -= rotatedDeltaX;
469 + imageData.panY -= rotatedDeltaY;
470
471 // Clamp pan to prevent whitespace
472 clampPan(imageData);