@humanspeak/svelte-motion 0.0.13 → 0.1.0
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/README.md +15 -5
- package/dist/html/_MotionContainer.svelte +97 -0
- package/dist/types.d.ts +2 -0
- package/package.json +14 -7
package/README.md
CHANGED
|
@@ -35,6 +35,17 @@ This package includes support for `MotionConfig`, which allows you to set defaul
|
|
|
35
35
|
</MotionConfig>
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
+
### Layout Animations (FLIP)
|
|
39
|
+
|
|
40
|
+
Svelte Motion supports minimal layout animations via FLIP using the `layout` prop:
|
|
41
|
+
|
|
42
|
+
```svelte
|
|
43
|
+
<motion.div layout transition={{ duration: 0.25 }} />
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
- **`layout`**: smoothly animates translation and scale between layout changes (size and position).
|
|
47
|
+
- **`layout="position"`**: only animates translation (no scale).
|
|
48
|
+
|
|
38
49
|
#### Current Limitations
|
|
39
50
|
|
|
40
51
|
Some Motion features are not yet implemented:
|
|
@@ -43,9 +54,7 @@ Some Motion features are not yet implemented:
|
|
|
43
54
|
- `features` configuration
|
|
44
55
|
- Performance optimizations like `transformPagePoint`
|
|
45
56
|
- Advanced transition controls
|
|
46
|
-
- `
|
|
47
|
-
|
|
48
|
-
We're actively working on adding these features. Check our GitHub issues for progress updates or to contribute.
|
|
57
|
+
- Shared layout / `layoutId` (planned)
|
|
49
58
|
|
|
50
59
|
## External Dependencies
|
|
51
60
|
|
|
@@ -63,8 +72,9 @@ This package carefully selects its dependencies to provide a robust and maintain
|
|
|
63
72
|
|
|
64
73
|
| Motion | Demo / Route | REPL |
|
|
65
74
|
| -------------------------------------------------------------------------------------------------------- | --------------------------------- | ---------------------------------------------------------------------------------------------- |
|
|
66
|
-
| [React - Enter Animation](https://examples.motion.dev/react/enter-animation) | `/tests/motion/enter-animation` | [View Example](https://svelte.dev/playground/7f60c347729f4ea48b1a4590c9dedc02?version=5.
|
|
67
|
-
| HTML Content (0→100 counter)
|
|
75
|
+
| [React - Enter Animation](https://examples.motion.dev/react/enter-animation) | `/tests/motion/enter-animation` | [View Example](https://svelte.dev/playground/7f60c347729f4ea48b1a4590c9dedc02?version=5.38.10) |
|
|
76
|
+
| [HTML Content (0→100 counter)](https://examples.motion.dev/react/html-content) | `/tests/motion/html-content` | [View Example](https://svelte.dev/playground/31cd72df4a3242b4b4589501a25e774f?version=5.38.10) |
|
|
77
|
+
| [Aspect Ratio](https://examples.motion.dev/react/aspect-ratio) | `/tests/motion/aspect-ratio` | [View Example](https://svelte.dev/playground/1bf60e745fae44f5becb4c830fde9b6e?version=5.38.10) |
|
|
68
78
|
| [Random - Shiny Button](https://www.youtube.com/watch?v=jcpLprT5F0I) by [@verse\_](https://x.com/verse_) | `/tests/random/shiny-button` | [View Example](https://svelte.dev/playground/96f9e0bf624f4396adaf06c519147450?version=5.38.10) |
|
|
69
79
|
| [Fancy Like Button](https://github.com/DRlFTER/fancyLikeButton) | `/tests/random/fancy-like-button` | [View Example](https://svelte.dev/playground/c34b7e53d41c48b0ab1eaf21ca120c6e?version=5.38.10) |
|
|
70
80
|
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
style: styleProp,
|
|
25
25
|
class: classProp,
|
|
26
26
|
whileTap: whileTapProp,
|
|
27
|
+
layout: layoutProp,
|
|
27
28
|
...rest
|
|
28
29
|
}: Props = $props()
|
|
29
30
|
let element: HTMLElement | null = $state(null)
|
|
@@ -76,6 +77,102 @@
|
|
|
76
77
|
}
|
|
77
78
|
}
|
|
78
79
|
|
|
80
|
+
// Minimal layout animation using FLIP when `layout` is enabled.
|
|
81
|
+
// When layout === 'position' we only translate.
|
|
82
|
+
// When layout === true we also scale to smoothly interpolate size changes.
|
|
83
|
+
let lastRect: DOMRect | null = null
|
|
84
|
+
$effect(() => {
|
|
85
|
+
if (!(element && layoutProp && isLoaded === 'ready')) return
|
|
86
|
+
|
|
87
|
+
// Initialize last rect on first ready frame
|
|
88
|
+
const measure = () => {
|
|
89
|
+
const prev = element!.style.transform
|
|
90
|
+
element!.style.transform = 'none'
|
|
91
|
+
const rect = element!.getBoundingClientRect()
|
|
92
|
+
element!.style.transform = prev
|
|
93
|
+
return rect
|
|
94
|
+
}
|
|
95
|
+
lastRect = measure()
|
|
96
|
+
// Hint compositor for smoother FLIP transforms
|
|
97
|
+
element!.style.willChange = 'transform'
|
|
98
|
+
element!.style.transformOrigin = '0 0'
|
|
99
|
+
|
|
100
|
+
let rafId: number | null = null
|
|
101
|
+
const runFlip = () => {
|
|
102
|
+
if (!lastRect) {
|
|
103
|
+
lastRect = measure()
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
const prev = element!.style.transform
|
|
107
|
+
element!.style.transform = 'none'
|
|
108
|
+
const next = element!.getBoundingClientRect()
|
|
109
|
+
element!.style.transform = prev
|
|
110
|
+
// Use top-left corner for FLIP to match transformOrigin '0 0'
|
|
111
|
+
const dx = lastRect.left - next.left
|
|
112
|
+
const dy = lastRect.top - next.top
|
|
113
|
+
const sx = next.width > 0 ? lastRect.width / next.width : 1
|
|
114
|
+
const sy = next.height > 0 ? lastRect.height / next.height : 1
|
|
115
|
+
|
|
116
|
+
const shouldTranslate = Math.abs(dx) > 0.5 || Math.abs(dy) > 0.5
|
|
117
|
+
const shouldScale =
|
|
118
|
+
layoutProp !== 'position' && (Math.abs(1 - sx) > 0.01 || Math.abs(1 - sy) > 0.01)
|
|
119
|
+
|
|
120
|
+
if (shouldTranslate || shouldScale) {
|
|
121
|
+
const keyframes: Record<string, unknown> = {}
|
|
122
|
+
if (shouldTranslate) {
|
|
123
|
+
keyframes.x = [dx, 0]
|
|
124
|
+
keyframes.y = [dy, 0]
|
|
125
|
+
}
|
|
126
|
+
if (shouldScale) {
|
|
127
|
+
keyframes.scaleX = [sx, 1]
|
|
128
|
+
keyframes.scaleY = [sy, 1]
|
|
129
|
+
;(keyframes as Record<string, unknown>).transformOrigin = '0 0'
|
|
130
|
+
}
|
|
131
|
+
// Apply the inverse transform immediately so we don't flash the new layout
|
|
132
|
+
// before the animation starts (classic FLIP: First, Last, Invert, Play)
|
|
133
|
+
const parts: string[] = []
|
|
134
|
+
if (shouldTranslate) parts.push(`translate(${dx}px, ${dy}px)`)
|
|
135
|
+
if (shouldScale) parts.push(`scale(${sx}, ${sy})`)
|
|
136
|
+
element!.style.transformOrigin = '0 0'
|
|
137
|
+
element!.style.transform = parts.join(' ')
|
|
138
|
+
animate(
|
|
139
|
+
element!,
|
|
140
|
+
keyframes as unknown as import('motion').DOMKeyframesDefinition,
|
|
141
|
+
(mergedTransition ?? {}) as import('motion').AnimationOptions
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
lastRect = next
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const scheduleFlip = () => {
|
|
148
|
+
if (rafId) cancelAnimationFrame(rafId)
|
|
149
|
+
rafId = requestAnimationFrame(() => {
|
|
150
|
+
rafId = null
|
|
151
|
+
runFlip()
|
|
152
|
+
})
|
|
153
|
+
}
|
|
154
|
+
const ro = new ResizeObserver(() => scheduleFlip())
|
|
155
|
+
ro.observe(element)
|
|
156
|
+
// Also observe attribute/class changes and nearby DOM mutations that commonly cause reflow/reposition
|
|
157
|
+
const mo = new MutationObserver(() => scheduleFlip())
|
|
158
|
+
mo.observe(element, { attributes: true, attributeFilter: ['class', 'style'] })
|
|
159
|
+
if (element.parentElement) {
|
|
160
|
+
mo.observe(element.parentElement, { childList: true, subtree: false, attributes: true })
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return () => {
|
|
164
|
+
ro.disconnect()
|
|
165
|
+
mo.disconnect()
|
|
166
|
+
lastRect = null
|
|
167
|
+
// Reset compositor hints on teardown
|
|
168
|
+
if (element) {
|
|
169
|
+
element.style.willChange = ''
|
|
170
|
+
element.style.transformOrigin = ''
|
|
171
|
+
element.style.transform = ''
|
|
172
|
+
}
|
|
173
|
+
if (rafId) cancelAnimationFrame(rafId)
|
|
174
|
+
}
|
|
175
|
+
})
|
|
79
176
|
// Merge style for before/after ready so styles carry through post-anim
|
|
80
177
|
// Merge styles directly in markup; keep effect solely for readiness logic
|
|
81
178
|
|
package/dist/types.d.ts
CHANGED
|
@@ -66,6 +66,8 @@ export type MotionProps = {
|
|
|
66
66
|
style?: string;
|
|
67
67
|
/** CSS classes */
|
|
68
68
|
class?: string;
|
|
69
|
+
/** Enable FLIP layout animations; "position" limits to translation only */
|
|
70
|
+
layout?: boolean | 'position';
|
|
69
71
|
};
|
|
70
72
|
/**
|
|
71
73
|
* Configuration properties for motion/animation components.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@humanspeak/svelte-motion",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
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",
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"micro-interactions",
|
|
15
15
|
"performance"
|
|
16
16
|
],
|
|
17
|
+
"homepage": "https://motion.svelte.page",
|
|
17
18
|
"bugs": {
|
|
18
19
|
"url": "https://github.com/humanspeak/svelte-motion/issues"
|
|
19
20
|
},
|
|
@@ -51,14 +52,15 @@
|
|
|
51
52
|
}
|
|
52
53
|
},
|
|
53
54
|
"dependencies": {
|
|
54
|
-
"motion": "^12.23.
|
|
55
|
+
"motion": "^12.23.15"
|
|
55
56
|
},
|
|
56
57
|
"devDependencies": {
|
|
58
|
+
"@changesets/cli": "^2.27.8",
|
|
57
59
|
"@eslint/compat": "^1.3.2",
|
|
58
60
|
"@eslint/js": "^9.35.0",
|
|
59
61
|
"@playwright/test": "^1.55.0",
|
|
60
62
|
"@sveltejs/adapter-auto": "^6.1.0",
|
|
61
|
-
"@sveltejs/kit": "^2.
|
|
63
|
+
"@sveltejs/kit": "^2.42.1",
|
|
62
64
|
"@sveltejs/package": "^2.5.2",
|
|
63
65
|
"@sveltejs/vite-plugin-svelte": "^6.2.0",
|
|
64
66
|
"@tailwindcss/aspect-ratio": "^0.4.2",
|
|
@@ -77,11 +79,12 @@
|
|
|
77
79
|
"husky": "^9.1.7",
|
|
78
80
|
"jsdom": "^27.0.0",
|
|
79
81
|
"prettier": "^3.6.2",
|
|
80
|
-
"prettier-plugin-organize-imports": "^4.
|
|
82
|
+
"prettier-plugin-organize-imports": "^4.3.0",
|
|
83
|
+
"prettier-plugin-sort-json": "^4.1.1",
|
|
81
84
|
"prettier-plugin-svelte": "^3.4.0",
|
|
82
85
|
"prettier-plugin-tailwindcss": "^0.6.14",
|
|
83
|
-
"publint": "^0.3.
|
|
84
|
-
"svelte": "^5.
|
|
86
|
+
"publint": "^0.3.13",
|
|
87
|
+
"svelte": "^5.39.2",
|
|
85
88
|
"svelte-check": "^4.3.1",
|
|
86
89
|
"tailwind-merge": "^3.3.1",
|
|
87
90
|
"tailwind-variants": "^3.1.1",
|
|
@@ -90,7 +93,7 @@
|
|
|
90
93
|
"tsx": "^4.20.5",
|
|
91
94
|
"typescript": "^5.9.2",
|
|
92
95
|
"typescript-eslint": "^8.44.0",
|
|
93
|
-
"vite": "^7.1.
|
|
96
|
+
"vite": "^7.1.6",
|
|
94
97
|
"vite-tsconfig-paths": "^5.1.4",
|
|
95
98
|
"vitest": "^3.2.4"
|
|
96
99
|
},
|
|
@@ -118,6 +121,9 @@
|
|
|
118
121
|
"build": "vite build && npm run package",
|
|
119
122
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
|
120
123
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
|
124
|
+
"cs:add": "changeset",
|
|
125
|
+
"cs:publish": "pnpm run build && changeset publish",
|
|
126
|
+
"cs:version": "changeset version && pnpm i --lockfile-only",
|
|
121
127
|
"dev": "vite dev",
|
|
122
128
|
"format": "prettier --write .",
|
|
123
129
|
"generate": "tsx scripts/generate-html.ts",
|
|
@@ -126,6 +132,7 @@
|
|
|
126
132
|
"lint:fix": "npm run format && eslint . --fix",
|
|
127
133
|
"package": "svelte-kit sync && svelte-package && publint",
|
|
128
134
|
"preview": "vite preview",
|
|
135
|
+
"release": "pnpm run cs:version && pnpm run cs:publish",
|
|
129
136
|
"test": "vitest run --coverage",
|
|
130
137
|
"test:all": "npm run test && npm run test:e2e",
|
|
131
138
|
"test:e2e": "playwright test",
|