From a8cca62336dd2436795d57eb082acfb0f0ffbe5d Mon Sep 17 00:00:00 2001 From: Siddharth Kumar Sah Date: Wed, 15 Apr 2026 23:11:27 +0800 Subject: [PATCH] feat(erase-object): auto-center compare slider at erased region Track the painted mask centroid and initialise the before/after slider at that position so the result is immediately visible without manual dragging. --- .../components/common/before-after-slider.tsx | 5 ++++- .../src/components/tools/compare-settings.tsx | 4 +++- .../tools/erase-object-settings.tsx | 8 ++++++++ .../src/components/tools/eraser-canvas.tsx | 15 ++++++++++++++ apps/web/src/lib/tool-registry.tsx | 3 ++- apps/web/src/pages/tool-page.tsx | 20 +++++++++++++++++++ 6 files changed, 52 insertions(+), 3 deletions(-) diff --git a/apps/web/src/components/common/before-after-slider.tsx b/apps/web/src/components/common/before-after-slider.tsx index a45df3ca..d30cca9f 100644 --- a/apps/web/src/components/common/before-after-slider.tsx +++ b/apps/web/src/components/common/before-after-slider.tsx @@ -9,6 +9,8 @@ interface BeforeAfterSliderProps { beforeSize?: number; /** Processed file size in bytes. */ afterSize?: number; + /** Initial divider position as a percentage (0–100). Defaults to 50. */ + initialPosition?: number; } function formatSize(bytes: number): string { @@ -29,9 +31,10 @@ export function BeforeAfterSlider({ afterSrc, beforeSize, afterSize, + initialPosition = 50, }: BeforeAfterSliderProps) { const containerRef = useRef(null); - const [position, setPosition] = useState(50); // percentage 0-100 + const [position, setPosition] = useState(initialPosition); // percentage 0-100 const [isDragging, setIsDragging] = useState(false); const updatePosition = useCallback((clientX: number) => { diff --git a/apps/web/src/components/tools/compare-settings.tsx b/apps/web/src/components/tools/compare-settings.tsx index 2e611916..b4286ab1 100644 --- a/apps/web/src/components/tools/compare-settings.tsx +++ b/apps/web/src/components/tools/compare-settings.tsx @@ -36,7 +36,9 @@ export function CompareSettings() { const result = await res.json(); setSimilarity(result.similarity); setDownloadUrl(result.downloadUrl); - setProcessedUrl(result.downloadUrl); + // Show the second image in the slider (not the diff) so the user can + // visually compare their two originals. The diff is still downloadable. + setProcessedUrl(URL.createObjectURL(secondFile)); } catch (err) { setError(err instanceof Error ? err.message : "Comparison failed"); } finally { diff --git a/apps/web/src/components/tools/erase-object-settings.tsx b/apps/web/src/components/tools/erase-object-settings.tsx index 117a8c2f..627a8daf 100644 --- a/apps/web/src/components/tools/erase-object-settings.tsx +++ b/apps/web/src/components/tools/erase-object-settings.tsx @@ -14,6 +14,7 @@ interface EraseObjectSettingsProps { hasStrokes: boolean; brushSize: number; onBrushSizeChange: (size: number) => void; + onMaskCenter?: (centerPct: number) => void; } export function EraseObjectSettings({ @@ -21,6 +22,7 @@ export function EraseObjectSettings({ hasStrokes, brushSize, onBrushSizeChange: setBrushSize, + onMaskCenter, }: EraseObjectSettingsProps) { const { files, processing, error, setProcessing, setError, setProcessedUrl, setSizes } = useFileStore(); @@ -41,6 +43,12 @@ export function EraseObjectSettings({ const maskBlob = await eraserRef.current.exportMask(); if (!maskBlob) return; + // Record where the user painted so the comparison slider starts at that location + const maskCenter = eraserRef.current.getMaskCenter(); + if (maskCenter !== null && onMaskCenter) { + onMaskCenter(maskCenter); + } + setError(null); setDownloadUrl(null); setProcessing(true); diff --git a/apps/web/src/components/tools/eraser-canvas.tsx b/apps/web/src/components/tools/eraser-canvas.tsx index 8eae014c..86db8105 100644 --- a/apps/web/src/components/tools/eraser-canvas.tsx +++ b/apps/web/src/components/tools/eraser-canvas.tsx @@ -5,6 +5,7 @@ type Stroke = { points: Point[]; size: number }; export interface EraserCanvasRef { exportMask: () => Promise; + getMaskCenter: () => number | null; clear: () => void; undo: () => void; } @@ -238,6 +239,20 @@ export const EraserCanvas = forwardRef(funct mask.toBlob((b) => resolve(b), "image/png"); }); }, + getMaskCenter: () => { + if (strokesRef.current.length === 0 || !canvasSize) return null; + let minX = Infinity; + let maxX = -Infinity; + for (const stroke of strokesRef.current) { + for (const pt of stroke.points) { + minX = Math.min(minX, pt.x - stroke.size / 2); + maxX = Math.max(maxX, pt.x + stroke.size / 2); + } + } + if (minX === Infinity) return null; + const centerX = (minX + maxX) / 2; + return Math.max(0, Math.min(100, (centerX / canvasSize.w) * 100)); + }, clear: () => { strokesRef.current = []; currentPointsRef.current = []; diff --git a/apps/web/src/lib/tool-registry.tsx b/apps/web/src/lib/tool-registry.tsx index 5f2e25de..3a7fb557 100644 --- a/apps/web/src/lib/tool-registry.tsx +++ b/apps/web/src/lib/tool-registry.tsx @@ -43,6 +43,7 @@ export interface EraserProps { hasStrokes: boolean; brushSize: number; onBrushSizeChange: (size: number) => void; + onMaskCenter?: (centerPct: number) => void; } // ── Registry entry ───────────────────────────────────────────────── @@ -433,7 +434,7 @@ export const toolRegistry = new Map([ [ "image-enhancement", { - displayMode: "live-preview" as DisplayMode, + displayMode: "before-after" as DisplayMode, livePreview: true, Settings: ImageEnhancementSettings as never, }, diff --git a/apps/web/src/pages/tool-page.tsx b/apps/web/src/pages/tool-page.tsx index b3ee28db..f2bc346b 100644 --- a/apps/web/src/pages/tool-page.tsx +++ b/apps/web/src/pages/tool-page.tsx @@ -179,6 +179,8 @@ export function ToolPage() { const eraserRef = useRef(null); const [eraserHasStrokes, setEraserHasStrokes] = useState(false); const [eraserBrushSize, setEraserBrushSize] = useState(30); + // Center of the painted mask as a 0-100 percentage — used to init the slider at the right spot + const [eraserSliderInitPos, setEraserSliderInitPos] = useState(null); // Reset crop state when the image changes useEffect(() => { @@ -188,6 +190,7 @@ export function ToolPage() { const handleFiles = useCallback( (newFiles: File[]) => { + setEraserSliderInitPos(null); reset(); setFiles(newFiles); }, @@ -196,6 +199,7 @@ export function ToolPage() { const handleUndo = useCallback(() => { undoProcessing(); + setEraserSliderInitPos(null); }, [undoProcessing]); const handleAddMore = useCallback(() => { @@ -276,6 +280,7 @@ export function ToolPage() { hasStrokes: eraserHasStrokes, brushSize: eraserBrushSize, onBrushSizeChange: setEraserBrushSize, + onMaskCenter: setEraserSliderInitPos, } : undefined, }; @@ -349,6 +354,21 @@ export function ToolPage() { ); } + // After erasing: compare clean original vs inpainted result. + // Initialise the divider at the center of the painted area so the comparison + // lands right where the object was removed. + if (displayMode === "interactive-eraser" && hasProcessed && originalBlobUrl) { + return ( + + ); + } + if (displayMode === "interactive-split" && hasFile && originalBlobUrl) { if (registryEntry?.ResultsPanel) { const Panel = registryEntry.ResultsPanel;