Skip to content

Commit f6f71ea

Browse files
committed
feat(tools): add Image Comparison Tool (Phase 1 - side-by-side, slider, onion skin)
1 parent 14ea9fa commit f6f71ea

3 files changed

Lines changed: 210 additions & 0 deletions

File tree

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
"use client";
2+
3+
import { useState } from "react";
4+
5+
type ViewMode = "side-by-side" | "slider" | "onion";
6+
7+
export default function ImageDiffViewer() {
8+
const [imageA, setImageA] = useState<string | null>(null);
9+
const [imageB, setImageB] = useState<string | null>(null);
10+
const [mode, setMode] = useState<ViewMode>("side-by-side");
11+
const [opacity, setOpacity] = useState<number>(0.5);
12+
const [sliderValue, setSliderValue] = useState<number>(50);
13+
14+
return (
15+
<div className="p-6">
16+
<h2 className="text-xl font-semibold mb-4">Image Comparison Tool</h2>
17+
18+
{/* Controls will go here */}
19+
20+
{/* Upload Controls */}
21+
<div className="flex gap-4 mb-6">
22+
<div>
23+
<label className="block text-sm mb-1">Upload Image A</label>
24+
<input
25+
type="file"
26+
accept="image/*"
27+
onChange={(e) => {
28+
const file = e.target.files?.[0];
29+
if (file) {
30+
const reader = new FileReader();
31+
reader.onload = () => setImageA(reader.result as string);
32+
reader.readAsDataURL(file);
33+
}
34+
}}
35+
/>
36+
</div>
37+
38+
<div>
39+
<label className="block text-sm mb-1">Upload Image B</label>
40+
<input
41+
type="file"
42+
accept="image/*"
43+
onChange={(e) => {
44+
const file = e.target.files?.[0];
45+
if (file) {
46+
const reader = new FileReader();
47+
reader.onload = () => setImageB(reader.result as string);
48+
reader.readAsDataURL(file);
49+
}
50+
}}
51+
/>
52+
</div>
53+
</div>
54+
55+
{/* Mode Selector */}
56+
<div className="flex gap-3 mb-6">
57+
<button
58+
onClick={() => setMode("side-by-side")}
59+
className={`px-3 py-1 rounded ${
60+
mode === "side-by-side" ? "bg-blue-600 text-white" : "bg-gray-200"
61+
}`}
62+
>
63+
Side by Side
64+
</button>
65+
66+
<button
67+
onClick={() => setMode("slider")}
68+
className={`px-3 py-1 rounded ${
69+
mode === "slider" ? "bg-blue-600 text-white" : "bg-gray-200"
70+
}`}
71+
>
72+
Slider
73+
</button>
74+
75+
<button
76+
onClick={() => setMode("onion")}
77+
className={`px-3 py-1 rounded ${
78+
mode === "onion" ? "bg-blue-600 text-white" : "bg-gray-200"
79+
}`}
80+
>
81+
Onion Skin
82+
</button>
83+
</div>
84+
85+
{mode === "slider" && (
86+
<div className="mb-4">
87+
<label className="block text-sm mb-1">
88+
Slide Position ({sliderValue}%)
89+
</label>
90+
<input
91+
type="range"
92+
min="0"
93+
max="100"
94+
value={sliderValue}
95+
onChange={(e) => setSliderValue(Number(e.target.value))}
96+
className="w-full"
97+
/>
98+
</div>
99+
)}
100+
{mode === "onion" && (
101+
<div className="mb-4">
102+
<label className="block text-sm mb-1">
103+
Opacity ({Math.round(opacity * 100)}%)
104+
</label>
105+
<input
106+
type="range"
107+
min="0"
108+
max="1"
109+
step="0.01"
110+
value={opacity}
111+
onChange={(e) => setOpacity(Number(e.target.value))}
112+
className="w-full"
113+
/>
114+
</div>
115+
)}
116+
{(!imageA || !imageB) && (
117+
<p className="text-gray-500 mt-4">
118+
Please upload both images to compare.
119+
</p>
120+
)}
121+
{/* Viewer */}
122+
{imageA && imageB && (
123+
<div className="mt-4">
124+
{mode === "side-by-side" && (
125+
<div className="flex gap-4">
126+
<div className="flex-1">
127+
<img
128+
src={imageA}
129+
alt="Image A"
130+
className="w-full object-contain border rounded"
131+
/>
132+
</div>
133+
134+
<div className="flex-1">
135+
<img
136+
src={imageB}
137+
alt="Image B"
138+
className="w-full object-contain border rounded"
139+
/>
140+
</div>
141+
</div>
142+
)}
143+
144+
{mode === "slider" && (
145+
<div className="relative w-full max-w-3xl mx-auto aspect-auto">
146+
<img
147+
src={imageA}
148+
alt="Image A"
149+
className="w-full object-contain border rounded"
150+
/>
151+
<img
152+
src={imageB}
153+
alt="Image B"
154+
className="absolute top-0 left-0 w-full object-contain border rounded"
155+
style={{ clipPath: `inset(0 ${100 - sliderValue}% 0 0)` }}
156+
/>
157+
</div>
158+
)}
159+
160+
{mode === "onion" && (
161+
<div className="relative w-full max-w-3xl mx-auto aspect-auto">
162+
<img
163+
src={imageA}
164+
alt="Image A"
165+
className="w-full h-auto object-contain border rounded"
166+
/>
167+
<img
168+
src={imageB}
169+
alt="Image B"
170+
className="absolute top-0 left-0 w-full h-auto object-contain border rounded"
171+
style={{ opacity }}
172+
/>
173+
</div>
174+
)}
175+
</div>
176+
)}
177+
</div>
178+
);
179+
}

