diff options
Diffstat (limited to 'tools')
| -rw-r--r-- | tools/adjust.html | 554 |
1 files changed, 205 insertions, 349 deletions
diff --git a/tools/adjust.html b/tools/adjust.html index 3b5b243..429bffc 100644 --- a/tools/adjust.html +++ b/tools/adjust.html @@ -1,419 +1,275 @@ -<!-- tools for https://colorifyai.art/photo-to-sketch/#playground in 'Ink Sketch' mode --> - <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> -<title>Align & Optimize</title> +<title>Align & Optimize - Explicit Fix</title> <style> -body{font-family:sans-serif;text-align:center;background:#f5f5f5} -canvas{border:1px solid #ccc;margin-top:10px;cursor:grab} -textarea{width:90%;height:90px;margin:10px;font-family:monospace;font-size:12px} -button{margin:5px} -button:disabled{opacity:0.5} -#status{font-size:12px;color:#666;height:16px} + body { font-family: sans-serif; text-align: center; background: #f5f5f5; margin: 20px; } + canvas { border: 1px solid #333; background: #fff; cursor: grab; box-shadow: 0 4px 10px rgba(0,0,0,0.1); } + .panel { background: white; padding: 20px; border-radius: 8px; display: inline-block; border: 1px solid #ccc; margin-bottom: 10px; } + textarea { width: 90%; height: 100px; font-family: monospace; font-size: 11px; margin-top: 10px; } + button { padding: 10px 15px; margin: 5px; cursor: pointer; font-weight: bold; } + #status { color: red; height: 20px; font-weight: bold; } </style> </head> <body> -<h3>Align & Optimize</h3> - -<textarea id="cmd"></textarea><br> -<button id="copy">Copy commands</button><br> - -<input type="file" id="f1"> -<input type="file" id="f2"><br><br> +<h3>Align & Optimize (Asymmetric)</h3> -<button id="opt">Optimize</button> -<button id="optZoom">Optimize Zoom</button> -<button id="crop">Crop</button> -<button id="dl" disabled>Download</button> -<br> -Iterations: <input type="range" id="iterSlider" min="2" max="30" value="8" style="vertical-align:middle;width:120px"> -<span id="iterVal">8</span> +<div class="panel"> + <input type="file" id="f1" accept="image/*"> + <input type="file" id="f2" accept="image/*"><br><br> + + <button id="opt">Optimize All</button> + <button id="optZoom">Optimize Zoom (Space)</button> + <button id="crop">Prepare Crop</button> + <button id="dl" disabled>Download Images</button> + <br><br> + Iterations: <input type="range" id="iterSlider" min="5" max="50" value="15"> + <span id="iterVal">15</span> +</div> -<div>MSE: <span id="score">-</span> Overlap: <span id="overlap">-</span></div> <div id="status"></div> +<div>MSE: <span id="score">-</span> | Overlap: <span id="overlap">-</span></div> -<canvas id="cv"></canvas> +<canvas id="cv"></canvas><br> + +<textarea id="cmd" readonly></textarea><br> +<button id="copy">Copy ImageMagick Commands</button> <script> const canvas = document.getElementById('cv'); -function ctx() { return canvas.getContext('2d'); } +const ctx = canvas.getContext('2d'); const scoreEl = document.getElementById('score'); const overlapEl = document.getElementById('overlap'); const statusEl = document.getElementById('status'); const cmdEl = document.getElementById('cmd'); let i1 = new Image(), i2 = new Image(), l1 = 0, l2 = 0; -let name1 = "image1.png", name2 = "image2.png"; - +let name1 = "img1.png", name2 = "img2.png"; let ox = 0, oy = 0, sx = 1, sy = 1; let mx = 0, my = 0, drag = 0, lx = 0, ly = 0; - -let out1, out2; -let pyr1 = [], pyr2 = []; +let pyr1 = [], pyr2 = [], out1, out2; const LEVELS = 4; - let optimizing = false; -// --- pyramid --- -// FIX: clamp x+1 within row to avoid cross-row bleed +// --- Pyramid logic --- function buildPyramid(img) { - let levels = []; - let cv = document.createElement('canvas'); - cv.width = img.width; cv.height = img.height; - let x = cv.getContext('2d'); - x.drawImage(img, 0, 0); - - let d = x.getImageData(0, 0, img.width, img.height).data; - let g = new Float32Array(img.width * img.height); - for (let i = 0, j = 0; i < d.length; i += 4, j++) - g[j] = 0.299 * d[i] + 0.587 * d[i+1] + 0.114 * d[i+2]; - levels.push({ w: img.width, h: img.height, data: g }); + let levels = []; + let c = document.createElement('canvas'); + c.width = img.width; c.height = img.height; + let x = c.getContext('2d'); + x.drawImage(img, 0, 0); + let d = x.getImageData(0, 0, img.width, img.height).data; + let g = new Float32Array(img.width * img.height); + for (let i = 0, j = 0; i < d.length; i += 4, j++) + g[j] = 0.299 * d[i] + 0.587 * d[i+1] + 0.114 * d[i+2]; + levels.push({ w: img.width, h: img.height, data: g }); - for (let l = 1; l < LEVELS; l++) { - let p = levels[l-1]; - let w2 = Math.floor(p.w / 2), h2 = Math.floor(p.h / 2); - let g2 = new Float32Array(w2 * h2); - for (let y = 0; y < h2; y++) { - for (let xi = 0; xi < w2; xi++) { - // clamp xi+1 to stay within the row (p.w-1) - let x0 = 2 * xi, x1 = Math.min(2 * xi + 1, p.w - 1); - let y0 = 2 * y, y1 = Math.min(2 * y + 1, p.h - 1); - g2[y * w2 + xi] = ( - p.data[y0 * p.w + x0] + p.data[y0 * p.w + x1] + - p.data[y1 * p.w + x0] + p.data[y1 * p.w + x1] - ) * 0.25; - } + for (let l = 1; l < LEVELS; l++) { + let p = levels[l-1], w2 = Math.floor(p.w / 2), h2 = Math.floor(p.h / 2); + let g2 = new Float32Array(w2 * h2); + for (let y = 0; y < h2; y++) { + for (let xi = 0; xi < w2; xi++) { + let x0 = 2 * xi, x1 = Math.min(2 * xi + 1, p.w - 1); + let y0 = 2 * y, y1 = Math.min(2 * y + 1, p.h - 1); + g2[y * w2 + xi] = (p.data[y0 * p.w + x0] + p.data[y0 * p.w + x1] + + p.data[y1 * p.w + x0] + p.data[y1 * p.w + x1]) * 0.25; + } + } + levels.push({ w: w2, h: h2, data: g2 }); } - levels.push({ w: w2, h: h2, data: g2 }); - } - return levels; + return levels; } -// --- bilinear sample with bounds check --- -// FIX: return NaN for out-of-bounds so caller can skip; clamp indices correctly function sample(img, x, y) { - if (x < 0 || y < 0 || x >= img.w || y >= img.h) return NaN; - let x0 = Math.floor(x), y0 = Math.floor(y); - let x1 = Math.min(x0 + 1, img.w - 1); // FIX: was img.w (off-by-one) - let y1 = Math.min(y0 + 1, img.h - 1); // FIX: was img.h (off-by-one) - let dx = x - x0, dy = y - y0; - let i00 = img.data[y0 * img.w + x0]; - let i10 = img.data[y0 * img.w + x1]; - let i01 = img.data[y1 * img.w + x0]; - let i11 = img.data[y1 * img.w + x1]; - return (i00 * (1-dx) + i10 * dx) * (1-dy) + (i01 * (1-dx) + i11 * dx) * dy; + if (x < 0 || y < 0 || x >= img.w || y >= img.h) return NaN; + let x0 = Math.floor(x), y0 = Math.floor(y), x1 = Math.min(x0 + 1, img.w - 1), y1 = Math.min(y0 + 1, img.h - 1); + let dx = x - x0, dy = y - y0; + return (img.data[y0 * img.w + x0] * (1-dx) + img.data[y0 * img.w + x1] * dx) * (1-dy) + + (img.data[y1 * img.w + x0] * (1-dx) + img.data[y1 * img.w + x1] * dx) * dy; } -// --- MSE over overlapping region only --- -// FIX: compute the intersection of A's rect and the mapped B rect in A-space, -// then only accumulate pixels where B is defined. -function computeMSELevel(level) { - if (!pyr1.length || !pyr2.length) return Infinity; - let A = pyr1[level], B = pyr2[level]; - let sF = 1 / (2 ** level); - - // B's bounding box in A-pixel space at this pyramid level - let tox = ox * sF, toy = oy * sF; - let tsx = sx, tsy = sy; - - // B covers [tox, tox + B.w*tsx] x [toy, toy + B.h*tsy] in A-space - let bx0 = tox, bx1 = tox + B.w * tsx; - let by0 = toy, by1 = toy + B.h * tsy; - - // Intersect with A's extent [0, A.w] x [0, A.h] - let ix0 = Math.max(0, Math.ceil(bx0)); - let iy0 = Math.max(0, Math.ceil(by0)); - let ix1 = Math.min(A.w, Math.floor(bx1)); - let iy1 = Math.min(A.h, Math.floor(by1)); - - if (ix1 <= ix0 || iy1 <= iy0) return Infinity; +function computeMSE(level, tx, ty, tsx, tsy) { + if (!l1 || !l2) return Infinity; + let A = pyr1[level], B = pyr2[level], sF = 1 / (2**level); + let lox = tx * sF, loy = ty * sF; + let ix0 = Math.max(0, Math.ceil(lox)), iy0 = Math.max(0, Math.ceil(loy)); + let ix1 = Math.min(A.w, Math.floor(lox + B.w * tsx)), iy1 = Math.min(A.h, Math.floor(loy + B.h * tsy)); + if (ix1 <= ix0 || iy1 <= iy0) return Infinity; + let mse = 0, n = 0; + for (let y = iy0; y < iy1; y++) { + for (let x = ix0; x < ix1; x++) { + let v = sample(B, (x - lox) / tsx, (y - loy) / tsy); + if (isNaN(v)) continue; + let d = A.data[y * A.w + x] - v; + mse += d * d; n++; + } + } + if (level === 0) overlapEl.textContent = n > 0 ? (n/(A.w*A.h)*100).toFixed(1) + '%' : '0%'; + return n ? mse / n : Infinity; +} - let mse = 0, n = 0; - for (let y = iy0; y < iy1; y++) { - for (let x = ix0; x < ix1; x++) { - let bx = (x - tox) / tsx; - let by = (y - toy) / tsy; - let bVal = sample(B, bx, by); - if (isNaN(bVal)) continue; - let d = A.data[y * A.w + x] - bVal; - mse += d * d; - n++; +function draw() { + if (!l1) return; + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.drawImage(i1, 0, 0); + if (l2) { + ctx.save(); ctx.globalAlpha = 0.5; + ctx.translate(ox, oy); ctx.scale(sx, sy); + ctx.drawImage(i2, 0, 0); ctx.restore(); } - } + scoreEl.textContent = computeMSE(0, ox, oy, sx, sy).toFixed(2); + updateCmd(); +} - // Update overlap display (only at level 0) - if (level === 0) { - let pct = n / (A.w * A.h) * 100; - overlapEl.textContent = n > 0 ? pct.toFixed(1) + '%' : '0%'; - } +// --- NEW WHEEL LOGIC (EXPLICIT) --- +canvas.onwheel = function(e) { + if (!l2) return; + e.preventDefault(); // Stop browser zoom - return n ? mse / n : Infinity; -} + // 1. Identify scroll direction and magnitude + // When Shift is pressed, many browsers move vertical scroll to deltaX + let delta = (Math.abs(e.deltaX) > Math.abs(e.deltaY)) ? e.deltaX : e.deltaY; + let factor = delta < 0 ? 1.05 : 0.95; -function computeMSE() { return computeMSELevel(0); } + // 2. Identify the fixed point (anchor) in image coordinates + // We want the point in i2 that is currently under the mouse to stay there + let imagePointX = (e.offsetX - ox) / sx; + let imagePointY = (e.offsetY - oy) / sy; -// --- draw --- -function draw() { - if (!l1) return; - const c = ctx(); - c.clearRect(0, 0, canvas.width, canvas.height); - c.drawImage(i1, 0, 0); - if (l2) { - c.save(); - c.globalAlpha = 0.5; - c.translate(ox, oy); - c.scale(sx, sy); - c.drawImage(i2, 0, 0); - c.restore(); - } - scoreEl.textContent = computeMSE().toFixed(2); - updateCmd(); -} + // 3. Apply scaling based on modifiers + if (e.shiftKey) { + // Shift + Wheel -> SY ONLY + sy *= factor; + } else if (e.ctrlKey) { + // Ctrl + Wheel -> SX ONLY + sx *= factor; + } else { + // Wheel Only -> UNIFORM + sx *= factor; + sy *= factor; + } -// --- load --- -f1.onchange = e => { - let f = e.target.files[0]; if (!f) return; - name1 = f.name; - i1 = new Image(); - i1.onload = () => { - l1 = 1; - canvas.width = i1.width; canvas.height = i1.height; - pyr1 = buildPyramid(i1); - draw(); - }; - i1.src = URL.createObjectURL(f); -}; + // 4. Update offsets to maintain the anchor point + // Formula: NewOffset = MousePos - (ImagePoint * NewScale) + ox = e.offsetX - (imagePointX * sx); + oy = e.offsetY - (imagePointY * sy); -f2.onchange = e => { - let f = e.target.files[0]; if (!f) return; - name2 = f.name; - i2 = new Image(); - i2.onload = () => { - l2 = 1; - pyr2 = buildPyramid(i2); - ox = (canvas.width - i2.width) / 2; - oy = (canvas.height - i2.height) / 2; draw(); - }; - i2.src = URL.createObjectURL(f); }; -// --- mouse --- +canvas.onmousedown = e => { drag = 1; lx = e.offsetX; ly = e.offsetY; canvas.style.cursor = "grabbing"; }; +canvas.onmouseup = canvas.onmouseleave = () => { drag = 0; canvas.style.cursor = "grab"; }; canvas.onmousemove = e => { - mx = e.offsetX; my = e.offsetY; - if (!drag) return; - ox += (e.offsetX - lx); - oy += (e.offsetY - ly); - lx = e.offsetX; ly = e.offsetY; - draw(); -}; -canvas.onmousedown = e => { - drag = 1; - lx = e.offsetX; ly = e.offsetY; - canvas.style.cursor = "grabbing"; -}; -canvas.onmouseup = canvas.onmouseleave = () => { - drag = 0; - canvas.style.cursor = "grab"; + mx = e.offsetX; my = e.offsetY; + if (drag) { ox += (mx - lx); oy += (my - ly); lx = mx; ly = my; draw(); } }; -// --- scroll to zoom (mouse-anchored) --- -canvas.onwheel = e => { - if (!l2) return; - e.preventDefault(); - let factor = e.deltaY < 0 ? 1.01 : 0.99; - let ix = (mx - ox) / sx; - let iy = (my - oy) / sy; - sx *= factor; sy *= factor; - ox = mx - ix * sx; - oy = my - iy * sy; - draw(); -}; - -// --- keyboard --- -document.addEventListener("keydown", e => { - // FIX: don't intercept keys when typing in textarea - if (document.activeElement === cmdEl) return; +window.onkeydown = e => { if(e.code === "Space" && !optimizing) { e.preventDefault(); optZoom.click(); } }; - if (!l2) return; - if (e.key.startsWith("Arrow")) e.preventDefault(); - const PAN_STEP = 1; - const SCALE_STEP = 0.002; - if (e.shiftKey) { - let ix = (mx - ox) / sx; - let iy = (my - oy) / sy; - if (e.key === "ArrowRight") sx += SCALE_STEP; - if (e.key === "ArrowLeft") sx -= SCALE_STEP; - if (e.key === "ArrowUp") sy += SCALE_STEP; - if (e.key === "ArrowDown") sy -= SCALE_STEP; - ox = mx - ix * sx; - oy = my - iy * sy; - } else { - if (e.key === "ArrowRight") ox += PAN_STEP; - if (e.key === "ArrowLeft") ox -= PAN_STEP; - if (e.key === "ArrowUp") oy -= PAN_STEP; - if (e.key === "ArrowDown") oy += PAN_STEP; - } - draw(); -}); +// --- Optimizers --- +async function runOpt(mode) { + if (!l1 || !l2) return; + optimizing = true; statusEl.textContent = "OPTIMIZING..."; + const iters = parseInt(iterSlider.value); + + // For Zoom optimization, we lock the anchor once at the current mouse position + const anchorX = (mx - ox) / sx; + const anchorY = (my - oy) / sy; -// FIX: space only fires optimize when not in textarea -document.addEventListener("keydown", e => { - if (document.activeElement === cmdEl) return; - if (e.code === "Space") { - e.preventDefault(); - opt.click(); - } -}); + for (let l = LEVELS - 1; l >= 0; l--) { + let currentBest = { ox, oy, sx, sy, score: computeMSE(l, ox, oy, sx, sy) }; + for (let i = 0; i < iters; i++) { + let stepO = (2 ** l) / (i + 1); + let stepF = 1 + 0.03 / (i + 1); -// --- slider --- -const iterSlider = document.getElementById('iterSlider'); -const iterVal = document.getElementById('iterVal'); -iterSlider.oninput = () => iterVal.textContent = iterSlider.value; -function numIters() { return parseInt(iterSlider.value); } + // Grid search around current parameters + for (let dx of (mode === 'all' ? [-1, 0, 1] : [0])) { + for (let dy of (mode === 'all' ? [-1, 0, 1] : [0])) { + for (let dsx of [-1, 0, 1]) { + for (let dsy of [-1, 0, 1]) { + // Calculate test candidates + let testSX = sx * Math.pow(stepF, dsx); + let testSY = sy * Math.pow(stepF, dsy); + let testOX, testOY; -// --- set busy state --- -function setBusy(busy) { - optimizing = busy; - opt.disabled = busy; - optZoom.disabled = busy; - statusEl.textContent = busy ? "Optimizing…" : ""; -} + if (mode === 'zoom') { + // Locked to mouse position + testOX = mx - (anchorX * testSX); + testOY = my - (anchorY * testSY); + } else { + // Free translation + testOX = ox + (dx * stepO); + testOY = oy + (dy * stepO); + } -// --- probe MSE without mutating globals --- -// Temporarily sets ox/oy/sx/sy, evaluates, then restores. -function probeMSE(tox, toy, tsx, tsy, level) { - let pox = ox, poy = oy, psx = sx, psy = sy; - ox = tox; oy = toy; sx = tsx; sy = tsy; - let s = computeMSELevel(level); - ox = pox; oy = poy; sx = psx; sy = psy; - return s; -} + if (testSX < 0.01 || testSY < 0.01) continue; -// --- optimizer (translation + uniform scale) --- -opt.onclick = async () => { - if (!l1 || !l2 || optimizing) return; - setBusy(true); - const ITERS = numIters(); - for (let level = LEVELS - 1; level >= 0; level--) { - let best = { ox, oy, sx, sy, score: computeMSELevel(level) }; - for (let iter = 0; iter < ITERS; iter++) { - let stepO = (2 ** level) / (iter + 1); - let stepS = 0.01 / (iter + 1); - for (let dx of [-1, 0, 1]) - for (let dy of [-1, 0, 1]) - for (let ds of [-1, 0, 1]) { - let tox = best.ox + dx * stepO; - let toy = best.oy + dy * stepO; - let ts = best.sx + ds * stepS; - if (ts <= 0.01) continue; - // FIX: probe without touching globals - let s = probeMSE(tox, toy, ts, ts, level); - if (s < best.score) - best = { ox: tox, oy: toy, sx: ts, sy: ts, score: s }; - } - // commit the best found this iteration - ox = best.ox; oy = best.oy; sx = best.sx; sy = best.sy; - draw(); - await new Promise(r => setTimeout(r, 0)); + let s = computeMSE(l, testOX, testOY, testSX, testSY); + if (s < currentBest.score) { + currentBest = { ox: testOX, oy: testOY, sx: testSX, sy: testSY, score: s }; + } + } + } + } + } + // Apply iteration results + ox = currentBest.ox; oy = currentBest.oy; sx = currentBest.sx; sy = currentBest.sy; + draw(); + await new Promise(r => setTimeout(r, 0)); + } } - } - setBusy(false); -}; - -// --- optimize zoom (mouse-anchor invariant) --- -optZoom.onclick = async () => { - if (!l1 || !l2 || optimizing) return; - setBusy(true); - const ITERS = numIters(); - let ax = mx, ay = my; - - for (let level = LEVELS - 1; level >= 0; level--) { - // best stores the full transform, not just sx - let best = { ox, oy, sx, sy, score: computeMSELevel(level) }; - - for (let iter = 0; iter < ITERS; iter++) { - let stepS = 0.02 / (iter + 1); - - for (let ds of [-1, 0, 1]) { - let ts = best.sx + ds * stepS; - if (ts <= 0.01) continue; - // anchor invariant: point (ax,ay) in canvas == point (ix,iy) in image2 - // must satisfy: ax = ox + ix*sx => ix = (ax - ox) / sx - // after scale change to ts: tox = ax - ix*ts - // derive from COMMITTED globals (best.ox/sx), not mid-loop state - let ix = (ax - best.ox) / best.sx; - let iy = (ay - best.oy) / best.sy; - let tox = ax - ix * ts; - let toy = ay - iy * ts; - let s = probeMSE(tox, toy, ts, ts, level); - if (s < best.score) best = { ox: tox, oy: toy, sx: ts, sy: ts, score: s }; - } + optimizing = false; statusEl.textContent = ""; +} - // commit best directly — no re-derivation needed - ox = best.ox; oy = best.oy; sx = best.sx; sy = best.sy; +opt.onclick = () => runOpt('all'); +optZoom.onclick = () => runOpt('zoom'); - draw(); - await new Promise(r => setTimeout(r, 0)); - } - } - setBusy(false); +// --- I/O --- +f1.onchange = e => { + let f = e.target.files[0]; if (!f) return; + name1 = f.name; i1 = new Image(); + i1.onload = () => { l1 = 1; canvas.width = i1.width; canvas.height = i1.height; pyr1 = buildPyramid(i1); draw(); }; + i1.src = URL.createObjectURL(f); +}; +f2.onchange = e => { + let f = e.target.files[0]; if (!f) return; + name2 = f.name; i2 = new Image(); + i2.onload = () => { l2 = 1; pyr2 = buildPyramid(i2); ox = (canvas.width-i2.width)/2; oy = (canvas.height-i2.height)/2; draw(); }; + i2.src = URL.createObjectURL(f); }; -// --- crop --- crop.onclick = () => { - let x2 = ox, y2 = oy, w2 = i2.width * sx, h2 = i2.height * sy; - let x0 = Math.max(0, x2), y0 = Math.max(0, y2); - let x1 = Math.min(canvas.width, x2 + w2), y1 = Math.min(canvas.height, y2 + h2); - let w = x1 - x0, h = y1 - y0; - if (w <= 0 || h <= 0) return alert("no overlap"); - - out1 = document.createElement('canvas'); - out2 = document.createElement('canvas'); - out1.width = out2.width = w; out1.height = out2.height = h; - - out1.getContext('2d').drawImage(i1, x0, y0, w, h, 0, 0, w, h); - out2.getContext('2d').drawImage( - i2, (x0 - ox) / sx, (y0 - oy) / sy, w / sx, h / sy, 0, 0, w, h - ); - - dl.disabled = 0; + let x0 = Math.max(0, ox), y0 = Math.max(0, oy); + let w = Math.floor(Math.min(canvas.width, ox + i2.width * sx) - x0); + let h = Math.floor(Math.min(canvas.height, oy + i2.height * sy) - y0); + if (w <= 5 || h <= 5) return alert("Overlap too small!"); + out1 = document.createElement('canvas'); out2 = document.createElement('canvas'); + out1.width = out2.width = w; out1.height = out2.height = h; + out1.getContext('2d').drawImage(i1, x0, y0, w, h, 0, 0, w, h); + out2.getContext('2d').drawImage(i2, (x0 - ox)/sx, (y0 - oy)/sy, w/sx, h/sy, 0, 0, w, h); + document.getElementById('dl').disabled = false; }; -// --- download --- -dl.onclick = () => { - let save = (cv, n) => { - let a = document.createElement('a'); - a.download = n + '.clipped.png'; - a.href = cv.toDataURL(); - a.click(); - }; - save(out1, 'image1'); - save(out2, 'image2'); +document.getElementById('dl').onclick = () => { + const s = (c, n) => { let a = document.createElement('a'); a.download = n; a.href = c.toDataURL(); a.click(); }; + s(out1, "clipped_" + name1); s(out2, "clipped_" + name2); }; -// --- copy --- -copy.onclick = () => navigator.clipboard.writeText(cmdEl.value); - -// --- ImageMagick command --- function updateCmd() { - if (!l1 || !l2) return; - let x2 = ox, y2 = oy, w2 = i2.width * sx, h2 = i2.height * sy; - let x0 = Math.max(0, x2), y0 = Math.max(0, y2); - let x1 = Math.min(canvas.width, x2 + w2), y1 = Math.min(canvas.height, y2 + h2); - let w = Math.floor(x1 - x0), h = Math.floor(y1 - y0); - if (w <= 0 || h <= 0) return; - let crop = `${w}x${h}+${Math.round(x0)}+${Math.round(y0)}`; - // AffineProjection: a,b,d,e,tx,ty (uniform scale → a=e=sx) - let a = sx, b = 0, d = 0, e = sy, tx = ox, ty = oy; - cmdEl.value = -`magick "${name1}" -crop ${crop} +repage "${name1}.clipped.png" -magick "${name2}" -virtual-pixel black -distort AffineProjection "${a},${b},${d},${e},${tx},${ty}" -crop ${crop} +repage "${name2}.clipped.png"`; + if (!l1 || !l2) return; + let x0 = Math.max(0, ox), y0 = Math.max(0, oy); + let w = Math.floor(Math.min(canvas.width, ox + i2.width * sx) - x0); + let h = Math.floor(Math.min(canvas.height, oy + i2.height * sy) - y0); + if (w <= 0 || h <= 0) return; + let cr = `${w}x${h}+${Math.round(x0)}+${Math.round(y0)}`; + cmdEl.value = `magick "${name1}" -crop ${cr} +repage "clipped_${name1}"\n` + + `magick "${name2}" -virtual-pixel black -distort AffineProjection "${sx.toFixed(6)},0,0,${sy.toFixed(6)},${ox.toFixed(2)},${oy.toFixed(2)}" -crop ${cr} +repage "clipped_${name2}"`; } -</script> +document.getElementById('copy').onclick = () => { cmdEl.select(); navigator.clipboard.writeText(cmdEl.value); }; +iterSlider.oninput = () => document.getElementById('iterVal').textContent = iterSlider.value; +</script> </body> </html>
\ No newline at end of file |
