import { Download } from "lucide-react"; import { useCallback, useEffect, useRef, useState } from "react"; import { ProgressCard } from "@/components/common/progress-card"; import { useTranslation } from "@/contexts/i18n-context"; import { useToolProcessor } from "@/hooks/use-tool-processor"; import { useFileStore } from "@/stores/file-store"; type PixelateMode = "whole" | "selection"; export function PixelateSettings({ onImageOverlay, onImageStyle, }: { onImageOverlay?: (children: React.ReactNode) => void; onImageStyle?: (style: React.CSSProperties | null) => void; }) { const { t } = useTranslation(); const { files } = useFileStore(); const entry = useFileStore((s) => s.entries[s.selectedIndex]); const blobUrl = entry?.blobUrl; const { processFiles, processAllFiles, processing, error, downloadUrl, progress } = useToolProcessor("pixelate"); const [blockSize, setBlockSize] = useState(12); const [mode, setMode] = useState("whole"); // Natural image dimensions (for converting normalized box to pixel region) const [dims, setDims] = useState<{ w: number; h: number } | null>(null); useEffect(() => { if (!blobUrl) { setDims(null); return; } const img = new Image(); img.onload = () => setDims({ w: img.naturalWidth, h: img.naturalHeight }); img.src = blobUrl; }, [blobUrl]); // Normalized selection box in 0..1 (default centered 40x40%) const [boxX, setBoxX] = useState(0.3); const [boxY, setBoxY] = useState(0.3); const [boxW, setBoxW] = useState(0.4); const [boxH, setBoxH] = useState(0.4); // Refs for latest values (stable pointer-event handlers read from these) const boxXRef = useRef(boxX); const boxYRef = useRef(boxY); const boxWRef = useRef(boxW); const boxHRef = useRef(boxH); useEffect(() => { boxXRef.current = boxX; }, [boxX]); useEffect(() => { boxYRef.current = boxY; }, [boxY]); useEffect(() => { boxWRef.current = boxW; }, [boxW]); useEffect(() => { boxHRef.current = boxH; }, [boxH]); const dragRef = useRef<{ startX: number; startY: number; origX: number; origY: number; } | null>(null); const containerRef = useRef(null); // Ref-to-latest for overlay/style callbacks (avoids stale closures) const onOverlayRef = useRef(onImageOverlay); useEffect(() => { onOverlayRef.current = onImageOverlay; }); const onStyleRef = useRef(onImageStyle); useEffect(() => { onStyleRef.current = onImageStyle; }); // Stable pointer-event handlers (read state from refs, never go stale) const handlePointerDown = useCallback((e: React.PointerEvent) => { e.currentTarget.setPointerCapture(e.pointerId); dragRef.current = { startX: e.clientX, startY: e.clientY, origX: boxXRef.current, origY: boxYRef.current, }; }, []); const handlePointerMove = useCallback((e: React.PointerEvent) => { if (!dragRef.current || !containerRef.current) return; const rect = containerRef.current.getBoundingClientRect(); if (rect.width === 0 || rect.height === 0) return; const dx = (e.clientX - dragRef.current.startX) / rect.width; const dy = (e.clientY - dragRef.current.startY) / rect.height; const nx = Math.min(Math.max(dragRef.current.origX + dx, 0), 1 - boxWRef.current); const ny = Math.min(Math.max(dragRef.current.origY + dy, 0), 1 - boxHRef.current); setBoxX(nx); setBoxY(ny); }, []); const handlePointerUp = useCallback(() => { dragRef.current = null; }, []); // Clamp box position when width/height sliders change useEffect(() => { setBoxX((prev) => Math.min(prev, 1 - boxW)); setBoxY((prev) => Math.min(prev, 1 - boxH)); }, [boxW, boxH]); // Manage overlay and imageWrapperStyle based on mode useEffect(() => { if (mode !== "selection") { onOverlayRef.current?.(null); onStyleRef.current?.(null); return; } // Activate the image wrapper div so overlay children render onStyleRef.current?.({}); const overlay = (
{ containerRef.current = el; }} style={{ position: "absolute" as const, inset: 0, zIndex: 10, pointerEvents: "none" as const, }} >
); onOverlayRef.current?.(overlay); return () => { onOverlayRef.current?.(null); onStyleRef.current?.(null); }; }, [mode, boxX, boxY, boxW, boxH, handlePointerDown, handlePointerMove, handlePointerUp]); const handleProcess = () => { const settings: Record = { blockSize }; if (mode === "selection" && dims) { const W = dims.w; const H = dims.h; const left = Math.round(boxX * W); const top = Math.round(boxY * H); const width = Math.max(1, Math.min(Math.round(boxW * W), W - left)); const height = Math.max(1, Math.min(Math.round(boxH * H), H - top)); settings.region = { left, top, width, height }; } if (files.length > 1) { processAllFiles(files, settings); } else { processFiles(files, settings); } }; const hasFile = files.length > 0; const canProcess = hasFile && !processing; const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (canProcess) handleProcess(); }; return (
{/* Mode toggle */}
Mode
{/* Block Size */}
{blockSize}px
setBlockSize(Number(e.target.value))} className="w-full mt-1" /> {mode === "whole" && (

Applies pixelation to the full image

)}
{/* Selection region controls */} {mode === "selection" && (
{Math.round(boxW * 100)}%
setBoxW(Number(e.target.value) / 100)} className="w-full mt-1" />
{Math.round(boxH * 100)}%
setBoxH(Number(e.target.value) / 100)} className="w-full mt-1" />
{hasFile && (

Drag the selection box on the image to reposition

)}
)} {error &&

{error}

} {processing ? ( ) : ( )} {downloadUrl && ( {t.common.download} )} ); }