first pass at option+click to rotate

Hunter
Monday, October 20th, 2025 at 12:57 PM

script.js+49-7

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