@humanspeak/svelte-motion 0.1.10 → 0.1.12
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.
- package/dist/html/_MotionContainer.svelte +75 -6
- package/dist/utils/focus.js +21 -4
- package/dist/utils/style.js +10 -0
- package/dist/utils/svg.d.ts +97 -0
- package/dist/utils/svg.js +226 -0
- package/package.json +8 -8
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
getInitialFalseContext
|
|
31
31
|
} from '../components/variantContext.context'
|
|
32
32
|
import { writable } from 'svelte/store'
|
|
33
|
+
import { transformSVGPathProperties, computeNormalizedSVGInitialAttrs } from '../utils/svg'
|
|
33
34
|
|
|
34
35
|
type Props = MotionProps & {
|
|
35
36
|
children?: Snippet
|
|
@@ -232,8 +233,22 @@
|
|
|
232
233
|
'data-path': dataPath
|
|
233
234
|
}
|
|
234
235
|
: {}),
|
|
236
|
+
// Apply normalized SVG path attributes synchronously on first render to avoid flash
|
|
237
|
+
// Compute via svg utils (no dynamic import in SSR/derived expressions)
|
|
238
|
+
...(() => {
|
|
239
|
+
if (!initialKeyframes) return {}
|
|
240
|
+
const attrs = computeNormalizedSVGInitialAttrs(
|
|
241
|
+
initialKeyframes as Record<string, unknown>
|
|
242
|
+
)
|
|
243
|
+
if (attrs) {
|
|
244
|
+
return attrs
|
|
245
|
+
}
|
|
246
|
+
return {}
|
|
247
|
+
})(),
|
|
235
248
|
style: mergeInlineStyles(
|
|
236
|
-
|
|
249
|
+
initialKeyframes && 'pathLength' in initialKeyframes && isLoaded === 'mounting'
|
|
250
|
+
? `${styleProp || ''};visibility:hidden`
|
|
251
|
+
: styleProp,
|
|
237
252
|
initialKeyframes as unknown as Record<string, unknown>,
|
|
238
253
|
resolvedAnimate as unknown as Record<string, unknown>
|
|
239
254
|
),
|
|
@@ -243,7 +258,20 @@
|
|
|
243
258
|
const runAnimation = () => {
|
|
244
259
|
if (!element || !resolvedAnimate) return
|
|
245
260
|
const transitionAnimate: MotionTransition = mergedTransition ?? {}
|
|
246
|
-
|
|
261
|
+
let payload = $state.snapshot(resolvedAnimate)
|
|
262
|
+
|
|
263
|
+
// Transform SVG path properties (pathLength, pathOffset) to their CSS equivalents
|
|
264
|
+
payload = transformSVGPathProperties(
|
|
265
|
+
element,
|
|
266
|
+
payload as Record<string, unknown>
|
|
267
|
+
) as typeof payload
|
|
268
|
+
|
|
269
|
+
// Ensure dash properties aren't pinned as inline styles
|
|
270
|
+
if (element && (element as HTMLElement).style) {
|
|
271
|
+
;(element as HTMLElement).style.removeProperty('stroke-dasharray')
|
|
272
|
+
;(element as HTMLElement).style.removeProperty('stroke-dashoffset')
|
|
273
|
+
}
|
|
274
|
+
|
|
247
275
|
animateWithLifecycle(
|
|
248
276
|
element,
|
|
249
277
|
payload as unknown as DOMKeyframesDefinition,
|
|
@@ -408,12 +436,14 @@
|
|
|
408
436
|
|
|
409
437
|
$effect(() => {
|
|
410
438
|
if (!(element && isLoaded === 'mounting')) return
|
|
439
|
+
|
|
411
440
|
if (effectiveAnimate) {
|
|
412
441
|
// If initial={false}, render at animate state immediately with no transition
|
|
413
442
|
if (effectiveInitialProp === false && resolvedAnimate) {
|
|
414
443
|
// Use Motion's animate() with duration:0 so it takes control of these properties
|
|
415
444
|
// This prevents inline styles from pinning the properties during future animations
|
|
416
|
-
|
|
445
|
+
let snapshot = $state.snapshot(resolvedAnimate) as Record<string, unknown>
|
|
446
|
+
snapshot = transformSVGPathProperties(element!, snapshot)
|
|
417
447
|
animate(element!, snapshot as DOMKeyframesDefinition, { duration: 0 })
|
|
418
448
|
// Mark that we've already applied this variant to avoid a second animate pass
|
|
419
449
|
mountedWithInitialFalse = true
|
|
@@ -424,16 +454,50 @@
|
|
|
424
454
|
isLoaded = 'ready'
|
|
425
455
|
} else if (isNotEmpty(initialKeyframes)) {
|
|
426
456
|
// Apply initial instantly BEFORE exposing 'initial' state
|
|
427
|
-
|
|
457
|
+
const transformedInitial = transformSVGPathProperties(
|
|
458
|
+
element!,
|
|
459
|
+
initialKeyframes as Record<string, unknown>
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
// For SVG paths, apply initial state truly synchronously to prevent flash
|
|
463
|
+
const hasSVGProps =
|
|
464
|
+
'strokeDasharray' in transformedInitial ||
|
|
465
|
+
'strokeDashoffset' in transformedInitial
|
|
466
|
+
if (hasSVGProps) {
|
|
467
|
+
// Apply presentation attributes to avoid pinning CSS properties
|
|
468
|
+
Object.entries(transformedInitial).forEach(([key, value]) => {
|
|
469
|
+
const v = String(Array.isArray(value) ? value[0] : value)
|
|
470
|
+
if (key === 'strokeDasharray' || key === 'stroke-dasharray') {
|
|
471
|
+
element!.setAttribute('stroke-dasharray', v)
|
|
472
|
+
}
|
|
473
|
+
if (key === 'strokeDashoffset' || key === 'stroke-dashoffset') {
|
|
474
|
+
element!.setAttribute('stroke-dashoffset', v)
|
|
475
|
+
}
|
|
476
|
+
})
|
|
477
|
+
// no-op
|
|
478
|
+
}
|
|
479
|
+
// Avoid pinning: strip stroke dash props from the animate(0) payload
|
|
480
|
+
const initialForAnimate = { ...(transformedInitial as Record<string, unknown>) }
|
|
481
|
+
delete (initialForAnimate as Record<string, unknown>).strokeDasharray
|
|
482
|
+
delete (initialForAnimate as Record<string, unknown>)['stroke-dasharray']
|
|
483
|
+
delete (initialForAnimate as Record<string, unknown>).strokeDashoffset
|
|
484
|
+
delete (initialForAnimate as Record<string, unknown>)['stroke-dashoffset']
|
|
485
|
+
|
|
486
|
+
animate(element!, initialForAnimate as DOMKeyframesDefinition, { duration: 0 })
|
|
487
|
+
// no-op
|
|
488
|
+
|
|
428
489
|
// Mark initial after styles are applied so tests read CSS=0 while state=initial
|
|
490
|
+
// This also removes visibility:hidden that was hiding SVG paths during mount
|
|
429
491
|
isLoaded = 'initial'
|
|
430
492
|
dataPath = 1
|
|
493
|
+
|
|
431
494
|
// Then promote to ready and run the enter animation
|
|
432
495
|
requestAnimationFrame(async () => {
|
|
433
496
|
if (isPlaywright) {
|
|
434
497
|
await sleep(10)
|
|
435
498
|
}
|
|
436
499
|
isLoaded = 'ready'
|
|
500
|
+
|
|
437
501
|
runAnimation()
|
|
438
502
|
})
|
|
439
503
|
} else {
|
|
@@ -447,7 +511,8 @@
|
|
|
447
511
|
resolvedAnimate
|
|
448
512
|
) {
|
|
449
513
|
// Apply variant styles instantly with duration:0
|
|
450
|
-
|
|
514
|
+
let snapshot = $state.snapshot(resolvedAnimate) as Record<string, unknown>
|
|
515
|
+
snapshot = transformSVGPathProperties(element!, snapshot)
|
|
451
516
|
animate(element!, snapshot as DOMKeyframesDefinition, { duration: 0 })
|
|
452
517
|
lastRanVariantKey = currentAnimateKey
|
|
453
518
|
} else {
|
|
@@ -456,7 +521,11 @@
|
|
|
456
521
|
}
|
|
457
522
|
} else if (isNotEmpty(initialKeyframes)) {
|
|
458
523
|
// Apply initial instantly BEFORE exposing 'initial' state
|
|
459
|
-
|
|
524
|
+
const transformedInitial = transformSVGPathProperties(
|
|
525
|
+
element!,
|
|
526
|
+
initialKeyframes as Record<string, unknown>
|
|
527
|
+
)
|
|
528
|
+
animate(element!, transformedInitial as DOMKeyframesDefinition, { duration: 0 })
|
|
460
529
|
dataPath = 3
|
|
461
530
|
isLoaded = 'initial'
|
|
462
531
|
requestAnimationFrame(async () => {
|
package/dist/utils/focus.js
CHANGED
|
@@ -41,6 +41,7 @@ export const computeFocusBaseline = (el, opts) => {
|
|
|
41
41
|
skewY: 0,
|
|
42
42
|
opacity: 1
|
|
43
43
|
};
|
|
44
|
+
const cs = getComputedStyle(el);
|
|
44
45
|
for (const key of Object.keys(whileFocusRecord)) {
|
|
45
46
|
if (Object.prototype.hasOwnProperty.call(animateRecord, key)) {
|
|
46
47
|
baseline[key] = animateRecord[key];
|
|
@@ -52,9 +53,17 @@ export const computeFocusBaseline = (el, opts) => {
|
|
|
52
53
|
baseline[key] = neutralTransformDefaults[key];
|
|
53
54
|
}
|
|
54
55
|
else {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
// Convert camelCase to kebab-case for CSS property access
|
|
57
|
+
const cssProperty = key.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
58
|
+
const value = cs.getPropertyValue(cssProperty);
|
|
59
|
+
// Always assign a baseline entry to ensure a removal keyframe exists.
|
|
60
|
+
if (value) {
|
|
61
|
+
baseline[key] = value;
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
// Fallback to inline style for that property, else explicit empty string
|
|
65
|
+
const inlineValue = el.style.getPropertyValue(cssProperty);
|
|
66
|
+
baseline[key] = inlineValue || '';
|
|
58
67
|
}
|
|
59
68
|
}
|
|
60
69
|
}
|
|
@@ -89,7 +98,15 @@ export const attachWhileFocus = (el, whileFocus, mergedTransition, callbacks, ba
|
|
|
89
98
|
};
|
|
90
99
|
const handleBlur = () => {
|
|
91
100
|
if (focusBaseline && Object.keys(focusBaseline).length > 0) {
|
|
92
|
-
|
|
101
|
+
const baselineForAnimation = { ...focusBaseline };
|
|
102
|
+
// For baseline entries that are empty string, proactively clear inline CSS
|
|
103
|
+
for (const [key, v] of Object.entries(baselineForAnimation)) {
|
|
104
|
+
if (v === '') {
|
|
105
|
+
const cssProperty = key.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
106
|
+
el.style.removeProperty(cssProperty);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
animate(el, baselineForAnimation, mergedTransition);
|
|
93
110
|
}
|
|
94
111
|
callbacks?.onEnd?.();
|
|
95
112
|
};
|
package/dist/utils/style.js
CHANGED
|
@@ -94,6 +94,16 @@ export const mergeInlineStyles = (existingStyle, initial, animateFallback) => {
|
|
|
94
94
|
case 'cursor':
|
|
95
95
|
setProp('cursor', value);
|
|
96
96
|
break;
|
|
97
|
+
// Skip SVG path animation properties - they'll be set by animate()
|
|
98
|
+
case 'pathLength':
|
|
99
|
+
case 'pathOffset':
|
|
100
|
+
case 'pathSpacing':
|
|
101
|
+
case 'strokeDasharray':
|
|
102
|
+
case 'stroke-dasharray':
|
|
103
|
+
case 'strokeDashoffset':
|
|
104
|
+
case 'stroke-dashoffset':
|
|
105
|
+
// Don't add these to inline styles - they interfere with animation
|
|
106
|
+
break;
|
|
97
107
|
default:
|
|
98
108
|
// Fallback: write raw as-is for simple CSS props
|
|
99
109
|
if (typeof value === 'string' || typeof value === 'number') {
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SVG-specific properties that need special handling during animation.
|
|
3
|
+
* These properties are not standard CSS properties and need to be transformed.
|
|
4
|
+
*/
|
|
5
|
+
export declare const SVG_PATH_PROPERTIES: Set<string>;
|
|
6
|
+
/**
|
|
7
|
+
* Check if an element is an SVG path element.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Determines whether the provided element is an SVGPathElement.
|
|
11
|
+
*
|
|
12
|
+
* @param {Element} element The candidate element to test.
|
|
13
|
+
* @returns {element is SVGPathElement} True when the element is an SVG path.
|
|
14
|
+
* @example
|
|
15
|
+
* const el = document.createElementNS('http://www.w3.org/2000/svg', 'path')
|
|
16
|
+
* if (isSVGPathElement(el)) {
|
|
17
|
+
* // el is now typed as SVGPathElement
|
|
18
|
+
* }
|
|
19
|
+
*/
|
|
20
|
+
export declare const isSVGPathElement: (element: Element) => element is SVGPathElement;
|
|
21
|
+
/**
|
|
22
|
+
* Check if an element is any SVG element.
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* Determines whether the provided element is an SVGElement.
|
|
26
|
+
*
|
|
27
|
+
* @param {Element} element The candidate element to test.
|
|
28
|
+
* @returns {element is SVGElement} True when the element is an SVG element.
|
|
29
|
+
* @example
|
|
30
|
+
* const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
|
|
31
|
+
* if (isSVGElement(svg)) {
|
|
32
|
+
* // svg is now typed as SVGElement
|
|
33
|
+
* }
|
|
34
|
+
*/
|
|
35
|
+
export declare const isSVGElement: (element: Element) => element is SVGElement;
|
|
36
|
+
/**
|
|
37
|
+
* Transform SVG path-specific animation properties to their CSS equivalents.
|
|
38
|
+
*
|
|
39
|
+
* Motion's pathLength property creates a line-drawing effect by manipulating
|
|
40
|
+
* strokeDasharray and strokeDashoffset. This function transforms:
|
|
41
|
+
* - pathLength: 0 -> 1 becomes strokeDasharray: "0 1" -> "1 1"
|
|
42
|
+
* - pathOffset: value becomes strokeDashoffset: -value (inverted for drawing direction)
|
|
43
|
+
*
|
|
44
|
+
* @param element - The SVG element being animated
|
|
45
|
+
* @param keyframes - The animation keyframes that may contain SVG properties
|
|
46
|
+
* @returns Transformed keyframes with CSS-compatible properties
|
|
47
|
+
*/
|
|
48
|
+
/**
|
|
49
|
+
* Transforms SVG path-specific animation properties into DOM-compatible attributes.
|
|
50
|
+
*
|
|
51
|
+
* Normalized behavior (React/Framer Motion parity):
|
|
52
|
+
* - Ensures `pathLength="1"` is set when any path prop is present
|
|
53
|
+
* - Maps `pathLength`/`pathSpacing` → `stroke-dasharray` (px)
|
|
54
|
+
* - Maps `pathOffset` → `stroke-dashoffset` (negative px)
|
|
55
|
+
*
|
|
56
|
+
* @param {Element} element The element being animated (must be an SVG path).
|
|
57
|
+
* @param {Record<string, unknown>} keyframes The input keyframes possibly containing path props.
|
|
58
|
+
* @returns {Record<string, unknown>} A transformed keyframe object safe for animation.
|
|
59
|
+
*/
|
|
60
|
+
export declare const transformSVGPathProperties: (element: Element, keyframes: Record<string, unknown>) => Record<string, unknown>;
|
|
61
|
+
/**
|
|
62
|
+
* Check if any keyframes contain SVG path properties.
|
|
63
|
+
*/
|
|
64
|
+
/**
|
|
65
|
+
* Checks if any SVG path-related properties are present in the keyframes object.
|
|
66
|
+
*
|
|
67
|
+
* @param {Record<string, unknown>} keyframes The keyframes to inspect.
|
|
68
|
+
* @returns {boolean} True if any of `pathLength`, `pathSpacing`, or `pathOffset` are present.
|
|
69
|
+
*/
|
|
70
|
+
export declare const hasSVGPathProperties: (keyframes: Record<string, unknown>) => boolean;
|
|
71
|
+
/**
|
|
72
|
+
* Transform initial SVG path properties for initial state setup.
|
|
73
|
+
* This ensures that the initial state also has the proper strokeDasharray values.
|
|
74
|
+
*/
|
|
75
|
+
/**
|
|
76
|
+
* Transforms initial keyframes for SVG paths so that the initial state uses
|
|
77
|
+
* normalized dash attributes.
|
|
78
|
+
*
|
|
79
|
+
* @param {Element} element The element being animated (must be an SVG path).
|
|
80
|
+
* @param {Record<string, unknown> | undefined} initial Initial keyframes, if provided.
|
|
81
|
+
* @returns {Record<string, unknown> | undefined} Transformed initial keyframes or the original value.
|
|
82
|
+
*/
|
|
83
|
+
export declare const transformInitialSVGPathProperties: (element: Element, initial: Record<string, unknown> | undefined) => Record<string, unknown> | undefined;
|
|
84
|
+
/**
|
|
85
|
+
* Computes normalized SVG path attributes for initial render without requiring an element.
|
|
86
|
+
*
|
|
87
|
+
* Behavior matches React/Framer Motion parity:
|
|
88
|
+
* - Always sets pathLength="1" whenever any of path props are present
|
|
89
|
+
* - stroke-dasharray = px(pathLength) + ' ' + px(pathSpacing ?? 1 - Number(pathLength))
|
|
90
|
+
* - stroke-dashoffset = px(-(pathOffset ?? 0))
|
|
91
|
+
*
|
|
92
|
+
* The returned object is suitable for direct DOM attribute assignment (dash-cased keys).
|
|
93
|
+
*
|
|
94
|
+
* @param {Record<string, unknown> | null | undefined} initial Incoming initial keyframes object
|
|
95
|
+
* @returns {Record<string, string> | null} Normalized attribute map or null if no path props
|
|
96
|
+
*/
|
|
97
|
+
export declare const computeNormalizedSVGInitialAttrs: (initial: Record<string, unknown> | null | undefined) => Record<string, string> | null;
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SVG-specific properties that need special handling during animation.
|
|
3
|
+
* These properties are not standard CSS properties and need to be transformed.
|
|
4
|
+
*/
|
|
5
|
+
export const SVG_PATH_PROPERTIES = new Set(['pathLength', 'pathOffset', 'pathSpacing']);
|
|
6
|
+
/**
|
|
7
|
+
* Check if an element is an SVG path element.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Determines whether the provided element is an SVGPathElement.
|
|
11
|
+
*
|
|
12
|
+
* @param {Element} element The candidate element to test.
|
|
13
|
+
* @returns {element is SVGPathElement} True when the element is an SVG path.
|
|
14
|
+
* @example
|
|
15
|
+
* const el = document.createElementNS('http://www.w3.org/2000/svg', 'path')
|
|
16
|
+
* if (isSVGPathElement(el)) {
|
|
17
|
+
* // el is now typed as SVGPathElement
|
|
18
|
+
* }
|
|
19
|
+
*/
|
|
20
|
+
export const isSVGPathElement = (element) => {
|
|
21
|
+
if (typeof SVGPathElement !== 'undefined') {
|
|
22
|
+
return element instanceof SVGPathElement;
|
|
23
|
+
}
|
|
24
|
+
const nsOk = element.namespaceURI === 'http://www.w3.org/2000/svg';
|
|
25
|
+
const tagOk = element.tagName?.toLowerCase() === 'path';
|
|
26
|
+
return !!(nsOk && tagOk);
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Check if an element is any SVG element.
|
|
30
|
+
*/
|
|
31
|
+
/**
|
|
32
|
+
* Determines whether the provided element is an SVGElement.
|
|
33
|
+
*
|
|
34
|
+
* @param {Element} element The candidate element to test.
|
|
35
|
+
* @returns {element is SVGElement} True when the element is an SVG element.
|
|
36
|
+
* @example
|
|
37
|
+
* const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
|
|
38
|
+
* if (isSVGElement(svg)) {
|
|
39
|
+
* // svg is now typed as SVGElement
|
|
40
|
+
* }
|
|
41
|
+
*/
|
|
42
|
+
export const isSVGElement = (element) => {
|
|
43
|
+
if (typeof SVGElement === 'undefined') {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
return element instanceof SVGElement;
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Transform SVG path-specific animation properties to their CSS equivalents.
|
|
50
|
+
*
|
|
51
|
+
* Motion's pathLength property creates a line-drawing effect by manipulating
|
|
52
|
+
* strokeDasharray and strokeDashoffset. This function transforms:
|
|
53
|
+
* - pathLength: 0 -> 1 becomes strokeDasharray: "0 1" -> "1 1"
|
|
54
|
+
* - pathOffset: value becomes strokeDashoffset: -value (inverted for drawing direction)
|
|
55
|
+
*
|
|
56
|
+
* @param element - The SVG element being animated
|
|
57
|
+
* @param keyframes - The animation keyframes that may contain SVG properties
|
|
58
|
+
* @returns Transformed keyframes with CSS-compatible properties
|
|
59
|
+
*/
|
|
60
|
+
/**
|
|
61
|
+
* Transforms SVG path-specific animation properties into DOM-compatible attributes.
|
|
62
|
+
*
|
|
63
|
+
* Normalized behavior (React/Framer Motion parity):
|
|
64
|
+
* - Ensures `pathLength="1"` is set when any path prop is present
|
|
65
|
+
* - Maps `pathLength`/`pathSpacing` → `stroke-dasharray` (px)
|
|
66
|
+
* - Maps `pathOffset` → `stroke-dashoffset` (negative px)
|
|
67
|
+
*
|
|
68
|
+
* @param {Element} element The element being animated (must be an SVG path).
|
|
69
|
+
* @param {Record<string, unknown>} keyframes The input keyframes possibly containing path props.
|
|
70
|
+
* @returns {Record<string, unknown>} A transformed keyframe object safe for animation.
|
|
71
|
+
*/
|
|
72
|
+
export const transformSVGPathProperties = (element, keyframes) => {
|
|
73
|
+
if (!isSVGPathElement(element)) {
|
|
74
|
+
return keyframes;
|
|
75
|
+
}
|
|
76
|
+
// logging removed
|
|
77
|
+
const transformed = { ...keyframes };
|
|
78
|
+
// let hasPathLength = false
|
|
79
|
+
// Transform normalized path props to dash attributes using pathLength="1" semantics
|
|
80
|
+
if ('pathLength' in transformed ||
|
|
81
|
+
'pathSpacing' in transformed ||
|
|
82
|
+
'pathOffset' in transformed) {
|
|
83
|
+
try {
|
|
84
|
+
element.setAttribute('pathLength', '1');
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
void 0;
|
|
88
|
+
}
|
|
89
|
+
const toNum = (v) => typeof v === 'number'
|
|
90
|
+
? v
|
|
91
|
+
: v != null && /^-?\d+(\.\d+)?(px)?$/i.test(String(v).trim())
|
|
92
|
+
? parseFloat(String(v))
|
|
93
|
+
: undefined;
|
|
94
|
+
const length = (() => {
|
|
95
|
+
const v = transformed.pathLength;
|
|
96
|
+
const n = toNum(v);
|
|
97
|
+
return n ?? v;
|
|
98
|
+
})();
|
|
99
|
+
const spacing = (() => {
|
|
100
|
+
const v = transformed.pathSpacing;
|
|
101
|
+
const n = toNum(v);
|
|
102
|
+
return n ?? v;
|
|
103
|
+
})();
|
|
104
|
+
const offset = (() => {
|
|
105
|
+
const v = transformed.pathOffset;
|
|
106
|
+
const n = toNum(v);
|
|
107
|
+
return n ?? v;
|
|
108
|
+
})();
|
|
109
|
+
const toPx = (v) => (typeof v === 'number' ? `${v}px` : String(v));
|
|
110
|
+
const buildDashArray = (len, spa) => `${toPx(len)} ${toPx(spa)}`;
|
|
111
|
+
// stroke-dasharray from pathLength/pathSpacing with default spacing = 1 - length
|
|
112
|
+
if (Array.isArray(length)) {
|
|
113
|
+
const lenArr = length;
|
|
114
|
+
const spaArr = Array.isArray(spacing)
|
|
115
|
+
? spacing
|
|
116
|
+
: lenArr.map((lv) => typeof lv === 'number' ? 1 - lv : spacing);
|
|
117
|
+
const dashArray = lenArr.map((lv, i) => buildDashArray(lv, spaArr[i]));
|
|
118
|
+
transformed.strokeDasharray = dashArray;
|
|
119
|
+
transformed['stroke-dasharray'] = dashArray;
|
|
120
|
+
}
|
|
121
|
+
else if (length !== undefined) {
|
|
122
|
+
const len = length;
|
|
123
|
+
const lenNum = toNum(len) ?? 0;
|
|
124
|
+
const spa = spacing !== undefined ? spacing : 1 - lenNum;
|
|
125
|
+
const dashArray = buildDashArray(len, spa);
|
|
126
|
+
transformed.strokeDasharray = dashArray;
|
|
127
|
+
transformed['stroke-dasharray'] = dashArray;
|
|
128
|
+
}
|
|
129
|
+
// stroke-dashoffset from -pathOffset
|
|
130
|
+
if (Array.isArray(offset)) {
|
|
131
|
+
const offs = offset.map((ov) => {
|
|
132
|
+
const n = toNum(ov);
|
|
133
|
+
return n !== undefined ? `${-n}px` : String(ov);
|
|
134
|
+
});
|
|
135
|
+
transformed.strokeDashoffset = offs;
|
|
136
|
+
transformed['stroke-dashoffset'] = offs;
|
|
137
|
+
}
|
|
138
|
+
else if (offset !== undefined) {
|
|
139
|
+
const n = toNum(offset);
|
|
140
|
+
const off = n !== undefined ? `${-n}px` : String(offset);
|
|
141
|
+
transformed.strokeDashoffset = off;
|
|
142
|
+
transformed['stroke-dashoffset'] = off;
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
// default 0
|
|
146
|
+
;
|
|
147
|
+
transformed.strokeDashoffset = '0px';
|
|
148
|
+
transformed['stroke-dashoffset'] = '0px';
|
|
149
|
+
}
|
|
150
|
+
delete transformed.pathLength;
|
|
151
|
+
delete transformed.pathSpacing;
|
|
152
|
+
delete transformed.pathOffset;
|
|
153
|
+
}
|
|
154
|
+
// logging removed
|
|
155
|
+
return transformed;
|
|
156
|
+
};
|
|
157
|
+
/**
|
|
158
|
+
* Check if any keyframes contain SVG path properties.
|
|
159
|
+
*/
|
|
160
|
+
/**
|
|
161
|
+
* Checks if any SVG path-related properties are present in the keyframes object.
|
|
162
|
+
*
|
|
163
|
+
* @param {Record<string, unknown>} keyframes The keyframes to inspect.
|
|
164
|
+
* @returns {boolean} True if any of `pathLength`, `pathSpacing`, or `pathOffset` are present.
|
|
165
|
+
*/
|
|
166
|
+
export const hasSVGPathProperties = (keyframes) => {
|
|
167
|
+
return Object.keys(keyframes).some((key) => SVG_PATH_PROPERTIES.has(key));
|
|
168
|
+
};
|
|
169
|
+
/**
|
|
170
|
+
* Transform initial SVG path properties for initial state setup.
|
|
171
|
+
* This ensures that the initial state also has the proper strokeDasharray values.
|
|
172
|
+
*/
|
|
173
|
+
/**
|
|
174
|
+
* Transforms initial keyframes for SVG paths so that the initial state uses
|
|
175
|
+
* normalized dash attributes.
|
|
176
|
+
*
|
|
177
|
+
* @param {Element} element The element being animated (must be an SVG path).
|
|
178
|
+
* @param {Record<string, unknown> | undefined} initial Initial keyframes, if provided.
|
|
179
|
+
* @returns {Record<string, unknown> | undefined} Transformed initial keyframes or the original value.
|
|
180
|
+
*/
|
|
181
|
+
export const transformInitialSVGPathProperties = (element, initial) => {
|
|
182
|
+
if (!initial || !isSVGPathElement(element)) {
|
|
183
|
+
return initial;
|
|
184
|
+
}
|
|
185
|
+
// logging removed
|
|
186
|
+
return transformSVGPathProperties(element, initial);
|
|
187
|
+
};
|
|
188
|
+
/**
|
|
189
|
+
* Computes normalized SVG path attributes for initial render without requiring an element.
|
|
190
|
+
*
|
|
191
|
+
* Behavior matches React/Framer Motion parity:
|
|
192
|
+
* - Always sets pathLength="1" whenever any of path props are present
|
|
193
|
+
* - stroke-dasharray = px(pathLength) + ' ' + px(pathSpacing ?? 1 - Number(pathLength))
|
|
194
|
+
* - stroke-dashoffset = px(-(pathOffset ?? 0))
|
|
195
|
+
*
|
|
196
|
+
* The returned object is suitable for direct DOM attribute assignment (dash-cased keys).
|
|
197
|
+
*
|
|
198
|
+
* @param {Record<string, unknown> | null | undefined} initial Incoming initial keyframes object
|
|
199
|
+
* @returns {Record<string, string> | null} Normalized attribute map or null if no path props
|
|
200
|
+
*/
|
|
201
|
+
export const computeNormalizedSVGInitialAttrs = (initial) => {
|
|
202
|
+
if (!initial)
|
|
203
|
+
return null;
|
|
204
|
+
const hasAny = 'pathLength' in initial || 'pathSpacing' in initial || 'pathOffset' in initial;
|
|
205
|
+
if (!hasAny)
|
|
206
|
+
return null;
|
|
207
|
+
const toPx = (v) => (typeof v === 'number' ? `${v}px` : String(v));
|
|
208
|
+
const negatePx = (v) => {
|
|
209
|
+
if (typeof v === 'number')
|
|
210
|
+
return `${-v}px`;
|
|
211
|
+
const s = String(v);
|
|
212
|
+
return s.startsWith('-') ? s : /^[\d.]+(px)?$/i.test(s) ? `-${s}` : s;
|
|
213
|
+
};
|
|
214
|
+
const len = initial.pathLength ?? 0;
|
|
215
|
+
const spa = initial.pathSpacing ??
|
|
216
|
+
(typeof len === 'number' ? 1 - len : 1);
|
|
217
|
+
const off = initial.pathOffset ?? 0;
|
|
218
|
+
const dashArray = `${toPx(len)} ${toPx(spa)}`;
|
|
219
|
+
const dashOffset = negatePx(off);
|
|
220
|
+
// logging removed
|
|
221
|
+
return {
|
|
222
|
+
pathLength: '1',
|
|
223
|
+
'stroke-dasharray': dashArray,
|
|
224
|
+
'stroke-dashoffset': dashOffset
|
|
225
|
+
};
|
|
226
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@humanspeak/svelte-motion",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.12",
|
|
4
4
|
"description": "A lightweight animation library for Svelte 5 that provides smooth, hardware-accelerated animations. Features include spring physics, custom easing, and fluid transitions. Built on top of the motion library, it offers a simple API for creating complex animations with minimal code. Perfect for interactive UIs, micro-interactions, and engaging user experiences.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"svelte",
|
|
@@ -59,9 +59,9 @@
|
|
|
59
59
|
"@changesets/cli": "^2.29.7",
|
|
60
60
|
"@eslint/compat": "^1.4.0",
|
|
61
61
|
"@eslint/js": "^9.37.0",
|
|
62
|
-
"@playwright/test": "^1.
|
|
62
|
+
"@playwright/test": "^1.56.0",
|
|
63
63
|
"@sveltejs/adapter-auto": "^6.1.1",
|
|
64
|
-
"@sveltejs/kit": "^2.
|
|
64
|
+
"@sveltejs/kit": "^2.46.4",
|
|
65
65
|
"@sveltejs/package": "^2.5.4",
|
|
66
66
|
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
|
67
67
|
"@tailwindcss/aspect-ratio": "^0.4.2",
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
"@tailwindcss/typography": "^0.5.19",
|
|
72
72
|
"@testing-library/jest-dom": "^6.9.1",
|
|
73
73
|
"@testing-library/svelte": "^5.2.8",
|
|
74
|
-
"@types/node": "^24.
|
|
74
|
+
"@types/node": "^24.7.1",
|
|
75
75
|
"@vitest/coverage-v8": "^3.2.4",
|
|
76
76
|
"concurrently": "^9.2.1",
|
|
77
77
|
"eslint": "^9.37.0",
|
|
@@ -89,9 +89,9 @@
|
|
|
89
89
|
"prettier-plugin-sort-json": "^4.1.1",
|
|
90
90
|
"prettier-plugin-svelte": "^3.4.0",
|
|
91
91
|
"prettier-plugin-tailwindcss": "^0.6.14",
|
|
92
|
-
"publint": "^0.3.
|
|
93
|
-
"svelte": "^5.39.
|
|
94
|
-
"svelte-check": "^4.3.
|
|
92
|
+
"publint": "^0.3.14",
|
|
93
|
+
"svelte": "^5.39.11",
|
|
94
|
+
"svelte-check": "^4.3.3",
|
|
95
95
|
"svg-tags": "^1.0.0",
|
|
96
96
|
"tailwind-merge": "^3.3.1",
|
|
97
97
|
"tailwind-variants": "^3.1.1",
|
|
@@ -99,7 +99,7 @@
|
|
|
99
99
|
"tailwindcss-animate": "^1.0.7",
|
|
100
100
|
"tsx": "^4.20.6",
|
|
101
101
|
"typescript": "^5.9.3",
|
|
102
|
-
"typescript-eslint": "^8.
|
|
102
|
+
"typescript-eslint": "^8.46.0",
|
|
103
103
|
"vite": "^7.1.9",
|
|
104
104
|
"vite-tsconfig-paths": "^5.1.4",
|
|
105
105
|
"vitest": "^3.2.4"
|