import React, { useRef, useEffect, useState, useCallback } from 'react'; import { RotateCcw } from 'lucide-react'; const SignatureField = ({ label = 'Signature', required = false, onChange, value, disabled = false, readOnly = false, backgroundColor = '#ffffff', penColor = '#000000', penWidth = 2, }) => { const canvasRef = useRef(null); const [isDrawing, setIsDrawing] = useState(false); const [lastPosition, setLastPosition] = useState({ x: 0, y: 0 }); const [isEmpty, setIsEmpty] = useState(true); const [svgPaths, setSvgPaths] = useState([]); const [currentPath, setCurrentPath] = useState(''); const [smoothingPoints, setSmoothingPoints] = useState([]); // Initialiser le canvas const initializeCanvas = useCallback(() => { const canvas = canvasRef.current; if (!canvas) return; const context = canvas.getContext('2d'); // Support High DPI / Retina displays const devicePixelRatio = window.devicePixelRatio || 1; const displayWidth = 400; const displayHeight = 200; // Ajuster la taille physique du canvas pour la haute résolution canvas.width = displayWidth * devicePixelRatio; canvas.height = displayHeight * devicePixelRatio; // Maintenir la taille d'affichage canvas.style.width = displayWidth + 'px'; canvas.style.height = displayHeight + 'px'; // Adapter le contexte à la densité de pixels context.scale(devicePixelRatio, devicePixelRatio); // Améliorer l'anti-aliasing et le rendu context.imageSmoothingEnabled = true; context.imageSmoothingQuality = 'high'; context.textRenderingOptimization = 'optimizeQuality'; // Configuration du style de dessin context.fillStyle = backgroundColor; context.fillRect(0, 0, displayWidth, displayHeight); context.strokeStyle = penColor; context.lineWidth = penWidth; context.lineCap = 'round'; context.lineJoin = 'round'; context.globalCompositeOperation = 'source-over'; }, [backgroundColor, penColor, penWidth]); useEffect(() => { initializeCanvas(); // Si une valeur est fournie (signature existante), la charger if (value && value !== '') { const canvas = canvasRef.current; const context = canvas.getContext('2d'); if (value.includes('svg+xml')) { // Charger une signature SVG const svgData = atob(value.split(',')[1]); const img = new Image(); const svg = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8', }); const url = URL.createObjectURL(svg); img.onload = () => { context.clearRect(0, 0, canvas.width, canvas.height); context.fillStyle = backgroundColor; context.fillRect(0, 0, canvas.width, canvas.height); context.drawImage(img, 0, 0); setIsEmpty(false); URL.revokeObjectURL(url); }; img.src = url; } else { // Charger une image classique const img = new Image(); img.onload = () => { context.clearRect(0, 0, canvas.width, canvas.height); context.fillStyle = backgroundColor; context.fillRect(0, 0, canvas.width, canvas.height); context.drawImage(img, 0, 0); setIsEmpty(false); }; img.src = value; } } }, [value, initializeCanvas, backgroundColor]); // Obtenir les coordonnées relatives au canvas const getCanvasPosition = (e) => { const canvas = canvasRef.current; const rect = canvas.getBoundingClientRect(); const scaleX = canvas.style.width ? parseFloat(canvas.style.width) / rect.width : 1; const scaleY = canvas.style.height ? parseFloat(canvas.style.height) / rect.height : 1; return { x: (e.clientX - rect.left) * scaleX, y: (e.clientY - rect.top) * scaleY, }; }; // Obtenir les coordonnées pour les événements tactiles const getTouchPosition = (e) => { const canvas = canvasRef.current; const rect = canvas.getBoundingClientRect(); const scaleX = canvas.style.width ? parseFloat(canvas.style.width) / rect.width : 1; const scaleY = canvas.style.height ? parseFloat(canvas.style.height) / rect.height : 1; return { x: (e.touches[0].clientX - rect.left) * scaleX, y: (e.touches[0].clientY - rect.top) * scaleY, }; }; // Commencer le dessin const startDrawing = useCallback( (e) => { if (disabled || readOnly) return; e.preventDefault(); setIsDrawing(true); const pos = e.type.includes('touch') ? getTouchPosition(e) : getCanvasPosition(e); setLastPosition(pos); // Commencer un nouveau path SVG setCurrentPath(`M ${pos.x},${pos.y}`); setSmoothingPoints([pos]); }, [disabled, readOnly] ); // Dessiner const draw = useCallback( (e) => { if (!isDrawing || disabled || readOnly) return; e.preventDefault(); const canvas = canvasRef.current; const context = canvas.getContext('2d'); const currentPos = e.type.includes('touch') ? getTouchPosition(e) : getCanvasPosition(e); // Calculer la distance pour déterminer si on doit interpoler const distance = Math.sqrt( Math.pow(currentPos.x - lastPosition.x, 2) + Math.pow(currentPos.y - lastPosition.y, 2) ); // Si la distance est grande, interpoler pour un tracé plus lisse if (distance > 2) { const midPoint = { x: (lastPosition.x + currentPos.x) / 2, y: (lastPosition.y + currentPos.y) / 2, }; // Utiliser une courbe quadratique pour un tracé plus lisse context.beginPath(); context.moveTo(lastPosition.x, lastPosition.y); context.quadraticCurveTo( lastPosition.x, lastPosition.y, midPoint.x, midPoint.y ); context.stroke(); setLastPosition(midPoint); setCurrentPath( (prev) => prev + ` Q ${lastPosition.x},${lastPosition.y} ${midPoint.x},${midPoint.y}` ); } else { // Tracé direct pour les mouvements lents context.beginPath(); context.moveTo(lastPosition.x, lastPosition.y); context.lineTo(currentPos.x, currentPos.y); context.stroke(); setLastPosition(currentPos); setCurrentPath((prev) => prev + ` L ${currentPos.x},${currentPos.y}`); } setIsEmpty(false); }, [isDrawing, lastPosition, disabled] ); // Arrêter le dessin const stopDrawing = useCallback( (e) => { if (!isDrawing) return; e.preventDefault(); setIsDrawing(false); // Ajouter le path terminé aux paths SVG if (currentPath) { setSvgPaths((prev) => [...prev, currentPath]); setCurrentPath(''); } // Notifier le parent du changement avec SVG if (onChange) { const newPaths = [...svgPaths, currentPath].filter((p) => p.length > 0); const svgData = generateSVG(newPaths); onChange(svgData); } }, [isDrawing, onChange, svgPaths, currentPath] ); // Générer le SVG à partir des paths const generateSVG = (paths) => { const svgContent = ` ${paths .map( (path) => `` ) .join('\n ')} `; return `data:image/svg+xml;base64,${btoa(svgContent)}`; }; // Effacer la signature const clearSignature = () => { const canvas = canvasRef.current; const context = canvas.getContext('2d'); // Effacer en tenant compte des dimensions d'affichage const displayWidth = 400; const displayHeight = 200; context.clearRect(0, 0, displayWidth, displayHeight); context.fillStyle = backgroundColor; context.fillRect(0, 0, displayWidth, displayHeight); setIsEmpty(true); setSvgPaths([]); setCurrentPath(''); setSmoothingPoints([]); if (onChange) { onChange(''); } }; return (
{label && ( )}
{readOnly ? isEmpty ? 'Aucune signature' : 'Signature' : isEmpty ? 'Signez dans la zone ci-dessus' : 'Signature capturée'}
{!readOnly && (
)}
{required && isEmpty && (
La signature est obligatoire
)}
); }; export default SignatureField;