Last active 23 hours ago

SignatureDrawing.tsx Raw
1'use client';
2
3import Image from "next/image";
4import { useEffect, useRef, useState } from "react";
5
6interface Props {
7 src?: string;
8 fillSrc?: string;
9 reverseOrder?: boolean;
10}
11
12export function SignatureDrawing({
13 src = "/sign.svg",
14 fillSrc,
15 reverseOrder = true,
16}: Props) {
17 const [paths, setPaths] = useState<string[]>([]);
18 const svgRef = useRef<SVGSVGElement>(null);
19 const [showFill, setShowFill] = useState(false);
20 const timeoutRef = useRef<NodeJS.Timeout | null>(null);
21
22 useEffect(() => {
23 let mounted = true;
24 fetch(src)
25 .then((res) => res.text())
26 .then((text) => {
27 if (!mounted) return;
28 const parser = new DOMParser();
29 const doc = parser.parseFromString(text, "image/svg+xml");
30 const extracted = Array.from(doc.querySelectorAll("path"))
31 .map((node) => node.getAttribute("d") ?? "")
32 .filter(Boolean);
33 const d = reverseOrder ? extracted.reverse() : extracted;
34 setShowFill(false);
35 setPaths(d);
36 })
37 .catch(() => setShowFill(true));
38 return () => {
39 mounted = false;
40 };
41 }, [src, reverseOrder]);
42
43 useEffect(() => {
44 if (!svgRef.current) return;
45 const nodes = svgRef.current.querySelectorAll<SVGPathElement>("path");
46 let cumulative = 0;
47 nodes.forEach((node) => {
48 const length = node.getTotalLength();
49 const duration = Math.max(0.08, length / 800);
50 node.style.setProperty("--path-length", `${length}`);
51 node.style.strokeDasharray = `${length}`;
52 node.style.strokeDashoffset = `${length}`;
53 node.style.animationDuration = `${duration}s`;
54 node.style.animationDelay = `${cumulative}s`;
55 cumulative += duration;
56 });
57 if (timeoutRef.current) clearTimeout(timeoutRef.current);
58 timeoutRef.current = setTimeout(() => setShowFill(true), cumulative * 1000 + 250);
59 return () => {
60 if (timeoutRef.current) clearTimeout(timeoutRef.current);
61 };
62 }, [paths]);
63
64 return (
65 <div className="relative w-40">
66 <svg
67 ref={svgRef}
68 viewBox="0 0 770 303"
69 className="signature-svg"
70 role="img"
71 aria-label="Handwritten signature animation"
72 >
73 {paths.map((d, idx) => (
74 <path key={idx} d={d} className="signature-stroke" />
75 ))}
76 </svg>
77 {showFill && (
78 <Image
79 src={fillSrc ?? src}
80 alt="Handwritten signature"
81 width={160}
82 height={70}
83 className="signature-fill"
84 priority
85 />
86 )}
87 </div>
88 );
89}
90