@cioky/ripple-transitions 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 +67 -0
- package/package.json +31 -0
- package/src/index.tsrx +37 -0
- package/src/layout.ts +247 -0
- package/src/motion.ts +812 -0
- package/src/presence.tsrx +286 -0
- package/src/slide.ts +220 -0
- package/src/spring.ts +165 -0
- package/src/stagger.ts +64 -0
- package/src/types.ts +48 -0
package/README.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# @cioky/ripple-transitions
|
|
2
|
+
|
|
3
|
+
Transition and animation library for the **Ripple** UI framework.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
You can install this package using your preferred package manager:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
bun add @cioky/ripple-transitions
|
|
11
|
+
# or
|
|
12
|
+
npm install @cioky/ripple-transitions
|
|
13
|
+
# or
|
|
14
|
+
yarn add @cioky/ripple-transitions
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Make sure you have `ripple` and `@ripple-ts/vite-plugin` configured in your project.
|
|
18
|
+
|
|
19
|
+
## Features
|
|
20
|
+
|
|
21
|
+
- **Presence**: Identity-based transition coordinator. Exiting child elements stay in the DOM until their exit animations complete, and new child elements mount concurrently.
|
|
22
|
+
- **Transition**: Show/hide transitions supporting custom handlers and `popLayout` mode.
|
|
23
|
+
- **Motion & Gestures**: Standard transitions (`fade`, `rise`, `scale`, `fly`, `blur`, `draw`, `svelteTransition`, `gestures`, `animate`).
|
|
24
|
+
- **Layout Animations**: FLIP-based layout animations using `layout()` and `useTransitionList()`.
|
|
25
|
+
- **Slide Transitions**: Axis-based height/width accordion slide transitions.
|
|
26
|
+
- **Spring Physics**: Physics-based animations with adjustable stiffness, damping, and mass.
|
|
27
|
+
- **Stagger**: Injects delays sequentially for lists or grid layouts.
|
|
28
|
+
|
|
29
|
+
## Usage
|
|
30
|
+
|
|
31
|
+
### Presence & Transition
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
import { Presence, Transition, fade, slide } from '@cioky/ripple-transitions';
|
|
35
|
+
|
|
36
|
+
export function MyComponent() @{
|
|
37
|
+
let &[show] = track(true);
|
|
38
|
+
|
|
39
|
+
<div>
|
|
40
|
+
<button onClick={() => show = !show}>Toggle</button>
|
|
41
|
+
|
|
42
|
+
<Presence>
|
|
43
|
+
@if (show) {
|
|
44
|
+
<div ref={fade({ duration: 300 })}>
|
|
45
|
+
Transitions on mount and unmount!
|
|
46
|
+
</div>
|
|
47
|
+
}
|
|
48
|
+
</Presence>
|
|
49
|
+
</div>
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Layout Transitions
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
import { layout } from '@cioky/ripple-transitions';
|
|
57
|
+
|
|
58
|
+
export function List() @{
|
|
59
|
+
<div ref={layout()}>
|
|
60
|
+
{/* Children animate automatically when they resize or shift positions */}
|
|
61
|
+
</div>
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## License
|
|
66
|
+
|
|
67
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cioky/ripple-transitions",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Transition and animation library for the Ripple framework",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.tsrx",
|
|
7
|
+
"types": "./src/index.tsrx",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./src/index.tsrx",
|
|
11
|
+
"default": "./src/index.tsrx"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"src",
|
|
16
|
+
"README.md",
|
|
17
|
+
"package.json"
|
|
18
|
+
],
|
|
19
|
+
"peerDependencies": {
|
|
20
|
+
"ripple": ">=0.3.0"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"ripple",
|
|
24
|
+
"transitions",
|
|
25
|
+
"animation",
|
|
26
|
+
"@cioky/ripple-transitions",
|
|
27
|
+
"svelte-transitions"
|
|
28
|
+
],
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"author": "Antigravity <antigravity@google.com>"
|
|
31
|
+
}
|
package/src/index.tsrx
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
TransitionOptions,
|
|
3
|
+
MotionConfig,
|
|
4
|
+
LayoutOptions,
|
|
5
|
+
GestureConfig,
|
|
6
|
+
SvelteTransitionConfig,
|
|
7
|
+
SvelteTransitionFn
|
|
8
|
+
} from './types.js';
|
|
9
|
+
|
|
10
|
+
export { springValues } from './spring.js';
|
|
11
|
+
|
|
12
|
+
export {
|
|
13
|
+
mergeRefs,
|
|
14
|
+
MotionTimingContext,
|
|
15
|
+
motion,
|
|
16
|
+
fade,
|
|
17
|
+
rise,
|
|
18
|
+
scale,
|
|
19
|
+
fly,
|
|
20
|
+
blur,
|
|
21
|
+
draw,
|
|
22
|
+
svelteTransition,
|
|
23
|
+
gestures,
|
|
24
|
+
animate
|
|
25
|
+
} from './motion.js';
|
|
26
|
+
|
|
27
|
+
export { stagger } from './stagger.js';
|
|
28
|
+
|
|
29
|
+
export { slide } from './slide.js';
|
|
30
|
+
|
|
31
|
+
export { layout, useTransitionList } from './layout.js';
|
|
32
|
+
|
|
33
|
+
export {
|
|
34
|
+
MotionProvider,
|
|
35
|
+
Transition,
|
|
36
|
+
Presence
|
|
37
|
+
} from './presence.tsrx';
|
package/src/layout.ts
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { track, tick } from 'ripple';
|
|
2
|
+
import { type LayoutOptions, type TransitionOptions } from './types.js';
|
|
3
|
+
import { getSpringKeyframes } from './spring.js';
|
|
4
|
+
|
|
5
|
+
export function layout(options: LayoutOptions = {}) {
|
|
6
|
+
const {
|
|
7
|
+
duration = 300,
|
|
8
|
+
easing = 'cubic-bezier(0.25, 1, 0.5, 1)',
|
|
9
|
+
type,
|
|
10
|
+
stiffness = 300,
|
|
11
|
+
damping = 30,
|
|
12
|
+
mass = 1
|
|
13
|
+
} = options;
|
|
14
|
+
const isSpring = type === 'spring';
|
|
15
|
+
|
|
16
|
+
return (el: HTMLElement) => {
|
|
17
|
+
if (!el) return;
|
|
18
|
+
|
|
19
|
+
const getRelativeRect = () => {
|
|
20
|
+
const rect = el.getBoundingClientRect();
|
|
21
|
+
const parent = el.offsetParent || el.parentElement || el;
|
|
22
|
+
const parentRect = parent.getBoundingClientRect();
|
|
23
|
+
return {
|
|
24
|
+
left: rect.left - parentRect.left,
|
|
25
|
+
top: rect.top - parentRect.top,
|
|
26
|
+
width: rect.width,
|
|
27
|
+
height: rect.height
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
let prevRect = getRelativeRect();
|
|
32
|
+
let animating = false;
|
|
33
|
+
let initialized = false;
|
|
34
|
+
|
|
35
|
+
setTimeout(() => {
|
|
36
|
+
initialized = true;
|
|
37
|
+
prevRect = getRelativeRect();
|
|
38
|
+
}, 150);
|
|
39
|
+
|
|
40
|
+
const ro = new ResizeObserver(() => {
|
|
41
|
+
const newRect = getRelativeRect();
|
|
42
|
+
if (!initialized) {
|
|
43
|
+
prevRect = newRect;
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (animating) return;
|
|
47
|
+
const dx = prevRect.left - newRect.left;
|
|
48
|
+
const dy = prevRect.top - newRect.top;
|
|
49
|
+
const dw = prevRect.width - newRect.width;
|
|
50
|
+
const dh = prevRect.height - newRect.height;
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
if (dx !== 0 || dy !== 0 || dh !== 0 || dw !== 0) {
|
|
54
|
+
animating = true;
|
|
55
|
+
const anims: Promise<any>[] = [];
|
|
56
|
+
|
|
57
|
+
if (dh !== 0 || dw !== 0) {
|
|
58
|
+
const oldOverflow = el.style.overflow;
|
|
59
|
+
el.style.overflow = 'hidden';
|
|
60
|
+
const sizeAnim = el.animate([
|
|
61
|
+
{ width: `${prevRect.width}px`, height: `${prevRect.height}px` },
|
|
62
|
+
{ width: `${newRect.width}px`, height: `${newRect.height}px` }
|
|
63
|
+
], { duration, easing });
|
|
64
|
+
|
|
65
|
+
anims.push(sizeAnim.finished.then(() => {
|
|
66
|
+
el.style.overflow = oldOverflow;
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (dx !== 0 || dy !== 0) {
|
|
71
|
+
const startState = { transform: `translate(${dx}px, ${dy}px)` };
|
|
72
|
+
const endState = { transform: 'translate(0px, 0px)' };
|
|
73
|
+
|
|
74
|
+
const keyframes = isSpring
|
|
75
|
+
? getSpringKeyframes(startState, endState, { delay: 0, duration, easing, type, stiffness, damping, mass } as Required<TransitionOptions>)
|
|
76
|
+
: [startState, endState];
|
|
77
|
+
|
|
78
|
+
const transAnim = el.animate(keyframes, {
|
|
79
|
+
duration,
|
|
80
|
+
easing: isSpring ? 'linear' : easing,
|
|
81
|
+
fill: 'both'
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
anims.push(transAnim.finished.then(() => {
|
|
85
|
+
try { transAnim.commitStyles(); transAnim.cancel(); } catch {}
|
|
86
|
+
el.style.transform = '';
|
|
87
|
+
}));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
Promise.all(anims).then(() => {
|
|
91
|
+
animating = false;
|
|
92
|
+
prevRect = getRelativeRect();
|
|
93
|
+
});
|
|
94
|
+
} else {
|
|
95
|
+
prevRect = newRect;
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
let parent = el.parentElement;
|
|
100
|
+
while (parent && parent !== document.body) {
|
|
101
|
+
ro.observe(parent);
|
|
102
|
+
parent = parent.parentElement;
|
|
103
|
+
}
|
|
104
|
+
ro.observe(el);
|
|
105
|
+
|
|
106
|
+
return () => {
|
|
107
|
+
ro.disconnect();
|
|
108
|
+
el.style.transition = '';
|
|
109
|
+
el.style.transform = '';
|
|
110
|
+
};
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function useTransitionList<T extends { id: string | number }>(
|
|
115
|
+
initialItems: T[],
|
|
116
|
+
options: LayoutOptions | (() => LayoutOptions) = {}
|
|
117
|
+
) {
|
|
118
|
+
const state = track(initialItems);
|
|
119
|
+
const rects = new Map<string | number, DOMRect>();
|
|
120
|
+
const elements = new Map<string | number, HTMLElement>();
|
|
121
|
+
const refCache = new Map<string | number, (el: HTMLElement) => void>();
|
|
122
|
+
|
|
123
|
+
const register = (key: string | number) => {
|
|
124
|
+
let cached = refCache.get(key);
|
|
125
|
+
if (!cached) {
|
|
126
|
+
cached = (el: HTMLElement) => {
|
|
127
|
+
if (el) {
|
|
128
|
+
elements.set(key, el);
|
|
129
|
+
} else {
|
|
130
|
+
elements.delete(key);
|
|
131
|
+
rects.delete(key);
|
|
132
|
+
refCache.delete(key);
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
refCache.set(key, cached);
|
|
136
|
+
}
|
|
137
|
+
return cached;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const read = () => {
|
|
141
|
+
for (const [key, el] of elements.entries()) {
|
|
142
|
+
rects.set(key, el.getBoundingClientRect());
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const flip = () => {
|
|
147
|
+
queueMicrotask(() => {
|
|
148
|
+
const resolvedOptions = typeof options === 'function' ? options() : options;
|
|
149
|
+
const {
|
|
150
|
+
duration = 300,
|
|
151
|
+
easing = 'cubic-bezier(0.25, 1, 0.5, 1)',
|
|
152
|
+
type,
|
|
153
|
+
stiffness = 300,
|
|
154
|
+
damping = 30,
|
|
155
|
+
mass = 1
|
|
156
|
+
} = resolvedOptions;
|
|
157
|
+
const isSpring = type === 'spring';
|
|
158
|
+
|
|
159
|
+
const toAnimate: Array<{ el: HTMLElement; dx: number; dy: number }> = [];
|
|
160
|
+
for (const [key, el] of elements.entries()) {
|
|
161
|
+
const first = rects.get(key);
|
|
162
|
+
if (!first) continue;
|
|
163
|
+
const last = el.getBoundingClientRect();
|
|
164
|
+
const dx = first.left - last.left;
|
|
165
|
+
const dy = first.top - last.top;
|
|
166
|
+
if (dx !== 0 || dy !== 0) {
|
|
167
|
+
el.style.transition = 'none';
|
|
168
|
+
el.style.transform = `translate(${dx}px, ${dy}px)`;
|
|
169
|
+
toAnimate.push({ el, dx, dy });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (toAnimate.length > 0) {
|
|
174
|
+
// Force reflow
|
|
175
|
+
toAnimate[0].el.offsetHeight;
|
|
176
|
+
|
|
177
|
+
for (const { el, dx, dy } of toAnimate) {
|
|
178
|
+
const startState = { transform: `translate(${dx}px, ${dy}px)` };
|
|
179
|
+
const endState = { transform: 'translate(0px, 0px)' };
|
|
180
|
+
|
|
181
|
+
const keyframes = isSpring
|
|
182
|
+
? getSpringKeyframes(startState, endState, { delay: 0, duration, easing, type, stiffness, damping, mass } as Required<TransitionOptions>)
|
|
183
|
+
: [startState, endState];
|
|
184
|
+
|
|
185
|
+
const transAnim = el.animate(keyframes, {
|
|
186
|
+
duration,
|
|
187
|
+
easing: isSpring ? 'linear' : easing,
|
|
188
|
+
fill: 'both'
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
transAnim.finished.then(() => {
|
|
192
|
+
try { transAnim.commitStyles(); transAnim.cancel(); } catch {}
|
|
193
|
+
el.style.transform = '';
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
get items() {
|
|
202
|
+
return state.value;
|
|
203
|
+
},
|
|
204
|
+
set items(v) {
|
|
205
|
+
read();
|
|
206
|
+
state.value = v;
|
|
207
|
+
flip();
|
|
208
|
+
},
|
|
209
|
+
ref: register,
|
|
210
|
+
push(item: T) {
|
|
211
|
+
read();
|
|
212
|
+
state.value = [...state.value, item];
|
|
213
|
+
flip();
|
|
214
|
+
},
|
|
215
|
+
insert(item: T, i: number) {
|
|
216
|
+
read();
|
|
217
|
+
const n = [...state.value];
|
|
218
|
+
n.splice(i, 0, item);
|
|
219
|
+
state.value = n;
|
|
220
|
+
flip();
|
|
221
|
+
},
|
|
222
|
+
shuffle() {
|
|
223
|
+
read();
|
|
224
|
+
const c = [...state.value];
|
|
225
|
+
for (let i = c.length - 1; i > 0; i--) {
|
|
226
|
+
const j = (Math.random() * (i + 1)) | 0;
|
|
227
|
+
[c[i], c[j]] = [c[j], c[i]];
|
|
228
|
+
}
|
|
229
|
+
state.value = c;
|
|
230
|
+
flip();
|
|
231
|
+
},
|
|
232
|
+
async remove(id: string | number, animateExit?: (el: HTMLElement) => Promise<any> | void) {
|
|
233
|
+
const el = elements.get(id);
|
|
234
|
+
const exitAnim = el ? ((el as any).__ripple_exit || animateExit) : null;
|
|
235
|
+
if (el && exitAnim) {
|
|
236
|
+
try {
|
|
237
|
+
await exitAnim(el);
|
|
238
|
+
} catch (e) {
|
|
239
|
+
console.error('Error during exit animation:', e);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
read();
|
|
243
|
+
state.value = state.value.filter(item => item.id !== id);
|
|
244
|
+
flip();
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
}
|