more intuitive panning controls on rotated images

Hunter
Monday, October 20th, 2025 at 1:10 PM

script.js+25-9

script.js
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);