fix scale/pan flash when adding or duplicating images
Hunter
Wednesday, July 29th, 2026 at 2:56 PM
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) {