| /** | |
| * @license | |
| * Copyright (C) 2026 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 {html, css, LitElement, PropertyValues} from 'lit'; | |
| import {customElement, property, state} from 'lit/decorators.js'; | |
| export interface ImageInfo { | |
| type: string; | |
| body: string; | |
| } | |
| @customElement('gr-opacity-diff-mode') | |
| export class OpacityDiffMode extends LitElement { | |
| static override get styles() { | |
| return [ | |
| css` | |
| :host { | |
| display: block; | |
| } | |
| .wrapper { | |
| box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); | |
| margin: 1em 0; | |
| } | |
| img { | |
| display: block; | |
| height: var(--img-height); | |
| margin: auto; | |
| position: absolute; | |
| width: var(--img-width); | |
| } | |
| #imageRevision { | |
| opacity: var(--my-opacity-value); | |
| z-index: 0.5; | |
| } | |
| #imageDiffContainer { | |
| height: var(--div-height); | |
| margin: auto; | |
| width: var(--div-width); | |
| } | |
| #controlsContainer { | |
| border-top: 1px solid var(--border-color, #ddd); | |
| display: flex; | |
| } | |
| #controlsBox { | |
| display: flex; | |
| justify-content: space-between; | |
| margin: 0 0.5em; | |
| min-width: 32em; | |
| width: 100%; | |
| } | |
| label { | |
| align-items: center; | |
| display: flex; | |
| padding: 1em 0.5em; | |
| } | |
| input { | |
| margin: 0.5em; | |
| } | |
| #opacitySlider { | |
| width: 10em; | |
| } | |
| ` | |
| ]; | |
| } | |
| @property({type: Object}) | |
| baseImage?: ImageInfo; | |
| @property({type: Object}) | |
| revisionImage?: ImageInfo; | |
| @property({type: Number}) | |
| opacityValue = 0.5; | |
| @state() | |
| protected maxHeight = 0; | |
| @state() | |
| protected maxWidth = 0; | |
| @state() | |
| protected scaledWidth: number | null = null; | |
| @state() | |
| protected scaledHeight: number | null = null; | |
| override render() { | |
| return html` | |
| <div | |
| class="wrapper" | |
| style=" | |
| --my-opacity-value: ${this.opacityValue}; | |
| --img-width: ${this.scaledWidth ? this.scaledWidth + 'px' : 'initial'}; | |
| --img-height: ${this.scaledHeight ? this.scaledHeight + 'px' : 'initial'}; | |
| --div-width: ${this.maxWidth ? this.maxWidth + 'px' : 'initial'}; | |
| --div-height: ${this.maxHeight ? this.maxHeight + 'px' : 'initial'}; | |
| " | |
| > | |
| <div id="imageDiffContainer"> | |
| <img | |
| @load=${this.onImageLoad} | |
| id="imageBase" | |
| src=${this.computeSrcString(this.baseImage)} | |
| /> | |
| <img | |
| @load=${this.onImageLoad} | |
| data-opacity=${this.opacityValue} | |
| id="imageRevision" | |
| src=${this.computeSrcString(this.revisionImage)} | |
| /> | |
| </div> | |
| <div id="controlsContainer"> | |
| <div id="controlsBox"> | |
| <label> | |
| <input | |
| id="scaleSizesToggle" | |
| @click=${this.handleScaleSizesToggle} | |
| type="checkbox" | |
| /> | |
| Scale to same size | |
| </label> | |
| <label> | |
| Revision image opacity | |
| <input | |
| id="opacitySlider" | |
| max="1.0" | |
| min="0.0" | |
| @input=${this.handleOpacityChange} | |
| step=".01" | |
| .value=${String(this.opacityValue ?? 0.5)} | |
| /> | |
| </label> | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| } | |
| override updated(changedProperties: PropertyValues) { | |
| if ( | |
| changedProperties.has('baseImage') || | |
| changedProperties.has('revisionImage') | |
| ) { | |
| this.handleImageChange(); | |
| } | |
| } | |
| protected onImageLoad(e: Event) { | |
| const target = e.target as HTMLImageElement; | |
| this.maxHeight = Math.max(this.maxHeight, target.naturalHeight); | |
| this.maxWidth = Math.max(this.maxWidth, target.naturalWidth); | |
| } | |
| protected handleImageChange() { | |
| if (this.baseImage === undefined || this.revisionImage === undefined) return; | |
| this.handleOpacityChange(); | |
| } | |
| handleOpacityChange(e?: Event) { | |
| const value = e ? (e.target as HTMLInputElement).value : this.opacityValue; | |
| this.opacityValue = Number(value); | |
| } | |
| computeSrcString(image?: ImageInfo) { | |
| if (!image) return ''; | |
| return 'data:' + image.type + ';base64, ' + image.body; | |
| } | |
| handleScaleSizesToggle(e?: Event) { | |
| const isChecked = e ? (e.target as HTMLInputElement).checked : false; | |
| if (isChecked) { | |
| this.scaledWidth = this.maxWidth; | |
| this.scaledHeight = this.maxHeight; | |
| } else { | |
| this.scaledWidth = null; | |
| this.scaledHeight = null; | |
| } | |
| } | |
| } | |
| declare global { | |
| interface HTMLElementTagNameMap { | |
| 'gr-opacity-diff-mode': OpacityDiffMode; | |
| } | |
| } |