fix zoom-at-cursor when image is rotated
Hunter
Monday, October 20th, 2025 at 1:39 PM
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