ltmarx / web /src /components /ComparisonView.tsx
harelcain's picture
Upload 37 files
f2f99a3 verified
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<HTMLCanvasElement>(null);
const origCanvasRef = useRef<HTMLCanvasElement>(null);
const wmCanvasRef = useRef<HTMLCanvasElement>(null);
const animRef = useRef<number>(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 (
<div className="space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="flex gap-1 rounded-md bg-zinc-800/50 p-0.5">
<button
onClick={() => setMode('side-by-side')}
className={`rounded-md px-3 py-1 text-xs font-medium transition-colors ${
mode === 'side-by-side' ? 'bg-zinc-700 text-zinc-100' : 'text-zinc-400 hover:text-zinc-200'
}`}
>
Side by Side
</button>
<button
onClick={() => setMode('difference')}
className={`rounded-md px-3 py-1 text-xs font-medium transition-colors ${
mode === 'difference' ? 'bg-zinc-700 text-zinc-100' : 'text-zinc-400 hover:text-zinc-200'
}`}
>
Difference
</button>
</div>
<button
onClick={() => setPlaying(!playing)}
className="rounded-md bg-zinc-800/50 px-2.5 py-1 text-xs text-zinc-400 hover:text-zinc-200 transition-colors"
>
{playing ? 'Pause' : 'Play'}
</button>
</div>
<div className="flex items-center gap-3">
{mode === 'difference' && (
<div className="flex items-center gap-2">
<span className="text-[10px] text-zinc-500">Amplify</span>
<input
type="range"
min={1}
max={10}
value={amplify}
onChange={(e) => 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"
/>
<span className="text-[10px] tabular-nums text-zinc-500">{amplify}x</span>
</div>
)}
<span className="text-[10px] tabular-nums text-zinc-600">
{currentFrame + 1}/{totalFrames}
</span>
</div>
</div>
{mode === 'side-by-side' ? (
<div className="grid grid-cols-2 gap-2">
<div className="space-y-1">
<p className="text-[10px] uppercase tracking-wider text-zinc-600">Original</p>
<canvas
ref={origCanvasRef}
width={width}
height={height}
className="w-full rounded-lg border border-zinc-800"
style={{ aspectRatio: aspect }}
/>
</div>
<div className="space-y-1">
<p className="text-[10px] uppercase tracking-wider text-zinc-600">Watermarked</p>
<canvas
ref={wmCanvasRef}
width={width}
height={height}
className="w-full rounded-lg border border-zinc-800"
style={{ aspectRatio: aspect }}
/>
</div>
</div>
) : (
<div className="space-y-1">
<p className="text-[10px] uppercase tracking-wider text-zinc-600">
Pixel Difference (amplified {amplify}x)
</p>
<canvas
ref={diffCanvasRef}
width={width}
height={height}
className="w-full rounded-lg border border-zinc-800"
style={{ aspectRatio: aspect }}
/>
</div>
)}
{/* Frame scrubber */}
<input
type="range"
min={0}
max={totalFrames - 1}
value={currentFrame}
onChange={(e) => { 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"
/>
</div>
);
}