@idealyst/animate 1.2.80 → 1.2.81
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/package.json +3 -3
- package/src/examples/AnimateExamples.tsx +106 -0
- package/src/useAnimatedStyle.ts +16 -3
- package/src/useAnimatedValue.ts +18 -0
- package/src/usePresence.ts +18 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@idealyst/animate",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.81",
|
|
4
4
|
"description": "Cross-platform animation hooks for React and React Native",
|
|
5
5
|
"readme": "README.md",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"publish:npm": "npm publish"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
|
-
"@idealyst/theme": "^1.2.
|
|
33
|
+
"@idealyst/theme": "^1.2.81",
|
|
34
34
|
"react": ">=16.8.0",
|
|
35
35
|
"react-native": ">=0.60.0",
|
|
36
36
|
"react-native-reanimated": ">=3.0.0",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
}
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
|
-
"@idealyst/theme": "^1.2.
|
|
51
|
+
"@idealyst/theme": "^1.2.81",
|
|
52
52
|
"@types/react": "^19.1.0",
|
|
53
53
|
"react": "^19.1.0",
|
|
54
54
|
"react-native": "^0.80.1",
|
|
@@ -268,6 +268,111 @@ const PresenceDemo: React.FC = () => {
|
|
|
268
268
|
);
|
|
269
269
|
};
|
|
270
270
|
|
|
271
|
+
// =============================================================================
|
|
272
|
+
// Spring Animation Demo (Web CSS Approximation)
|
|
273
|
+
// =============================================================================
|
|
274
|
+
|
|
275
|
+
const SpringAnimationDemo: React.FC = () => {
|
|
276
|
+
const [isPressed, setIsPressed] = useState(false);
|
|
277
|
+
const [springType, setSpringType] = useState<'spring' | 'springStiff' | 'springBouncy'>('spring');
|
|
278
|
+
|
|
279
|
+
// Using spring easing - on web, this automatically uses CSS approximation
|
|
280
|
+
// On native, it uses actual Reanimated spring physics
|
|
281
|
+
const buttonStyle = useAnimatedStyle(
|
|
282
|
+
{
|
|
283
|
+
transform: { scale: isPressed ? 0.95 : 1 },
|
|
284
|
+
},
|
|
285
|
+
{ easing: springType }
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
const cardStyle = useAnimatedStyle(
|
|
289
|
+
{
|
|
290
|
+
transform: { y: isPressed ? 4 : 0 },
|
|
291
|
+
opacity: isPressed ? 0.9 : 1,
|
|
292
|
+
},
|
|
293
|
+
{ easing: springType }
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
return (
|
|
297
|
+
<Card padding="md" gap="md">
|
|
298
|
+
<Text typography="h4" weight="semibold">Spring Animations</Text>
|
|
299
|
+
<Text color="secondary">
|
|
300
|
+
Spring easings are automatically converted to CSS approximations on web,
|
|
301
|
+
providing GPU-accelerated animations that match native spring feel.
|
|
302
|
+
</Text>
|
|
303
|
+
|
|
304
|
+
<View direction="row" gap="sm" style={{ justifyContent: 'center' }}>
|
|
305
|
+
<Button
|
|
306
|
+
type={springType === 'spring' ? 'contained' : 'outlined'}
|
|
307
|
+
onPress={() => setSpringType('spring')}
|
|
308
|
+
size="sm"
|
|
309
|
+
>
|
|
310
|
+
Spring
|
|
311
|
+
</Button>
|
|
312
|
+
<Button
|
|
313
|
+
type={springType === 'springStiff' ? 'contained' : 'outlined'}
|
|
314
|
+
onPress={() => setSpringType('springStiff')}
|
|
315
|
+
size="sm"
|
|
316
|
+
>
|
|
317
|
+
Stiff
|
|
318
|
+
</Button>
|
|
319
|
+
<Button
|
|
320
|
+
type={springType === 'springBouncy' ? 'contained' : 'outlined'}
|
|
321
|
+
onPress={() => setSpringType('springBouncy')}
|
|
322
|
+
size="sm"
|
|
323
|
+
>
|
|
324
|
+
Bouncy
|
|
325
|
+
</Button>
|
|
326
|
+
</View>
|
|
327
|
+
|
|
328
|
+
<View style={{ alignItems: 'center', padding: 20 }}>
|
|
329
|
+
<View
|
|
330
|
+
style={{
|
|
331
|
+
width: 160,
|
|
332
|
+
height: 100,
|
|
333
|
+
backgroundColor: '#6366f1',
|
|
334
|
+
borderRadius: 12,
|
|
335
|
+
justifyContent: 'center',
|
|
336
|
+
alignItems: 'center',
|
|
337
|
+
cursor: 'pointer',
|
|
338
|
+
...buttonStyle,
|
|
339
|
+
...cardStyle,
|
|
340
|
+
}}
|
|
341
|
+
// @ts-ignore - web events
|
|
342
|
+
onMouseDown={() => setIsPressed(true)}
|
|
343
|
+
onMouseUp={() => setIsPressed(false)}
|
|
344
|
+
onMouseLeave={() => setIsPressed(false)}
|
|
345
|
+
>
|
|
346
|
+
<Text style={{ color: '#fff' }} weight="semibold">Press Me</Text>
|
|
347
|
+
<Text style={{ color: 'rgba(255,255,255,0.8)' }} typography="caption">
|
|
348
|
+
{springType}
|
|
349
|
+
</Text>
|
|
350
|
+
</View>
|
|
351
|
+
</View>
|
|
352
|
+
|
|
353
|
+
<View background="secondary" padding="sm" radius="sm">
|
|
354
|
+
<Text typography="caption" style={{ fontFamily: 'monospace' }}>
|
|
355
|
+
{`// Spring easings work cross-platform!
|
|
356
|
+
const style = useAnimatedStyle({
|
|
357
|
+
transform: { scale: isPressed ? 0.95 : 1 },
|
|
358
|
+
}, { easing: '${springType}' });
|
|
359
|
+
|
|
360
|
+
// Web: CSS cubic-bezier approximation
|
|
361
|
+
// Native: Reanimated withSpring`}
|
|
362
|
+
</Text>
|
|
363
|
+
</View>
|
|
364
|
+
|
|
365
|
+
<View padding="sm" background="tertiary" radius="sm">
|
|
366
|
+
<Text typography="caption" color="secondary">
|
|
367
|
+
<Text weight="semibold">Web Performance: </Text>
|
|
368
|
+
Spring animations use CSS transitions with calculated bezier curves,
|
|
369
|
+
running entirely on the GPU compositor thread. No JavaScript RAF loops.
|
|
370
|
+
</Text>
|
|
371
|
+
</View>
|
|
372
|
+
</Card>
|
|
373
|
+
);
|
|
374
|
+
};
|
|
375
|
+
|
|
271
376
|
// =============================================================================
|
|
272
377
|
// useGradientBorder Demo
|
|
273
378
|
// =============================================================================
|
|
@@ -391,6 +496,7 @@ export const AnimateExamples: React.FC = () => {
|
|
|
391
496
|
<Divider />
|
|
392
497
|
|
|
393
498
|
<AnimatedStyleDemo />
|
|
499
|
+
<SpringAnimationDemo />
|
|
394
500
|
<AnimatedValueDemo />
|
|
395
501
|
<PresenceDemo />
|
|
396
502
|
<GradientBorderDemo />
|
package/src/useAnimatedStyle.ts
CHANGED
|
@@ -3,10 +3,16 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Creates an animated style that transitions when properties change.
|
|
5
5
|
* Uses CSS transitions for smooth, performant animations.
|
|
6
|
+
*
|
|
7
|
+
* For spring easings, automatically converts to CSS approximations
|
|
8
|
+
* with calculated durations that match the spring's settling time.
|
|
6
9
|
*/
|
|
7
10
|
|
|
8
11
|
import { useMemo } from 'react';
|
|
9
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
resolveDuration,
|
|
14
|
+
resolveEasingWithDuration,
|
|
15
|
+
} from '@idealyst/theme/animation';
|
|
10
16
|
import type {
|
|
11
17
|
AnimatableProperties,
|
|
12
18
|
UseAnimatedStyleOptions,
|
|
@@ -103,8 +109,15 @@ export function useAnimatedStyle(
|
|
|
103
109
|
return web.transition;
|
|
104
110
|
}
|
|
105
111
|
|
|
106
|
-
const
|
|
107
|
-
|
|
112
|
+
const baseDuration = resolveDuration(finalDuration);
|
|
113
|
+
|
|
114
|
+
// For spring easings, resolveEasingWithDuration calculates the optimal
|
|
115
|
+
// duration based on the spring's settling time, giving better approximation
|
|
116
|
+
const { css: easingCss, duration: durationMs } = resolveEasingWithDuration(
|
|
117
|
+
finalEasing,
|
|
118
|
+
baseDuration
|
|
119
|
+
);
|
|
120
|
+
|
|
108
121
|
const delayStr = finalDelay > 0 ? ` ${finalDelay}ms` : '';
|
|
109
122
|
|
|
110
123
|
// Use 'all' to animate any property that changes
|
package/src/useAnimatedValue.ts
CHANGED
|
@@ -3,12 +3,28 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Creates an animated numeric value that can be interpolated.
|
|
5
5
|
* Uses requestAnimationFrame for smooth animations on web.
|
|
6
|
+
*
|
|
7
|
+
* NOTE: This hook uses RAF which is less performant than CSS transitions.
|
|
8
|
+
* Consider using useAnimatedStyle for style-based animations instead.
|
|
6
9
|
*/
|
|
7
10
|
|
|
8
11
|
import { useState, useRef, useCallback, useMemo } from 'react';
|
|
9
12
|
import { resolveDuration, resolveEasing } from '@idealyst/theme/animation';
|
|
10
13
|
import type { AnimatedValue, AnimationOptions, InterpolationConfig } from './types';
|
|
11
14
|
|
|
15
|
+
// Warn about RAF usage in development (once per session)
|
|
16
|
+
let hasWarnedAboutRAF = false;
|
|
17
|
+
function warnRAFUsage(hookName: string) {
|
|
18
|
+
if (process.env.NODE_ENV === 'development' && !hasWarnedAboutRAF) {
|
|
19
|
+
hasWarnedAboutRAF = true;
|
|
20
|
+
console.warn(
|
|
21
|
+
`[@idealyst/animate] ${hookName} uses requestAnimationFrame for animations, ` +
|
|
22
|
+
`which is less performant than CSS transitions. Consider using useAnimatedStyle ` +
|
|
23
|
+
`for style-based animations to leverage GPU-accelerated CSS transitions.`
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
12
28
|
// Bezier curve evaluation for custom easing
|
|
13
29
|
function bezierEval(t: number, p1: number, p2: number, p3: number, p4: number): number {
|
|
14
30
|
const cx = 3 * p1;
|
|
@@ -102,6 +118,8 @@ export function useAnimatedValue(initialValue: number): AnimatedValue {
|
|
|
102
118
|
// Set value with animation
|
|
103
119
|
const set = useCallback(
|
|
104
120
|
(target: number, options: AnimationOptions = {}) => {
|
|
121
|
+
warnRAFUsage('useAnimatedValue');
|
|
122
|
+
|
|
105
123
|
const { duration = 'normal', easing = 'easeOut', delay = 0 } = options;
|
|
106
124
|
const durationMs = resolveDuration(duration);
|
|
107
125
|
const easingCss = resolveEasing(easing);
|
package/src/usePresence.ts
CHANGED
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Manages mount/unmount animations by keeping elements in the DOM
|
|
5
5
|
* during exit animations.
|
|
6
|
+
*
|
|
7
|
+
* NOTE: This hook uses double RAF for DOM synchronization. The actual
|
|
8
|
+
* animation uses CSS transitions (performant), but the RAF is needed
|
|
9
|
+
* to ensure proper timing of state changes.
|
|
6
10
|
*/
|
|
7
11
|
|
|
8
12
|
import { useState, useEffect, useRef, useMemo, useCallback } from 'react';
|
|
@@ -16,6 +20,19 @@ import type {
|
|
|
16
20
|
} from './types';
|
|
17
21
|
import { isTransformObject, normalizeTransform } from './normalizeTransform';
|
|
18
22
|
|
|
23
|
+
// Warn about RAF usage in development (once per session)
|
|
24
|
+
let hasWarnedAboutPresenceRAF = false;
|
|
25
|
+
function warnPresenceRAF() {
|
|
26
|
+
if (process.env.NODE_ENV === 'development' && !hasWarnedAboutPresenceRAF) {
|
|
27
|
+
hasWarnedAboutPresenceRAF = true;
|
|
28
|
+
console.warn(
|
|
29
|
+
`[@idealyst/animate] usePresence uses requestAnimationFrame for DOM synchronization. ` +
|
|
30
|
+
`The animation itself uses CSS transitions, but RAF is required to coordinate ` +
|
|
31
|
+
`mount/unmount timing with the browser's rendering cycle.`
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
19
36
|
/**
|
|
20
37
|
* Hook that manages presence animations for mount/unmount.
|
|
21
38
|
* The element stays rendered during exit animation.
|
|
@@ -61,6 +78,7 @@ export function usePresence(isVisible: boolean, options: UsePresenceOptions): Us
|
|
|
61
78
|
// Entering: mount immediately, then animate in
|
|
62
79
|
setIsPresent(true);
|
|
63
80
|
// Use double RAF to ensure DOM is ready before animating
|
|
81
|
+
warnPresenceRAF();
|
|
64
82
|
requestAnimationFrame(() => {
|
|
65
83
|
requestAnimationFrame(() => {
|
|
66
84
|
setIsEntering(true);
|