import React, { useRef, useEffect, useState, useCallback } from 'react'; interface CanvasComponentProps { penColor: string; penSize: number; isEraserMode: boolean; dataURLToLoad?: string | null; onDrawEnd: (dataURL: string) => void; canvasPhysicalWidth: number; canvasPhysicalHeight: number; zoomLevel: number; // New prop } const CanvasComponent: React.FC = ({ penColor, penSize, isEraserMode, dataURLToLoad, onDrawEnd, canvasPhysicalWidth, canvasPhysicalHeight, zoomLevel, // Use zoomLevel }) => { const canvasRef = useRef(null); const [isDrawing, setIsDrawing] = useState(false); const lastPositionRef = useRef<{ x: number; y: number } | null>(null); const getCanvasContext = useCallback((): CanvasRenderingContext2D | null => { const canvas = canvasRef.current; return canvas ? canvas.getContext('2d') : null; }, []); useEffect(() => { const canvas = canvasRef.current; const context = getCanvasContext(); if (canvas && context) { if (dataURLToLoad) { const img = new Image(); img.onload = () => { context.fillStyle = '#FFFFFF'; context.fillRect(0, 0, canvas.width, canvas.height); context.drawImage(img, 0, 0); }; img.onerror = () => { console.error("Failed to load image from dataURL for canvas. Displaying a blank canvas."); context.fillStyle = '#FFFFFF'; context.fillRect(0, 0, canvas.width, canvas.height); } img.src = dataURLToLoad; } else { context.fillStyle = '#FFFFFF'; context.fillRect(0, 0, canvas.width, canvas.height); } } }, [dataURLToLoad, getCanvasContext, canvasPhysicalWidth, canvasPhysicalHeight]); const getRelativePosition = useCallback((event: MouseEvent | TouchEvent): { x: number; y: number } | null => { const canvas = canvasRef.current; if (!canvas) return null; const rect = canvas.getBoundingClientRect(); // This rect is of the visually scaled canvas let clientX, clientY; if (event instanceof TouchEvent) { if (event.touches.length === 0) return null; clientX = event.touches[0].clientX; clientY = event.touches[0].clientY; } else { clientX = event.clientX; clientY = event.clientY; } // Adjust coordinates based on zoom level // (clientX - rect.left) gives position relative to the scaled canvas's top-left on screen // Divide by zoomLevel to get position relative to the unscaled canvas's internal coordinates return { x: (clientX - rect.left) / zoomLevel, y: (clientY - rect.top) / zoomLevel, }; }, [zoomLevel]); // Add zoomLevel to dependencies const startDrawing = useCallback((event: MouseEvent | TouchEvent) => { event.preventDefault(); const pos = getRelativePosition(event); if (!pos) return; const context = getCanvasContext(); if(!context) return; setIsDrawing(true); lastPositionRef.current = pos; context.lineCap = 'round'; context.lineJoin = 'round'; if (isEraserMode) { context.globalCompositeOperation = 'destination-out'; context.strokeStyle = "rgba(0,0,0,1)"; context.fillStyle = "rgba(0,0,0,1)"; } else { context.globalCompositeOperation = 'source-over'; context.strokeStyle = penColor; context.fillStyle = penColor; } context.beginPath(); // For single click dots, use arc. For lines, it's just the start point. // The actual drawing of the dot happens here. context.arc(pos.x, pos.y, penSize / (2 * zoomLevel), 0, Math.PI * 2); // Scale dot size with zoom for visual consistency context.lineWidth = penSize / zoomLevel; // Scale line width for visual consistency context.fill(); }, [getCanvasContext, penColor, penSize, isEraserMode, getRelativePosition, zoomLevel]); const draw = useCallback((event: MouseEvent | TouchEvent) => { if (!isDrawing) return; event.preventDefault(); const pos = getRelativePosition(event); if (!pos || !lastPositionRef.current) return; const context = getCanvasContext(); if (context) { context.lineWidth = penSize / zoomLevel; // Scale line width context.lineCap = 'round'; context.lineJoin = 'round'; if (isEraserMode) { context.globalCompositeOperation = 'destination-out'; context.strokeStyle = "rgba(0,0,0,1)"; } else { context.globalCompositeOperation = 'source-over'; context.strokeStyle = penColor; } context.beginPath(); context.moveTo(lastPositionRef.current.x, lastPositionRef.current.y); context.lineTo(pos.x, pos.y); context.stroke(); lastPositionRef.current = pos; } }, [isDrawing, getCanvasContext, penColor, penSize, isEraserMode, getRelativePosition, zoomLevel]); const endDrawing = useCallback(() => { if (!isDrawing) return; setIsDrawing(false); const canvas = canvasRef.current; if (canvas) { const context = getCanvasContext(); if (context) { context.globalCompositeOperation = 'source-over'; // Reset composite operation } onDrawEnd(canvas.toDataURL('image/png')); } lastPositionRef.current = null; }, [isDrawing, onDrawEnd, getCanvasContext]); useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; // Passive false is important for preventDefault() to work on touch events and prevent scrolling page. const eventOptions = { passive: false }; canvas.addEventListener('mousedown', startDrawing, eventOptions); canvas.addEventListener('mousemove', draw, eventOptions); canvas.addEventListener('mouseup', endDrawing, eventOptions); canvas.addEventListener('mouseleave', endDrawing, eventOptions); canvas.addEventListener('touchstart', startDrawing, eventOptions); canvas.addEventListener('touchmove', draw, eventOptions); canvas.addEventListener('touchend', endDrawing, eventOptions); canvas.addEventListener('touchcancel', endDrawing, eventOptions); return () => { canvas.removeEventListener('mousedown', startDrawing); canvas.removeEventListener('mousemove', draw); canvas.removeEventListener('mouseup', endDrawing); canvas.removeEventListener('mouseleave', endDrawing); canvas.removeEventListener('touchstart', startDrawing); canvas.removeEventListener('touchmove', draw); canvas.removeEventListener('touchend', endDrawing); canvas.removeEventListener('touchcancel', endDrawing); }; }, [startDrawing, draw, endDrawing]); // Re-bind if these handlers change (e.g. due to zoomLevel) return ( ); }; export default CanvasComponent;