renderdiff: add viewer for image differences (#8768)
- Modify the compare script to output more details of a comparison. This will include the source/golden directory, the comparison directory (the new renderings), and a file path to difference images if the golden does not match the rendered image. - The image_diff script can now output a TIFF that is the difference of two input TIFFs. - Add a viewer for examining the differences between rendered output and golden images. - The viewer consists of a simple server of web API endpoints for querying difference results (along with rendered images in TIFF). - And a web-based (html + lit-element) UI for looking at the rendered images and differences.
This commit is contained in:
@@ -0,0 +1,225 @@
|
||||
// Copyright (C) 2025 The Android Open Source Project
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { LitElement, html, css } from "https://cdn.jsdelivr.net/gh/lit/dist@3/all/lit-all.min.js";
|
||||
|
||||
// Generated by Gemini
|
||||
export class RadioButtonGroup extends LitElement {
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
.radio-group-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s ease-in-out;
|
||||
}
|
||||
label:hover {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
input[type="radio"] {
|
||||
margin-right: 8px;
|
||||
cursor: pointer;
|
||||
/* Custom radio button appearance */
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border: 2px solid #ccc;
|
||||
border-radius: 50%;
|
||||
outline: none;
|
||||
transition: border-color 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
|
||||
}
|
||||
input[type="radio"]:checked {
|
||||
border-color: #656565;
|
||||
background-color: #656565; /* Optional: fill color when checked */
|
||||
}
|
||||
input[type="radio"]:checked::before {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
margin: 3px; /* Adjust to center the dot */
|
||||
background-color: white;
|
||||
border-radius: 50%;
|
||||
}
|
||||
input[type="radio"]:focus {
|
||||
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
|
||||
}
|
||||
.label-text {
|
||||
font-size: 1rem;
|
||||
}
|
||||
`;
|
||||
static properties = {
|
||||
/**
|
||||
* An array of strings representing the choices for the radio buttons.
|
||||
* @type {Array<string>}
|
||||
*/
|
||||
choices: { type: Array },
|
||||
|
||||
/**
|
||||
* The name for the radio button group. This is important for accessibility
|
||||
* and ensuring only one radio button in the group can be selected.
|
||||
* @type {string}
|
||||
*/
|
||||
name: { type: String },
|
||||
|
||||
/**
|
||||
* The currently selected value.
|
||||
* @type {string}
|
||||
*/
|
||||
value: { type: String, reflect: true },
|
||||
|
||||
/**
|
||||
* The label or title for the radio group.
|
||||
* @type {string}
|
||||
*/
|
||||
groupLabel: { type: String }
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.choices = [];
|
||||
this.name = 'radio-group'; // Default name
|
||||
this.value = '';
|
||||
this.groupLabel = '';
|
||||
}
|
||||
|
||||
_handleChange(event) {
|
||||
const selectedValue = event.target.value;
|
||||
if (this.value !== selectedValue) {
|
||||
this.value = selectedValue;
|
||||
// Dispatch a custom event with the new value
|
||||
this.dispatchEvent(new CustomEvent('radio-change', {
|
||||
detail: { value: this.value, radioId: this.id },
|
||||
bubbles: true, // Allows the event to bubble up through the DOM
|
||||
composed: true // Allows the event to cross shadow DOM boundaries
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div class="radio-group-container" role="radiogroup" aria-labelledby="group-label">
|
||||
${this.groupLabel ? html`<span id="group-label" class="group-label">${this.groupLabel}</span>` : ''}
|
||||
${this.choices.map(choice => html`
|
||||
<label>
|
||||
<input
|
||||
type="radio"
|
||||
name=${this.name}
|
||||
.value=${choice}
|
||||
.checked=${choice === this.value}
|
||||
@change=${this._handleChange}
|
||||
>
|
||||
<span class="label-text">${choice}</span>
|
||||
</label>
|
||||
`)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('radio-button-group', RadioButtonGroup);
|
||||
|
||||
// Generated by Gemini with some modifications
|
||||
class ModalDialog extends LitElement {
|
||||
static styles = css`
|
||||
:host {
|
||||
display: none; /* Hidden by default */
|
||||
}
|
||||
|
||||
:host([open]) {
|
||||
display: block; /* Show when open attribute is present */
|
||||
}
|
||||
|
||||
.backdrop {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: rgba(0, 0, 0, 0.5); /* Semi-transparent black */
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000; /* Ensure it's on top */
|
||||
}
|
||||
|
||||
.dialog {
|
||||
background-color: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
min-width: 300px; /* Or your desired width */
|
||||
z-index: 1001; /* Above the backdrop */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin: 0 15px;
|
||||
}
|
||||
`;
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
open: { type: Boolean, reflect: true },
|
||||
};
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.open = false;
|
||||
}
|
||||
|
||||
_handleBackdropClick(event) {
|
||||
// Close only if the click is directly on the backdrop, not on the dialog itself
|
||||
if (event.target === this.shadowRoot.querySelector('.backdrop')) {
|
||||
this.open = false;
|
||||
this.dispatchEvent(new CustomEvent('dialog-closed', { bubbles: true, composed: true }));
|
||||
}
|
||||
}
|
||||
|
||||
_handleDialogClick(event) {
|
||||
// Prevent clicks inside the dialog from bubbling up to the backdrop
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.open) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="backdrop" @click="${this._handleBackdropClick}">
|
||||
<div class="dialog" @click="${this._handleDialogClick}">
|
||||
<slot name="header"><h2>Default Header</h2></slot>
|
||||
<slot>
|
||||
<p>This is the default content of the modal.</p>
|
||||
</slot>
|
||||
<slot name="footer">
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
customElements.define('modal-dialog', ModalDialog);
|
||||
Reference in New Issue
Block a user