feat: added undo button while drawing signature (#480)

This commit is contained in:
18feb06
2023-12-16 07:20:59 +05:00
committed by Mythie
parent de4a0b2560
commit 27b7e29be7

View File

@ -3,6 +3,7 @@
import type { HTMLAttributes, MouseEvent, PointerEvent, TouchEvent } from 'react'; import type { HTMLAttributes, MouseEvent, PointerEvent, TouchEvent } from 'react';
import { useEffect, useMemo, useRef, useState } from 'react'; import { useEffect, useMemo, useRef, useState } from 'react';
import { Undo2 } from 'lucide-react';
import type { StrokeOptions } from 'perfect-freehand'; import type { StrokeOptions } from 'perfect-freehand';
import { getStroke } from 'perfect-freehand'; import { getStroke } from 'perfect-freehand';
@ -27,7 +28,8 @@ export const SignaturePad = ({
const $el = useRef<HTMLCanvasElement>(null); const $el = useRef<HTMLCanvasElement>(null);
const [isPressed, setIsPressed] = useState(false); const [isPressed, setIsPressed] = useState(false);
const [points, setPoints] = useState<Point[]>([]); const [lines, setLines] = useState<Point[][]>([]);
const [currentLine, setCurrentLine] = useState<Point[]>([]);
const perfectFreehandOptions = useMemo(() => { const perfectFreehandOptions = useMemo(() => {
const size = $el.current ? Math.min($el.current.height, $el.current.width) * 0.03 : 10; const size = $el.current ? Math.min($el.current.height, $el.current.width) * 0.03 : 10;
@ -52,26 +54,7 @@ export const SignaturePad = ({
const point = Point.fromEvent(event, DPI, $el.current); const point = Point.fromEvent(event, DPI, $el.current);
const newPoints = [...points, point]; setCurrentLine([point]);
setPoints(newPoints);
if ($el.current) {
const ctx = $el.current.getContext('2d');
if (ctx) {
ctx.save();
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
const pathData = new Path2D(
getSvgPathFromStroke(getStroke(newPoints, perfectFreehandOptions)),
);
ctx.fill(pathData);
}
}
}; };
const onMouseMove = (event: MouseEvent | PointerEvent | TouchEvent) => { const onMouseMove = (event: MouseEvent | PointerEvent | TouchEvent) => {
@ -85,31 +68,36 @@ export const SignaturePad = ({
const point = Point.fromEvent(event, DPI, $el.current); const point = Point.fromEvent(event, DPI, $el.current);
if (point.distanceTo(points[points.length - 1]) > 5) { if (point.distanceTo(currentLine[currentLine.length - 1]) > 5) {
const newPoints = [...points, point]; setCurrentLine([...currentLine, point]);
setPoints(newPoints);
// Update the canvas here to draw the lines
if ($el.current) { if ($el.current) {
const ctx = $el.current.getContext('2d'); const ctx = $el.current.getContext('2d');
if (ctx) { if (ctx) {
ctx.restore(); ctx.restore();
ctx.imageSmoothingEnabled = true; ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high'; ctx.imageSmoothingQuality = 'high';
lines.forEach((line) => {
const pathData = new Path2D( const pathData = new Path2D(
getSvgPathFromStroke(getStroke(points, perfectFreehandOptions)), getSvgPathFromStroke(getStroke(line, perfectFreehandOptions)),
); );
ctx.fill(pathData);
});
const pathData = new Path2D(
getSvgPathFromStroke(getStroke([...currentLine, point], perfectFreehandOptions)),
);
ctx.fill(pathData); ctx.fill(pathData);
} }
} }
} }
}; };
const onMouseUp = (event: MouseEvent | PointerEvent | TouchEvent, addPoint = true) => { const onMouseUp = (event: MouseEvent | PointerEvent | TouchEvent, addLine = true) => {
if (event.cancelable) { if (event.cancelable) {
event.preventDefault(); event.preventDefault();
} }
@ -118,15 +106,16 @@ export const SignaturePad = ({
const point = Point.fromEvent(event, DPI, $el.current); const point = Point.fromEvent(event, DPI, $el.current);
const newPoints = [...points]; const newLines = [...lines];
if (addPoint) { if (addLine && currentLine.length > 0) {
newPoints.push(point); newLines.push([...currentLine, point]);
setCurrentLine([]);
setPoints(newPoints);
} }
if ($el.current && newPoints.length > 0) { setLines(newLines);
if ($el.current && newLines.length > 0) {
const ctx = $el.current.getContext('2d'); const ctx = $el.current.getContext('2d');
if (ctx) { if (ctx) {
@ -135,19 +124,18 @@ export const SignaturePad = ({
ctx.imageSmoothingEnabled = true; ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high'; ctx.imageSmoothingQuality = 'high';
newLines.forEach((line) => {
const pathData = new Path2D( const pathData = new Path2D(
getSvgPathFromStroke(getStroke(newPoints, perfectFreehandOptions)), getSvgPathFromStroke(getStroke(line, perfectFreehandOptions)),
); );
ctx.fill(pathData); ctx.fill(pathData);
});
onChange?.($el.current.toDataURL());
ctx.save(); ctx.save();
} }
onChange?.($el.current.toDataURL());
} }
setPoints([]);
}; };
const onMouseEnter = (event: MouseEvent | PointerEvent | TouchEvent) => { const onMouseEnter = (event: MouseEvent | PointerEvent | TouchEvent) => {
@ -177,7 +165,29 @@ export const SignaturePad = ({
onChange?.(null); onChange?.(null);
setPoints([]); setLines([]);
setCurrentLine([]);
};
const onUndoClick = () => {
if (lines.length === 0) {
return;
}
const newLines = [...lines];
newLines.pop(); // Remove the last line
setLines(newLines);
// Clear the canvas
if ($el.current) {
const ctx = $el.current.getContext('2d');
ctx?.clearRect(0, 0, $el.current.width, $el.current.height);
newLines.forEach((line) => {
const pathData = new Path2D(getSvgPathFromStroke(getStroke(line, perfectFreehandOptions)));
ctx?.fill(pathData);
});
}
}; };
useEffect(() => { useEffect(() => {
@ -217,15 +227,29 @@ export const SignaturePad = ({
{...props} {...props}
/> />
<div className="absolute bottom-4 right-4"> <div className="absolute bottom-4 right-4 flex gap-2">
<button <button
type="button" type="button"
className="focus-visible:ring-ring ring-offset-background text-muted-foreground rounded-full p-0 text-xs focus-visible:outline-none focus-visible:ring-2" className="focus-visible:ring-ring ring-offset-background text-muted-foreground/60 hover:text-muted-foreground rounded-full p-0 text-xs focus-visible:outline-none focus-visible:ring-2"
onClick={() => onClearClick()} onClick={() => onClearClick()}
> >
Clear Signature Clear Signature
</button> </button>
</div> </div>
{lines.length > 0 && (
<div className="absolute bottom-4 left-4 flex gap-2">
<button
type="button"
title="undo"
className="focus-visible:ring-ring ring-offset-background text-muted-foreground/60 hover:text-muted-foreground rounded-full p-0 text-xs focus-visible:outline-none focus-visible:ring-2"
onClick={() => onUndoClick()}
>
<Undo2 className="h-4 w-4" />
<span className="sr-only">Undo</span>
</button>
</div>
)}
</div> </div>
); );
}; };