import { useRef, useState, useEffect, useCallback } from 'react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { X, ArrowRight, Circle, Type, Undo2, Download, Check, } from 'lucide-react'; type Tool = 'arrow' | 'circle' | 'text'; interface Annotation { type: Tool; x1: number; y1: number; x2: number; y2: number; text?: string; } interface AnnotationEditorProps { imageSrc: string; onSave: (annotatedDataUrl: string) => void; onClose: () => void; } export function AnnotationEditor({ imageSrc, onSave, onClose }: AnnotationEditorProps) { const canvasRef = useRef(null); const imgRef = useRef(null); const [tool, setTool] = useState('arrow'); const [annotations, setAnnotations] = useState([]); const [drawing, setDrawing] = useState(false); const [start, setStart] = useState({ x: 0, y: 0 }); const [current, setCurrent] = useState({ x: 0, y: 0 }); const [pendingText, setPendingText] = useState<{ x: number; y: number } | null>(null); const [textValue, setTextValue] = useState(''); const [canvasSize, setCanvasSize] = useState({ w: 800, h: 450 }); // Load image useEffect(() => { const img = new Image(); img.onload = () => { imgRef.current = img; // Fit within container while preserving aspect ratio const maxW = Math.min(900, window.innerWidth - 80); const maxH = Math.min(600, window.innerHeight - 200); const scale = Math.min(maxW / img.width, maxH / img.height, 1); setCanvasSize({ w: Math.round(img.width * scale), h: Math.round(img.height * scale) }); }; img.src = imageSrc; }, [imageSrc]); const getPos = useCallback((e: React.MouseEvent) => { const rect = canvasRef.current!.getBoundingClientRect(); return { x: ((e.clientX - rect.left) / rect.width) * canvasSize.w, y: ((e.clientY - rect.top) / rect.height) * canvasSize.h, }; }, [canvasSize]); // Draw everything const redraw = useCallback((extra?: Annotation) => { const ctx = canvasRef.current?.getContext('2d'); const img = imgRef.current; if (!ctx || !img) return; ctx.clearRect(0, 0, canvasSize.w, canvasSize.h); ctx.drawImage(img, 0, 0, canvasSize.w, canvasSize.h); const allAnnotations = extra ? [...annotations, extra] : annotations; allAnnotations.forEach((a) => drawAnnotation(ctx, a)); }, [annotations, canvasSize]); useEffect(() => { redraw(); }, [redraw]); const drawAnnotation = (ctx: CanvasRenderingContext2D, a: Annotation) => { ctx.strokeStyle = '#ef4444'; ctx.fillStyle = '#ef4444'; ctx.lineWidth = 2.5; ctx.font = 'bold 16px monospace'; if (a.type === 'arrow') { const dx = a.x2 - a.x1; const dy = a.y2 - a.y1; const angle = Math.atan2(dy, dx); const len = Math.sqrt(dx * dx + dy * dy); if (len < 3) return; ctx.beginPath(); ctx.moveTo(a.x1, a.y1); ctx.lineTo(a.x2, a.y2); ctx.stroke(); // Arrowhead const headLen = Math.min(14, len * 0.3); ctx.beginPath(); ctx.moveTo(a.x2, a.y2); ctx.lineTo(a.x2 - headLen * Math.cos(angle - 0.4), a.y2 - headLen * Math.sin(angle - 0.4)); ctx.lineTo(a.x2 - headLen * Math.cos(angle + 0.4), a.y2 - headLen * Math.sin(angle + 0.4)); ctx.closePath(); ctx.fill(); } else if (a.type === 'circle') { const rx = Math.abs(a.x2 - a.x1) / 2; const ry = Math.abs(a.y2 - a.y1) / 2; const cx = (a.x1 + a.x2) / 2; const cy = (a.y1 + a.y2) / 2; ctx.beginPath(); ctx.ellipse(cx, cy, Math.max(rx, 2), Math.max(ry, 2), 0, 0, Math.PI * 2); ctx.stroke(); } else if (a.type === 'text' && a.text) { // Background const metrics = ctx.measureText(a.text); const pad = 4; ctx.fillStyle = 'rgba(0,0,0,0.6)'; ctx.fillRect(a.x1 - pad, a.y1 - 16 - pad, metrics.width + pad * 2, 20 + pad * 2); ctx.fillStyle = '#ffffff'; ctx.fillText(a.text, a.x1, a.y1); } }; const handleMouseDown = (e: React.MouseEvent) => { if (pendingText) return; const pos = getPos(e); if (tool === 'text') { setPendingText(pos); setTextValue(''); return; } setDrawing(true); setStart(pos); setCurrent(pos); }; const handleMouseMove = (e: React.MouseEvent) => { if (!drawing) return; const pos = getPos(e); setCurrent(pos); redraw({ type: tool, x1: start.x, y1: start.y, x2: pos.x, y2: pos.y }); }; const handleMouseUp = () => { if (!drawing) return; setDrawing(false); setAnnotations((prev) => [ ...prev, { type: tool, x1: start.x, y1: start.y, x2: current.x, y2: current.y }, ]); }; const confirmText = () => { if (!pendingText || !textValue.trim()) { setPendingText(null); return; } setAnnotations((prev) => [ ...prev, { type: 'text', x1: pendingText.x, y1: pendingText.y, x2: pendingText.x, y2: pendingText.y, text: textValue.trim() }, ]); setPendingText(null); setTextValue(''); }; const undo = () => setAnnotations((prev) => prev.slice(0, -1)); const handleSave = () => { // Render full resolution const img = imgRef.current; if (!img) return; const offscreen = document.createElement('canvas'); offscreen.width = img.width; offscreen.height = img.height; const ctx = offscreen.getContext('2d')!; const scaleX = img.width / canvasSize.w; const scaleY = img.height / canvasSize.h; ctx.drawImage(img, 0, 0); ctx.save(); ctx.scale(scaleX, scaleY); annotations.forEach((a) => drawAnnotation(ctx, a)); ctx.restore(); onSave(offscreen.toDataURL('image/png')); }; const tools: { id: Tool; icon: typeof ArrowRight; label: string }[] = [ { id: 'arrow', icon: ArrowRight, label: 'Seta' }, { id: 'circle', icon: Circle, label: 'Círculo' }, { id: 'text', icon: Type, label: 'Texto' }, ]; return (
{/* Toolbar */}
{tools.map((t) => ( ))}
{/* Canvas area */}
{/* Text input overlay */} {pendingText && (
setTextValue(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && confirmText()} className="h-7 w-40 text-xs" placeholder="Digite o texto…" />
)}

{tool === 'arrow' && 'Clique e arraste para desenhar uma seta'} {tool === 'circle' && 'Clique e arraste para desenhar um círculo'} {tool === 'text' && 'Clique no ponto desejado para adicionar texto'}

); }