fix scale/pan flash when adding or duplicating images

Hunter
Wednesday, July 29th, 2026 at 2:56 PM

resources/images.js+18-36

resources/images.js
 34  	const img = new Image();
 35  	const dimensions = await new Promise(resolve => {
 36  		img.onload = () => {
    -			const aspectRatio = img.width / img.height;
    -			resolve(calculateImageDimensions(aspectRatio));
 37 +			const aspectRatio = img.naturalWidth / img.naturalHeight;
 38 +			resolve({
 39 +				...calculateImageDimensions(aspectRatio),
 40 +				naturalWidth: img.naturalWidth,
 41 +				naturalHeight: img.naturalHeight
 42 +			});
 43  		};
 44  		img.src = dataUrl;
 45  	});

 81  
 82  	let wrappedOffset = 0;
 83  
    -	for (const { idx, dataUrl, widthCells, heightCells } of imageDataArray) {
 84 +	for (const { idx, dataUrl, widthCells, heightCells, naturalWidth, naturalHeight } of imageDataArray) {
 85  		// Calculate position with diagonal offset
 86  		let xCell = baseXCell + idx;
 87  		let yCell = baseYCell + idx;

 98  			wrappedOffset++;
 99  		}
100  
    -		const imageData = addImage(dataUrl, xCell, yCell, widthCells, heightCells);
101 +		const imageData = addImage(dataUrl, xCell, yCell, widthCells, heightCells, { naturalWidth, naturalHeight });
102  		imageData.container.style.zIndex = baseZIndex + idx;
103  	}
104  }
105  
    -function addImage(src, xCell, yCell, widthCells, heightCells) {
106 +function addImage(src, xCell, yCell, widthCells, heightCells, initialState) {
107  	const container = document.createElement('div');
108  	container.className = 'image-container';
109  	highestZIndex++;

150  		// Store natural image dimensions for calculations
151  		naturalWidth: 0,
152  		naturalHeight: 0,
    -		baseScale: 1  // scale needed to cover container
153 +		baseScale: 1,  // scale needed to cover container
154 +		// Known up front when copying an already-loaded image, so the first paint is correct
155 +		...initialState
156  	};
157  
158  	// Calculate dimensions and scale once image loads
159  	img.onload = () => {
160  		imageData.naturalWidth = img.naturalWidth;
161  		imageData.naturalHeight = img.naturalHeight;
    -
    -		// Calculate base scale to cover container (mimics object-fit: cover)
    -		const bounds = getPixelPerfectBounds(imageData.xCell, imageData.yCell, imageData.widthCells, imageData.heightCells);
    -		const scaleX = bounds.width / img.naturalWidth;
    -		const scaleY = bounds.height / img.naturalHeight;
    -		imageData.baseScale = Math.max(scaleX, scaleY);
    -
162  		updateImagePosition(imageData);
163  	};
164  	images.push(imageData);

217  // Copy an image (and its pan/zoom/rotation) to a new grid position
218  function duplicateImage(imageData, newXCell, newYCell) {
219  	const imgElement = imageData.container.querySelector('img');
    -	const newImageData = addImage(
220 +	const { panX, panY, userScale, rotation, naturalWidth, naturalHeight } = imageData;
221 +
222 +	// The source is already loaded, so the copy's crop can be set before it ever paints
223 +	return addImage(
224  		imgElement.src,
225  		newXCell,
226  		newYCell,
227  		imageData.widthCells,
    -		imageData.heightCells
228 +		imageData.heightCells,
229 +		{ panX, panY, userScale, rotation, naturalWidth, naturalHeight }
230  	);
    -
    -	const { panX, panY, userScale, rotation } = imageData;
    -	const applySettings = () => {
    -		newImageData.panX = panX;
    -		newImageData.panY = panY;
    -		newImageData.userScale = userScale;
    -		newImageData.rotation = rotation;
    -		updateImagePosition(newImageData);
    -	};
    -
    -	// Settings can only be applied once the copy's natural dimensions are known
    -	const newImg = newImageData.container.querySelector('img');
    -	const originalOnload = newImg.onload;
    -	newImg.onload = () => {
    -		if (originalOnload) originalOnload.call(newImg);
    -		applySettings();
    -	};
    -
    -	if (newImg.complete && newImageData.naturalWidth > 0) applySettings();
    -
    -	return newImageData;
231  }
232  
233  function deleteImage(imageData) {