app/libs/constants.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ import XmlToJsonConverter from '../components/developmentToolsComponent/xmlToJso
186186
import XorCalculator from '../components/developmentToolsComponent/xorCalculator';
187187
import CurlToCodeConverter from '../components/developmentToolsComponent/curlToCodeConverter';
188188
import YAMLFormatterAndBeautifier from '../components/developmentToolsComponent/yamlFormatterAndBeautifier';
189+
import ImageDiffViewer from '../components/developmentToolsComponent/imageDiffViewer';
189190

190191
export const WEB_URL = 'https://www.betterbugs.io';
191192

@@ -1718,6 +1719,7 @@ export const PATHS = {
17181719
SHUFFLE_TEXT_LINES: '/shuffle-text-lines',
17191720
RANDOM_IP_GENERATOR: '/random-ip-generator',
17201721
JSON_COMPARE: '/json-compare',
1722+
IMAGE_DIFF_VIEWER: "/image-diff-viewer",
17211723
TEXT_COMPARE: '/text-compare',
17221724
URL_DECODE: '/url-decode',
17231725
URL_ENCODE: '/url-encode',
@@ -1816,6 +1818,10 @@ export const developmentToolsRoutes = [
18161818
path: PATHS.JSON_MINIFIER,
18171819
component: <JsonMinifierComponent />,
18181820
},
1821+
{
1822+
path: PATHS.IMAGE_DIFF_VIEWER,
1823+
component: <ImageDiffViewer />,
1824+
},
18191825
{
18201826
path: PATHS.JSON_PRETTIFIER,
18211827
component: <JsonPrettifierComponent />,

app/libs/developmentToolsConstant.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17361,4 +17361,29 @@ family[1]: "Beth"`,
1736117361
og_image: '/images/og-images/Cover.png',
1736217362
},
1736317363
},
17364+
17365+
'image-diff-viewer': {
17366+
hero_section: {
17367+
title: 'Image Comparison Tool',
17368+
description:
17369+
'A visual tool to compare two images using side-by-side, slider, and onion skin modes.',
17370+
},
17371+
development_tools_list: [],
17372+
development_tools_about_details: {
17373+
about_title: 'What is the Image Comparison Tool?',
17374+
about_description: [
17375+
{
17376+
description:
17377+
'This tool allows developers to compare two images and visually detect differences.',
17378+
},
17379+
],
17380+
},
17381+
development_tools_steps_guide: {
17382+
guide_title: 'Step-by-Step Guide',
17383+
guide_description: 'How to use the image comparison tool:',
17384+
steps: [],
17385+
},
17386+
},
1736417387
};
17388+
17389+

0 commit comments

Comments
 (0)