diff --git a/test/renderdiff/src/viewer_html/app.js b/test/renderdiff/src/viewer_html/app.js index 7b60fe2668..62f371888c 100644 --- a/test/renderdiff/src/viewer_html/app.js +++ b/test/renderdiff/src/viewer_html/app.js @@ -53,10 +53,27 @@ class ExpandedPassedResult extends LitElement { render() { if (this.content) { const url = getGoldenUrl(this.content); - return html``; + return html` +
+
+ +
+ +
+ `; } return html``; } + + _onMagnifierToggle(ev) { + const viewer = this.shadowRoot.getElementById('passedViewer'); + if (viewer) { + viewer.magnifierEnabled = ev.target.checked; + } + } } customElements.define('expanded-passed-result', ExpandedPassedResult); @@ -97,6 +114,7 @@ class ExpandedComparisonResult extends LitElement { leftViewType: { type: String }, rightViewType: { type: String }, disableDropdowns: { type: Boolean }, + magnifierEnabled: { type: Boolean }, }; } @@ -116,6 +134,12 @@ class ExpandedComparisonResult extends LitElement { margin-top: 22px; margin-bottom: 5px; } + .diff-container { + position: relative; + } + .viewer-container { + position: relative; + } .selector { margin: 8px 0; } @@ -132,6 +156,10 @@ class ExpandedComparisonResult extends LitElement { this.leftViewType = 'golden'; this.rightViewType = 'golden'; this.disableDropdowns = false; + this.magnifierEnabled = true; + this.diffCanvasRect = null; + this.originalDiffImageData = null; + this.globalMousePosition = null; this.addEventListener( 'image-loaded', @@ -156,7 +184,9 @@ class ExpandedComparisonResult extends LitElement {
${current.name}
+ fileurl="${url}" + ?magnifier-enabled="${this.magnifierEnabled}" + disable-mouse-handlers> `; } @@ -233,6 +263,8 @@ class ExpandedComparisonResult extends LitElement { if (diff.result == RES_DIFFERENT_PIXELS) { this.diffResult = diff; this.showDiff = true; + // Reset original diff data when new diff is computed + this.originalDiffImageData = null; } } @@ -263,6 +295,228 @@ 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) { + this.originalDiffImageData = new ImageData(diffResult.diffImg.slice(), diffResult.dim.width, diffResult.dim.height); + } + 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(); + } + + 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; + + const canvas = leftViewer.shadowRoot?.querySelector('canvas'); + if (!canvas) return; + + const rect = canvas.getBoundingClientRect(); + const leftMagnifier = leftViewer.shadowRoot?.getElementById('magnifier'); + + if (!leftMagnifier) return; + + // Get image data from the viewer + const imageData = leftViewer.imageData; + if (!imageData) { + leftMagnifier.hide(); + return; + } + + // 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) { + return event.clientX >= rect.left && + event.clientX <= rect.right && + event.clientY >= rect.top && + event.clientY <= rect.bottom; + } + + _isMouseOverAnyView(event) { + // Check if mouse is over left viewer + const leftViewer = this.shadowRoot.querySelector('#viewer-left'); + if (leftViewer) { + const leftCanvas = leftViewer.shadowRoot?.querySelector('canvas'); + if (leftCanvas && this._isMouseOverElement(event, leftCanvas.getBoundingClientRect())) { + return true; + } + } + + // Check if mouse is over right viewer + const rightViewer = this.shadowRoot.querySelector('#viewer-right'); + if (rightViewer) { + const rightCanvas = rightViewer.shadowRoot?.querySelector('canvas'); + if (rightCanvas && this._isMouseOverElement(event, rightCanvas.getBoundingClientRect())) { + return true; + } + } + + // Check if mouse is over diff canvas + if (this.showDiff && this.diffCanvasRect) { + if (this._isMouseOverElement(event, this.diffCanvasRect)) { + return true; + } + } + + return false; + } + + _calculateEquivalentPosition(event, targetRect, targetImageData) { + // 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'); + + let sourceRect = null; + let sourceImageData = null; + + // Determine source view + if (leftViewer) { + const leftCanvas = leftViewer.shadowRoot?.querySelector('canvas'); + if (leftCanvas && this._isMouseOverElement(event, leftCanvas.getBoundingClientRect())) { + sourceRect = leftCanvas.getBoundingClientRect(); + sourceImageData = leftViewer.imageData; + } + } + + if (!sourceRect && rightViewer) { + const rightCanvas = rightViewer.shadowRoot?.querySelector('canvas'); + if (rightCanvas && this._isMouseOverElement(event, rightCanvas.getBoundingClientRect())) { + sourceRect = rightCanvas.getBoundingClientRect(); + sourceImageData = rightViewer.imageData; + } + } + + if (!sourceRect && this.showDiff && this.diffCanvasRect) { + if (this._isMouseOverElement(event, this.diffCanvasRect)) { + sourceRect = this.diffCanvasRect; + sourceImageData = this.currentDiffImageData; + } + } + + if (!sourceRect || !sourceImageData) { + // Fallback to target rect + sourceRect = targetRect; + sourceImageData = targetImageData; + } + + // Calculate relative position in source + const sourceMouseX = event.clientX - sourceRect.left; + const sourceMouseY = event.clientY - sourceRect.top; + const sourceScaleX = sourceImageData.width / sourceRect.width; + const sourceScaleY = sourceImageData.height / sourceRect.height; + const sourceImageX = Math.floor(sourceMouseX * sourceScaleX); + const sourceImageY = Math.floor(sourceMouseY * sourceScaleY); + + // Convert to target coordinates + const targetScaleX = targetImageData.width / targetRect.width; + const targetScaleY = targetImageData.height / targetRect.height; + const targetMouseX = sourceImageX / targetScaleX; + const targetMouseY = sourceImageY / targetScaleY; + + return { + imageX: sourceImageX, + imageY: sourceImageY, + mouseX: targetMouseX, + mouseY: targetMouseY + }; } render() { @@ -274,22 +528,34 @@ class ExpandedComparisonResult extends LitElement { 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) => { + const multiplierValue = this.shadowRoot.querySelector('#multiplierValue'); + multiplierValue.textContent = ev.target.value; this._updateDiffCanvas(this.diffResult, ev.target.value); }; + const onMagnifierToggle = (ev) => { + this.magnifierEnabled = ev.target.checked; + }; return html`
-
+
${v1} -
- +
+ +
${v2}
-
Difference Multiplier
+
Difference Multiplier: 1
+
+ +
`; diff --git a/test/renderdiff/src/viewer_html/tiff-viewer.js b/test/renderdiff/src/viewer_html/tiff-viewer.js index 1c587dfe03..dc5a36e71b 100644 --- a/test/renderdiff/src/viewer_html/tiff-viewer.js +++ b/test/renderdiff/src/viewer_html/tiff-viewer.js @@ -14,11 +14,154 @@ import { LitElement, html, css } from "https://cdn.jsdelivr.net/gh/lit/dist@3/all/lit-all.min.js"; +class ImageMagnifier extends LitElement { + static styles = css` + :host { + position: absolute; + pointer-events: none; + z-index: 1000; + display: none; + } + + :host([visible]) { + display: block; + } + + .magnifier { + width: 150px; + height: 150px; + border: 2px solid #000; + border-radius: 75px; + background: white; + box-shadow: 0 4px 8px rgba(0,0,0,0.3); + position: relative; + } + + .magnifier-canvas { + width: 100%; + height: 100%; + border-radius: 73px; + } + + .pixel-info { + position: absolute; + top: -65px; + left: 0; + background: rgba(0,0,0,0.8); + color: white; + padding: 4px 8px; + border-radius: 4px; + font-family: monospace; + font-size: 11px; + white-space: pre-line; + max-width: 250px; + } + `; + + static properties = { + visible: {type: Boolean, reflect: true}, + }; + + constructor() { + super(); + this.visible = false; + } + + render() { + return html` +
+ +
+
+ `; + } + + updateMagnifier(imageData, imageX, imageY, mouseX, mouseY, canvasRect, zoomFactor = 8, originalImageData = null) { + if (!imageData) return; + + if (imageX < 0 || imageX >= imageData.width || imageY < 0 || imageY >= imageData.height) { + this.visible = false; + return; + } + + const pixelIndex = (imageY * imageData.width + imageX) * 4; + const r = imageData.data[pixelIndex]; + const g = imageData.data[pixelIndex + 1]; + const b = imageData.data[pixelIndex + 2]; + const a = imageData.data[pixelIndex + 3]; + + let pixelInfoText = `RGBA(${r}, ${g}, ${b}, ${a})\n@ (${imageX}, ${imageY})`; + + // If original image data is provided, show the unmultiplied values too + if (originalImageData) { + const origR = originalImageData.data[pixelIndex]; + 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})`; + } + + const magnifierSize = 150; + const sourceSize = magnifierSize / zoomFactor; + const halfSource = sourceSize / 2; + + const sourceX = Math.max(0, Math.min(imageData.width - sourceSize, imageX - halfSource)); + const sourceY = Math.max(0, Math.min(imageData.height - sourceSize, imageY - halfSource)); + + const magnifierCanvas = this.shadowRoot.getElementById('magnifierCanvas'); + const pixelInfo = this.shadowRoot.getElementById('pixelInfo'); + + magnifierCanvas.width = magnifierSize; + magnifierCanvas.height = magnifierSize; + const magnifierCtx = magnifierCanvas.getContext('2d'); + + magnifierCtx.imageSmoothingEnabled = false; + + const tempCanvas = document.createElement('canvas'); + tempCanvas.width = imageData.width; + tempCanvas.height = imageData.height; + const tempCtx = tempCanvas.getContext('2d'); + tempCtx.putImageData(imageData, 0, 0); + + magnifierCtx.drawImage( + tempCanvas, + sourceX, sourceY, sourceSize, sourceSize, + 0, 0, magnifierSize, magnifierSize + ); + + const centerX = magnifierSize / 2; + const centerY = magnifierSize / 2; + magnifierCtx.strokeStyle = 'red'; + magnifierCtx.lineWidth = 1; + magnifierCtx.beginPath(); + magnifierCtx.moveTo(centerX - 5, centerY); + magnifierCtx.lineTo(centerX + 5, centerY); + magnifierCtx.moveTo(centerX, centerY - 5); + magnifierCtx.lineTo(centerX, centerY + 5); + magnifierCtx.stroke(); + + // Position relative to the TiffViewer container + this.style.left = (mouseX - magnifierSize / 2) + 'px'; + this.style.top = (mouseY - magnifierSize / 2) + 'px'; + + pixelInfo.textContent = pixelInfoText; + + this.visible = true; + } + + hide() { + this.visible = false; + } +} + +customElements.define('image-magnifier', ImageMagnifier); + // Generated by Gemini with some modifications export class TiffViewer extends LitElement { static styles = css` :host { display: block; + position: relative; } canvas { border: 1px solid #ccc; @@ -31,6 +174,8 @@ export class TiffViewer extends LitElement { 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'}, }; constructor() { @@ -38,13 +183,20 @@ export class TiffViewer extends LitElement { this.fileurl = null; this.name = null; this.failedToFetch = false; + this.magnifierEnabled = false; + this.disableMouseHandlers = false; + this.imageData = null; + this.canvasRect = null; } render() { if (this.failedToFetch) { return html``; } - return html``; + return html` + + + `; } updated(props) { @@ -112,6 +264,8 @@ 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.dispatchEvent(new CustomEvent('image-loaded', { bubbles: true, composed: true, @@ -133,6 +287,34 @@ export class TiffViewer extends LitElement { ctx.clearRect(0, 0, canvas.width, canvas.height); } } + + _onMouseEnter(event) { + if (this.disableMouseHandlers || !this.magnifierEnabled || !this.imageData) return; + this.canvasRect = event.target.getBoundingClientRect(); + } + + _onMouseLeave(event) { + if (this.disableMouseHandlers || !this.magnifierEnabled) return; + const magnifier = this.shadowRoot.getElementById('magnifier'); + magnifier.hide(); + } + + _onMouseMove(event) { + if (this.disableMouseHandlers || !this.magnifierEnabled || !this.imageData || !this.canvasRect) return; + + const rect = this.canvasRect; + const scaleX = this.imageData.width / rect.width; + const scaleY = this.imageData.height / rect.height; + + const mouseX = event.clientX - rect.left; + const mouseY = event.clientY - rect.top; + + const imageX = Math.floor(mouseX * scaleX); + const imageY = Math.floor(mouseY * scaleY); + + const magnifier = this.shadowRoot.getElementById('magnifier'); + magnifier.updateMagnifier(this.imageData, imageX, imageY, event.clientX, event.clientY, rect); + } } customElements.define('tiff-viewer', TiffViewer);