import { useRef, useEffect, useState } from 'react'; interface ComparisonViewProps { originalFrames: ImageData[]; watermarkedFrames: ImageData[]; width: number; height: number; fps: number; } export default function ComparisonView({ originalFrames, watermarkedFrames, width, height, fps, }: ComparisonViewProps) { const [mode, setMode] = useState<'side-by-side' | 'difference'>('side-by-side'); const [amplify, setAmplify] = useState(1); const [playing, setPlaying] = useState(false); const [currentFrame, setCurrentFrame] = useState(0); const diffCanvasRef = useRef(null); const origCanvasRef = useRef(null); const wmCanvasRef = useRef(null); const animRef = useRef(0); const lastFrameTime = useRef(0); const totalFrames = originalFrames.length; // Render side-by-side canvases for current frame useEffect(() => { if (mode !== 'side-by-side') return; const origCtx = origCanvasRef.current?.getContext('2d'); const wmCtx = wmCanvasRef.current?.getContext('2d'); if (origCtx && originalFrames[currentFrame]) { origCtx.putImageData(originalFrames[currentFrame], 0, 0); } if (wmCtx && watermarkedFrames[currentFrame]) { wmCtx.putImageData(watermarkedFrames[currentFrame], 0, 0); } }, [originalFrames, watermarkedFrames, mode, currentFrame]); // Render diff frame useEffect(() => { if (mode !== 'difference') return; const ctx = diffCanvasRef.current?.getContext('2d'); if (!ctx || !originalFrames[currentFrame] || !watermarkedFrames[currentFrame]) return; const orig = originalFrames[currentFrame]; const wm = watermarkedFrames[currentFrame]; const diff = new ImageData(width, height); for (let i = 0; i < orig.data.length; i += 4) { const dr = Math.abs(orig.data[i] - wm.data[i]); const dg = Math.abs(orig.data[i + 1] - wm.data[i + 1]); const db = Math.abs(orig.data[i + 2] - wm.data[i + 2]); const d = Math.max(dr, dg, db); const amplified = Math.min(255, d * amplify); diff.data[i] = amplified; diff.data[i + 1] = 0; diff.data[i + 2] = 255 - amplified; diff.data[i + 3] = 255; } ctx.putImageData(diff, 0, 0); }, [originalFrames, watermarkedFrames, mode, amplify, currentFrame, width, height]); // Playback loop useEffect(() => { if (!playing) return; const interval = 1000 / Math.min(fps, 5); const step = (time: number) => { if (time - lastFrameTime.current >= interval) { setCurrentFrame((f) => (f + 1) % totalFrames); lastFrameTime.current = time; } animRef.current = requestAnimationFrame(step); }; animRef.current = requestAnimationFrame(step); return () => cancelAnimationFrame(animRef.current); }, [playing, fps, totalFrames]); const aspect = `${width} / ${height}`; return (
{mode === 'difference' && (
Amplify setAmplify(parseInt(e.target.value))} className="h-1 w-20 appearance-none rounded-full bg-zinc-700 accent-violet-500 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3 [&::-webkit-slider-thumb]:h-3 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-violet-500" /> {amplify}x
)} {currentFrame + 1}/{totalFrames}
{mode === 'side-by-side' ? (

Original

Watermarked

) : (

Pixel Difference (amplified {amplify}x)

)} {/* Frame scrubber */} { setCurrentFrame(parseInt(e.target.value)); setPlaying(false); }} className="w-full h-1 appearance-none rounded-full bg-zinc-700 accent-blue-500 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3 [&::-webkit-slider-thumb]:h-3 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-blue-500" />
); }