1/** 2 * Whole-document transforms: rotate by 90/180/270° or flip horizontal/ 3 * vertical. These mutate every layer's canvas + the offset map + the 4 * document's overall width/height so the result feels like the whole 5 * image rotated as one piece. 6 * 7 * Pure-ish â reads/writes shared state directly; the factory takes a 8 * small dep bag for the orchestration plumbing (undo snapshot, canvas 9 * loading overlay, fit-zoom-to-viewport, composite redraw). 10 * 11 * @param {{ 12 * saveState: (label?: string) => void, 13 * composite: () => void, 14 * fitZoom: () => void, 15 * showCanvasLoading: (label: string) => void, 16 * hideCanvasLoading: () => void, 17 * }} deps 18 */ 19import { state } from './state.js'; 20 21export function createCanvasTransforms({ saveState, composite, fitZoom, showCanvasLoading, hideCanvasLoading }) { 22 return { 23 /** 24 * Rotate the entire document by `deg` (90 / 180 / 270). 90 and 270 25 * swap canvas dimensions. Each layer is rotated around its own 26 * centre, then its centre is rotated around the old image centre 27 * and translated into the new image's frame. 28 * 29 * Wrapped in requestAnimationFrame because the rotation pass can 30 * block the UI for 0.5â2 s on big images â the spinner overlay 31 * paints before we block. 32 */ 33 rotateAll(deg) { 34 if (!state.layers.length) return; 35 saveState(`Rotate ${deg}°`); 36 showCanvasLoading('Rotatingâ¦'); 37 const oldW = state.imgWidth, oldH = state.imgHeight; 38 const swap = (deg === 90 || deg === 270); 39 const newW = swap ? oldH : oldW; 40 const newH = swap ? oldW : oldH; 41 const rad = (deg * Math.PI) / 180; 42 const cos = Math.cos(rad), sin = Math.sin(rad); 43 requestAnimationFrame(() => { 44 try { 45 for (const layer of state.layers) { 46 const lw = layer.canvas.width, lh = layer.canvas.height; 47 const off = state.layerOffsets.get(layer.id) || { x: 0, y: 0 }; 48 // Layer centre in old image coords. 49 const cx = off.x + lw / 2; 50 const cy = off.y + lh / 2; 51 // Rotate the centre around the old image centre and 52 // translate so the new image centre lands at (newW/2, newH/2). 53 const dx = cx - oldW / 2; 54 const dy = cy - oldH / 2; 55 const nx = dx * cos - dy * sin + newW / 2; 56 const ny = dx * sin + dy * cos + newH / 2; 57 // New per-layer dims: swap when 90/270. 58 const newLw = swap ? lh : lw; 59 const newLh = swap ? lw : lh; 60 const tmp = document.createElement('canvas'); 61 tmp.width = newLw; tmp.height = newLh; 62 const tctx = tmp.getContext('2d'); 63 tctx.translate(newLw / 2, newLh / 2); 64 tctx.rotate(rad); 65 tctx.drawImage(layer.canvas, -lw / 2, -lh / 2); 66 layer.canvas.width = newLw; 67 layer.canvas.height = newLh; 68 layer.ctx.drawImage(tmp, 0, 0); 69 // The adjustment-render caches are keyed only by the adjustment 70 // signature, which rotation doesn't change â so composite would draw 71 // the STALE pre-rotation cache (the "had to click twice" bug). Drop 72 // them so the next composite re-renders from the rotated canvas. 73 layer._adjCacheKey = null; 74 layer._adjFinalKey = null; 75 state.layerOffsets.set(layer.id, { 76 x: Math.round(nx - newLw / 2), 77 y: Math.round(ny - newLh / 2), 78 }); 79 } 80 state.imgWidth = newW; 81 state.imgHeight = newH; 82 state.mainCanvas.width = newW; 83 state.mainCanvas.height = newH; 84 if (state.maskCanvas) { 85 state.maskCanvas.width = newW; 86 state.maskCanvas.height = newH; 87 } 88 const sizeLabel = document.getElementById('ge-canvas-size'); 89 if (sizeLabel) sizeLabel.textContent = `${newW}Ã${newH}`; 90 fitZoom(); 91 composite(); 92 } finally { 93 hideCanvasLoading(); 94 } 95 }); 96 }, 97 98 /** 99 * Mirror every layer horizontally ('h') or vertically ('v'). 100 * Canvas dimensions don't change. Each layer offset is reflected 101 * around the image centre. 102 */ 103 flipAll(axis) { 104 if (!state.layers.length) return; 105 saveState(axis === 'h' ? 'Flip horizontal' : 'Flip vertical'); 106 for (const layer of state.layers) { 107 const lw = layer.canvas.width, lh = layer.canvas.height; 108 const tmp = document.createElement('canvas'); 109 tmp.width = lw; tmp.height = lh; 110 const tctx = tmp.getContext('2d'); 111 tctx.save(); 112 if (axis === 'h') { tctx.translate(lw, 0); tctx.scale(-1, 1); } 113 else { tctx.translate(0, lh); tctx.scale(1, -1); } 114 tctx.drawImage(layer.canvas, 0, 0);
115 tctx.restore(); 116 layer.ctx.clearRect(0, 0, lw, lh); 117 layer.ctx.drawImage(tmp, 0, 0); 118 // Invalidate the adjustment-render caches (keyed by adjustment sig only) 119 // so composite redraws from the flipped canvas, not a stale cache. 120 layer._adjCacheKey = null; 121 layer._adjFinalKey = null; 122 const off = state.layerOffsets.get(layer.id) || { x: 0, y: 0 }; 123 if (axis === 'h') { 124 state.layerOffsets.set(layer.id, { x: state.imgWidth - off.x - lw, y: off.y }); 125 } else { 126 state.layerOffsets.set(layer.id, { x: off.x, y: state.imgHeight - off.y - lh }); 127 } 128 } 129 composite(); 130 }, 131 }; 132}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.