SME-Web/src/components/setting/Signature/DrawSignatureCanvas.tsx

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;