first pass at option+click to rotate
Hunter
Monday, October 20th, 2025 at 12:57 PM
159 panX: 0,
160 panY: 0,
161 userScale: 1, // user zoom level (1-5)
162 + rotation: 0, // rotation in degrees (0, 90, 180, 270)
163 // Store natural image dimensions for calculations
164 naturalWidth: 0,
165 naturalHeight: 0,
198 return { maxPanX: 0, maxPanY: 0 };
199 }
200
201 + // When rotated 90° or 270°, the image dimensions are effectively swapped
202 + const isRotated90or270 = imageData.rotation % 180 !== 0;
203 + const effectiveWidth = isRotated90or270 ? imageData.naturalHeight : imageData.naturalWidth;
204 + const effectiveHeight = isRotated90or270 ? imageData.naturalWidth : imageData.naturalHeight;
205 +
206 // Calculate actual rendered size with both base scale and user scale
207 const totalScale = imageData.baseScale * imageData.userScale;
- const renderedWidth = imageData.naturalWidth * totalScale;
- const renderedHeight = imageData.naturalHeight * totalScale;
208 + const renderedWidth = effectiveWidth * totalScale;
209 + const renderedHeight = effectiveHeight * totalScale;
210
211 // Calculate maximum pan in each direction
212 const maxPanX = Math.max(0, (renderedWidth - containerWidth) / 2);
237 if (img.naturalWidth > 0 && img.naturalHeight > 0) {
238 const containerWidth = img.widthCells * CELL_SIZE_PX;
239 const containerHeight = img.heightCells * CELL_SIZE_PX;
- const scaleX = containerWidth / img.naturalWidth;
- const scaleY = containerHeight / img.naturalHeight;
240 +
241 + // When rotated 90° or 270°, the image dimensions are effectively swapped
242 + const isRotated90or270 = img.rotation % 180 !== 0;
243 + const effectiveWidth = isRotated90or270 ? img.naturalHeight : img.naturalWidth;
244 + const effectiveHeight = isRotated90or270 ? img.naturalWidth : img.naturalHeight;
245 +
246 + const scaleX = containerWidth / effectiveWidth;
247 + const scaleY = containerHeight / effectiveHeight;
248 img.baseScale = Math.max(scaleX, scaleY);
249
250 // Reclamp pan after recalculating base scale
255 const imgElement = img.container.querySelector('img');
256 if (imgElement) {
257 const totalScale = img.baseScale * img.userScale;
- // Transform: translate from center (-50%, -50%), then pan, then scale
- imgElement.style.transform = `translate(-50%, -50%) translate(${img.panX}px, ${img.panY}px) scale(${totalScale})`;
258 + // Transform: translate from center (-50%, -50%), scale, rotate, then pan
259 + // Pan is applied after rotation so it stays relative to the image's rotated state
260 + imgElement.style.transform = `translate(-50%, -50%) scale(${totalScale}) rotate(${img.rotation}deg) translate(${img.panX}px, ${img.panY}px)`;
261 }
262 }
263
290 return;
291 }
292
293 + // Option-click (or Alt-click on Windows/Linux) to rotate
294 + if (e.altKey) {
295 + // Rotate 90 degrees clockwise
296 + const oldRotation = imageData.rotation;
297 + imageData.rotation = (imageData.rotation + 90) % 360;
298 +
299 + // Don't rotate pan coordinates - they stay in the image's original coordinate system
300 + // The CSS transform applies rotation before pan, so pan is relative to the rotated image
301 +
302 + // When rotating between portrait and landscape (90° or 270°), we need to swap dimensions
303 + if ((oldRotation % 180 === 0 && imageData.rotation % 180 !== 0) ||
304 + (oldRotation % 180 !== 0 && imageData.rotation % 180 === 0)) {
305 + // Swap width and height
306 + const temp = imageData.widthCells;
307 + imageData.widthCells = imageData.heightCells;
308 + imageData.heightCells = temp;
309 +
310 + // Adjust position to keep image within bounds after dimension swap
311 + imageData.xCell = Math.max(0, Math.min(GRID_COLS - imageData.widthCells, imageData.xCell));
312 + imageData.yCell = Math.max(0, Math.min(GRID_ROWS - imageData.heightCells, imageData.yCell));
313 + }
314 +
315 + updateImagePosition(imageData);
316 + return;
317 + }
318 +
319 // Cmd-click (or Ctrl-click on Windows/Linux) to duplicate
320 if (e.metaKey || e.ctrlKey) {
321 // Calculate target position (2 cells to the right if there's room)
345 imageData.heightCells
346 );
347
- // Copy pan and zoom settings from original
348 + // Copy pan, zoom, and rotation settings from original
349 // Store the original settings to apply after image loads
350 const originalPanX = imageData.panX;
351 const originalPanY = imageData.panY;
352 const originalUserScale = imageData.userScale;
353 + const originalRotation = imageData.rotation;
354
355 // Override the onload to copy settings
356 const newImg = newImageData.container.querySelector('img');
363 newImageData.panX = originalPanX;
364 newImageData.panY = originalPanY;
365 newImageData.userScale = originalUserScale;
366 + newImageData.rotation = originalRotation;
367 updateImagePosition(newImageData);
368 };
369
372 newImageData.panX = originalPanX;
373 newImageData.panY = originalPanY;
374 newImageData.userScale = originalUserScale;
375 + newImageData.rotation = originalRotation;
376 updateImagePosition(newImageData);
377 }
378