@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.
@@ -0,0 +1,286 @@
1
+ import { track, tick, effect, Context, type Children } from 'ripple';
2
+ import { type MotionTiming } from './types.js';
3
+ import { MotionTimingContext } from './motion.js';
4
+
5
+ export function MotionProvider(props: {
6
+ children: Children;
7
+ type?: 'tween' | 'spring';
8
+ duration?: number;
9
+ stiffness?: number;
10
+ damping?: number;
11
+ mass?: number;
12
+ easing?: string;
13
+ }) @{
14
+ let parent: MotionTiming | null = null;
15
+ try {
16
+ parent = MotionTimingContext.get();
17
+ } catch {}
18
+ const merged: MotionTiming = {
19
+ delay: 0,
20
+ duration: props.duration ?? parent?.duration ?? 300,
21
+ type: props.type ?? parent?.type ?? 'tween',
22
+ stiffness: props.stiffness ?? parent?.stiffness ?? 300,
23
+ damping: props.damping ?? parent?.damping ?? 30,
24
+ mass: props.mass ?? parent?.mass ?? 1,
25
+ easing: props.easing ?? parent?.easing,
26
+ };
27
+ try {
28
+ MotionTimingContext.set(merged);
29
+ } catch {}
30
+
31
+ <div style="display:contents">{props.children}</div>
32
+ }
33
+
34
+ function getTargets(wrap: HTMLElement): HTMLElement[] {
35
+ const targets: HTMLElement[] = [];
36
+ const elements = wrap.querySelectorAll('*');
37
+ for (let i = 0; i < elements.length; i++) {
38
+ const el = elements[i] as HTMLElement;
39
+ if ((el as any).__ripple_exit) {
40
+ targets.push(el);
41
+ }
42
+ }
43
+ if (targets.length === 0) {
44
+ let child = wrap.firstElementChild;
45
+ while (child) {
46
+ targets.push(child as HTMLElement);
47
+ child = child.nextElementSibling;
48
+ }
49
+ }
50
+ if (targets.length === 0) {
51
+ targets.push(wrap);
52
+ }
53
+ return targets;
54
+ }
55
+
56
+ export function Transition(props: {
57
+ show: boolean;
58
+ children: Children;
59
+ onEnter?: (el: HTMLElement) => void;
60
+ onLeave?: (el: HTMLElement) => Promise<any> | void;
61
+ mode?: 'sync' | 'popLayout';
62
+ }) @{
63
+ const mode = props.mode ?? 'sync';
64
+ let &[renderChild] = track(props.show);
65
+ let wrap: HTMLDivElement | undefined;
66
+ let activeExitCleanup: (() => void) | null = null;
67
+
68
+ effect(() => {
69
+ if (props.show) {
70
+ renderChild = true;
71
+ if (activeExitCleanup) {
72
+ activeExitCleanup();
73
+ activeExitCleanup = null;
74
+ }
75
+ tick().then(() => {
76
+ if (wrap) {
77
+ const targets = getTargets(wrap);
78
+ for (const target of targets) {
79
+ if (props.onEnter) {
80
+ props.onEnter(target);
81
+ } else if ((target as any).__ripple_enter) {
82
+ if ((target as any).__ripple_mounted) {
83
+ delete (target as any).__ripple_mounted;
84
+ } else {
85
+ (target as any).__ripple_enter(target);
86
+ }
87
+ }
88
+ }
89
+ }
90
+ });
91
+ } else {
92
+ if (wrap) {
93
+ const targets = getTargets(wrap);
94
+ const exitPromises = targets.map(target => {
95
+ const exitFn = props.onLeave || (target as any).__ripple_exit;
96
+ if (exitFn) {
97
+ return Promise.resolve(exitFn(target)).catch(() => {});
98
+ }
99
+ return Promise.resolve();
100
+ });
101
+
102
+ if (mode === 'popLayout' && wrap.parentElement) {
103
+ const parent = wrap.parentElement;
104
+ const parentBefore = parent.offsetHeight;
105
+ const parentRect = parent.getBoundingClientRect();
106
+ const oldPosition = parent.style.position;
107
+ const oldOverflow = parent.style.overflow;
108
+ const oldHeight = parent.style.height;
109
+
110
+ if (!oldPosition || oldPosition === 'static') {
111
+ parent.style.position = 'relative';
112
+ }
113
+ parent.style.overflow = 'hidden';
114
+
115
+ const restores: Array<() => void> = [];
116
+ for (const target of targets) {
117
+ const rect = target.getBoundingClientRect();
118
+ const left = rect.left - parentRect.left;
119
+ const top = rect.top - parentRect.top;
120
+
121
+ const origPosition = target.style.position;
122
+ const origLeft = target.style.left;
123
+ const origTop = target.style.top;
124
+ const origWidth = target.style.width;
125
+ const origHeight = target.style.height;
126
+ const origMargin = target.style.margin;
127
+ const origPointerEvents = target.style.pointerEvents;
128
+
129
+ target.style.position = 'absolute';
130
+ target.style.left = `${left}px`;
131
+ target.style.top = `${top}px`;
132
+ target.style.width = `${rect.width}px`;
133
+ target.style.height = `${rect.height}px`;
134
+ target.style.margin = '0';
135
+ target.style.pointerEvents = 'none';
136
+
137
+ restores.push(() => {
138
+ target.style.position = origPosition;
139
+ target.style.left = origLeft;
140
+ target.style.top = origTop;
141
+ target.style.width = origWidth;
142
+ target.style.height = origHeight;
143
+ target.style.margin = origMargin;
144
+ target.style.pointerEvents = origPointerEvents;
145
+ });
146
+ }
147
+
148
+ void parent.offsetHeight;
149
+ const parentAfter = parent.offsetHeight;
150
+ parent.style.height = `${parentBefore}px`;
151
+
152
+ const parentAnim = parent.animate([
153
+ { height: `${parentBefore}px` },
154
+ { height: `${parentAfter}px` }
155
+ ], { duration: 200, easing: 'ease-in-out', fill: 'both' });
156
+ const cleanup = () => {
157
+ parentAnim.cancel();
158
+ parent.style.position = oldPosition;
159
+ parent.style.overflow = oldOverflow;
160
+ parent.style.height = oldHeight;
161
+ for (const restore of restores) restore();
162
+ };
163
+ activeExitCleanup = cleanup;
164
+
165
+ Promise.all([
166
+ Promise.all(exitPromises),
167
+ parentAnim.finished.catch(() => {})
168
+ ]).then(() => {
169
+ if (activeExitCleanup === cleanup) {
170
+ cleanup();
171
+ activeExitCleanup = null;
172
+ if (!props.show) renderChild = false;
173
+ }
174
+ });
175
+ } else {
176
+ const cleanup = () => {};
177
+ activeExitCleanup = cleanup;
178
+ Promise.all(exitPromises).then(() => {
179
+ if (activeExitCleanup === cleanup) {
180
+ activeExitCleanup = null;
181
+ if (!props.show) renderChild = false;
182
+ }
183
+ });
184
+ }
185
+ } else {
186
+ renderChild = false;
187
+ }
188
+ }
189
+ });
190
+
191
+ @if (renderChild) {
192
+ <div ref={wrap}>
193
+ {props.children}
194
+ </div>
195
+ }
196
+ }
197
+
198
+ interface ExitingItem {
199
+ id: number;
200
+ children: Children;
201
+ style: string;
202
+ }
203
+
204
+ export function Presence(props: {
205
+ children: Children;
206
+ mode?: 'sync' | 'wait';
207
+ }) @{
208
+ const mode = props.mode ?? 'sync';
209
+ let &[live] = track<Children | undefined>(undefined);
210
+ let &[exitingItems] = track<ExitingItem[]>([]);
211
+ let prevChildren: Children | undefined;
212
+ let liveRef: HTMLDivElement | undefined;
213
+ let nextExitId = 1;
214
+ const exitingRefs = new Map<number, HTMLDivElement>();
215
+
216
+ effect(() => {
217
+ const newChildren = props.children;
218
+
219
+ if (prevChildren !== undefined && newChildren !== prevChildren) {
220
+ if (liveRef && liveRef.parentElement) {
221
+ const parent = liveRef.parentElement;
222
+ const parentRect = parent.getBoundingClientRect();
223
+ const childEl = liveRef.firstElementChild as HTMLElement || liveRef;
224
+ const childRect = childEl.getBoundingClientRect();
225
+ const left = childRect.left - parentRect.left;
226
+ const top = childRect.top - parentRect.top;
227
+
228
+ const itemId = nextExitId++;
229
+ const newItem: ExitingItem = {
230
+ id: itemId,
231
+ children: prevChildren,
232
+ style: `position:absolute;left:${left}px;top:${top}px;width:${childRect.width}px;height:${childRect.height}px;pointer-events:none`
233
+ };
234
+
235
+ exitingItems = [...exitingItems, newItem];
236
+
237
+ tick().then(() => {
238
+ const wrapper = exitingRefs.get(itemId);
239
+ if (wrapper) {
240
+ const target = wrapper.firstElementChild as HTMLElement || wrapper;
241
+ target.getAnimations().forEach(a => a.cancel());
242
+ const exitFn = (target as any).__ripple_exit;
243
+ if (exitFn) {
244
+ Promise.resolve(exitFn(target)).catch(() => {}).then(() => {
245
+ exitingItems = exitingItems.filter(x => x.id !== itemId);
246
+ exitingRefs.delete(itemId);
247
+ if (mode === 'wait' && exitingItems.length === 0) {
248
+ live = props.children;
249
+ }
250
+ });
251
+ } else {
252
+ exitingItems = exitingItems.filter(x => x.id !== itemId);
253
+ exitingRefs.delete(itemId);
254
+ if (mode === 'wait' && exitingItems.length === 0) {
255
+ live = props.children;
256
+ }
257
+ }
258
+ }
259
+ });
260
+ }
261
+
262
+ if (mode === 'wait') {
263
+ live = undefined;
264
+ } else {
265
+ live = newChildren;
266
+ }
267
+ } else if (prevChildren === undefined) {
268
+ live = newChildren;
269
+ }
270
+
271
+ prevChildren = newChildren;
272
+ });
273
+
274
+ const hasExiting = exitingItems.length > 0;
275
+
276
+ <div style={hasExiting && live ? 'position:relative' : ''}>
277
+ <div ref={liveRef} style="display:contents">
278
+ {live}
279
+ </div>
280
+ @for (const item of exitingItems; key item.id) {
281
+ <div ref={(el) => { if (el) exitingRefs.set(item.id, el as HTMLDivElement); }} style={item.style}>
282
+ {item.children}
283
+ </div>
284
+ }
285
+ </div>
286
+ }
package/src/slide.ts ADDED
@@ -0,0 +1,220 @@
1
+ import { type TransitionOptions } from './types.js';
2
+ import { MotionTimingContext } from './motion.js';
3
+
4
+ function withTiming(options: TransitionOptions = {}): TransitionOptions {
5
+ try {
6
+ const ctx = MotionTimingContext.get();
7
+ if (!ctx) return options;
8
+ return {
9
+ delay: options.delay ?? 0,
10
+ duration: options.duration ?? ctx.duration,
11
+ type: options.type ?? ctx.type,
12
+ stiffness: options.stiffness ?? ctx.stiffness,
13
+ damping: options.damping ?? ctx.damping,
14
+ mass: options.mass ?? ctx.mass,
15
+ easing: options.easing ?? ctx.easing,
16
+ };
17
+ } catch {
18
+ return options;
19
+ }
20
+ }
21
+
22
+ function getNaturalSize(el: HTMLElement, isY: boolean): { size: number; paddingStart: string; paddingEnd: string } {
23
+ const originalStyle = el.style.cssText;
24
+ const prop = isY ? 'height' : 'width';
25
+ const padStart = isY ? 'paddingTop' : 'paddingLeft';
26
+ const padEnd = isY ? 'paddingBottom' : 'paddingRight';
27
+
28
+ // Temporarily clear styles to measure natural size
29
+ el.style[prop as any] = '';
30
+ el.style.overflow = '';
31
+ el.style[padStart as any] = '';
32
+ el.style[padEnd as any] = '';
33
+
34
+ const size = isY ? el.offsetHeight : el.offsetWidth;
35
+ const computed = getComputedStyle(el);
36
+ const paddingStartVal = computed[padStart as any];
37
+ const paddingEndVal = computed[padEnd as any];
38
+
39
+ el.style.cssText = originalStyle;
40
+
41
+ return { size, paddingStart: paddingStartVal, paddingEnd: paddingEndVal };
42
+ }
43
+
44
+ function getCurrentSlideState(el: HTMLElement, isY: boolean, withFade: boolean) {
45
+ const cs = getComputedStyle(el);
46
+ const prop = isY ? 'height' : 'width';
47
+ const padStart = isY ? 'paddingTop' : 'paddingLeft';
48
+ const padEnd = isY ? 'paddingBottom' : 'paddingRight';
49
+
50
+ const state: Record<string, any> = {
51
+ [prop]: cs[prop as any],
52
+ [padStart]: cs[padStart as any],
53
+ [padEnd]: cs[padEnd as any],
54
+ overflow: 'hidden'
55
+ };
56
+ if (withFade) {
57
+ state.opacity = parseFloat(cs.opacity || '1');
58
+ }
59
+ return state;
60
+ }
61
+
62
+ function applyStyles(target: HTMLElement, styles: Record<string, any>) {
63
+ for (const [key, val] of Object.entries(styles)) {
64
+ if (val === undefined || val === null) continue;
65
+ if (key === 'transform' || key === 'opacity' || key === 'overflow') {
66
+ (target.style as any)[key] = String(val);
67
+ } else {
68
+ (target.style as any)[key] = typeof val === 'number' ? `${val}px` : String(val);
69
+ }
70
+ }
71
+ }
72
+ export function slide(options: TransitionOptions & { fade?: boolean; axis?: 'x' | 'y' } = {}) {
73
+ const { fade: withFade = true, axis = 'y', ...transitionOpts } = options;
74
+ const isY = axis === 'y';
75
+ const prop = isY ? 'height' : 'width';
76
+ const padStart = isY ? 'paddingTop' : 'paddingLeft';
77
+ const padEnd = isY ? 'paddingBottom' : 'paddingRight';
78
+
79
+ return (el: HTMLElement) => {
80
+ if (!el) return;
81
+
82
+ const transition = withTiming(transitionOpts);
83
+ const { delay = 0, duration = 300, easing = 'ease-in-out' } = transition;
84
+
85
+ const enterFn = (target: HTMLElement) => {
86
+ const prevExit = (target as any).__ripple_exit_anim;
87
+ let startState: Record<string, any>;
88
+ let adjDuration = duration;
89
+
90
+ if (prevExit && prevExit.playState !== 'finished') {
91
+ // Interrupted exit: read current state and calculate progress
92
+ const progress = prevExit.currentTime ? Math.max(0, Math.min(1, Number(prevExit.currentTime) / duration)) : 0.5;
93
+ adjDuration = Math.max(50, duration * progress);
94
+
95
+ startState = getCurrentSlideState(target, isY, withFade);
96
+ prevExit.cancel();
97
+ delete (target as any).__ripple_exit_anim;
98
+ } else {
99
+ // Normal enter starts at 0
100
+ startState = {
101
+ [prop]: '0px',
102
+ [padStart]: '0px',
103
+ [padEnd]: '0px',
104
+ overflow: 'hidden'
105
+ };
106
+ if (withFade) {
107
+ startState.opacity = 0;
108
+ }
109
+ }
110
+
111
+ const { size, paddingStart, paddingEnd } = getNaturalSize(target, isY);
112
+ const endState: Record<string, any> = {
113
+ [prop]: `${size}px`,
114
+ [padStart]: paddingStart,
115
+ [padEnd]: paddingEnd,
116
+ overflow: 'hidden'
117
+ };
118
+ if (withFade) {
119
+ endState.opacity = 1;
120
+ }
121
+
122
+ const prevAnim = (target as any).__ripple_anim;
123
+ if (prevAnim && prevAnim.playState !== 'finished') {
124
+ prevAnim.cancel();
125
+ }
126
+
127
+ applyStyles(target, startState);
128
+ const mountAnim = target.animate([startState, endState], { delay, duration: adjDuration, easing, fill: 'both' });
129
+ (target as any).__ripple_anim = mountAnim;
130
+
131
+ mountAnim.finished.then(() => {
132
+ if ((target as any).__ripple_anim === mountAnim) {
133
+ try { mountAnim.commitStyles(); mountAnim.cancel(); } catch {}
134
+ target.style[prop as any] = '';
135
+ target.style.overflow = '';
136
+ target.style[padStart as any] = '';
137
+ target.style[padEnd as any] = '';
138
+ if (withFade) target.style.opacity = '';
139
+ delete (target as any).__ripple_anim;
140
+ }
141
+ }).catch(() => {});
142
+
143
+ return mountAnim.finished;
144
+ };
145
+
146
+ const exitFn = (target: HTMLElement) => {
147
+ const prevAnim = (target as any).__ripple_anim;
148
+ let startState: Record<string, any>;
149
+ let adjDuration = duration;
150
+
151
+ if (prevAnim && prevAnim.playState !== 'finished') {
152
+ // Interrupted enter: read current state and calculate progress
153
+ const progress = prevAnim.currentTime ? Math.max(0, Math.min(1, Number(prevAnim.currentTime) / duration)) : 0.5;
154
+ adjDuration = Math.max(50, duration * progress);
155
+
156
+ startState = getCurrentSlideState(target, isY, withFade);
157
+ prevAnim.cancel();
158
+ delete (target as any).__ripple_anim;
159
+ } else {
160
+ // Normal exit starts at current natural size
161
+ const { size, paddingStart, paddingEnd } = getNaturalSize(target, isY);
162
+ startState = {
163
+ [prop]: `${size}px`,
164
+ [padStart]: paddingStart,
165
+ [padEnd]: paddingEnd,
166
+ overflow: 'hidden'
167
+ };
168
+ if (withFade) {
169
+ startState.opacity = 1;
170
+ }
171
+ }
172
+
173
+ const endState: Record<string, any> = {
174
+ [prop]: '0px',
175
+ [padStart]: '0px',
176
+ [padEnd]: '0px',
177
+ overflow: 'hidden'
178
+ };
179
+ if (withFade) {
180
+ endState.opacity = 0;
181
+ }
182
+
183
+ const prevExit = (target as any).__ripple_exit_anim;
184
+ if (prevExit && prevExit.playState !== 'finished') {
185
+ prevExit.cancel();
186
+ }
187
+
188
+ applyStyles(target, startState);
189
+ const exitAnim = target.animate([startState, endState], { delay, duration: adjDuration, easing, fill: 'both' });
190
+ (target as any).__ripple_exit_anim = exitAnim;
191
+ exitAnim.finished.then(() => {
192
+ if ((target as any).__ripple_exit_anim === exitAnim) {
193
+ try { exitAnim.commitStyles(); exitAnim.cancel(); } catch {}
194
+ delete (target as any).__ripple_exit_anim;
195
+ }
196
+ }).catch(() => {});
197
+ return exitAnim.finished;
198
+ };
199
+
200
+ // Register handlers
201
+ (el as any).__ripple_enter = enterFn;
202
+ (el as any).__ripple_exit = exitFn;
203
+
204
+ // Run enter immediately
205
+ (el as any).__ripple_mounted = true;
206
+ enterFn(el);
207
+
208
+ return () => {
209
+ const a = (el as any).__ripple_anim;
210
+ if (a && a.playState !== 'finished') a.cancel();
211
+ const ea = (el as any).__ripple_exit_anim;
212
+ if (ea && ea.playState !== 'finished') ea.cancel();
213
+
214
+ delete (el as any).__ripple_enter;
215
+ delete (el as any).__ripple_exit;
216
+ delete (el as any).__ripple_anim;
217
+ delete (el as any).__ripple_exit_anim;
218
+ };
219
+ };
220
+ }
package/src/spring.ts ADDED
@@ -0,0 +1,165 @@
1
+ import { type TransitionOptions } from './types.js';
2
+
3
+ export function springValues(
4
+ from: number,
5
+ to: number,
6
+ options: { stiffness?: number; damping?: number; mass?: number; frames?: number } = {}
7
+ ): number[] {
8
+ const { stiffness = 300, damping = 30, mass = 1, frames = 60 } = options;
9
+ const omega0 = Math.sqrt(stiffness / mass);
10
+ const zeta = damping / (2 * Math.sqrt(mass * stiffness));
11
+ const omega1 = omega0 * Math.sqrt(Math.max(0, 1 - zeta * zeta));
12
+ const duration = Math.min(2, zeta > 0 ? 3 / (zeta * omega0) : 2);
13
+ const values: number[] = [];
14
+ for (let i = 0; i <= frames; i++) {
15
+ const t = (i / frames) * duration;
16
+ const decay = Math.exp(-zeta * omega0 * t);
17
+ let norm: number;
18
+ if (zeta < 1) {
19
+ norm = 1 - decay * Math.cos(omega1 * t) - (zeta / Math.sqrt(1 - zeta * zeta)) * decay * Math.sin(omega1 * t);
20
+ } else {
21
+ norm = 1 - decay * (1 + omega0 * t);
22
+ }
23
+ values.push(from + (to - from) * norm);
24
+ }
25
+ return values;
26
+ }
27
+
28
+ interface TransformComponents {
29
+ translateX: number;
30
+ translateY: number;
31
+ scaleX: number;
32
+ scaleY: number;
33
+ rotate: number;
34
+ }
35
+
36
+ function parseTransform(val: string): TransformComponents {
37
+ const comps: TransformComponents = {
38
+ translateX: 0,
39
+ translateY: 0,
40
+ scaleX: 1,
41
+ scaleY: 1,
42
+ rotate: 0
43
+ };
44
+
45
+ if (!val || val === 'none' || val === 'undefined') {
46
+ return comps;
47
+ }
48
+
49
+ if (val.startsWith('matrix')) {
50
+ const nums = val.match(/-?\d*\.?\d+/g)?.map(Number) || [];
51
+ if (nums.length === 6) {
52
+ const [a, b, c, d, tx, ty] = nums;
53
+ comps.translateX = tx;
54
+ comps.translateY = ty;
55
+ comps.scaleX = Math.sqrt(a * a + b * b);
56
+ comps.scaleY = Math.sqrt(c * c + d * d);
57
+ comps.rotate = Math.atan2(b, a) * (180 / Math.PI);
58
+ } else if (nums.length === 16) {
59
+ comps.scaleX = Math.sqrt(nums[0]*nums[0] + nums[1]*nums[1] + nums[2]*nums[2]);
60
+ comps.scaleY = Math.sqrt(nums[4]*nums[4] + nums[5]*nums[5] + nums[6]*nums[6]);
61
+ comps.translateX = nums[12];
62
+ comps.translateY = nums[13];
63
+ comps.rotate = Math.atan2(nums[1], nums[0]) * (180 / Math.PI);
64
+ }
65
+ return comps;
66
+ }
67
+
68
+ const translateMatch = val.match(/translate\(\s*(-?\d+\.?\d*)(px)?\s*,\s*(-?\d+\.?\d*)(px)?\s*\)/);
69
+ if (translateMatch) {
70
+ comps.translateX = parseFloat(translateMatch[1]);
71
+ comps.translateY = parseFloat(translateMatch[3]);
72
+ }
73
+ const translateXMatch = val.match(/translateX\(\s*(-?\d+\.?\d*)(px)?\s*\)/);
74
+ if (translateXMatch) comps.translateX = parseFloat(translateXMatch[1]);
75
+
76
+ const translateYMatch = val.match(/translateY\(\s*(-?\d+\.?\d*)(px)?\s*\)/);
77
+ if (translateYMatch) comps.translateY = parseFloat(translateYMatch[1]);
78
+
79
+ const scaleMatch = val.match(/scale\(\s*(-?\d+\.?\d*)\s*\)/);
80
+ if (scaleMatch) {
81
+ const s = parseFloat(scaleMatch[1]);
82
+ comps.scaleX = s;
83
+ comps.scaleY = s;
84
+ }
85
+ const scaleXYMatch = val.match(/scale\(\s*(-?\d+\.?\d*)\s*,\s*(-?\d+\.?\d*)\s*\)/);
86
+ if (scaleXYMatch) {
87
+ comps.scaleX = parseFloat(scaleXYMatch[1]);
88
+ comps.scaleY = parseFloat(scaleXYMatch[2]);
89
+ }
90
+
91
+ const rotateMatch = val.match(/rotate\(\s*(-?\d+\.?\d*)(deg|rad)?\s*\)/);
92
+ if (rotateMatch) {
93
+ let r = parseFloat(rotateMatch[1]);
94
+ if (rotateMatch[2] === 'rad') {
95
+ r = r * (180 / Math.PI);
96
+ }
97
+ comps.rotate = r;
98
+ }
99
+
100
+ return comps;
101
+ }
102
+ export function getSpringKeyframes(initial: any, animate: any, options: Required<TransitionOptions>): Keyframe[] {
103
+ const keys = Object.keys(animate);
104
+ const frames = 60;
105
+ const interpolations: Record<string, Array<{ values: number[] } | string>> = {};
106
+
107
+ for (const k of keys) {
108
+ let fromVal = String(initial?.[k] ?? animate[k]);
109
+ let toVal = String(animate[k]);
110
+
111
+ if (k === 'transform') {
112
+ const fromComps = parseTransform(fromVal);
113
+ const toComps = parseTransform(toVal);
114
+ fromVal = `translate(${fromComps.translateX}px, ${fromComps.translateY}px) scale(${fromComps.scaleX}, ${fromComps.scaleY}) rotate(${fromComps.rotate}deg)`;
115
+ toVal = `translate(${toComps.translateX}px, ${toComps.translateY}px) scale(${toComps.scaleX}, ${toComps.scaleY}) rotate(${toComps.rotate}deg)`;
116
+ }
117
+
118
+ const numRegex = /-?\d*\.?\d+/g;
119
+ const fromNums = fromVal.match(numRegex)?.map(Number) || [];
120
+ const toNums = toVal.match(numRegex)?.map(Number) || [];
121
+
122
+ if (fromNums.length > 0 && fromNums.length === toNums.length) {
123
+ const parts = toVal.split(numRegex);
124
+ const springData: Array<{ values: number[] } | string> = [];
125
+
126
+ for (let idx = 0; idx < fromNums.length; idx++) {
127
+ springData.push(parts[idx]);
128
+ let vals = springValues(fromNums[idx], toNums[idx], {
129
+ stiffness: options.stiffness,
130
+ damping: options.damping,
131
+ mass: options.mass,
132
+ frames
133
+ });
134
+ if (k === 'opacity') {
135
+ vals = vals.map(v => Math.max(0, Math.min(1, v)));
136
+ }
137
+ springData.push({ values: vals });
138
+ }
139
+ springData.push(parts[parts.length - 1]);
140
+ interpolations[k] = springData;
141
+ } else {
142
+ interpolations[k] = [toVal];
143
+ }
144
+ }
145
+
146
+ const result: Keyframe[] = [];
147
+ for (let i = 0; i <= frames; i++) {
148
+ const kf: Record<string, any> = {};
149
+ for (const k of keys) {
150
+ const data = interpolations[k];
151
+ let str = '';
152
+ for (const item of data) {
153
+ if (typeof item === 'string') {
154
+ str += item;
155
+ } else {
156
+ str += item.values[i];
157
+ }
158
+ }
159
+ kf[k] = str;
160
+ }
161
+ result.push(kf);
162
+ }
163
+ return result;
164
+ }
165
+