Last active 1 day ago

suko revised this gist 1 day ago. Go to revision

1 file changed, 89 insertions

SignatureDrawing.tsx(file created)

@@ -0,0 +1,89 @@
1 + 'use client';
2 +
3 + import Image from "next/image";
4 + import { useEffect, useRef, useState } from "react";
5 +
6 + interface Props {
7 + src?: string;
8 + fillSrc?: string;
9 + reverseOrder?: boolean;
10 + }
11 +
12 + export 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 + }
Newer Older