"use client"; import { useRef, useState, useEffect, useCallback } from "react"; import { X } from "lucide-react"; interface SelectionBounds { x: number; y: number; width: number; height: number; } interface SelectionData { bounds: SelectionBounds; maskBase64: string; } interface TextSelectionCanvasProps { imageUrl: string; onSelectionComplete: (data: SelectionData) => void; onCancel: () => void; } // Target dimensions for the mask (matching zine page size) const MASK_WIDTH = 768; const MASK_HEIGHT = 1024; export default function TextSelectionCanvas({ imageUrl, onSelectionComplete, onCancel, }: TextSelectionCanvasProps) { const canvasRef = useRef(null); const containerRef = useRef(null); const [isDrawing, setIsDrawing] = useState(false); const [startPoint, setStartPoint] = useState<{ x: number; y: number } | null>(null); const [currentRect, setCurrentRect] = useState(null); const [imageLoaded, setImageLoaded] = useState(false); const [canvasSize, setCanvasSize] = useState({ width: 0, height: 0 }); // Load and draw the background image useEffect(() => { const canvas = canvasRef.current; const container = containerRef.current; if (!canvas || !container) return; const img = new Image(); img.crossOrigin = "anonymous"; img.onload = () => { // Calculate canvas size to fit container while maintaining aspect ratio const containerWidth = container.clientWidth; const containerHeight = container.clientHeight; const imgAspect = img.width / img.height; const containerAspect = containerWidth / containerHeight; let canvasWidth: number; let canvasHeight: number; if (imgAspect > containerAspect) { canvasWidth = containerWidth; canvasHeight = containerWidth / imgAspect; } else { canvasHeight = containerHeight; canvasWidth = containerHeight * imgAspect; } canvas.width = canvasWidth; canvas.height = canvasHeight; setCanvasSize({ width: canvasWidth, height: canvasHeight }); const ctx = canvas.getContext("2d"); if (ctx) { ctx.drawImage(img, 0, 0, canvasWidth, canvasHeight); setImageLoaded(true); } }; img.src = imageUrl; }, [imageUrl]); // Redraw canvas with selection rectangle const redrawCanvas = useCallback((rect: SelectionBounds | null) => { const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext("2d"); if (!ctx) return; // Reload and draw the image const img = new Image(); img.crossOrigin = "anonymous"; img.onload = () => { ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.drawImage(img, 0, 0, canvas.width, canvas.height); // Draw semi-transparent overlay ctx.fillStyle = "rgba(0, 0, 0, 0.4)"; ctx.fillRect(0, 0, canvas.width, canvas.height); if (rect && rect.width > 0 && rect.height > 0) { // Clear the selected area (show original image) ctx.clearRect(rect.x, rect.y, rect.width, rect.height); ctx.drawImage( img, (rect.x / canvas.width) * img.width, (rect.y / canvas.height) * img.height, (rect.width / canvas.width) * img.width, (rect.height / canvas.height) * img.height, rect.x, rect.y, rect.width, rect.height ); // Draw selection border ctx.strokeStyle = "#22c55e"; ctx.lineWidth = 2; ctx.setLineDash([5, 5]); ctx.strokeRect(rect.x, rect.y, rect.width, rect.height); // Draw corner handles ctx.setLineDash([]); ctx.fillStyle = "#22c55e"; const handleSize = 8; const corners = [ { x: rect.x, y: rect.y }, { x: rect.x + rect.width, y: rect.y }, { x: rect.x, y: rect.y + rect.height }, { x: rect.x + rect.width, y: rect.y + rect.height }, ]; corners.forEach(({ x, y }) => { ctx.fillRect(x - handleSize / 2, y - handleSize / 2, handleSize, handleSize); }); } }; img.src = imageUrl; }, [imageUrl]); // Get canvas coordinates from mouse/touch event const getCanvasCoords = (e: React.MouseEvent | React.TouchEvent) => { const canvas = canvasRef.current; if (!canvas) return { x: 0, y: 0 }; const rect = canvas.getBoundingClientRect(); const clientX = "touches" in e ? e.touches[0].clientX : e.clientX; const clientY = "touches" in e ? e.touches[0].clientY : e.clientY; return { x: clientX - rect.left, y: clientY - rect.top, }; }; const handleStart = (e: React.MouseEvent | React.TouchEvent) => { e.preventDefault(); const coords = getCanvasCoords(e); setIsDrawing(true); setStartPoint(coords); setCurrentRect(null); }; const handleMove = (e: React.MouseEvent | React.TouchEvent) => { if (!isDrawing || !startPoint) return; e.preventDefault(); const coords = getCanvasCoords(e); const rect: SelectionBounds = { x: Math.min(startPoint.x, coords.x), y: Math.min(startPoint.y, coords.y), width: Math.abs(coords.x - startPoint.x), height: Math.abs(coords.y - startPoint.y), }; setCurrentRect(rect); redrawCanvas(rect); }; const handleEnd = () => { setIsDrawing(false); setStartPoint(null); }; // Generate mask and complete selection const handleConfirm = useCallback(() => { if (!currentRect || currentRect.width < 20 || currentRect.height < 20) { return; } // Create mask canvas at target dimensions const maskCanvas = document.createElement("canvas"); maskCanvas.width = MASK_WIDTH; maskCanvas.height = MASK_HEIGHT; const maskCtx = maskCanvas.getContext("2d"); if (!maskCtx) return; // Fill with black (preserve area) maskCtx.fillStyle = "black"; maskCtx.fillRect(0, 0, MASK_WIDTH, MASK_HEIGHT); // Scale selection coordinates to mask dimensions const scaleX = MASK_WIDTH / canvasSize.width; const scaleY = MASK_HEIGHT / canvasSize.height; const scaledRect = { x: currentRect.x * scaleX, y: currentRect.y * scaleY, width: currentRect.width * scaleX, height: currentRect.height * scaleY, }; // Draw white rectangle (inpaint area) maskCtx.fillStyle = "white"; maskCtx.fillRect(scaledRect.x, scaledRect.y, scaledRect.width, scaledRect.height); // Convert to base64 (without data: prefix) const maskDataUrl = maskCanvas.toDataURL("image/png"); const maskBase64 = maskDataUrl.replace(/^data:image\/png;base64,/, ""); onSelectionComplete({ bounds: scaledRect, maskBase64, }); }, [currentRect, canvasSize, onSelectionComplete]); const hasValidSelection = currentRect && currentRect.width >= 20 && currentRect.height >= 20; // Handle keyboard events (Enter to confirm, Escape to cancel) useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === "Enter" && hasValidSelection) { e.preventDefault(); handleConfirm(); } else if (e.key === "Escape") { e.preventDefault(); onCancel(); } }; window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); }, [hasValidSelection, handleConfirm, onCancel]); return (
{/* Header */}

Draw a rectangle around the text to edit

{/* Canvas container */}
{!imageLoaded && (
Loading image...
)}
{/* Action buttons */}
{/* Help text */}

Click and drag to select the text region • Press Enter to confirm or Esc to cancel

); }