286 lines
11 KiB
TypeScript
286 lines
11 KiB
TypeScript
import React, { useRef, useState, useEffect } from 'react';
|
|
|
|
type Props = {
|
|
onSignatureChange: (data: { fullSignature: string | null; initials: string | null }) => void;
|
|
initialSignatures?: { fullSignature: string | null; initials: string | null };
|
|
};
|
|
|
|
const DrawSignatureCanvas: React.FC<Props> = ({ onSignatureChange, initialSignatures }) => {
|
|
const canvasRef1 = useRef<HTMLCanvasElement>(null);
|
|
const canvasRef2 = useRef<HTMLCanvasElement>(null);
|
|
const [isDrawing, setIsDrawing] = useState(false);
|
|
const [paths1, setPaths1] = useState<Array<Array<{ x: number; y: number }>>>([]);
|
|
const [paths2, setPaths2] = useState<Array<Array<{ x: number; y: number }>>>([]);
|
|
const [currentPath, setCurrentPath] = useState<Array<{ x: number; y: number }>>([]);
|
|
const [activeCanvas, setActiveCanvas] = useState<'full' | 'initials' | null>(null);
|
|
const [initialLoaded, setInitialLoaded] = useState(false);
|
|
|
|
// Store initial signature images to avoid reloading
|
|
const [initialImage1, setInitialImage1] = useState<HTMLImageElement | null>(null);
|
|
const [initialImage2, setInitialImage2] = useState<HTMLImageElement | null>(null);
|
|
|
|
// Setup canvas dimensions when component mounts
|
|
useEffect(() => {
|
|
const setupCanvas = () => {
|
|
if (canvasRef1.current) {
|
|
const rect1 = canvasRef1.current.getBoundingClientRect();
|
|
canvasRef1.current.width = rect1.width;
|
|
canvasRef1.current.height = rect1.height;
|
|
}
|
|
if (canvasRef2.current) {
|
|
const rect2 = canvasRef2.current.getBoundingClientRect();
|
|
canvasRef2.current.width = rect2.width;
|
|
canvasRef2.current.height = rect2.height;
|
|
}
|
|
};
|
|
|
|
setupCanvas();
|
|
|
|
// Load initial signatures after canvas setup
|
|
if (initialSignatures && !initialLoaded) {
|
|
setTimeout(() => {
|
|
loadInitialSignatures();
|
|
}, 100);
|
|
}
|
|
}, []);
|
|
|
|
const loadInitialSignatures = () => {
|
|
if (!initialSignatures) return;
|
|
|
|
let loadedCount = 0;
|
|
const totalToLoad = (initialSignatures.fullSignature ? 1 : 0) + (initialSignatures.initials ? 1 : 0);
|
|
|
|
const checkComplete = () => {
|
|
loadedCount++;
|
|
if (loadedCount === totalToLoad) {
|
|
setInitialLoaded(true);
|
|
}
|
|
};
|
|
|
|
// Load full signature
|
|
if (initialSignatures.fullSignature && canvasRef1.current) {
|
|
const img1 = new Image();
|
|
img1.onload = () => {
|
|
setInitialImage1(img1);
|
|
const ctx1 = canvasRef1.current?.getContext('2d');
|
|
if (ctx1) {
|
|
ctx1.clearRect(0, 0, canvasRef1.current!.width, canvasRef1.current!.height);
|
|
ctx1.drawImage(img1, 0, 0);
|
|
}
|
|
checkComplete();
|
|
};
|
|
img1.src = initialSignatures.fullSignature;
|
|
}
|
|
|
|
// Load initials
|
|
if (initialSignatures.initials && canvasRef2.current) {
|
|
const img2 = new Image();
|
|
img2.onload = () => {
|
|
setInitialImage2(img2);
|
|
const ctx2 = canvasRef2.current?.getContext('2d');
|
|
if (ctx2) {
|
|
ctx2.clearRect(0, 0, canvasRef2.current!.width, canvasRef2.current!.height);
|
|
ctx2.drawImage(img2, 0, 0);
|
|
}
|
|
checkComplete();
|
|
};
|
|
img2.src = initialSignatures.initials;
|
|
}
|
|
|
|
if (totalToLoad === 0) {
|
|
setInitialLoaded(true);
|
|
}
|
|
};
|
|
|
|
const handleMouseDown = (e: React.MouseEvent<HTMLCanvasElement>) => {
|
|
setIsDrawing(true);
|
|
const rect = e.currentTarget.getBoundingClientRect();
|
|
const point = { x: e.clientX - rect.left, y: e.clientY - rect.top };
|
|
setCurrentPath([point]);
|
|
setActiveCanvas(e.currentTarget === canvasRef1.current ? 'full' : 'initials');
|
|
};
|
|
|
|
const handleMouseMove = (e: React.MouseEvent<HTMLCanvasElement>) => {
|
|
if (!isDrawing) return;
|
|
const rect = e.currentTarget.getBoundingClientRect();
|
|
const point = { x: e.clientX - rect.left, y: e.clientY - rect.top };
|
|
const newPath = [...currentPath, point];
|
|
setCurrentPath(newPath);
|
|
|
|
// Draw immediately for smooth experience
|
|
drawCurrentPath(e.currentTarget as HTMLCanvasElement, newPath);
|
|
};
|
|
|
|
const drawCurrentPath = (canvas: HTMLCanvasElement, path: Array<{ x: number; y: number }>) => {
|
|
if (!canvas || path.length < 2) return;
|
|
const ctx = canvas.getContext('2d');
|
|
if (!ctx) return;
|
|
|
|
// Only draw the current path segment without clearing the canvas
|
|
ctx.strokeStyle = '#000';
|
|
ctx.lineWidth = 2;
|
|
ctx.lineJoin = 'round';
|
|
ctx.lineCap = 'round';
|
|
|
|
const lastPoint = path[path.length - 2];
|
|
const currentPoint = path[path.length - 1];
|
|
|
|
ctx.beginPath();
|
|
ctx.moveTo(lastPoint.x, lastPoint.y);
|
|
ctx.lineTo(currentPoint.x, currentPoint.y);
|
|
ctx.stroke();
|
|
};
|
|
|
|
const exportCanvasAsImage = (canvas: HTMLCanvasElement | null): string | null => {
|
|
if (!canvas) return null;
|
|
return canvas.toDataURL('image/png');
|
|
};
|
|
|
|
const handleMouseUp = () => {
|
|
setIsDrawing(false);
|
|
if (currentPath.length === 0 || !activeCanvas) return;
|
|
|
|
if (activeCanvas === 'full') {
|
|
const newPaths = [...paths1, currentPath];
|
|
setPaths1(newPaths);
|
|
} else {
|
|
const newPaths = [...paths2, currentPath];
|
|
setPaths2(newPaths);
|
|
}
|
|
|
|
setCurrentPath([]);
|
|
|
|
// Export both canvases and notify parent
|
|
const fullSignature = exportCanvasAsImage(canvasRef1.current);
|
|
const initials = exportCanvasAsImage(canvasRef2.current);
|
|
onSignatureChange({ fullSignature, initials });
|
|
};
|
|
|
|
const redrawCanvas = (canvas: HTMLCanvasElement, paths: Array<Array<{ x: number; y: number }>>, initialImage: HTMLImageElement | null) => {
|
|
const ctx = canvas.getContext('2d');
|
|
if (!ctx) return;
|
|
|
|
// Clear canvas
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
|
|
// Draw initial signature if exists
|
|
if (initialImage) {
|
|
ctx.drawImage(initialImage, 0, 0);
|
|
}
|
|
|
|
// Draw all paths
|
|
ctx.strokeStyle = '#000';
|
|
ctx.lineWidth = 2;
|
|
ctx.lineJoin = 'round';
|
|
ctx.lineCap = 'round';
|
|
|
|
paths.forEach((path) => {
|
|
if (path.length < 2) return;
|
|
ctx.beginPath();
|
|
ctx.moveTo(path[0].x, path[0].y);
|
|
for (let i = 1; i < path.length; i++) {
|
|
ctx.lineTo(path[i].x, path[i].y);
|
|
}
|
|
ctx.stroke();
|
|
});
|
|
};
|
|
|
|
const clearCanvas = () => {
|
|
if (canvasRef1.current) {
|
|
const ctx1 = canvasRef1.current.getContext('2d');
|
|
ctx1?.clearRect(0, 0, canvasRef1.current.width, canvasRef1.current.height);
|
|
}
|
|
if (canvasRef2.current) {
|
|
const ctx2 = canvasRef2.current.getContext('2d');
|
|
ctx2?.clearRect(0, 0, canvasRef2.current.width, canvasRef2.current.height);
|
|
}
|
|
|
|
setPaths1([]);
|
|
setPaths2([]);
|
|
setInitialImage1(null);
|
|
setInitialImage2(null);
|
|
setInitialLoaded(false);
|
|
onSignatureChange({ fullSignature: null, initials: null });
|
|
};
|
|
|
|
const undoLastPath = () => {
|
|
if (activeCanvas === 'full' && paths1.length > 0) {
|
|
const newPaths = paths1.slice(0, -1);
|
|
setPaths1(newPaths);
|
|
redrawCanvas(canvasRef1.current!, newPaths, initialImage1);
|
|
} else if (activeCanvas === 'initials' && paths2.length > 0) {
|
|
const newPaths = paths2.slice(0, -1);
|
|
setPaths2(newPaths);
|
|
redrawCanvas(canvasRef2.current!, newPaths, initialImage2);
|
|
}
|
|
|
|
// Update parent with new signature data
|
|
setTimeout(() => {
|
|
const fullSignature = exportCanvasAsImage(canvasRef1.current);
|
|
const initials = exportCanvasAsImage(canvasRef2.current);
|
|
onSignatureChange({ fullSignature, initials });
|
|
}, 10);
|
|
};
|
|
|
|
return (
|
|
<div className="h-full flex flex-col">
|
|
<div className="mb-6">
|
|
<h2 className="text-xl font-semibold text-gray-900 mb-2">Draw Signature</h2>
|
|
<p className="text-sm text-gray-600">Create your signature by drawing directly on the canvas</p>
|
|
</div>
|
|
|
|
<div className="flex-1 flex flex-col">
|
|
<div className="grid grid-cols-12 gap-6 mb-2">
|
|
{/* Full Signature Canvas */}
|
|
<div className="col-span-8">
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">Full Signature</label>
|
|
<div className="border-2 border-dashed border-gray-300 rounded-lg bg-gray-50 hover:bg-gray-100 transition-colors">
|
|
<canvas
|
|
ref={canvasRef1}
|
|
className="w-full h-48 cursor-crosshair"
|
|
onMouseDown={handleMouseDown}
|
|
onMouseMove={handleMouseMove}
|
|
onMouseUp={handleMouseUp}
|
|
onMouseLeave={handleMouseUp}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Initials Canvas */}
|
|
<div className="col-span-4">
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">Initials</label>
|
|
<div className="border-2 border-dashed border-gray-300 rounded-lg bg-gray-50 hover:bg-gray-100 transition-colors">
|
|
<canvas
|
|
ref={canvasRef2}
|
|
className="w-full h-48 cursor-crosshair"
|
|
onMouseDown={handleMouseDown}
|
|
onMouseMove={handleMouseMove}
|
|
onMouseUp={handleMouseUp}
|
|
onMouseLeave={handleMouseUp}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Controls */}
|
|
<div className="flex gap-4">
|
|
<button
|
|
className="px-4 py-2 text-sm border border-gray-300 rounded-md hover:bg-gray-50 transition-colors"
|
|
onClick={clearCanvas}
|
|
>
|
|
Clear All
|
|
</button>
|
|
<button
|
|
className="px-4 py-2 text-sm border border-gray-300 rounded-md hover:bg-gray-50 transition-colors"
|
|
onClick={undoLastPath}
|
|
disabled={!activeCanvas || (activeCanvas === 'full' ? paths1.length === 0 : paths2.length === 0)}
|
|
>
|
|
Undo Last
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default DrawSignatureCanvas; |