fix zoom-at-cursor when image is rotated

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

script.js+17-9

script.js
458  			const newUserScale = Math.max(1, Math.min(5, oldUserScale * (1 + zoomDelta)));
459  			const newTotalScale = imageData.baseScale * newUserScale;
460  
    -			// Convert cursor position to pre-scale image space
    -			const cursorXInImageSpace = cursorX / oldTotalScale;
    -			const cursorYInImageSpace = cursorY / oldTotalScale;
    -
    -			// Adjust pan to keep the point under cursor fixed during zoom
    -			// Pan is in pre-scale image space, so we work entirely in that space
    -			const scaleRatio = newTotalScale / oldTotalScale;
    -			imageData.panX = cursorXInImageSpace * (1 - scaleRatio) + imageData.panX * scaleRatio;
    -			imageData.panY = cursorYInImageSpace * (1 - scaleRatio) + imageData.panY * scaleRatio;
461 +			// Transform cursor position to account for rotation
462 +			// The CSS transform applies rotation before pan, so we need to rotate the cursor position
463 +			// into the image's coordinate system (inverse rotation)
464 +			const angle = -imageData.rotation * Math.PI / 180; // Negative for inverse rotation
465 +			const cos = Math.cos(angle);
466 +			const sin = Math.sin(angle);
467 +			const rotatedCursorX = cursorX * cos - cursorY * sin;
468 +			const rotatedCursorY = cursorX * sin + cursorY * cos;
469 +
470 +			// The point under the cursor in pre-scale image space is:
471 +			// imagePoint = (cursorScreen / oldTotalScale) - panX
472 +			// We want: (imagePoint + newPanX) * newTotalScale = cursorScreen
473 +			// So: newPanX = (cursorScreen / newTotalScale) - imagePoint
474 +			//            = (cursorScreen / newTotalScale) - (cursorScreen / oldTotalScale - panX)
475 +			//            = cursorScreen * (1/newTotalScale - 1/oldTotalScale) + panX
476 +			imageData.panX = rotatedCursorX * (1/newTotalScale - 1/oldTotalScale) + imageData.panX;
477 +			imageData.panY = rotatedCursorY * (1/newTotalScale - 1/oldTotalScale) + imageData.panY;
478  			imageData.userScale = newUserScale;
479  
480  			// Clamp after zooming to prevent whitespace