From 6a2b19919d82d71ec83470141ab6334fa0e5a7eb Mon Sep 17 00:00:00 2001 From: SnapOtter Date: Mon, 18 May 2026 10:19:47 +0800 Subject: [PATCH] feat: add background preview support to before-after slider Render custom background layers (solid color, image, checkerboard) and drop shadow in the after panel, constrained to the actual image content area. Supports remove-bg tool preview effects. --- .../components/common/before-after-slider.tsx | 94 +++++++++++++++---- 1 file changed, 74 insertions(+), 20 deletions(-) diff --git a/apps/web/src/components/common/before-after-slider.tsx b/apps/web/src/components/common/before-after-slider.tsx index 41aa5184..5fae8b9c 100644 --- a/apps/web/src/components/common/before-after-slider.tsx +++ b/apps/web/src/components/common/before-after-slider.tsx @@ -1,6 +1,6 @@ import { type PointerEvent, useCallback, useEffect, useRef, useState } from "react"; +import type { BgPreviewState } from "@/components/common/image-viewer"; import { useTranslation } from "@/contexts/i18n-context"; -import { format } from "@/lib/format"; interface BeforeAfterSliderProps { /** URL or data URL of original image. */ @@ -13,6 +13,8 @@ interface BeforeAfterSliderProps { afterSize?: number; /** Initial divider position as a percentage (0–100). Defaults to 50. */ initialPosition?: number; + /** Optional CSS preview layers for the "after" panel (remove-bg effects). */ + bgPreview?: BgPreviewState | null; } function formatSize(bytes: number): string { @@ -21,19 +23,21 @@ function formatSize(bytes: number): string { return `${(bytes / (1024 * 1024)).toFixed(2)} MB`; } -/** - * Before/After image comparison slider. - * - * Shows two images overlapping with a draggable vertical divider. - * The "before" image is on the left, "after" on the right. - * Supports mouse and touch interaction via pointer events. - */ +const CHECKERBOARD_STYLE: React.CSSProperties = { + backgroundColor: "#ffffff", + backgroundImage: + "linear-gradient(45deg, #e0e0e0 25%, transparent 25%), linear-gradient(-45deg, #e0e0e0 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #e0e0e0 75%), linear-gradient(-45deg, transparent 75%, #e0e0e0 75%)", + backgroundSize: "16px 16px", + backgroundPosition: "0 0, 0 8px, 8px -8px, -8px 0px", +}; + export function BeforeAfterSlider({ beforeSrc, afterSrc, beforeSize, afterSize, initialPosition = 50, + bgPreview, }: BeforeAfterSliderProps) { const { t } = useTranslation(); const containerRef = useRef(null); @@ -85,6 +89,33 @@ export function BeforeAfterSlider({ ? ((1 - afterSize / beforeSize) * 100).toFixed(1) : null; + const hasBgLayers = bgPreview && (bgPreview.containerBackground || bgPreview.backgroundSrc); + + const [contentBox, setContentBox] = useState<{ + top: number; + left: number; + width: number; + height: number; + } | null>(null); + + const handleAfterImgLoad = useCallback((e: React.SyntheticEvent) => { + const img = e.currentTarget; + const cw = img.clientWidth; + const ch = img.clientHeight; + const nw = img.naturalWidth; + const nh = img.naturalHeight; + if (!nw || !nh) return; + const scale = Math.min(cw / nw, ch / nh); + const rw = nw * scale; + const rh = nh * scale; + setContentBox({ + left: (cw - rw) / 2, + top: (ch - rh) / 2, + width: rw, + height: rh, + }); + }, []); + return (
{/* Slider container */} @@ -115,21 +146,44 @@ export function BeforeAfterSlider({ draggable={false} /> - {/* After image (clipped, top layer) */} -
+ {/* After panel (clipped, top layer) */} +
+ {/* Background layers constrained to the image content area */} + {contentBox && ( +
+ {bgPreview?.backgroundSrc ? ( + + ) : bgPreview?.containerBackground ? ( +
+ ) : null} +
+ )} + + {/* Processed image */} Processed