From fdec0f79a261f83283a1d78cf0c9614a896261f1 Mon Sep 17 00:00:00 2001 From: Powei Feng Date: Fri, 3 Oct 2025 14:44:53 -0700 Subject: [PATCH] renderdiff: [viewer] fix magnifier (#9290) - Fix the magnifier positioning for both compare and standalone mode. Simplified a lot of the logic and streamlined passing of states between components. - Slight css adjustments --- test/renderdiff/src/viewer_html/app.js | 249 ++++++------------ .../renderdiff/src/viewer_html/tiff-viewer.js | 99 ++++--- 2 files changed, 150 insertions(+), 198 deletions(-) diff --git a/test/renderdiff/src/viewer_html/app.js b/test/renderdiff/src/viewer_html/app.js index 62f371888c..5357e65752 100644 --- a/test/renderdiff/src/viewer_html/app.js +++ b/test/renderdiff/src/viewer_html/app.js @@ -57,11 +57,11 @@ class ExpandedPassedResult extends LitElement {
- +
`; } @@ -91,12 +91,12 @@ class ExpandedFailedResult extends LitElement { } return html` `; } @@ -106,15 +106,15 @@ customElements.define('expanded-failed-result', ExpandedFailedResult); class ExpandedComparisonResult extends LitElement { static get properties() { return { + labelWithType: {type: Boolean, attribute: "label-with-type" }, left: { type: Object }, right: { type: Object }, tests: { type: Array }, diffResult: { type: Object }, - showDiff: {type: Boolean }, leftViewType: { type: String }, rightViewType: { type: String }, - disableDropdowns: { type: Boolean }, magnifierEnabled: { type: Boolean }, + currentDiffImageData: {type: Object }, }; } @@ -127,17 +127,6 @@ class ExpandedComparisonResult extends LitElement { .viewer-container { display: flex; flex-direction: row; - } - #diffCanvas { - width: 100%; - height: 100%; - margin-top: 22px; - margin-bottom: 5px; - } - .diff-container { - position: relative; - } - .viewer-container { position: relative; } .selector { @@ -147,27 +136,25 @@ class ExpandedComparisonResult extends LitElement { constructor() { super(); + this.labelWithType = false; this.left = null; this.right = null; this.tests = []; - this.showDiff = false; this.leftImageLoaded = false; this.rightImageLoaded = false; this.leftViewType = 'golden'; this.rightViewType = 'golden'; - this.disableDropdowns = false; this.magnifierEnabled = true; - this.diffCanvasRect = null; this.originalDiffImageData = null; - this.globalMousePosition = null; + this.currentDiffImageData = null; this.addEventListener( 'image-loaded', (ev) => { - if (ev.detail.name == this.left.name) { + if (ev.detail.url == this._getUrl(this.leftViewType, this.left)) { this.leftImageLoaded = true; } - if (ev.detail.name == this.right.name) { + if (ev.detail.url == this._getUrl(this.rightViewType, this.right)) { this.rightImageLoaded = true; } if (this.leftImageLoaded && this.rightImageLoaded) { @@ -177,13 +164,17 @@ class ExpandedComparisonResult extends LitElement { ); } + _getUrl(viewType, test) { + return viewType == 'rendered' ? getCompUrl(test) : getGoldenUrl(test); + } + _viewer(name, choices, current, viewType) { - const url = viewType == 'rendered' ? getCompUrl(current) : getGoldenUrl(current); + const url = this._getUrl(viewType, current); + const label = this.labelWithType ? viewType : current.name; return html`
-
${current.name}
+
${label}
@@ -210,10 +201,8 @@ class ExpandedComparisonResult extends LitElement { }; } - const ctxLeft = canvasLeft.getContext('2d'); - const ctxRight = canvasRight.getContext('2d'); - const imgLeft = ctxLeft.getImageData(0, 0, canvasLeft.width, canvasLeft.height); - const imgRight = ctxRight.getImageData(0, 0, canvasRight.width, canvasRight.height); + const imgLeft = tiffViewerLeft.imgdata; + const imgRight = tiffViewerRight.imgdata; if (imgLeft.width !== imgRight.width || imgLeft.height !== imgRight.height) { console.error("Images have different dimensions"); @@ -262,25 +251,19 @@ class ExpandedComparisonResult extends LitElement { const diff = this._computeDiff(); if (diff.result == RES_DIFFERENT_PIXELS) { this.diffResult = diff; - this.showDiff = true; // Reset original diff data when new diff is computed this.originalDiffImageData = null; - } - } - - updated(props) { - if (this.showDiff && this.diffResult) { - const mult = this.shadowRoot.querySelector('#diffMultiplier').value; - this._updateDiffCanvas(this.diffResult, mult); + const multDiv = this.shadowRoot.querySelector('#diffMultiplier'); + if (multDiv) { + this._updateDiffCanvas(this.diffResult, multDiv.value); + } + } else { + this.diffResult = null; + this.currentDiffImageData = null; } } _updateDiffCanvas(diffResult, mult) { - const diffCanvas = this.shadowRoot.querySelector('#diffCanvas'); - const diffCtx = diffCanvas.getContext('2d'); - diffCanvas.width = diffResult.dim.width; - diffCanvas.height = diffResult.dim.height; - // Create a fresh copy of the original diff data to avoid mutation. const diffImgCopy = diffResult.diffImg.slice(); @@ -294,7 +277,6 @@ class ExpandedComparisonResult extends LitElement { // Create the ImageData from the modified copy. const imgData = new ImageData(diffImgCopy, diffResult.dim.width, diffResult.dim.height); - diffCtx.putImageData(imgData, 0, 0); // Store both original and current diff image data for magnifier if (!this.originalDiffImageData) { @@ -303,124 +285,47 @@ class ExpandedComparisonResult extends LitElement { this.currentDiffImageData = imgData; } - _onDiffMouseEnter(event) { - if (!this.showDiff || !this.currentDiffImageData) return; - this.diffCanvasRect = event.target.getBoundingClientRect(); - } - _onGlobalMouseLeave(event) { // Hide all magnifiers when mouse leaves the viewer container - const leftViewer = this.shadowRoot.querySelector('#viewer-left'); - const rightViewer = this.shadowRoot.querySelector('#viewer-right'); - const diffMagnifier = this.shadowRoot.getElementById('diffMagnifier'); - - if (leftViewer) { - const leftMagnifier = leftViewer.shadowRoot?.getElementById('magnifier'); - if (leftMagnifier) leftMagnifier.hide(); + for (const name of ['left', 'right', 'diff']) { + const viewer = this.shadowRoot.querySelector('#viewer-' + name); + if (viewer) { + const mag = viewer.shadowRoot?.getElementById('magnifier'); + if (mag) { + mag.hide(); + } + } } - - if (rightViewer) { - const rightMagnifier = rightViewer.shadowRoot?.getElementById('magnifier'); - if (rightMagnifier) rightMagnifier.hide(); - } - - if (diffMagnifier) diffMagnifier.hide(); } _onGlobalMouseMove(event) { - this.globalMousePosition = { clientX: event.clientX, clientY: event.clientY }; - - // Update all three magnifiers - this._updateLeftMagnifier(event); - this._updateRightMagnifier(event); - this._updateDiffMagnifier(event); - } - - _updateLeftMagnifier(event) { if (!this.magnifierEnabled) return; - const leftViewer = this.shadowRoot.querySelector('#viewer-left'); - if (!leftViewer) return; + for (const name of ['left', 'right', 'diff']) { + const viewer = this.shadowRoot.querySelector('#viewer-' + name); + if (!viewer) continue; - const canvas = leftViewer.shadowRoot?.querySelector('canvas'); - if (!canvas) return; + const canvas = viewer.shadowRoot?.querySelector('canvas'); + if (!canvas) continue; - const rect = canvas.getBoundingClientRect(); - const leftMagnifier = leftViewer.shadowRoot?.getElementById('magnifier'); + // Get image data from the viewer + const imageData = viewer.imgdata; + const magnifier = viewer.shadowRoot?.getElementById('magnifier'); - if (!leftMagnifier) return; + // Check if mouse is over any of the three views (left, , or diff) + if (!this._isMouseOverAnyView(event)) { + magnifier.hide(); + continue; + } - // Get image data from the viewer - const imageData = leftViewer.imageData; - if (!imageData) { - leftMagnifier.hide(); - return; + const rect = canvas.getBoundingClientRect(); + + // Calculate the equivalent position on the image + const { imageX, imageY, mouseX, mouseY } = this._calculateEquivalentPosition(event, rect, imageData); + + // Position magnifier relative to the canvas within the viewer + const origData = name == 'diff' ? this.originalDiffImageData : null; + viewer.updateMagnifier(imageX, imageY, origData); } - - // Check if mouse is over any of the three views (left, right, or diff) - if (!this._isMouseOverAnyView(event)) { - leftMagnifier.hide(); - return; - } - - // Calculate the equivalent position on the left image - const { imageX, imageY, mouseX, mouseY } = this._calculateEquivalentPosition(event, rect, imageData); - - // Position magnifier relative to the canvas within the left viewer - leftMagnifier.updateMagnifier(imageData, imageX, imageY, mouseX, mouseY, rect); - } - - _updateRightMagnifier(event) { - if (!this.magnifierEnabled) return; - const rightViewer = this.shadowRoot.querySelector('#viewer-right'); - if (!rightViewer) return; - - const canvas = rightViewer.shadowRoot?.querySelector('canvas'); - if (!canvas) return; - - const rect = canvas.getBoundingClientRect(); - const rightMagnifier = rightViewer.shadowRoot?.getElementById('magnifier'); - - if (!rightMagnifier) return; - - // Get image data from the viewer - const imageData = rightViewer.imageData; - if (!imageData) { - rightMagnifier.hide(); - return; - } - - // Check if mouse is over any of the three views (left, right, or diff) - if (!this._isMouseOverAnyView(event)) { - rightMagnifier.hide(); - return; - } - - // Calculate the equivalent position on the right image - const { imageX, imageY, mouseX, mouseY } = this._calculateEquivalentPosition(event, rect, imageData); - - // Position magnifier relative to the canvas within the right viewer - rightMagnifier.updateMagnifier(imageData, imageX, imageY, mouseX, mouseY, rect); - } - - _updateDiffMagnifier(event) { - if (!this.magnifierEnabled || !this.showDiff || !this.currentDiffImageData || !this.diffCanvasRect) return; - - const rect = this.diffCanvasRect; - const diffMagnifier = this.shadowRoot.getElementById('diffMagnifier'); - - if (!diffMagnifier) return; - - // Check if mouse is over any of the three views (left, right, or diff) - if (!this._isMouseOverAnyView(event)) { - diffMagnifier.hide(); - return; - } - - // Calculate the equivalent position on the diff image - const { imageX, imageY, mouseX, mouseY } = this._calculateEquivalentPosition(event, rect, this.currentDiffImageData); - - // Position magnifier relative to the diff canvas - diffMagnifier.updateMagnifier(this.currentDiffImageData, imageX, imageY, mouseX, mouseY, rect, 8, this.originalDiffImageData); } _isMouseOverElement(event, rect) { @@ -449,13 +354,14 @@ class ExpandedComparisonResult extends LitElement { } } - // Check if mouse is over diff canvas - if (this.showDiff && this.diffCanvasRect) { - if (this._isMouseOverElement(event, this.diffCanvasRect)) { + // Check if mouse is over diff viewer + const diffViewer = this.shadowRoot.querySelector('#viewer-diff'); + if (diffViewer) { + const diffCanvas = diffViewer.shadowRoot?.querySelector('canvas'); + if (diffCanvas && this._isMouseOverElement(event, diffCanvas.getBoundingClientRect())) { return true; } } - return false; } @@ -463,6 +369,7 @@ class ExpandedComparisonResult extends LitElement { // Find which view the mouse is actually over and calculate the equivalent position const leftViewer = this.shadowRoot.querySelector('#viewer-left'); const rightViewer = this.shadowRoot.querySelector('#viewer-right'); + const diffViewer = this.shadowRoot.querySelector('#viewer-diff'); let sourceRect = null; let sourceImageData = null; @@ -472,7 +379,7 @@ class ExpandedComparisonResult extends LitElement { const leftCanvas = leftViewer.shadowRoot?.querySelector('canvas'); if (leftCanvas && this._isMouseOverElement(event, leftCanvas.getBoundingClientRect())) { sourceRect = leftCanvas.getBoundingClientRect(); - sourceImageData = leftViewer.imageData; + sourceImageData = leftViewer.imgdata; } } @@ -480,14 +387,15 @@ class ExpandedComparisonResult extends LitElement { const rightCanvas = rightViewer.shadowRoot?.querySelector('canvas'); if (rightCanvas && this._isMouseOverElement(event, rightCanvas.getBoundingClientRect())) { sourceRect = rightCanvas.getBoundingClientRect(); - sourceImageData = rightViewer.imageData; + sourceImageData = rightViewer.imgdata; } } - if (!sourceRect && this.showDiff && this.diffCanvasRect) { - if (this._isMouseOverElement(event, this.diffCanvasRect)) { - sourceRect = this.diffCanvasRect; - sourceImageData = this.currentDiffImageData; + if (!sourceRect && this.diffResult && diffViewer) { + const diffCanvas = diffViewer.shadowRoot?.querySelector('canvas'); + if (diffCanvas && this._isMouseOverElement(event, diffCanvas.getBoundingClientRect())) { + sourceRect = diffCanvas.getBoundingClientRect(); + sourceImageData = diffViewer.imgdata; } } @@ -524,7 +432,8 @@ class ExpandedComparisonResult extends LitElement { return html``; } - const diffStyle = !this.showDiff ? "display:none;" : "display:flex; flex-direction: column;"; + const showDiff = !!this.diffResult; + const diffStyle = !showDiff ? "display:none;" : "display:flex; flex-direction: column; margin: 0 5px;"; const v1 = this._viewer("left", this.tests, this.left, this.leftViewType); const v2 = this._viewer("right", this.tests, this.right, this.rightViewType); const onMultiplierChange = (ev) => { @@ -539,9 +448,13 @@ class ExpandedComparisonResult extends LitElement {
${v1} -
- - +
+
 
+
${v2}
@@ -563,9 +476,13 @@ class ExpandedComparisonResult extends LitElement { } customElements.define('expanded-comparison-result', ExpandedComparisonResult); - class App extends LitElement { static styles = css` + :host { + display: flex; + flex-direction: column; + align-items: center; + } .test-label { margin-bottom: 10px; font-size: 12px; @@ -586,9 +503,9 @@ class App extends LitElement { .app { display: flex; flex-direction: column; - width: 100%; + max-width: 800px; align-items: center; - padding-top: 20px; + padding: 20px 0; } .results { display: flex; diff --git a/test/renderdiff/src/viewer_html/tiff-viewer.js b/test/renderdiff/src/viewer_html/tiff-viewer.js index dc5a36e71b..e7702fd7dd 100644 --- a/test/renderdiff/src/viewer_html/tiff-viewer.js +++ b/test/renderdiff/src/viewer_html/tiff-viewer.js @@ -45,16 +45,16 @@ class ImageMagnifier extends LitElement { .pixel-info { position: absolute; - top: -65px; - left: 0; + top: -40px; + left: 15px; background: rgba(0,0,0,0.8); color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; - font-size: 11px; + font-size: 9px; white-space: pre-line; - max-width: 250px; + max-width: 300px; } `; @@ -76,7 +76,8 @@ class ImageMagnifier extends LitElement { `; } - updateMagnifier(imageData, imageX, imageY, mouseX, mouseY, canvasRect, zoomFactor = 8, originalImageData = null) { + updateMagnifier(imageData, parentRect, imageX, imageY, originalImageData = null) { + const zoomFactor = 8; if (!imageData) return; if (imageX < 0 || imageX >= imageData.width || imageY < 0 || imageY >= imageData.height) { @@ -90,7 +91,7 @@ class ImageMagnifier extends LitElement { const b = imageData.data[pixelIndex + 2]; const a = imageData.data[pixelIndex + 3]; - let pixelInfoText = `RGBA(${r}, ${g}, ${b}, ${a})\n@ (${imageX}, ${imageY})`; + let pixelInfoText = `(${r}, ${g}, ${b}, ${a})\n@ (${imageX}, ${imageY})`; // If original image data is provided, show the unmultiplied values too if (originalImageData) { @@ -98,9 +99,10 @@ class ImageMagnifier extends LitElement { const origG = originalImageData.data[pixelIndex + 1]; const origB = originalImageData.data[pixelIndex + 2]; const origA = originalImageData.data[pixelIndex + 3]; - pixelInfoText = `Diff: RGBA(${origR}, ${origG}, ${origB}, ${origA})\nDisplay: RGBA(${r}, ${g}, ${b}, ${a})\n@ (${imageX}, ${imageY})`; + pixelInfoText = `Orig: (${origR}, ${origG}, ${origB}, ${origA})\nMult: (${r}, ${g}, ${b}, ${a})\n@ (${imageX}, ${imageY})`; } + const magnifierSize = 150; const sourceSize = magnifierSize / zoomFactor; const halfSource = sourceSize / 2; @@ -131,19 +133,25 @@ class ImageMagnifier extends LitElement { const centerX = magnifierSize / 2; const centerY = magnifierSize / 2; + const lineWidth = 1; + const boxWidth = zoomFactor + lineWidth; magnifierCtx.strokeStyle = 'red'; - magnifierCtx.lineWidth = 1; + magnifierCtx.lineWidth = lineWidth; magnifierCtx.beginPath(); - magnifierCtx.moveTo(centerX - 5, centerY); - magnifierCtx.lineTo(centerX + 5, centerY); - magnifierCtx.moveTo(centerX, centerY - 5); - magnifierCtx.lineTo(centerX, centerY + 5); + magnifierCtx.moveTo(centerX, centerY); + magnifierCtx.lineTo(centerX + boxWidth, centerY); + magnifierCtx.lineTo(centerX + boxWidth, centerY + boxWidth); + magnifierCtx.lineTo(centerX, centerY + boxWidth); + magnifierCtx.lineTo(centerX, centerY); magnifierCtx.stroke(); // Position relative to the TiffViewer container - this.style.left = (mouseX - magnifierSize / 2) + 'px'; - this.style.top = (mouseY - magnifierSize / 2) + 'px'; - + this.style.left = Math.round(-centerX + + (imageX / imageData.width) * parentRect.width - + boxWidth) + 'px'; + this.style.top = Math.round(-centerY + + (imageY / imageData.height) * parentRect.height - + boxWidth) + 'px'; pixelInfo.textContent = pixelInfoText; this.visible = true; @@ -172,20 +180,20 @@ export class TiffViewer extends LitElement { static properties = { fileurl: {type: String, attribute: 'fileurl'}, - name: {type: String, attribute: 'name'}, failedToFetch: {type: Boolean }, magnifierEnabled: {type: Boolean, attribute: 'magnifier-enabled'}, disableMouseHandlers: {type: Boolean, attribute: 'disable-mouse-handlers'}, + srcdata: {type: Object, attribute: 'srcdata'}, }; constructor() { super(); this.fileurl = null; - this.name = null; this.failedToFetch = false; this.magnifierEnabled = false; this.disableMouseHandlers = false; - this.imageData = null; + this.imgdata = null; + this.srcdata = null; this.canvasRect = null; } @@ -194,7 +202,11 @@ export class TiffViewer extends LitElement { return html``; } return html` - + + `; } @@ -202,7 +214,20 @@ export class TiffViewer extends LitElement { updated(props) { if (props.has('fileurl') && this.fileurl) { this._updateImage(this.fileurl); + return; } + if (props.has('srcdata') && this.srcdata) { + this._drawImage(this.srcdata); + } + } + + _drawImage(imageData) { + const canvas = this.shadowRoot.getElementById('tiffCanvas'); + const ctx = canvas.getContext('2d'); + canvas.width = imageData.width; + canvas.height = imageData.height; + ctx.putImageData(imageData, 0, 0); + this.imgdata = imageData; } async _updateImage(fileurl) { @@ -233,9 +258,6 @@ export class TiffViewer extends LitElement { }); this.dispatchEvent(event); } - const canvas = this.shadowRoot.getElementById('tiffCanvas'); - const ctx = canvas.getContext('2d'); - try { const arrayBuffer = fileblob; const ifds = UTIF.decode(arrayBuffer); // Parse TIFF data @@ -251,6 +273,10 @@ export class TiffViewer extends LitElement { UTIF.decodeImage(arrayBuffer, firstImage, ifds); // Decode the actual pixel data const rgba = UTIF.toRGBA8(firstImage); // Convert to RGBA + + // This needs to be present so that the ImageData has the right dimension to begin with + // (without initial resizing due to flex's css styling and then resized again in _drawImage()). + const canvas = this.shadowRoot.getElementById('tiffCanvas'); canvas.width = firstImage.width; canvas.height = firstImage.height; @@ -262,18 +288,17 @@ export class TiffViewer extends LitElement { } const imageData = new ImageData(new Uint8ClampedArray(rgba), firstImage.width, firstImage.height); - ctx.putImageData(imageData, 0, 0); - - this.imageData = imageData; + this.imgdata = imageData; this.dispatchEvent(new CustomEvent('image-loaded', { bubbles: true, composed: true, detail: { - name: this.name, + url: this.fileurl, img: imageData, } })); + this._drawImage(imageData); } catch (error) { console.error('Error processing TIFF file:', error); this._clearCanvas(); @@ -289,7 +314,7 @@ export class TiffViewer extends LitElement { } _onMouseEnter(event) { - if (this.disableMouseHandlers || !this.magnifierEnabled || !this.imageData) return; + if (this.disableMouseHandlers || !this.magnifierEnabled || !this.imgdata) return; this.canvasRect = event.target.getBoundingClientRect(); } @@ -300,11 +325,11 @@ export class TiffViewer extends LitElement { } _onMouseMove(event) { - if (this.disableMouseHandlers || !this.magnifierEnabled || !this.imageData || !this.canvasRect) return; - + if (this.disableMouseHandlers || !this.canvasRect) return; const rect = this.canvasRect; - const scaleX = this.imageData.width / rect.width; - const scaleY = this.imageData.height / rect.height; + + const scaleX = this.imgdata.width / rect.width; + const scaleY = this.imgdata.height / rect.height; const mouseX = event.clientX - rect.left; const mouseY = event.clientY - rect.top; @@ -312,8 +337,18 @@ export class TiffViewer extends LitElement { const imageX = Math.floor(mouseX * scaleX); const imageY = Math.floor(mouseY * scaleY); + this.updateMagnifier(imageX, imageY); + } + + updateMagnifier(imageX, imageY, origData = null) { + let rect = null; + const canvas = this.shadowRoot.getElementById('tiffCanvas'); + if (canvas) { + rect = canvas.getBoundingClientRect(); + } + if (!this.magnifierEnabled || !this.imgdata || !rect) return; const magnifier = this.shadowRoot.getElementById('magnifier'); - magnifier.updateMagnifier(this.imageData, imageX, imageY, event.clientX, event.clientY, rect); + magnifier.updateMagnifier(this.imgdata, rect, imageX, imageY, origData); } }