Skip to content

Commit f76a4bf

Browse files
committed
Image comparison improvements
1 parent 834e266 commit f76a4bf

File tree

1 file changed

+67
-88
lines changed

1 file changed

+67
-88
lines changed

web/src/lib/components/diff/ImageDiff.svelte

Lines changed: 67 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import Spinner from "$lib/components/Spinner.svelte";
44
import { getDimensions, type ImageDimensions } from "$lib/image";
55
import AddedOrRemovedImageLabel from "$lib/components/diff/AddedOrRemovedImageLabel.svelte";
6-
import { on } from "svelte/events";
6+
import { ElementSize } from "runed";
77
88
interface Props {
99
fileA: string;
@@ -16,86 +16,43 @@
1616
type DimensionData = {
1717
a: ImageDimensions;
1818
b: ImageDimensions;
19-
widest: string;
20-
narrowerMaxW: string;
2119
};
2220
23-
let dimensions: Promise<DimensionData> = $derived.by(async () => {
21+
let imageDimensions: Promise<DimensionData> = $derived.by(async () => {
2422
const [a, b] = await Promise.all([getDimensions(fileA), getDimensions(fileB)]);
25-
const widest = a.width > b.width ? "a" : "b";
26-
const widerDims = widest === "a" ? a : b;
27-
const narrowerDims = widest === "a" ? b : a;
28-
const narrowerMaxW = (narrowerDims.width / widerDims.width) * 100;
29-
return { a, b, widest, narrowerMaxW: `max-width: ${narrowerMaxW}%;` };
23+
return { a, b };
3024
});
3125
let mode: Mode = $state("side-by-side");
32-
let percentShown: number = $state(50);
33-
let percentDragged: number = $state(50);
34-
let dragging: boolean = $state(false);
26+
let slidePercent: number = $state(50);
3527
let fadePercent: number = $state(50);
3628
37-
function getMaxW(img: string, dims: DimensionData): string {
38-
if (img === "a") {
39-
if (dims.widest === "b") {
40-
return dims.narrowerMaxW;
29+
function getStyle(side: "a" | "b", dims: DimensionData): string {
30+
let style = "";
31+
if (side === "a") {
32+
if (dims.b.width > dims.a.width) {
33+
const scale = dims.a.width / dims.b.width;
34+
style += `max-width: ${scale * 100}%;`;
4135
}
42-
} else if (img === "b") {
43-
if (dims.widest === "a") {
44-
return dims.narrowerMaxW;
36+
} else {
37+
if (dims.a.width > dims.b.width) {
38+
const scale = dims.b.width / dims.a.width;
39+
style += `max-width: ${scale * 100}%;`;
4540
}
4641
}
47-
return "";
42+
return style;
4843
}
4944
50-
let overlayImgB: HTMLImageElement;
51-
52-
function dragSlider(node: HTMLElement) {
53-
let containerWidth: number;
54-
let containerLeft: number;
55-
let imgBWidth: number;
56-
let imgBLeft: number;
57-
58-
function handleMouseDown(event: MouseEvent) {
59-
if (event.button !== 0) {
60-
return;
61-
}
62-
63-
if (node.parentElement && overlayImgB) {
64-
const parentRect = node.parentElement.getBoundingClientRect();
65-
containerWidth = parentRect.width;
66-
containerLeft = parentRect.left;
67-
68-
const imgBRect = overlayImgB.getBoundingClientRect();
69-
imgBWidth = imgBRect.width;
70-
imgBLeft = imgBRect.left;
71-
72-
dragging = true;
73-
}
74-
}
75-
76-
function handleMouseMove(event: MouseEvent) {
77-
if (dragging) {
78-
percentDragged = Math.max(0, Math.min(100, ((event.clientX - containerLeft) / containerWidth) * 100));
79-
percentShown = Math.max(0, Math.min(100, ((event.clientX - imgBLeft) / imgBWidth) * 100));
80-
}
81-
}
82-
83-
function handleMouseUp() {
84-
dragging = false;
45+
let overlayImgA: HTMLDivElement | undefined = $state();
46+
let overlayImgASize = new ElementSize(() => overlayImgA);
47+
let overlayImgB: HTMLImageElement | undefined = $state();
48+
let overlayImgBSize = new ElementSize(() => overlayImgB);
49+
let overlayContainerStyle = $derived.by(() => {
50+
if (overlayImgASize.height < overlayImgBSize.height) {
51+
// pad image A's height to match B's height
52+
return `height: ${overlayImgBSize.height}px;`;
8553
}
86-
87-
const removeMouseDown = on(node, "mousedown", handleMouseDown);
88-
const removeMouseMove = on(window, "mousemove", handleMouseMove);
89-
const removeMouseUp = on(window, "mouseup", handleMouseUp);
90-
91-
return {
92-
destroy() {
93-
removeMouseDown();
94-
removeMouseMove();
95-
removeMouseUp();
96-
},
97-
};
98-
}
54+
return "";
55+
});
9956
</script>
10057

10158
{#snippet modeSelector()}
@@ -119,11 +76,11 @@
11976
{#snippet sideBySide(dims: DimensionData)}
12077
<div class="grid w-full grid-cols-1 gap-4 sm:grid-cols-2">
12178
<div class="flex flex-col items-center justify-center gap-4">
122-
<img src={fileA} alt="A" class="png-bg h-auto border-2 border-red-600 shadow-md" style={getMaxW("a", dims)} />
79+
<img src={fileA} alt="A" class="png-bg h-auto border-2 border-red-600 shadow-md" style={getStyle("a", dims)} />
12380
<AddedOrRemovedImageLabel mode="remove" dims={dims.a} />
12481
</div>
12582
<div class="flex flex-col items-center justify-center gap-4">
126-
<img src={fileB} alt="B" class="png-bg h-auto border-2 border-green-600 shadow-md" style={getMaxW("b", dims)} />
83+
<img src={fileB} alt="B" class="png-bg h-auto border-2 border-green-600 shadow-md" style={getStyle("b", dims)} />
12784
<AddedOrRemovedImageLabel mode="add" dims={dims.b} />
12885
</div>
12986
</div>
@@ -132,23 +89,38 @@
13289
{#snippet slide(dims: DimensionData)}
13390
<div class="flex flex-row items-center gap-4">
13491
<AddedOrRemovedImageLabel mode="remove" dims={dims.a} />
135-
<div class="relative grid grid-cols-1 gap-4">
136-
<img src={fileA} alt="A" class="png-bg h-auto w-full border-2 border-red-600 shadow-md" draggable="false" style={getMaxW("a", dims)} />
92+
<div class="relative grid grid-cols-1 gap-4" style={overlayContainerStyle}>
13793
<img
138-
bind:this={overlayImgB}
139-
src={fileB}
140-
alt="B"
141-
class="png-bg absolute h-auto max-w-full place-self-center border-2 border-green-600 shadow-md"
94+
bind:this={overlayImgA}
95+
src={fileA}
96+
alt="A"
97+
class="png-bg h-auto w-full place-self-center border-2 border-red-600 shadow-md"
14298
draggable="false"
143-
style="clip-path: inset(0 0 0 {percentShown}%); {getMaxW('b', dims)}"
99+
style={getStyle("a", dims)}
144100
/>
145-
<div class="absolute top-1/2 h-full w-0.5 -translate-x-1/2 -translate-y-1/2 bg-gray-600" style="left: calc({percentDragged}%);"></div>
146-
<div
147-
use:dragSlider
148-
class="absolute top-1/2 flex -translate-x-1/2 -translate-y-1/2 cursor-col-resize items-center justify-center rounded-sm bg-neutral px-0.5 py-1 shadow-sm select-none"
149-
style="left: calc({percentDragged}%);"
150-
>
151-
<span class="iconify size-4 octicon--grabber-16"></span>
101+
<div class="absolute flex size-full">
102+
<Slider.Root
103+
type="single"
104+
thumbPositioning="exact"
105+
bind:value={slidePercent}
106+
class="relative m-auto flex size-fit touch-none select-none"
107+
style={getStyle("b", dims)}
108+
>
109+
<img
110+
bind:this={overlayImgB}
111+
src={fileB}
112+
alt="B"
113+
class="png-bg size-full border-2 border-green-600 shadow-md"
114+
draggable="false"
115+
style="clip-path: inset(0 0 0 {slidePercent}%);"
116+
/>
117+
<span class="absolute h-full w-0.5 -translate-x-1/2 bg-em-disabled/80" style="left: calc({slidePercent}%);"></span>
118+
<Slider.Thumb index={0} class="group absolute flex h-full cursor-col-resize select-none">
119+
<div class="flex place-self-center rounded-sm bg-neutral px-0.5 py-1 shadow-sm group-data-active:scale-[0.95]">
120+
<span class="iconify size-4 place-self-center octicon--grabber-16"></span>
121+
</div>
122+
</Slider.Thumb>
123+
</Slider.Root>
152124
</div>
153125
</div>
154126
<AddedOrRemovedImageLabel mode="add" dims={dims.b} />
@@ -158,15 +130,22 @@
158130
{#snippet fade(dims: DimensionData)}
159131
<div class="flex flex-row items-center gap-4">
160132
<AddedOrRemovedImageLabel mode="remove" dims={dims.a} />
161-
<div class="relative grid grid-cols-1 gap-4">
162-
<img src={fileA} alt="A" class="png-bg h-auto w-full border-2 border-red-600 shadow-md" draggable="false" style={getMaxW("a", dims)} />
133+
<div class="relative grid grid-cols-1 gap-4" style={overlayContainerStyle}>
134+
<img
135+
bind:this={overlayImgA}
136+
src={fileA}
137+
alt="A"
138+
class="png-bg h-auto w-full place-self-center border-2 border-red-600 shadow-md"
139+
draggable="false"
140+
style={getStyle("a", dims)}
141+
/>
163142
<img
164143
bind:this={overlayImgB}
165144
src={fileB}
166145
alt="B"
167146
class="png-bg absolute h-auto max-w-full place-self-center border-2 border-green-600 shadow-md"
168147
draggable="false"
169-
style="opacity: {fadePercent}%; {getMaxW('b', dims)}"
148+
style="opacity: {fadePercent}%; {getStyle('b', dims)}"
170149
/>
171150
</div>
172151
<AddedOrRemovedImageLabel mode="add" dims={dims.b} />
@@ -183,7 +162,7 @@
183162

184163
<div class="flex flex-col items-center justify-center bg-neutral-2 p-4">
185164
{@render modeSelector()}
186-
{#await dimensions}
165+
{#await imageDimensions}
187166
<Spinner />
188167
{:then dims}
189168
{#if mode === "side-by-side"}

0 commit comments

Comments
 (0)