From 1f555869a03ec1940fa68c18d4ebdf58a6a1ed9f Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 25 Apr 2026 04:02:43 +0100 Subject: [PATCH] Add viewer inspect zoom controls --- frontend/src/app.ts | 26 ++++++++++++++++++++------ src/repixelizer/gui_static/app.js | 22 +++++++++++++++++----- src/repixelizer/gui_static/index.html | 12 ++++++++++++ src/repixelizer/gui_static/styles.css | 25 +++++++++++++++++++++++++ 4 files changed, 74 insertions(+), 11 deletions(-) diff --git a/frontend/src/app.ts b/frontend/src/app.ts index aace17e..b3b9aaf 100644 --- a/frontend/src/app.ts +++ b/frontend/src/app.ts @@ -104,6 +104,7 @@ type AppState = { solverStepBudget: number; lossAxisMax: number | null; eventLog: LogItem[]; + viewerInspectZoom: number; paintColor: [number, number, number, number]; editorBaseAsset: ImageAsset | null; editorDirty: boolean; @@ -143,6 +144,7 @@ const leftCanvas = byId("leftCanvas"); const rightCanvas = byId("rightCanvas"); const leftVizLabel = byId("leftVizLabel"); const rightVizLabel = byId("rightVizLabel"); +const inspectZoomInput = byId("inspectZoomInput"); const paintSwatch = byId("paintSwatch"); const zoomInput = byId("zoomInput"); const zoomValue = byId("zoomValue"); @@ -222,6 +224,7 @@ const state: AppState = { solverStepBudget: 0, lossAxisMax: null, eventLog: [], + viewerInspectZoom: Number(inspectZoomInput.value), paintColor: [255, 255, 255, 255], editorBaseAsset: null, editorDirty: false, @@ -423,16 +426,19 @@ async function drawAssetInspection( const imageWidth = image.naturalWidth || asset.width; const imageHeight = image.naturalHeight || asset.height; - const viewportWidth = Math.min(displayWidth, imageWidth); - const viewportHeight = Math.min(displayHeight, imageHeight); + const zoomMultiplier = Math.max(1, state.viewerInspectZoom || 1); + const viewportWidth = Math.max(1, Math.min(Math.floor(displayWidth / zoomMultiplier), imageWidth)); + const viewportHeight = Math.max(1, Math.min(Math.floor(displayHeight / zoomMultiplier), imageHeight)); + const destWidth = Math.min(displayWidth, viewportWidth * zoomMultiplier); + const destHeight = Math.min(displayHeight, viewportHeight * zoomMultiplier); const focusX = Math.max(0, Math.min(1, focus.xRatio)); const focusY = Math.max(0, Math.min(1, focus.yRatio)); const centerX = imageWidth <= displayWidth ? imageWidth * 0.5 : focusX * imageWidth; const centerY = imageHeight <= displayHeight ? imageHeight * 0.5 : focusY * imageHeight; const sourceX = Math.max(0, Math.min(imageWidth - viewportWidth, Math.round(centerX - viewportWidth * 0.5))); const sourceY = Math.max(0, Math.min(imageHeight - viewportHeight, Math.round(centerY - viewportHeight * 0.5))); - const destX = Math.floor((displayWidth - viewportWidth) * 0.5); - const destY = Math.floor((displayHeight - viewportHeight) * 0.5); + const destX = Math.floor((displayWidth - destWidth) * 0.5); + const destY = Math.floor((displayHeight - destHeight) * 0.5); context.drawImage( image, @@ -442,8 +448,8 @@ async function drawAssetInspection( viewportHeight, destX, destY, - viewportWidth, - viewportHeight, + destWidth, + destHeight, ); } @@ -1549,6 +1555,14 @@ runButton.addEventListener("click", () => { void startRun(); }); +inspectZoomInput.addEventListener("change", () => { + const value = Number(inspectZoomInput.value); + state.viewerInspectZoom = Number.isFinite(value) && value > 0 ? value : 1; + if (viewerInspect) { + scheduleViewerRender(); + } +}); + zoomInput.addEventListener("input", () => { setZoom(Number(zoomInput.value)); }); diff --git a/src/repixelizer/gui_static/app.js b/src/repixelizer/gui_static/app.js index ab02910..a7efe93 100644 --- a/src/repixelizer/gui_static/app.js +++ b/src/repixelizer/gui_static/app.js @@ -24,6 +24,7 @@ const leftCanvas = byId("leftCanvas"); const rightCanvas = byId("rightCanvas"); const leftVizLabel = byId("leftVizLabel"); const rightVizLabel = byId("rightVizLabel"); +const inspectZoomInput = byId("inspectZoomInput"); const paintSwatch = byId("paintSwatch"); const zoomInput = byId("zoomInput"); const zoomValue = byId("zoomValue"); @@ -99,6 +100,7 @@ const state = { solverStepBudget: 0, lossAxisMax: null, eventLog: [], + viewerInspectZoom: Number(inspectZoomInput.value), paintColor: [255, 255, 255, 255], editorBaseAsset: null, editorDirty: false, @@ -279,17 +281,20 @@ async function drawAssetInspection(canvas, asset, focus) { context.fillRect(0, 0, displayWidth, displayHeight); const imageWidth = image.naturalWidth || asset.width; const imageHeight = image.naturalHeight || asset.height; - const viewportWidth = Math.min(displayWidth, imageWidth); - const viewportHeight = Math.min(displayHeight, imageHeight); + const zoomMultiplier = Math.max(1, state.viewerInspectZoom || 1); + const viewportWidth = Math.max(1, Math.min(Math.floor(displayWidth / zoomMultiplier), imageWidth)); + const viewportHeight = Math.max(1, Math.min(Math.floor(displayHeight / zoomMultiplier), imageHeight)); + const destWidth = Math.min(displayWidth, viewportWidth * zoomMultiplier); + const destHeight = Math.min(displayHeight, viewportHeight * zoomMultiplier); const focusX = Math.max(0, Math.min(1, focus.xRatio)); const focusY = Math.max(0, Math.min(1, focus.yRatio)); const centerX = imageWidth <= displayWidth ? imageWidth * 0.5 : focusX * imageWidth; const centerY = imageHeight <= displayHeight ? imageHeight * 0.5 : focusY * imageHeight; const sourceX = Math.max(0, Math.min(imageWidth - viewportWidth, Math.round(centerX - viewportWidth * 0.5))); const sourceY = Math.max(0, Math.min(imageHeight - viewportHeight, Math.round(centerY - viewportHeight * 0.5))); - const destX = Math.floor((displayWidth - viewportWidth) * 0.5); - const destY = Math.floor((displayHeight - viewportHeight) * 0.5); - context.drawImage(image, sourceX, sourceY, viewportWidth, viewportHeight, destX, destY, viewportWidth, viewportHeight); + const destX = Math.floor((displayWidth - destWidth) * 0.5); + const destY = Math.floor((displayHeight - destHeight) * 0.5); + context.drawImage(image, sourceX, sourceY, viewportWidth, viewportHeight, destX, destY, destWidth, destHeight); } async function fileToImageAsset(file) { const dataUrl = await new Promise((resolve, reject) => { @@ -1292,6 +1297,13 @@ dropzone.addEventListener("drop", (event) => { runButton.addEventListener("click", () => { void startRun(); }); +inspectZoomInput.addEventListener("change", () => { + const value = Number(inspectZoomInput.value); + state.viewerInspectZoom = Number.isFinite(value) && value > 0 ? value : 1; + if (viewerInspect) { + scheduleViewerRender(); + } +}); zoomInput.addEventListener("input", () => { setZoom(Number(zoomInput.value)); }); diff --git a/src/repixelizer/gui_static/index.html b/src/repixelizer/gui_static/index.html index d45fc0d..1ba6047 100644 --- a/src/repixelizer/gui_static/index.html +++ b/src/repixelizer/gui_static/index.html @@ -61,6 +61,18 @@ +
+ +

Hold left mouse in either preview to inspect both panels together. Drag while holding to pan around. Release to drop back to fit view.

+
diff --git a/src/repixelizer/gui_static/styles.css b/src/repixelizer/gui_static/styles.css index 77bc568..e1ecd20 100644 --- a/src/repixelizer/gui_static/styles.css +++ b/src/repixelizer/gui_static/styles.css @@ -652,6 +652,27 @@ p { gap: 10px; } +.viewer-inspect-row { + display: flex; + align-items: end; + gap: 14px; + flex-wrap: wrap; + padding: 8px 2px 0; +} + +.viewer-inspect-field { + width: min(100%, 208px); + flex: 0 0 auto; +} + +.viewer-inspect-copy { + flex: 1 1 320px; + margin: 0; + font-size: 1rem; + line-height: 1.2; + color: var(--ui-text-muted); +} + .viz-card { margin: 0; padding: 10px; @@ -1019,6 +1040,10 @@ select:focus-visible { grid-template-columns: 1fr; } + .viewer-inspect-row { + align-items: start; + } + .card-head { flex-wrap: wrap; }