animate-vector 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +73 -0
  2. package/package.json +26 -0
  3. package/svg-animator.js +480 -0
package/README.md ADDED
@@ -0,0 +1,73 @@
1
+ # animate-vector
2
+
3
+ A React component that animates SVG paths by drawing them sequentially. It supports loading SVGs from a string or a URL and allows for customizable animation timings.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install animate-vector
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```jsx
14
+ import React from 'react';
15
+ import SVGPathAnimator from 'animate-vector';
16
+
17
+ const App = () => {
18
+ return (
19
+ <div>
20
+ {/* Load from URL */}
21
+ <SVGPathAnimator
22
+ src="/path/to/image.svg"
23
+ duration={3000}
24
+ stagger={100}
25
+ />
26
+
27
+ {/* Or using an SVG string */}
28
+ <SVGPathAnimator
29
+ svgString="<svg>...</svg>"
30
+ />
31
+ </div>
32
+ );
33
+ };
34
+ ```
35
+
36
+ ## Props
37
+
38
+ | Prop | Type | Default | Description |
39
+ |------|------|---------|-------------|
40
+ | `src` | `string` | - | URL to the SVG file. |
41
+ | `svgString` | `string` | - | Raw SVG content string. |
42
+ | `duration` | `number` | `2000` | Duration of the drawing animation per path (ms). |
43
+ | `fillDelay` | `number` | `250` | Delay before the fill animation starts after drawing (ms). |
44
+ | `reverseDuration` | `number` | `300` | Duration of the reverse (undraw) animation (ms). |
45
+ | `stagger` | `number` | `0.25` | Delay between starting animation for each path (ms). |
46
+ | `enableReverse` | `boolean` | `true` | Enables double-click/touch interaction to reverse the animation. |
47
+
48
+ ## SVG Data Attributes
49
+
50
+ You can also embed configuration directly into your SVG file using `data-` attributes on the root `<svg>` element. These values override the props passed to the component.
51
+
52
+ ```xml
53
+ <svg
54
+ viewBox="0 0 100 100"
55
+ data-duration="3000"
56
+ data-stagger="100"
57
+ data-fill-delay="500"
58
+ data-enable-reverse="false"
59
+ >
60
+ ...
61
+ </svg>
62
+ ```
63
+
64
+ ## Features
65
+
66
+ - **Automatic Path Detection**: Finds and animates all paths in the SVG.
67
+ - **Interaction**: Double-click or touch to reverse and restart the animation (configurable).
68
+ - **Reduced Motion**: Respects the user's `prefers-reduced-motion` setting.
69
+
70
+ ## Author
71
+
72
+ **Oliver R. Fox**
73
+ [https://ofox.co.uk](https://ofox.co.uk)
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "animate-vector",
3
+ "version": "1.0.2",
4
+ "description": "A React component to animate SVG paths",
5
+ "main": "svg-animator.js",
6
+ "scripts": {
7
+ "test": "echo \"Error: no test specified\" && exit 1"
8
+ },
9
+ "keywords": [
10
+ "react",
11
+ "svg",
12
+ "animation",
13
+ "path",
14
+ "drawing"
15
+ ],
16
+ "author": "Oliver R. Fox <https://ofox.co.uk>",
17
+ "homepage": "https://ofox.co.uk",
18
+ "license": "ISC",
19
+ "peerDependencies": {
20
+ "react": ">=16.8.0",
21
+ "react-dom": ">=16.8.0"
22
+ },
23
+ "publishConfig": {
24
+ "access": "public"
25
+ }
26
+ }
@@ -0,0 +1,480 @@
1
+ import React, {useRef, useEffect, useState} from 'react';
2
+ import './landing.css';
3
+
4
+ function getInheritedStyleOrAttribute(node, propertyName) {
5
+ let currentNode = node;
6
+ // Traverse up from the current node until the SVG root or the attribute/style is found
7
+ while (currentNode && currentNode.nodeName && currentNode.nodeName.toLowerCase() !== 'svg') {
8
+ if (typeof currentNode.getAttribute === 'function') { // Ensure it's an element node
9
+ // Check for direct attribute first
10
+ const directAttrValue = currentNode.getAttribute(propertyName);
11
+ if (directAttrValue !== null && directAttrValue.toLowerCase() !== 'inherit') {
12
+ return directAttrValue;
13
+ }
14
+
15
+ // Check for inline style attribute if direct attribute wasn't found or was 'inherit'
16
+ const styleAttr = currentNode.getAttribute('style');
17
+ if (styleAttr) {
18
+ // Simple parsing for the specific property
19
+ const declarations = styleAttr.split(';');
20
+ for (const declaration of declarations) {
21
+ // Split only on the first colon to handle values like data URIs or complex gradients
22
+ const parts = declaration.split(/:(.+)/);
23
+ if (parts.length === 3) { // Ensure we have a property and a value part
24
+ const prop = parts[0];
25
+ const value = parts[1]; // The rest of the string after the first colon
26
+ if (prop && value && prop.trim() === propertyName) {
27
+ const trimmedValue = value.trim();
28
+ if (trimmedValue.toLowerCase() !== 'inherit') {
29
+ return trimmedValue;
30
+ }
31
+ // If 'inherit' is found in style, break style parsing for this node and continue search upwards
32
+ break;
33
+ }
34
+ }
35
+ }
36
+ }
37
+ }
38
+ // Move to parent if not found or if 'inherit' was specified
39
+ if (!currentNode.parentNode) break;
40
+ currentNode = currentNode.parentNode;
41
+ }
42
+
43
+ // Check SVG root last (as a fallback) - handles attributes/styles set directly on the <svg> tag
44
+ if (currentNode && currentNode.nodeName && currentNode.nodeName.toLowerCase() === 'svg') {
45
+ if (typeof currentNode.getAttribute === 'function') {
46
+ // Check direct attribute on SVG root
47
+ const svgRootAttrValue = currentNode.getAttribute(propertyName);
48
+ if (svgRootAttrValue !== null && svgRootAttrValue.toLowerCase() !== 'inherit') {
49
+ return svgRootAttrValue;
50
+ }
51
+ // Check style attribute on SVG root
52
+ const svgRootStyleAttr = currentNode.getAttribute('style');
53
+ if (svgRootStyleAttr) {
54
+ const declarations = svgRootStyleAttr.split(';');
55
+ for (const declaration of declarations) {
56
+ const parts = declaration.split(/:(.+)/);
57
+ if (parts.length === 3) {
58
+ const prop = parts[0];
59
+ const value = parts[1];
60
+ if (prop && value && prop.trim() === propertyName) {
61
+ const trimmedValue = value.trim();
62
+ if (trimmedValue.toLowerCase() !== 'inherit') {
63
+ return trimmedValue;
64
+ }
65
+ break; // Stop checking styles on SVG root if 'inherit'
66
+ }
67
+ }
68
+ }
69
+ }
70
+ }
71
+ }
72
+
73
+ return null; // Attribute/style not found in the hierarchy up to the SVG element
74
+ }
75
+
76
+ // Helper function to optimise path data by rounding numbers to one decimal place
77
+ function optimizePathData(d) {
78
+ if (!d) return '';
79
+ // Regex to find commands (letters) and numbers (including decimals and negatives)
80
+ const parts = d.match(/[a-df-zA-DF-Z]|[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?/g);
81
+ if (!parts) return d; // Return original if parsing fails
82
+
83
+ let optimizedD = "";
84
+ let currentCommand = "";
85
+
86
+ for (const part of parts) {
87
+ if (/[a-zA-Z]/.test(part)) { // If it's a command letter
88
+ currentCommand = part;
89
+ optimizedD += currentCommand;
90
+ } else { // If it's a number (or part of a number sequence like "10,20")
91
+ const numbers = part.split(/[\s,]+/).filter(Boolean);
92
+ const roundedNumbers = numbers.map(numStr => {
93
+ const num = parseFloat(numStr);
94
+ if (!isNaN(num)) {
95
+ return (Math.round(num * 10) / 10).toString(); // Round to 1 decimal place
96
+ }
97
+ return numStr;
98
+ });
99
+ optimizedD += roundedNumbers.join(' ');
100
+ if (optimizedD.length > 0 && !/[a-zA-Z]$/.test(optimizedD.slice(-1))) {
101
+ optimizedD += " ";
102
+ }
103
+ }
104
+ }
105
+ return optimizedD.trim();
106
+ }
107
+
108
+
109
+ // Component to parse and animate SVG paths
110
+ const SVGPathAnimator = ({
111
+ svgString,
112
+ src,
113
+ duration = 2000,
114
+ fillDelay = 250,
115
+ reverseDuration = 300,
116
+ stagger = 0.25,
117
+ enableReverse = true
118
+ }) => {
119
+ const [internalSvgString, setInternalSvgString] = useState(null);
120
+ const [pathsData, setPathsData] = useState([]);
121
+ const [svgAttributes, setSvgAttributes] = useState({});
122
+ const [animationKey, setAnimationKey] = useState(Date.now());
123
+ const [isReversing, setIsReversing] = useState(false);
124
+ const [isAnimating, setIsAnimating] = useState(false);
125
+ const [prefersReducedMotion, setPrefersReducedMotion] = useState(false);
126
+
127
+ const lastTapTimeRef = useRef(0);
128
+ const lastTapTargetRef = useRef(null);
129
+ const reverseAnimationTimeoutRef = useRef(null);
130
+ const animationCompletionTimeoutRef = useRef(null);
131
+
132
+ const configRef = useRef({
133
+ duration,
134
+ fillDelay,
135
+ reverseDuration,
136
+ stagger,
137
+ enableReverse
138
+ });
139
+
140
+ useEffect(() => {
141
+ const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
142
+ setPrefersReducedMotion(mediaQuery.matches);
143
+ const handleChange = (event) => setPrefersReducedMotion(event.matches);
144
+ mediaQuery.addEventListener('change', handleChange);
145
+
146
+ return () => {
147
+ if (reverseAnimationTimeoutRef.current) clearTimeout(reverseAnimationTimeoutRef.current);
148
+ if (animationCompletionTimeoutRef.current) clearTimeout(animationCompletionTimeoutRef.current);
149
+ mediaQuery.removeEventListener('change', handleChange);
150
+ };
151
+ }, []);
152
+
153
+ useEffect(() => {
154
+ if (svgString) {
155
+ setInternalSvgString(svgString);
156
+ } else if (src) {
157
+ fetch(src)
158
+ .then(res => {
159
+ if (!res.ok) throw new Error("Failed to fetch SVG");
160
+ return res.text();
161
+ })
162
+ .then(text => setInternalSvgString(text))
163
+ .catch(err => {
164
+ console.error("Error loading SVG from src:", err);
165
+ setInternalSvgString(null);
166
+ });
167
+ } else {
168
+ setInternalSvgString(null);
169
+ }
170
+ }, [svgString, src]);
171
+
172
+ useEffect(() => {
173
+ if (!internalSvgString) {
174
+ setPathsData([]);
175
+ setSvgAttributes({});
176
+ setIsAnimating(false);
177
+ return;
178
+ }
179
+
180
+ if (reverseAnimationTimeoutRef.current) clearTimeout(reverseAnimationTimeoutRef.current);
181
+ if (animationCompletionTimeoutRef.current) clearTimeout(animationCompletionTimeoutRef.current);
182
+
183
+ setIsReversing(false);
184
+ setAnimationKey(Date.now());
185
+ setIsAnimating(true);
186
+
187
+ const parser = new DOMParser();
188
+ const svgDoc = parser.parseFromString(internalSvgString, "image/svg+xml");
189
+ const svgElement = svgDoc.documentElement;
190
+
191
+ if (!svgElement || svgElement.nodeName.toLowerCase() !== 'svg') {
192
+ console.error("Invalid SVG content or root element is not <svg>. Found:", svgElement ? svgElement.nodeName : "null");
193
+ const parserError = svgDoc.querySelector('parsererror');
194
+ if (parserError) console.error("SVG Parsing Error:", parserError.textContent);
195
+ setPathsData([]); setSvgAttributes({}); setIsAnimating(false); return;
196
+ }
197
+
198
+ const attrDuration = svgElement.getAttribute('data-duration');
199
+ const attrFillDelay = svgElement.getAttribute('data-fill-delay');
200
+ const attrReverseDuration = svgElement.getAttribute('data-reverse-duration');
201
+ const attrStagger = svgElement.getAttribute('data-stagger');
202
+ const attrEnableReverse = svgElement.getAttribute('data-enable-reverse');
203
+
204
+ const effectiveDuration = attrDuration ? parseFloat(attrDuration) : duration;
205
+ const effectiveFillDelay = attrFillDelay ? parseFloat(attrFillDelay) : fillDelay;
206
+ const effectiveReverseDuration = attrReverseDuration ? parseFloat(attrReverseDuration) : reverseDuration;
207
+ const effectiveStagger = attrStagger ? parseFloat(attrStagger) : stagger;
208
+ const effectiveEnableReverse = attrEnableReverse !== null ? attrEnableReverse === 'true' : enableReverse;
209
+
210
+ configRef.current = {
211
+ duration: effectiveDuration,
212
+ fillDelay: effectiveFillDelay,
213
+ reverseDuration: effectiveReverseDuration,
214
+ stagger: effectiveStagger,
215
+ enableReverse: effectiveEnableReverse
216
+ };
217
+
218
+ const attributes = {};
219
+ for (const attr of svgElement.attributes) {
220
+ if (!['data-duration', 'data-fill-delay', 'data-reverse-duration', 'data-stagger', 'data-enable-reverse'].includes(attr.name)) {
221
+ attributes[attr.name] = attr.value;
222
+ }
223
+ }
224
+
225
+ if (!attributes.width || attributes.width === "100%") attributes.width = "100%";
226
+ if (!attributes.height || attributes.height === "100%") attributes.height = attributes.viewBox ? "auto" : "300px";
227
+ setSvgAttributes(attributes);
228
+
229
+ const pathNodes = svgDoc.querySelectorAll('path');
230
+ const extractedPaths = [];
231
+ let maxTotalAnimationTimeMs = 0;
232
+
233
+ let tempMeasurementDiv = document.getElementById('temp-svg-path-measurement-div');
234
+ if (!tempMeasurementDiv) {
235
+ tempMeasurementDiv = document.createElement('div');
236
+ tempMeasurementDiv.id = 'temp-svg-path-measurement-div';
237
+ tempMeasurementDiv.style.cssText = 'visibility:hidden; position:absolute; width:0; height:0; top:-1000px; left:-1000px; overflow:hidden;';
238
+ document.body.appendChild(tempMeasurementDiv);
239
+ }
240
+ const tempMeasureSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
241
+ if (attributes.viewBox) tempMeasureSvg.setAttribute('viewBox', attributes.viewBox);
242
+ tempMeasurementDiv.appendChild(tempMeasureSvg);
243
+
244
+ pathNodes.forEach((pathNode, index) => {
245
+ const originalD = pathNode.getAttribute('d');
246
+ if (!originalD) return;
247
+
248
+ const optimizedD = optimizePathData(originalD);
249
+
250
+ const pathFillAttr = getInheritedStyleOrAttribute(pathNode, 'fill');
251
+ const pathStrokeAttr = getInheritedStyleOrAttribute(pathNode, 'stroke');
252
+ const pathStrokeWidthAttr = getInheritedStyleOrAttribute(pathNode, 'stroke-width');
253
+
254
+ const originalStroke = pathStrokeAttr !== null ? pathStrokeAttr : 'currentColor';
255
+ const originalStrokeWidth = pathStrokeWidthAttr !== null ? pathStrokeWidthAttr : '0.5';
256
+
257
+ let effectiveFill;
258
+ if (pathFillAttr === 'none') effectiveFill = 'none';
259
+ else if (pathFillAttr === null) effectiveFill = 'black';
260
+ else effectiveFill = pathFillAttr;
261
+
262
+ const tempPath = pathNode.cloneNode(false);
263
+ tempPath.setAttribute('d', optimizedD);
264
+ if (originalStrokeWidth !== '1') tempPath.setAttribute('stroke-width', originalStrokeWidth);
265
+
266
+ tempMeasureSvg.appendChild(tempPath);
267
+ let rawLength = 0;
268
+ try { rawLength = tempPath.getTotalLength(); }
269
+ catch (e) { console.warn(`Could not measure path ${index}. D=${optimizedD}`, e); }
270
+ tempMeasureSvg.removeChild(tempPath);
271
+
272
+ const length = Math.round(rawLength * 10) / 10;
273
+
274
+ if (length >= 1) {
275
+ const currentPathBaseDelayRaw = index * (effectiveStagger / 1000);
276
+ const currentPathBaseDelay = Math.round(currentPathBaseDelayRaw * 1000) / 1000;
277
+
278
+ const currentPathDrawDuration = effectiveDuration / 1000;
279
+ const fillAndStrokeRemoveEffectiveDuration = (effectiveDuration - effectiveFillDelay) / 1000;
280
+
281
+ const pathTotalTime = (currentPathBaseDelay * 1000) + effectiveDuration; // Use original delay for total time calculation
282
+ if (pathTotalTime > maxTotalAnimationTimeMs) maxTotalAnimationTimeMs = pathTotalTime;
283
+
284
+ extractedPaths.push({
285
+ id: `animated-path-${index}-${Date.now()}`,
286
+ d: optimizedD,
287
+ length,
288
+ originalStroke, originalStrokeWidth, effectiveFill,
289
+ animationDelay: `${currentPathBaseDelay}s`,
290
+ animationDuration: `${currentPathDrawDuration}s`,
291
+ fillAnimationDuration: `${fillAndStrokeRemoveEffectiveDuration > 0 ? fillAndStrokeRemoveEffectiveDuration : 0}s`,
292
+ strokeRemoveDuration: `${fillAndStrokeRemoveEffectiveDuration > 0 ? fillAndStrokeRemoveEffectiveDuration : 0}s`,
293
+ });
294
+ }
295
+ });
296
+
297
+ if (tempMeasureSvg.parentNode === tempMeasurementDiv) tempMeasurementDiv.removeChild(tempMeasureSvg);
298
+ setPathsData(extractedPaths);
299
+
300
+ animationCompletionTimeoutRef.current = setTimeout(() => {
301
+ setIsAnimating(false);
302
+ console.log("Initial animation complete.");
303
+ }, maxTotalAnimationTimeMs );
304
+
305
+ }, [internalSvgString, duration, fillDelay, reverseDuration, stagger, enableReverse]);
306
+
307
+ const triggerAnimationRestart = (e) => {
308
+ if (!configRef.current.enableReverse) return;
309
+ if (isAnimating) { console.log("Animation in progress, restart ignored."); return; }
310
+ if (e) e.stopPropagation();
311
+
312
+ console.log("Interaction detected, initiating reverse animation.");
313
+ setIsAnimating(true); setIsReversing(true);
314
+
315
+ if (reverseAnimationTimeoutRef.current) clearTimeout(reverseAnimationTimeoutRef.current);
316
+ if (animationCompletionTimeoutRef.current) clearTimeout(animationCompletionTimeoutRef.current);
317
+
318
+ const { reverseDuration, duration } = configRef.current;
319
+
320
+ reverseAnimationTimeoutRef.current = setTimeout(() => {
321
+ console.log("Reverse animation finished, starting forward animation.");
322
+ setIsReversing(false); setAnimationKey(Date.now());
323
+
324
+ let maxTotalAnimationTimeMs = 0;
325
+ if (pathsData.length > 0) {
326
+ pathsData.forEach(p => {
327
+ const currentPathBaseDelay = parseFloat(p.animationDelay.replace('s','')) * 1000; // Use the stored (possibly rounded) delay
328
+ const pathTotalTime = currentPathBaseDelay + duration;
329
+ if(pathTotalTime > maxTotalAnimationTimeMs) maxTotalAnimationTimeMs = pathTotalTime;
330
+ });
331
+ } else { maxTotalAnimationTimeMs = duration; }
332
+
333
+ animationCompletionTimeoutRef.current = setTimeout(() => {
334
+ setIsAnimating(false); console.log("Restarted animation complete.");
335
+ }, maxTotalAnimationTimeMs);
336
+ }, reverseDuration);
337
+ };
338
+
339
+ const handleTouchEnd = (e) => {
340
+ if (!configRef.current.enableReverse) return;
341
+ const currentTime = new Date().getTime();
342
+ const tapLength = currentTime - lastTapTimeRef.current;
343
+ if (tapLength < 300 && tapLength > 0 && lastTapTargetRef.current && lastTapTargetRef.current.contains(e.target)) {
344
+ triggerAnimationRestart(e); lastTapTimeRef.current = 0;
345
+ } else {
346
+ lastTapTimeRef.current = currentTime; lastTapTargetRef.current = e.currentTarget;
347
+ }
348
+ };
349
+
350
+ if (!internalSvgString || (pathsData.length === 0 && Object.keys(svgAttributes).length === 0)) return null;
351
+
352
+ const { style: rawStyleAttribute, ...otherSvgAttributes } = svgAttributes;
353
+ const parsedStyleFromSvg = {};
354
+ if (typeof rawStyleAttribute === 'string') {
355
+ rawStyleAttribute.split(';').forEach(declaration => {
356
+ if (declaration.trim() === '') return;
357
+ const parts = declaration.split(/:(.+)/);
358
+ if (parts.length === 3) {
359
+ const property = parts[0]; const value = parts[1];
360
+ if (property && value) {
361
+ const propName = property.trim();
362
+ const camelCasePropName = propName.replace(/-([a-z])/g, (match, letter) => letter.toUpperCase());
363
+ if (camelCasePropName) parsedStyleFromSvg[camelCasePropName] = value.trim();
364
+ }
365
+ }
366
+ });
367
+ } else if (typeof rawStyleAttribute === 'object' && rawStyleAttribute !== null) {
368
+ Object.assign(parsedStyleFromSvg, rawStyleAttribute);
369
+ }
370
+
371
+ const finalSvgElementStyle = { ...parsedStyleFromSvg, overflow: 'visible', cursor: configRef.current.enableReverse ? 'pointer' : 'default' };
372
+ const { fillDelay: effectiveFillDelay, reverseDuration: effectiveReverseDuration } = configRef.current;
373
+
374
+ if (internalSvgString && pathsData.length === 0 && Object.keys(svgAttributes).length > 0) {
375
+ return (
376
+ <div className="text-center text-gray-500 p-4">
377
+ <div onDoubleClick={triggerAnimationRestart} onTouchEnd={handleTouchEnd} style={{ display: 'inline-block', cursor: configRef.current.enableReverse ? 'pointer' : 'default' }}>
378
+ <svg {...otherSvgAttributes} style={finalSvgElementStyle} data-reduced-motion={prefersReducedMotion}></svg>
379
+ </div>
380
+ <p className="mt-2">SVG loaded, but no animatable &lt;path&gt; elements with length >= 1 found.</p>
381
+ </div>
382
+ );
383
+ }
384
+
385
+ return (
386
+ <svg key={animationKey} {...otherSvgAttributes} style={finalSvgElementStyle}
387
+ onDoubleClick={triggerAnimationRestart} onTouchEnd={handleTouchEnd} data-reduced-motion={prefersReducedMotion} >
388
+ <style>
389
+ {`
390
+ .animated-svg-path {
391
+ fill-opacity: 0;
392
+ fill: var(--path-fill, black);
393
+ stroke: var(--path-stroke-draw-color, currentColor);
394
+ stroke-opacity: 1;
395
+ stroke-dasharray: var(--path-length);
396
+ stroke-dashoffset: var(--path-length);
397
+ will-change: stroke-dashoffset, stroke-opacity, fill-opacity;
398
+ }
399
+
400
+ svg:not([data-reduced-motion="true"]) .animated-svg-path:not(.reversing) {
401
+ animation:
402
+ drawPath var(--animation-duration) ease-in-out var(--animation-delay, 0s) forwards,
403
+ fillPath var(--fill-animation-effective-duration) ease-in calc(var(--animation-delay) + ${effectiveFillDelay / 1000}s) forwards,
404
+ removeStroke var(--fill-animation-effective-duration) ease-in calc(var(--animation-delay) + ${effectiveFillDelay / 1000}s) forwards;
405
+ }
406
+ svg:not([data-reduced-motion="true"]) .animated-svg-path[fill="none"]:not(.reversing) {
407
+ animation: drawPath var(--animation-duration) ease-in-out var(--animation-delay, 0s) forwards;
408
+ fill-opacity: 0 !important; fill: none !important;
409
+ stroke: var(--original-stroke-color, currentColor); stroke-opacity: 1 !important;
410
+ }
411
+ svg:not([data-reduced-motion="true"]) .animated-svg-path.reversing {
412
+ stroke-dashoffset: 0; fill-opacity: 1; stroke-opacity: 1;
413
+ stroke: var(--path-stroke-draw-color);
414
+ animation: quickReverse ${effectiveReverseDuration / 1000}s ease-out forwards;
415
+ }
416
+ svg:not([data-reduced-motion="true"]) .animated-svg-path[fill="none"].reversing {
417
+ stroke-dashoffset: 0; stroke-opacity: 1 !important; fill-opacity: 0 !important;
418
+ fill: none !important; stroke: var(--original-stroke-color, currentColor) !important;
419
+ animation: unDrawPathReverseOriginalStroke ${effectiveReverseDuration / 1000}s ease-out forwards;
420
+ }
421
+
422
+ svg[data-reduced-motion="true"] .animated-svg-path:not([fill="none"]):not(.reversing) {
423
+ stroke-dashoffset: 0; stroke-opacity: 0;
424
+ animation: fillPath var(--animation-duration) ease-in calc(var(--animation-delay) + ${effectiveFillDelay / 1000}s) forwards;
425
+ }
426
+ svg[data-reduced-motion="true"] .animated-svg-path[fill="none"]:not(.reversing) {
427
+ stroke-dashoffset: 0; stroke: var(--original-stroke-color, currentColor);
428
+ stroke-opacity: 1 !important; fill-opacity: 0 !important; fill: none !important;
429
+ animation: none;
430
+ }
431
+ svg[data-reduced-motion="true"] .animated-svg-path:not([fill="none"]).reversing {
432
+ fill-opacity: 1; stroke-opacity: 0; stroke-dashoffset: 0;
433
+ animation: quickReverseFillOnlyReducedMotion ${effectiveReverseDuration / 1000}s ease-out forwards;
434
+ }
435
+ svg[data-reduced-motion="true"] .animated-svg-path[fill="none"].reversing {
436
+ stroke-dashoffset: 0; stroke: var(--original-stroke-color, currentColor);
437
+ stroke-opacity: 1 !important; fill-opacity: 0 !important; fill: none !important;
438
+ animation: none;
439
+ }
440
+
441
+ @keyframes drawPath { to { stroke-dashoffset: 0; } }
442
+ @keyframes fillPath { to { fill-opacity: 1; } }
443
+ @keyframes removeStroke { to { stroke-opacity: 0; } }
444
+ @keyframes quickReverse {
445
+ from { fill-opacity: 1; stroke-dashoffset: 0; stroke-opacity: 1; }
446
+ to { fill-opacity: 0; stroke-dashoffset: var(--path-length); stroke-opacity: 1; }
447
+ }
448
+ @keyframes unDrawPathReverseOriginalStroke {
449
+ from { stroke-dashoffset: 0; stroke-opacity: 1; }
450
+ to { stroke-dashoffset: var(--path-length); stroke-opacity: 1; }
451
+ }
452
+ @keyframes quickReverseFillOnlyReducedMotion {
453
+ from { fill-opacity: 1; } to { fill-opacity: 0; }
454
+ }
455
+ `}
456
+ </style>
457
+ {pathsData.map((path) => (
458
+ <path
459
+ key={path.id}
460
+ d={path.d}
461
+ strokeWidth={path.originalStrokeWidth}
462
+ className={`animated-svg-path ${isReversing ? 'reversing' : ''}`}
463
+ style={{
464
+ '--path-length': path.length, // Rounded length
465
+ '--animation-duration': path.animationDuration,
466
+ '--animation-delay': path.animationDelay, // Rounded delay
467
+ '--fill-animation-effective-duration': path.fillAnimationDuration,
468
+ '--path-fill': path.effectiveFill === 'none' ? 'none' : path.effectiveFill,
469
+ '--path-stroke-draw-color': path.effectiveFill === 'none' ? path.originalStroke : path.effectiveFill,
470
+ '--original-stroke-color': path.originalStroke,
471
+ }}
472
+ fill={path.effectiveFill}
473
+ stroke={path.originalStroke}
474
+ />
475
+ ))}
476
+ </svg>
477
+ );
478
+ };
479
+
480
+ export default SVGPathAnimator;