mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
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.
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { type PointerEvent, useCallback, useEffect, useRef, useState } from "react";
|
import { type PointerEvent, useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
import type { BgPreviewState } from "@/components/common/image-viewer";
|
||||||
import { useTranslation } from "@/contexts/i18n-context";
|
import { useTranslation } from "@/contexts/i18n-context";
|
||||||
import { format } from "@/lib/format";
|
|
||||||
|
|
||||||
interface BeforeAfterSliderProps {
|
interface BeforeAfterSliderProps {
|
||||||
/** URL or data URL of original image. */
|
/** URL or data URL of original image. */
|
||||||
@@ -13,6 +13,8 @@ interface BeforeAfterSliderProps {
|
|||||||
afterSize?: number;
|
afterSize?: number;
|
||||||
/** Initial divider position as a percentage (0–100). Defaults to 50. */
|
/** Initial divider position as a percentage (0–100). Defaults to 50. */
|
||||||
initialPosition?: number;
|
initialPosition?: number;
|
||||||
|
/** Optional CSS preview layers for the "after" panel (remove-bg effects). */
|
||||||
|
bgPreview?: BgPreviewState | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatSize(bytes: number): string {
|
function formatSize(bytes: number): string {
|
||||||
@@ -21,19 +23,21 @@ function formatSize(bytes: number): string {
|
|||||||
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
const CHECKERBOARD_STYLE: React.CSSProperties = {
|
||||||
* Before/After image comparison slider.
|
backgroundColor: "#ffffff",
|
||||||
*
|
backgroundImage:
|
||||||
* Shows two images overlapping with a draggable vertical divider.
|
"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%)",
|
||||||
* The "before" image is on the left, "after" on the right.
|
backgroundSize: "16px 16px",
|
||||||
* Supports mouse and touch interaction via pointer events.
|
backgroundPosition: "0 0, 0 8px, 8px -8px, -8px 0px",
|
||||||
*/
|
};
|
||||||
|
|
||||||
export function BeforeAfterSlider({
|
export function BeforeAfterSlider({
|
||||||
beforeSrc,
|
beforeSrc,
|
||||||
afterSrc,
|
afterSrc,
|
||||||
beforeSize,
|
beforeSize,
|
||||||
afterSize,
|
afterSize,
|
||||||
initialPosition = 50,
|
initialPosition = 50,
|
||||||
|
bgPreview,
|
||||||
}: BeforeAfterSliderProps) {
|
}: BeforeAfterSliderProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -85,6 +89,33 @@ export function BeforeAfterSlider({
|
|||||||
? ((1 - afterSize / beforeSize) * 100).toFixed(1)
|
? ((1 - afterSize / beforeSize) * 100).toFixed(1)
|
||||||
: null;
|
: 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<HTMLImageElement>) => {
|
||||||
|
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 (
|
return (
|
||||||
<div className="flex flex-col items-center gap-3 w-full max-w-2xl mx-auto h-full min-h-0">
|
<div className="flex flex-col items-center gap-3 w-full max-w-2xl mx-auto h-full min-h-0">
|
||||||
{/* Slider container */}
|
{/* Slider container */}
|
||||||
@@ -115,21 +146,44 @@ export function BeforeAfterSlider({
|
|||||||
draggable={false}
|
draggable={false}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* After image (clipped, top layer) */}
|
{/* After panel (clipped, top layer) */}
|
||||||
<div
|
<div className="absolute inset-0" style={{ clipPath: `inset(0 0 0 ${position}%)` }}>
|
||||||
className="absolute inset-0"
|
{/* Background layers constrained to the image content area */}
|
||||||
style={{
|
{contentBox && (
|
||||||
clipPath: `inset(0 0 0 ${position}%)`,
|
<div
|
||||||
backgroundImage:
|
className="absolute overflow-hidden"
|
||||||
"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%)",
|
style={{
|
||||||
backgroundSize: "16px 16px",
|
top: contentBox.top,
|
||||||
backgroundPosition: "0 0, 0 8px, 8px -8px, -8px 0px",
|
left: contentBox.left,
|
||||||
}}
|
width: contentBox.width,
|
||||||
>
|
height: contentBox.height,
|
||||||
|
...(hasBgLayers ? {} : CHECKERBOARD_STYLE),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{bgPreview?.backgroundSrc ? (
|
||||||
|
<img
|
||||||
|
src={bgPreview.backgroundSrc}
|
||||||
|
alt=""
|
||||||
|
className="absolute inset-0 w-full h-full object-cover"
|
||||||
|
style={{ filter: bgPreview.backgroundBlur || undefined }}
|
||||||
|
draggable={false}
|
||||||
|
/>
|
||||||
|
) : bgPreview?.containerBackground ? (
|
||||||
|
<div
|
||||||
|
className="absolute inset-0"
|
||||||
|
style={{ background: bgPreview.containerBackground }}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Processed image */}
|
||||||
<img
|
<img
|
||||||
src={afterSrc}
|
src={afterSrc}
|
||||||
alt="Processed"
|
alt="Processed"
|
||||||
className="w-full h-full object-contain"
|
className="relative w-full h-full object-contain"
|
||||||
|
style={{ filter: bgPreview?.dropShadow || undefined }}
|
||||||
|
onLoad={handleAfterImgLoad}
|
||||||
draggable={false}
|
draggable={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user