Add viewer inspect zoom controls

This commit is contained in:
Meta Vens 2026-04-25 04:02:43 +01:00
commit 1f555869a0
4 changed files with 74 additions and 11 deletions

View file

@ -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<HTMLCanvasElement>("leftCanvas");
const rightCanvas = byId<HTMLCanvasElement>("rightCanvas");
const leftVizLabel = byId<HTMLDivElement>("leftVizLabel");
const rightVizLabel = byId<HTMLDivElement>("rightVizLabel");
const inspectZoomInput = byId<HTMLSelectElement>("inspectZoomInput");
const paintSwatch = byId<HTMLDivElement>("paintSwatch");
const zoomInput = byId<HTMLInputElement>("zoomInput");
const zoomValue = byId<HTMLSpanElement>("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));
});

View file

@ -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));
});

View file

@ -61,6 +61,18 @@
<canvas id="rightCanvas"></canvas>
</figure>
</div>
<div class="viewer-inspect-row">
<label class="field viewer-inspect-field">
<span>Inspect Zoom</span>
<select id="inspectZoomInput">
<option value="1">Native source</option>
<option value="2">2x</option>
<option value="4">4x</option>
<option value="8">8x</option>
</select>
</label>
<p class="viewer-inspect-copy">Hold left mouse in either preview to inspect both panels together. Drag while holding to pan around. Release to drop back to fit view.</p>
</div>
</section>
<section class="card editor-panel">

View file

@ -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;
}