suisuyy
Add generated image URL handling in AiEditModal and update useAiFeatures hook; improve prompt change logic
90f5e5b | import React, { useRef, useEffect, useState, useCallback } from 'react'; | |
| import { TFunction } from '../types'; | |
| interface CanvasComponentProps { | |
| penColor: string; | |
| penSize: number; | |
| isEraserMode: boolean; | |
| dataURLToLoad?: string | null; | |
| onDrawEnd: (dataURL: string) => void; | |
| canvasPhysicalWidth: number; | |
| canvasPhysicalHeight: number; | |
| zoomLevel: number; // New prop | |
| t: TFunction; | |
| } | |
| const CanvasComponent: React.FC<CanvasComponentProps> = ({ | |
| penColor, | |
| penSize, | |
| isEraserMode, | |
| dataURLToLoad, | |
| onDrawEnd, | |
| canvasPhysicalWidth, | |
| canvasPhysicalHeight, | |
| zoomLevel, // Use zoomLevel | |
| t, | |
| }) => { | |
| const canvasRef = useRef<HTMLCanvasElement>(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 ('touches' in event) { // Use property check for robust touch event detection | |
| 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 ( | |
| <canvas | |
| ref={canvasRef} | |
| width={canvasPhysicalWidth} | |
| height={canvasPhysicalHeight} | |
| className="border-2 border-slate-400 rounded-lg shadow-2xl touch-none bg-white cursor-crosshair" | |
| // Style for imageRendering is good for pixel art, ensure it doesn't conflict with scaling needs. | |
| // Visual scaling is handled by the parent div's transform. | |
| style={{ imageRendering: 'pixelated' }} | |
| aria-label={t('drawingCanvas')} | |
| /> | |
| ); | |
| }; | |
| export default CanvasComponent; |