@tamagui/animations-react-native 2.0.0-rc.3 → 2.0.0-rc.30
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/cjs/createAnimations.cjs +85 -24
- package/dist/cjs/createAnimations.native.js +115 -32
- package/dist/cjs/createAnimations.native.js.map +1 -1
- package/dist/cjs/index.js +12 -9
- package/dist/esm/createAnimations.mjs +86 -25
- package/dist/esm/createAnimations.mjs.map +1 -1
- package/dist/esm/createAnimations.native.js +116 -33
- package/dist/esm/createAnimations.native.js.map +1 -1
- package/dist/esm/index.js +1 -2
- package/dist/esm/index.js.map +1 -6
- package/package.json +10 -13
- package/src/createAnimations.tsx +179 -25
- package/types/createAnimations.d.ts +4 -1
- package/types/createAnimations.d.ts.map +5 -10
- package/types/index.d.ts.map +2 -2
- package/types/polyfill.d.ts.map +2 -2
- package/dist/cjs/createAnimations.js +0 -297
- package/dist/cjs/createAnimations.js.map +0 -6
- package/dist/cjs/polyfill.js +0 -2
- package/dist/cjs/polyfill.js.map +0 -6
- package/dist/esm/createAnimations.js +0 -278
- package/dist/esm/createAnimations.js.map +0 -6
- package/dist/esm/polyfill.js +0 -2
- package/dist/esm/polyfill.js.map +0 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tamagui/animations-react-native",
|
|
3
|
-
"version": "2.0.0-rc.
|
|
3
|
+
"version": "2.0.0-rc.30",
|
|
4
4
|
"gitHead": "a49cc7ea6b93ba384e77a4880ae48ac4a5635c14",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"source": "src/index.ts",
|
|
@@ -20,15 +20,12 @@
|
|
|
20
20
|
"./package.json": "./package.json",
|
|
21
21
|
".": {
|
|
22
22
|
"types": "./types/index.d.ts",
|
|
23
|
-
"react-native":
|
|
24
|
-
|
|
25
|
-
"import": "./dist/esm/index.native.js",
|
|
26
|
-
"require": "./dist/cjs/index.native.js"
|
|
27
|
-
},
|
|
23
|
+
"react-native": "./dist/esm/index.native.js",
|
|
24
|
+
"browser": "./dist/esm/index.mjs",
|
|
28
25
|
"module": "./dist/esm/index.mjs",
|
|
29
26
|
"import": "./dist/esm/index.mjs",
|
|
30
27
|
"require": "./dist/cjs/index.cjs",
|
|
31
|
-
"default": "./dist/
|
|
28
|
+
"default": "./dist/esm/index.mjs"
|
|
32
29
|
}
|
|
33
30
|
},
|
|
34
31
|
"publishConfig": {
|
|
@@ -41,15 +38,15 @@
|
|
|
41
38
|
"clean:build": "tamagui-build clean:build"
|
|
42
39
|
},
|
|
43
40
|
"dependencies": {
|
|
44
|
-
"@tamagui/animation-helpers": "2.0.0-rc.
|
|
45
|
-
"@tamagui/constants": "2.0.0-rc.
|
|
46
|
-
"@tamagui/use-presence": "2.0.0-rc.
|
|
47
|
-
"@tamagui/web": "2.0.0-rc.
|
|
41
|
+
"@tamagui/animation-helpers": "2.0.0-rc.30",
|
|
42
|
+
"@tamagui/constants": "2.0.0-rc.30",
|
|
43
|
+
"@tamagui/use-presence": "2.0.0-rc.30",
|
|
44
|
+
"@tamagui/web": "2.0.0-rc.30"
|
|
48
45
|
},
|
|
49
46
|
"devDependencies": {
|
|
50
|
-
"@tamagui/build": "2.0.0-rc.
|
|
47
|
+
"@tamagui/build": "2.0.0-rc.30",
|
|
51
48
|
"react": ">=19",
|
|
52
|
-
"react-native": "0.
|
|
49
|
+
"react-native": "0.83.2"
|
|
53
50
|
},
|
|
54
51
|
"peerDependencies": {
|
|
55
52
|
"react": ">=19",
|
package/src/createAnimations.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getEffectiveAnimation, normalizeTransition } from '@tamagui/animation-helpers'
|
|
2
2
|
import { isWeb, useIsomorphicLayoutEffect } from '@tamagui/constants'
|
|
3
3
|
import { ResetPresence, usePresence } from '@tamagui/use-presence'
|
|
4
4
|
import type {
|
|
@@ -13,6 +13,10 @@ import { useEvent, useThemeWithState } from '@tamagui/web'
|
|
|
13
13
|
import React from 'react'
|
|
14
14
|
import { Animated, type Text, type View } from 'react-native'
|
|
15
15
|
|
|
16
|
+
// detect Fabric (New Architecture) — Paper doesn't support native driver for all style keys
|
|
17
|
+
const isFabric =
|
|
18
|
+
!isWeb && typeof global !== 'undefined' && !!global.__nativeFabricUIManager
|
|
19
|
+
|
|
16
20
|
// Helper to resolve dynamic theme values like {dynamic: {dark: "value", light: undefined}}
|
|
17
21
|
const resolveDynamicValue = (value: any, isDark: boolean): any => {
|
|
18
22
|
if (value && typeof value === 'object' && 'dynamic' in value) {
|
|
@@ -59,7 +63,7 @@ const colorStyleKey = {
|
|
|
59
63
|
borderBottomColor: true,
|
|
60
64
|
}
|
|
61
65
|
|
|
62
|
-
// these
|
|
66
|
+
// these style keys are costly to animate and only work with native driver on Fabric
|
|
63
67
|
const costlyToAnimateStyleKey = {
|
|
64
68
|
borderRadius: true,
|
|
65
69
|
borderTopLeftRadius: true,
|
|
@@ -72,7 +76,11 @@ const costlyToAnimateStyleKey = {
|
|
|
72
76
|
borderTopWidth: true,
|
|
73
77
|
borderBottomWidth: true,
|
|
74
78
|
...colorStyleKey,
|
|
75
|
-
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
type CreateAnimationsOptions = {
|
|
82
|
+
// override native driver detection (default: auto-detect Fabric)
|
|
83
|
+
useNativeDriver?: boolean
|
|
76
84
|
}
|
|
77
85
|
|
|
78
86
|
export const AnimatedView: Animated.AnimatedComponent<typeof View> = Animated.View
|
|
@@ -121,7 +129,7 @@ export function useAnimatedNumber(
|
|
|
121
129
|
const composite = Animated.spring(val, {
|
|
122
130
|
...config,
|
|
123
131
|
toValue: next,
|
|
124
|
-
useNativeDriver:
|
|
132
|
+
useNativeDriver: isFabric,
|
|
125
133
|
})
|
|
126
134
|
composite.start(handleFinish)
|
|
127
135
|
state.current.composite = composite
|
|
@@ -130,7 +138,7 @@ export function useAnimatedNumber(
|
|
|
130
138
|
const composite = Animated.timing(val, {
|
|
131
139
|
...config,
|
|
132
140
|
toValue: next,
|
|
133
|
-
useNativeDriver:
|
|
141
|
+
useNativeDriver: isFabric,
|
|
134
142
|
})
|
|
135
143
|
composite.start(handleFinish)
|
|
136
144
|
state.current.composite = composite
|
|
@@ -165,13 +173,18 @@ export const useAnimatedNumberStyle: UseAnimatedNumberStyle<RNAnimatedNum> = (
|
|
|
165
173
|
}
|
|
166
174
|
|
|
167
175
|
export function createAnimations<A extends AnimationsConfig>(
|
|
168
|
-
animations: A
|
|
176
|
+
animations: A,
|
|
177
|
+
options?: CreateAnimationsOptions
|
|
169
178
|
): AnimationDriver<A> {
|
|
179
|
+
const nativeDriver = options?.useNativeDriver ?? isFabric
|
|
180
|
+
|
|
170
181
|
return {
|
|
171
182
|
isReactNative: true,
|
|
172
183
|
inputStyle: 'value',
|
|
173
184
|
outputStyle: 'inline',
|
|
185
|
+
avoidReRenders: true,
|
|
174
186
|
animations,
|
|
187
|
+
needsCustomComponent: true,
|
|
175
188
|
View: AnimatedView,
|
|
176
189
|
Text: AnimatedText,
|
|
177
190
|
useAnimatedNumber,
|
|
@@ -179,7 +192,14 @@ export function createAnimations<A extends AnimationsConfig>(
|
|
|
179
192
|
useAnimatedNumberStyle,
|
|
180
193
|
usePresence,
|
|
181
194
|
ResetPresence,
|
|
182
|
-
useAnimations: ({
|
|
195
|
+
useAnimations: ({
|
|
196
|
+
props,
|
|
197
|
+
onDidAnimate,
|
|
198
|
+
style,
|
|
199
|
+
componentState,
|
|
200
|
+
presence,
|
|
201
|
+
useStyleEmitter,
|
|
202
|
+
}) => {
|
|
183
203
|
const isDisabled = isWeb && componentState.unmounted === true
|
|
184
204
|
const isExiting = presence?.[0] === false
|
|
185
205
|
const sendExitComplete = presence?.[1]
|
|
@@ -202,6 +222,25 @@ export function createAnimations<A extends AnimationsConfig>(
|
|
|
202
222
|
>()
|
|
203
223
|
)
|
|
204
224
|
|
|
225
|
+
// exit cycle guards to prevent stale/duplicate completion
|
|
226
|
+
const exitCycleIdRef = React.useRef(0)
|
|
227
|
+
const exitCompletedRef = React.useRef(false)
|
|
228
|
+
const wasExitingRef = React.useRef(false)
|
|
229
|
+
|
|
230
|
+
// detect transition into/out of exiting state
|
|
231
|
+
const justStartedExiting = isExiting && !wasExitingRef.current
|
|
232
|
+
const justStoppedExiting = !isExiting && wasExitingRef.current
|
|
233
|
+
|
|
234
|
+
// start new exit cycle only on transition INTO exiting
|
|
235
|
+
if (justStartedExiting) {
|
|
236
|
+
exitCycleIdRef.current++
|
|
237
|
+
exitCompletedRef.current = false
|
|
238
|
+
}
|
|
239
|
+
// invalidate pending callbacks when exit is canceled/interrupted
|
|
240
|
+
if (justStoppedExiting) {
|
|
241
|
+
exitCycleIdRef.current++
|
|
242
|
+
}
|
|
243
|
+
|
|
205
244
|
const animateOnly = (props.animateOnly as string[]) || []
|
|
206
245
|
const hasTransitionOnly = !!props.animateOnly
|
|
207
246
|
|
|
@@ -221,19 +260,9 @@ export function createAnimations<A extends AnimationsConfig>(
|
|
|
221
260
|
!!onDidAnimate,
|
|
222
261
|
isDark,
|
|
223
262
|
justFinishedEntering,
|
|
263
|
+
hasTransitionOnly,
|
|
224
264
|
]
|
|
225
265
|
|
|
226
|
-
// check if there is any style that is not supported by native driver
|
|
227
|
-
const isThereNoNativeStyleKeys = React.useMemo(() => {
|
|
228
|
-
if (isWeb) return true
|
|
229
|
-
return Object.keys(style).some((key) => {
|
|
230
|
-
if (animateOnly) {
|
|
231
|
-
return !animatedStyleKey[key] && animateOnly.indexOf(key) === -1
|
|
232
|
-
}
|
|
233
|
-
return !animatedStyleKey[key]
|
|
234
|
-
})
|
|
235
|
-
}, args)
|
|
236
|
-
|
|
237
266
|
const res = React.useMemo(() => {
|
|
238
267
|
const runners: Function[] = []
|
|
239
268
|
const completions: Promise<void>[] = []
|
|
@@ -292,6 +321,18 @@ export function createAnimations<A extends AnimationsConfig>(
|
|
|
292
321
|
}
|
|
293
322
|
}
|
|
294
323
|
|
|
324
|
+
const animatedTransformStyle =
|
|
325
|
+
animatedTranforms.current.length > 0
|
|
326
|
+
? {
|
|
327
|
+
transform: animatedTranforms.current.map((r) => {
|
|
328
|
+
const key = Object.keys(r)[0]
|
|
329
|
+
const val =
|
|
330
|
+
animationsState.current!.get(r[key])?.interpolation || r[key]
|
|
331
|
+
return { [key]: val }
|
|
332
|
+
}),
|
|
333
|
+
}
|
|
334
|
+
: {}
|
|
335
|
+
|
|
295
336
|
const animatedStyle = {
|
|
296
337
|
...Object.fromEntries(
|
|
297
338
|
Object.entries(animateStyles.current).map(([k, v]) => [
|
|
@@ -299,11 +340,7 @@ export function createAnimations<A extends AnimationsConfig>(
|
|
|
299
340
|
animationsState.current!.get(v)?.interpolation || v,
|
|
300
341
|
])
|
|
301
342
|
),
|
|
302
|
-
|
|
303
|
-
const key = Object.keys(r)[0]
|
|
304
|
-
const val = animationsState.current!.get(r[key])?.interpolation || r[key]
|
|
305
|
-
return { [key]: val }
|
|
306
|
-
}),
|
|
343
|
+
...animatedTransformStyle,
|
|
307
344
|
}
|
|
308
345
|
|
|
309
346
|
return {
|
|
@@ -371,7 +408,7 @@ export function createAnimations<A extends AnimationsConfig>(
|
|
|
371
408
|
function getAnimation() {
|
|
372
409
|
return Animated[animationConfig.type || 'spring'](value, {
|
|
373
410
|
toValue: animateToValue,
|
|
374
|
-
useNativeDriver:
|
|
411
|
+
useNativeDriver: nativeDriver,
|
|
375
412
|
...animationConfig,
|
|
376
413
|
})
|
|
377
414
|
}
|
|
@@ -384,7 +421,9 @@ export function createAnimations<A extends AnimationsConfig>(
|
|
|
384
421
|
: getAnimation()
|
|
385
422
|
|
|
386
423
|
animation.start(({ finished }) => {
|
|
387
|
-
|
|
424
|
+
// always resolve during exit (element is leaving anyway)
|
|
425
|
+
// for non-exit, only resolve on successful completion
|
|
426
|
+
if (finished || isExiting) {
|
|
388
427
|
resolve()
|
|
389
428
|
}
|
|
390
429
|
})
|
|
@@ -411,13 +450,37 @@ export function createAnimations<A extends AnimationsConfig>(
|
|
|
411
450
|
}
|
|
412
451
|
}, args)
|
|
413
452
|
|
|
453
|
+
// track previous exiting state
|
|
454
|
+
React.useEffect(() => {
|
|
455
|
+
wasExitingRef.current = isExiting
|
|
456
|
+
})
|
|
457
|
+
|
|
414
458
|
useIsomorphicLayoutEffect(() => {
|
|
415
459
|
res.runners.forEach((r) => r())
|
|
460
|
+
|
|
461
|
+
// capture current cycle id
|
|
462
|
+
const cycleId = exitCycleIdRef.current
|
|
463
|
+
|
|
464
|
+
// handle zero-completion case immediately
|
|
465
|
+
if (res.completions.length === 0) {
|
|
466
|
+
onDidAnimate?.()
|
|
467
|
+
if (isExiting && !exitCompletedRef.current) {
|
|
468
|
+
exitCompletedRef.current = true
|
|
469
|
+
sendExitComplete?.()
|
|
470
|
+
}
|
|
471
|
+
return
|
|
472
|
+
}
|
|
473
|
+
|
|
416
474
|
let cancel = false
|
|
417
475
|
Promise.all(res.completions).then(() => {
|
|
418
476
|
if (cancel) return
|
|
477
|
+
// guard against stale cycle completion
|
|
478
|
+
if (isExiting && cycleId !== exitCycleIdRef.current) return
|
|
479
|
+
if (isExiting && exitCompletedRef.current) return
|
|
480
|
+
|
|
419
481
|
onDidAnimate?.()
|
|
420
482
|
if (isExiting) {
|
|
483
|
+
exitCompletedRef.current = true
|
|
421
484
|
sendExitComplete?.()
|
|
422
485
|
}
|
|
423
486
|
})
|
|
@@ -426,6 +489,94 @@ export function createAnimations<A extends AnimationsConfig>(
|
|
|
426
489
|
}
|
|
427
490
|
}, args)
|
|
428
491
|
|
|
492
|
+
// avoidReRenders: receive style changes imperatively from tamagui
|
|
493
|
+
// and update Animated.Values directly without React re-renders
|
|
494
|
+
// reuses the same update() + runner pattern as the useMemo path
|
|
495
|
+
useStyleEmitter?.((nextStyle) => {
|
|
496
|
+
for (const key in nextStyle) {
|
|
497
|
+
const rawVal = nextStyle[key]
|
|
498
|
+
const val = resolveDynamicValue(rawVal, isDark)
|
|
499
|
+
if (val === undefined) continue
|
|
500
|
+
|
|
501
|
+
if (key === 'transform' && Array.isArray(val)) {
|
|
502
|
+
for (const [index, transform] of val.entries()) {
|
|
503
|
+
if (!transform) continue
|
|
504
|
+
const tkey = Object.keys(transform)[0]
|
|
505
|
+
const currentTransform = animatedTranforms.current[index]?.[tkey]
|
|
506
|
+
animatedTranforms.current[index] = {
|
|
507
|
+
[tkey]: update(tkey, currentTransform, transform[tkey]),
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
} else if (animatedStyleKey[key] != null || costlyToAnimateStyleKey[key]) {
|
|
511
|
+
animateStyles.current[key] = update(key, animateStyles.current[key], val)
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// run the queued animations immediately
|
|
516
|
+
res.runners.forEach((r) => r())
|
|
517
|
+
|
|
518
|
+
function update(
|
|
519
|
+
key: string,
|
|
520
|
+
animated: Animated.Value | undefined,
|
|
521
|
+
valIn: string | number
|
|
522
|
+
) {
|
|
523
|
+
const isColor = colorStyleKey[key]
|
|
524
|
+
const [numVal, type] = isColor ? [0, undefined] : getValue(valIn)
|
|
525
|
+
let animateToValue = numVal
|
|
526
|
+
const value = animated || new Animated.Value(numVal)
|
|
527
|
+
const curInterpolation = animationsState.current.get(value)
|
|
528
|
+
|
|
529
|
+
if (type) {
|
|
530
|
+
animationsState.current.set(value, {
|
|
531
|
+
interpolation: value.interpolate(
|
|
532
|
+
getInterpolated(
|
|
533
|
+
curInterpolation?.current ?? value['_value'],
|
|
534
|
+
numVal,
|
|
535
|
+
type
|
|
536
|
+
)
|
|
537
|
+
),
|
|
538
|
+
current: numVal,
|
|
539
|
+
})
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
if (isColor) {
|
|
543
|
+
animateToValue = curInterpolation?.animateToValue ? 0 : 1
|
|
544
|
+
animationsState.current.set(value, {
|
|
545
|
+
current: valIn,
|
|
546
|
+
interpolation: value.interpolate(
|
|
547
|
+
getColorInterpolated(
|
|
548
|
+
curInterpolation?.current as string,
|
|
549
|
+
valIn as string,
|
|
550
|
+
animateToValue
|
|
551
|
+
)
|
|
552
|
+
),
|
|
553
|
+
animateToValue: curInterpolation?.animateToValue ? 0 : 1,
|
|
554
|
+
})
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const animationConfig = getAnimationConfig(
|
|
558
|
+
key,
|
|
559
|
+
animations,
|
|
560
|
+
props.transition,
|
|
561
|
+
'default'
|
|
562
|
+
)
|
|
563
|
+
res.runners.push(() => {
|
|
564
|
+
value.stopAnimation()
|
|
565
|
+
const anim = Animated[animationConfig.type || 'spring'](value, {
|
|
566
|
+
toValue: animateToValue,
|
|
567
|
+
useNativeDriver: nativeDriver,
|
|
568
|
+
...animationConfig,
|
|
569
|
+
})
|
|
570
|
+
;(animationConfig.delay
|
|
571
|
+
? Animated.sequence([Animated.delay(animationConfig.delay), anim])
|
|
572
|
+
: anim
|
|
573
|
+
).start()
|
|
574
|
+
})
|
|
575
|
+
|
|
576
|
+
return value
|
|
577
|
+
}
|
|
578
|
+
})
|
|
579
|
+
|
|
429
580
|
if (process.env.NODE_ENV === 'development') {
|
|
430
581
|
if (props['debug'] === 'verbose') {
|
|
431
582
|
console.info(`Animated`, { response: res, inputStyle: style, isExiting })
|
|
@@ -507,6 +658,9 @@ function getAnimationConfig(
|
|
|
507
658
|
const found = animationType ? animations[animationType] : {}
|
|
508
659
|
return {
|
|
509
660
|
...found,
|
|
661
|
+
// Apply global spring config overrides (from transition={['bouncy', { stiffness: 1000 }]})
|
|
662
|
+
...normalized.config,
|
|
663
|
+
// Property-specific config takes highest precedence
|
|
510
664
|
...extraConf,
|
|
511
665
|
}
|
|
512
666
|
}
|
|
@@ -8,13 +8,16 @@ type TimingConfig = {
|
|
|
8
8
|
type: "timing";
|
|
9
9
|
} & Partial<Animated.TimingAnimationConfig>;
|
|
10
10
|
type AnimationConfig = SpringConfig | TimingConfig;
|
|
11
|
+
type CreateAnimationsOptions = {
|
|
12
|
+
useNativeDriver?: boolean;
|
|
13
|
+
};
|
|
11
14
|
export declare const AnimatedView: Animated.AnimatedComponent<typeof View>;
|
|
12
15
|
export declare const AnimatedText: Animated.AnimatedComponent<typeof Text>;
|
|
13
16
|
export declare function useAnimatedNumber(initial: number): UniversalAnimatedNumber<Animated.Value>;
|
|
14
17
|
type RNAnimatedNum = UniversalAnimatedNumber<Animated.Value>;
|
|
15
18
|
export declare const useAnimatedNumberReaction: UseAnimatedNumberReaction<RNAnimatedNum>;
|
|
16
19
|
export declare const useAnimatedNumberStyle: UseAnimatedNumberStyle<RNAnimatedNum>;
|
|
17
|
-
export declare function createAnimations<A extends AnimationsConfig>(animations: A): AnimationDriver<A>;
|
|
20
|
+
export declare function createAnimations<A extends AnimationsConfig>(animations: A, options?: CreateAnimationsOptions): AnimationDriver<A>;
|
|
18
21
|
export {};
|
|
19
22
|
|
|
20
23
|
//# sourceMappingURL=createAnimations.d.ts.map
|
|
@@ -1,16 +1,11 @@
|
|
|
1
1
|
{
|
|
2
|
-
"mappings": "AAGA,cAEE,iBAEA,yBACA,2BACA,8BACK;AAGP,SAAS,eAAe,WAAW,YAAY;
|
|
3
|
-
"names": [
|
|
4
|
-
"AnimatedView: Animated.AnimatedComponent<typeof View>",
|
|
5
|
-
"AnimatedText: Animated.AnimatedComponent<typeof Text>",
|
|
6
|
-
"useAnimatedNumberReaction: UseAnimatedNumberReaction<RNAnimatedNum>",
|
|
7
|
-
"useAnimatedNumberStyle: UseAnimatedNumberStyle<RNAnimatedNum>"
|
|
8
|
-
],
|
|
2
|
+
"mappings": "AAGA,cAEE,iBAEA,yBACA,2BACA,8BACK;AAGP,SAAS,eAAe,WAAW,YAAY;KAe1C,iBAAiB,6BAA6B,aAAa,KAAI;KAE/D,eAAe;CAAE,OAAO;IAAa,QACxC,KACE,SAAS,uBACP,UACA,eACA,YACA,aACA,SACA,sBACA,UACA,cACA,YACA;KAID,eAAe;CAAE,MAAM;IAAa,QAAQ,SAAS;KAErD,kBAAkB,eAAe;KAgCjC,0BAA0B;CAE7B;;AAGF,OAAO,cAAM,cAAc,SAAS,yBAAyB;AAC7D,OAAO,cAAM,cAAc,SAAS,yBAAyB;AAE7D,OAAO,iBAAS,kBACd,kBACC,wBAAwB,SAAS;KA2D/B,gBAAgB,wBAAwB,SAAS;AAEtD,OAAO,cAAM,2BAA2B,0BAA0B;AAgBlE,OAAO,cAAM,wBAAwB,uBAAuB;AAO5D,OAAO,iBAAS,iBAAiB,UAAU,kBACzC,YAAY,GACZ,UAAU,0BACT,gBAAgB",
|
|
3
|
+
"names": [],
|
|
9
4
|
"sources": [
|
|
10
5
|
"src/createAnimations.tsx"
|
|
11
6
|
],
|
|
7
|
+
"version": 3,
|
|
12
8
|
"sourcesContent": [
|
|
13
|
-
"import { normalizeTransition, getEffectiveAnimation } from '@tamagui/animation-helpers'\nimport { isWeb, useIsomorphicLayoutEffect } from '@tamagui/constants'\nimport { ResetPresence, usePresence } from '@tamagui/use-presence'\nimport type {\n AnimatedNumberStrategy,\n AnimationDriver,\n TransitionProp,\n UniversalAnimatedNumber,\n UseAnimatedNumberReaction,\n UseAnimatedNumberStyle,\n} from '@tamagui/web'\nimport { useEvent, useThemeWithState } from '@tamagui/web'\nimport React from 'react'\nimport { Animated, type Text, type View } from 'react-native'\n\n// Helper to resolve dynamic theme values like {dynamic: {dark: \"value\", light: undefined}}\nconst resolveDynamicValue = (value: any, isDark: boolean): any => {\n if (value && typeof value === 'object' && 'dynamic' in value) {\n const dynamicValue = isDark ? value.dynamic.dark : value.dynamic.light\n return dynamicValue\n }\n return value\n}\n\ntype AnimationsConfig<A extends object = any> = { [Key in keyof A]: AnimationConfig }\n\ntype SpringConfig = { type?: 'spring' } & Partial<\n Pick<\n Animated.SpringAnimationConfig,\n | 'delay'\n | 'bounciness'\n | 'damping'\n | 'friction'\n | 'mass'\n | 'overshootClamping'\n | 'speed'\n | 'stiffness'\n | 'tension'\n | 'velocity'\n >\n>\n\ntype TimingConfig = { type: 'timing' } & Partial<Animated.TimingAnimationConfig>\n\ntype AnimationConfig = SpringConfig | TimingConfig\n\nconst animatedStyleKey = {\n transform: true,\n opacity: true,\n}\n\nconst colorStyleKey = {\n backgroundColor: true,\n color: true,\n borderColor: true,\n borderLeftColor: true,\n borderRightColor: true,\n borderTopColor: true,\n borderBottomColor: true,\n}\n\n// these are the styles that are costly to animate because they don't support useNativeDriver and some of them are changing layout\nconst costlyToAnimateStyleKey = {\n borderRadius: true,\n borderTopLeftRadius: true,\n borderTopRightRadius: true,\n borderBottomLeftRadius: true,\n borderBottomRightRadius: true,\n borderWidth: true,\n borderLeftWidth: true,\n borderRightWidth: true,\n borderTopWidth: true,\n borderBottomWidth: true,\n ...colorStyleKey,\n // TODO for other keys like height or width, it's better to not add them here till layout animations are ready\n}\n\nexport const AnimatedView: Animated.AnimatedComponent<typeof View> = Animated.View\nexport const AnimatedText: Animated.AnimatedComponent<typeof Text> = Animated.Text\n\nexport function useAnimatedNumber(\n initial: number\n): UniversalAnimatedNumber<Animated.Value> {\n const state = React.useRef(\n null as any as {\n val: Animated.Value\n composite: Animated.CompositeAnimation | null\n strategy: AnimatedNumberStrategy\n }\n )\n if (!state.current) {\n state.current = {\n composite: null,\n val: new Animated.Value(initial),\n strategy: { type: 'spring' },\n }\n }\n\n return {\n getInstance() {\n return state.current.val\n },\n getValue() {\n return state.current.val['_value']\n },\n stop() {\n state.current.composite?.stop()\n state.current.composite = null\n },\n setValue(next: number, { type, ...config } = { type: 'spring' }, onFinish) {\n const val = state.current.val\n\n const handleFinish = onFinish\n ? ({ finished }) => (finished ? onFinish() : null)\n : undefined\n\n if (type === 'direct') {\n val.setValue(next)\n } else if (type === 'spring') {\n state.current.composite?.stop()\n const composite = Animated.spring(val, {\n ...config,\n toValue: next,\n useNativeDriver: !isWeb,\n })\n composite.start(handleFinish)\n state.current.composite = composite\n } else {\n state.current.composite?.stop()\n const composite = Animated.timing(val, {\n ...config,\n toValue: next,\n useNativeDriver: !isWeb,\n })\n composite.start(handleFinish)\n state.current.composite = composite\n }\n },\n }\n}\n\ntype RNAnimatedNum = UniversalAnimatedNumber<Animated.Value>\n\nexport const useAnimatedNumberReaction: UseAnimatedNumberReaction<RNAnimatedNum> = (\n { value },\n onValue\n) => {\n const onChange = useEvent((current) => {\n onValue(current.value)\n })\n\n React.useEffect(() => {\n const id = value.getInstance().addListener(onChange)\n return () => {\n value.getInstance().removeListener(id)\n }\n }, [value, onChange])\n}\n\nexport const useAnimatedNumberStyle: UseAnimatedNumberStyle<RNAnimatedNum> = (\n value,\n getStyle\n) => {\n return getStyle(value.getInstance())\n}\n\nexport function createAnimations<A extends AnimationsConfig>(\n animations: A\n): AnimationDriver<A> {\n return {\n isReactNative: true,\n inputStyle: 'value',\n outputStyle: 'inline',\n animations,\n View: AnimatedView,\n Text: AnimatedText,\n useAnimatedNumber,\n useAnimatedNumberReaction,\n useAnimatedNumberStyle,\n usePresence,\n ResetPresence,\n useAnimations: ({ props, onDidAnimate, style, componentState, presence }) => {\n const isDisabled = isWeb && componentState.unmounted === true\n const isExiting = presence?.[0] === false\n const sendExitComplete = presence?.[1]\n const [, themeState] = useThemeWithState({})\n // Check scheme first, then fall back to checking theme name for 'dark'\n const isDark = themeState?.scheme === 'dark' || themeState?.name?.startsWith('dark')\n\n /** store Animated value of each key e.g: color: AnimatedValue */\n const animateStyles = React.useRef<Record<string, Animated.Value>>({})\n const animatedTranforms = React.useRef<{ [key: string]: Animated.Value }[]>([])\n const animationsState = React.useRef(\n new WeakMap<\n Animated.Value,\n {\n interpolation: Animated.AnimatedInterpolation<any>\n current?: number | string | undefined\n // only for colors\n animateToValue?: number\n }\n >()\n )\n\n const animateOnly = (props.animateOnly as string[]) || []\n const hasTransitionOnly = !!props.animateOnly\n\n // Track if we just finished entering (transition from entering to not entering)\n // must be declared before args array that uses justFinishedEntering\n const isEntering = !!componentState.unmounted\n const wasEnteringRef = React.useRef(isEntering)\n const justFinishedEntering = wasEnteringRef.current && !isEntering\n React.useEffect(() => {\n wasEnteringRef.current = isEntering\n })\n\n const args = [\n JSON.stringify(style),\n componentState,\n isExiting,\n !!onDidAnimate,\n isDark,\n justFinishedEntering,\n ]\n\n // check if there is any style that is not supported by native driver\n const isThereNoNativeStyleKeys = React.useMemo(() => {\n if (isWeb) return true\n return Object.keys(style).some((key) => {\n if (animateOnly) {\n return !animatedStyleKey[key] && animateOnly.indexOf(key) === -1\n }\n return !animatedStyleKey[key]\n })\n }, args)\n\n const res = React.useMemo(() => {\n const runners: Function[] = []\n const completions: Promise<void>[] = []\n\n // Determine animation state for enter/exit transitions\n // Use 'enter' if we're entering OR if we just finished entering\n const animationState: 'enter' | 'exit' | 'default' = isExiting\n ? 'exit'\n : isEntering || justFinishedEntering\n ? 'enter'\n : 'default'\n\n const nonAnimatedStyle = {}\n\n for (const key in style) {\n const rawVal = style[key]\n // Resolve dynamic theme values (like $theme-dark)\n const val = resolveDynamicValue(rawVal, isDark)\n if (val === undefined) continue\n\n if (isDisabled) {\n continue\n }\n\n if (animatedStyleKey[key] == null && !costlyToAnimateStyleKey[key]) {\n nonAnimatedStyle[key] = val\n continue\n }\n\n if (hasTransitionOnly && !animateOnly.includes(key)) {\n nonAnimatedStyle[key] = val\n continue\n }\n\n if (key !== 'transform') {\n animateStyles.current[key] = update(key, animateStyles.current[key], val)\n continue\n }\n // key: 'transform'\n // for now just support one transform key\n if (!val) continue\n if (typeof val === 'string') {\n console.warn(`Warning: Tamagui can't animate string transforms yet!`)\n continue\n }\n\n for (const [index, transform] of val.entries()) {\n if (!transform) continue\n // tkey: e.g: 'translateX'\n const tkey = Object.keys(transform)[0]\n const currentTransform = animatedTranforms.current[index]?.[tkey]\n animatedTranforms.current[index] = {\n [tkey]: update(tkey, currentTransform, transform[tkey]),\n }\n animatedTranforms.current = [...animatedTranforms.current]\n }\n }\n\n const animatedStyle = {\n ...Object.fromEntries(\n Object.entries(animateStyles.current).map(([k, v]) => [\n k,\n animationsState.current!.get(v)?.interpolation || v,\n ])\n ),\n transform: animatedTranforms.current.map((r) => {\n const key = Object.keys(r)[0]\n const val = animationsState.current!.get(r[key])?.interpolation || r[key]\n return { [key]: val }\n }),\n }\n\n return {\n runners,\n completions,\n style: [nonAnimatedStyle, animatedStyle],\n }\n\n function update(\n key: string,\n animated: Animated.Value | undefined,\n valIn: string | number\n ) {\n const isColorStyleKey = colorStyleKey[key]\n const [val, type] = isColorStyleKey ? [0, undefined] : getValue(valIn)\n let animateToValue = val\n const value = animated || new Animated.Value(val)\n const curInterpolation = animationsState.current.get(value)\n\n let interpolateArgs: any\n if (type) {\n interpolateArgs = getInterpolated(\n curInterpolation?.current ?? value['_value'],\n val,\n type\n )\n animationsState.current!.set(value, {\n interpolation: value.interpolate(interpolateArgs),\n current: val,\n })\n }\n\n if (isColorStyleKey) {\n animateToValue = curInterpolation?.animateToValue ? 0 : 1\n interpolateArgs = getColorInterpolated(\n curInterpolation?.current as string,\n // valIn is the next color\n valIn as string,\n animateToValue\n )\n animationsState.current!.set(value, {\n current: valIn,\n interpolation: value.interpolate(interpolateArgs),\n animateToValue: curInterpolation?.animateToValue ? 0 : 1,\n })\n }\n\n if (value) {\n const animationConfig = getAnimationConfig(\n key,\n animations,\n props.transition,\n animationState\n )\n\n let resolve\n const promise = new Promise<void>((res) => {\n resolve = res\n })\n completions.push(promise)\n\n runners.push(() => {\n value.stopAnimation()\n\n function getAnimation() {\n return Animated[animationConfig.type || 'spring'](value, {\n toValue: animateToValue,\n useNativeDriver: !isWeb && !isThereNoNativeStyleKeys,\n ...animationConfig,\n })\n }\n\n const animation = animationConfig.delay\n ? Animated.sequence([\n Animated.delay(animationConfig.delay),\n getAnimation(),\n ])\n : getAnimation()\n\n animation.start(({ finished }) => {\n if (finished) {\n resolve()\n }\n })\n })\n }\n\n if (process.env.NODE_ENV === 'development') {\n if (props['debug'] === 'verbose') {\n // prettier-ignore\n console.info(\n ' 💠 animate',\n key,\n `from (${value['_value']}) to`,\n valIn,\n `(${val})`,\n 'type',\n type,\n 'interpolate',\n interpolateArgs\n )\n }\n }\n return value\n }\n }, args)\n\n useIsomorphicLayoutEffect(() => {\n res.runners.forEach((r) => r())\n let cancel = false\n Promise.all(res.completions).then(() => {\n if (cancel) return\n onDidAnimate?.()\n if (isExiting) {\n sendExitComplete?.()\n }\n })\n return () => {\n cancel = true\n }\n }, args)\n\n if (process.env.NODE_ENV === 'development') {\n if (props['debug'] === 'verbose') {\n console.info(`Animated`, { response: res, inputStyle: style, isExiting })\n }\n }\n\n return res\n },\n }\n}\n\nfunction getColorInterpolated(\n currentColor: string | undefined,\n nextColor: string,\n animateToValue: number\n) {\n const inputRange = [0, 1]\n const outputRange = [currentColor ? currentColor : nextColor, nextColor]\n if (animateToValue === 0) {\n // because we are animating from value 1 to 0, we need to put target color at the beginning\n outputRange.reverse()\n }\n return {\n inputRange,\n outputRange,\n }\n}\n\nfunction getInterpolated(current: number, next: number, postfix = 'deg') {\n if (next === current) {\n current = next - 0.000000001\n }\n const inputRange = [current, next]\n const outputRange = [`${current}${postfix}`, `${next}${postfix}`]\n if (next < current) {\n inputRange.reverse()\n outputRange.reverse()\n }\n return {\n inputRange,\n outputRange,\n }\n}\n\nfunction getAnimationConfig(\n key: string,\n animations: AnimationsConfig,\n transition?: TransitionProp,\n animationState: 'enter' | 'exit' | 'default' = 'default'\n): AnimationConfig {\n const normalized = normalizeTransition(transition)\n const shortKey = transformShorthands[key]\n\n // Check for property-specific animation\n const propAnimation = normalized.properties[key] ?? normalized.properties[shortKey]\n\n let animationType: string | null = null\n let extraConf: any = {}\n\n if (typeof propAnimation === 'string') {\n // Direct animation name: { x: 'quick' }\n animationType = propAnimation\n } else if (propAnimation && typeof propAnimation === 'object') {\n // Config object: { x: { type: 'quick', delay: 100 } }\n // Use effective animation based on state if no explicit type in config\n animationType =\n propAnimation.type || getEffectiveAnimation(normalized, animationState)\n extraConf = propAnimation\n } else {\n // Fall back to effective animation based on state (enter/exit/default)\n animationType = getEffectiveAnimation(normalized, animationState)\n }\n\n // Apply global delay if no property-specific delay\n if (normalized.delay && !extraConf.delay) {\n extraConf = { ...extraConf, delay: normalized.delay }\n }\n\n const found = animationType ? animations[animationType] : {}\n return {\n ...found,\n ...extraConf,\n }\n}\n\n// try both combos\nconst transformShorthands = {\n x: 'translateX',\n y: 'translateY',\n translateX: 'x',\n translateY: 'y',\n}\n\nfunction getValue(input: number | string, isColor = false) {\n if (typeof input !== 'string') {\n return [input] as const\n }\n const [_, number, after] = input.match(/([-0-9]+)(deg|%|px)/) ?? []\n return [+number, after] as const\n}\n"
|
|
14
|
-
]
|
|
15
|
-
"version": 3
|
|
9
|
+
"import { getEffectiveAnimation, normalizeTransition } from '@tamagui/animation-helpers'\nimport { isWeb, useIsomorphicLayoutEffect } from '@tamagui/constants'\nimport { ResetPresence, usePresence } from '@tamagui/use-presence'\nimport type {\n AnimatedNumberStrategy,\n AnimationDriver,\n TransitionProp,\n UniversalAnimatedNumber,\n UseAnimatedNumberReaction,\n UseAnimatedNumberStyle,\n} from '@tamagui/web'\nimport { useEvent, useThemeWithState } from '@tamagui/web'\nimport React from 'react'\nimport { Animated, type Text, type View } from 'react-native'\n\n// detect Fabric (New Architecture) — Paper doesn't support native driver for all style keys\nconst isFabric =\n !isWeb && typeof global !== 'undefined' && !!global.__nativeFabricUIManager\n\n// Helper to resolve dynamic theme values like {dynamic: {dark: \"value\", light: undefined}}\nconst resolveDynamicValue = (value: any, isDark: boolean): any => {\n if (value && typeof value === 'object' && 'dynamic' in value) {\n const dynamicValue = isDark ? value.dynamic.dark : value.dynamic.light\n return dynamicValue\n }\n return value\n}\n\ntype AnimationsConfig<A extends object = any> = { [Key in keyof A]: AnimationConfig }\n\ntype SpringConfig = { type?: 'spring' } & Partial<\n Pick<\n Animated.SpringAnimationConfig,\n | 'delay'\n | 'bounciness'\n | 'damping'\n | 'friction'\n | 'mass'\n | 'overshootClamping'\n | 'speed'\n | 'stiffness'\n | 'tension'\n | 'velocity'\n >\n>\n\ntype TimingConfig = { type: 'timing' } & Partial<Animated.TimingAnimationConfig>\n\ntype AnimationConfig = SpringConfig | TimingConfig\n\nconst animatedStyleKey = {\n transform: true,\n opacity: true,\n}\n\nconst colorStyleKey = {\n backgroundColor: true,\n color: true,\n borderColor: true,\n borderLeftColor: true,\n borderRightColor: true,\n borderTopColor: true,\n borderBottomColor: true,\n}\n\n// these style keys are costly to animate and only work with native driver on Fabric\nconst costlyToAnimateStyleKey = {\n borderRadius: true,\n borderTopLeftRadius: true,\n borderTopRightRadius: true,\n borderBottomLeftRadius: true,\n borderBottomRightRadius: true,\n borderWidth: true,\n borderLeftWidth: true,\n borderRightWidth: true,\n borderTopWidth: true,\n borderBottomWidth: true,\n ...colorStyleKey,\n}\n\ntype CreateAnimationsOptions = {\n // override native driver detection (default: auto-detect Fabric)\n useNativeDriver?: boolean\n}\n\nexport const AnimatedView: Animated.AnimatedComponent<typeof View> = Animated.View\nexport const AnimatedText: Animated.AnimatedComponent<typeof Text> = Animated.Text\n\nexport function useAnimatedNumber(\n initial: number\n): UniversalAnimatedNumber<Animated.Value> {\n const state = React.useRef(\n null as any as {\n val: Animated.Value\n composite: Animated.CompositeAnimation | null\n strategy: AnimatedNumberStrategy\n }\n )\n if (!state.current) {\n state.current = {\n composite: null,\n val: new Animated.Value(initial),\n strategy: { type: 'spring' },\n }\n }\n\n return {\n getInstance() {\n return state.current.val\n },\n getValue() {\n return state.current.val['_value']\n },\n stop() {\n state.current.composite?.stop()\n state.current.composite = null\n },\n setValue(next: number, { type, ...config } = { type: 'spring' }, onFinish) {\n const val = state.current.val\n\n const handleFinish = onFinish\n ? ({ finished }) => (finished ? onFinish() : null)\n : undefined\n\n if (type === 'direct') {\n val.setValue(next)\n } else if (type === 'spring') {\n state.current.composite?.stop()\n const composite = Animated.spring(val, {\n ...config,\n toValue: next,\n useNativeDriver: isFabric,\n })\n composite.start(handleFinish)\n state.current.composite = composite\n } else {\n state.current.composite?.stop()\n const composite = Animated.timing(val, {\n ...config,\n toValue: next,\n useNativeDriver: isFabric,\n })\n composite.start(handleFinish)\n state.current.composite = composite\n }\n },\n }\n}\n\ntype RNAnimatedNum = UniversalAnimatedNumber<Animated.Value>\n\nexport const useAnimatedNumberReaction: UseAnimatedNumberReaction<RNAnimatedNum> = (\n { value },\n onValue\n) => {\n const onChange = useEvent((current) => {\n onValue(current.value)\n })\n\n React.useEffect(() => {\n const id = value.getInstance().addListener(onChange)\n return () => {\n value.getInstance().removeListener(id)\n }\n }, [value, onChange])\n}\n\nexport const useAnimatedNumberStyle: UseAnimatedNumberStyle<RNAnimatedNum> = (\n value,\n getStyle\n) => {\n return getStyle(value.getInstance())\n}\n\nexport function createAnimations<A extends AnimationsConfig>(\n animations: A,\n options?: CreateAnimationsOptions\n): AnimationDriver<A> {\n const nativeDriver = options?.useNativeDriver ?? isFabric\n\n return {\n isReactNative: true,\n inputStyle: 'value',\n outputStyle: 'inline',\n avoidReRenders: true,\n animations,\n needsCustomComponent: true,\n View: AnimatedView,\n Text: AnimatedText,\n useAnimatedNumber,\n useAnimatedNumberReaction,\n useAnimatedNumberStyle,\n usePresence,\n ResetPresence,\n useAnimations: ({\n props,\n onDidAnimate,\n style,\n componentState,\n presence,\n useStyleEmitter,\n }) => {\n const isDisabled = isWeb && componentState.unmounted === true\n const isExiting = presence?.[0] === false\n const sendExitComplete = presence?.[1]\n const [, themeState] = useThemeWithState({})\n // Check scheme first, then fall back to checking theme name for 'dark'\n const isDark = themeState?.scheme === 'dark' || themeState?.name?.startsWith('dark')\n\n /** store Animated value of each key e.g: color: AnimatedValue */\n const animateStyles = React.useRef<Record<string, Animated.Value>>({})\n const animatedTranforms = React.useRef<{ [key: string]: Animated.Value }[]>([])\n const animationsState = React.useRef(\n new WeakMap<\n Animated.Value,\n {\n interpolation: Animated.AnimatedInterpolation<any>\n current?: number | string | undefined\n // only for colors\n animateToValue?: number\n }\n >()\n )\n\n // exit cycle guards to prevent stale/duplicate completion\n const exitCycleIdRef = React.useRef(0)\n const exitCompletedRef = React.useRef(false)\n const wasExitingRef = React.useRef(false)\n\n // detect transition into/out of exiting state\n const justStartedExiting = isExiting && !wasExitingRef.current\n const justStoppedExiting = !isExiting && wasExitingRef.current\n\n // start new exit cycle only on transition INTO exiting\n if (justStartedExiting) {\n exitCycleIdRef.current++\n exitCompletedRef.current = false\n }\n // invalidate pending callbacks when exit is canceled/interrupted\n if (justStoppedExiting) {\n exitCycleIdRef.current++\n }\n\n const animateOnly = (props.animateOnly as string[]) || []\n const hasTransitionOnly = !!props.animateOnly\n\n // Track if we just finished entering (transition from entering to not entering)\n // must be declared before args array that uses justFinishedEntering\n const isEntering = !!componentState.unmounted\n const wasEnteringRef = React.useRef(isEntering)\n const justFinishedEntering = wasEnteringRef.current && !isEntering\n React.useEffect(() => {\n wasEnteringRef.current = isEntering\n })\n\n const args = [\n JSON.stringify(style),\n componentState,\n isExiting,\n !!onDidAnimate,\n isDark,\n justFinishedEntering,\n hasTransitionOnly,\n ]\n\n const res = React.useMemo(() => {\n const runners: Function[] = []\n const completions: Promise<void>[] = []\n\n // Determine animation state for enter/exit transitions\n // Use 'enter' if we're entering OR if we just finished entering\n const animationState: 'enter' | 'exit' | 'default' = isExiting\n ? 'exit'\n : isEntering || justFinishedEntering\n ? 'enter'\n : 'default'\n\n const nonAnimatedStyle = {}\n\n for (const key in style) {\n const rawVal = style[key]\n // Resolve dynamic theme values (like $theme-dark)\n const val = resolveDynamicValue(rawVal, isDark)\n if (val === undefined) continue\n\n if (isDisabled) {\n continue\n }\n\n if (animatedStyleKey[key] == null && !costlyToAnimateStyleKey[key]) {\n nonAnimatedStyle[key] = val\n continue\n }\n\n if (hasTransitionOnly && !animateOnly.includes(key)) {\n nonAnimatedStyle[key] = val\n continue\n }\n\n if (key !== 'transform') {\n animateStyles.current[key] = update(key, animateStyles.current[key], val)\n continue\n }\n // key: 'transform'\n // for now just support one transform key\n if (!val) continue\n if (typeof val === 'string') {\n console.warn(`Warning: Tamagui can't animate string transforms yet!`)\n continue\n }\n\n for (const [index, transform] of val.entries()) {\n if (!transform) continue\n // tkey: e.g: 'translateX'\n const tkey = Object.keys(transform)[0]\n const currentTransform = animatedTranforms.current[index]?.[tkey]\n animatedTranforms.current[index] = {\n [tkey]: update(tkey, currentTransform, transform[tkey]),\n }\n animatedTranforms.current = [...animatedTranforms.current]\n }\n }\n\n const animatedTransformStyle =\n animatedTranforms.current.length > 0\n ? {\n transform: animatedTranforms.current.map((r) => {\n const key = Object.keys(r)[0]\n const val =\n animationsState.current!.get(r[key])?.interpolation || r[key]\n return { [key]: val }\n }),\n }\n : {}\n\n const animatedStyle = {\n ...Object.fromEntries(\n Object.entries(animateStyles.current).map(([k, v]) => [\n k,\n animationsState.current!.get(v)?.interpolation || v,\n ])\n ),\n ...animatedTransformStyle,\n }\n\n return {\n runners,\n completions,\n style: [nonAnimatedStyle, animatedStyle],\n }\n\n function update(\n key: string,\n animated: Animated.Value | undefined,\n valIn: string | number\n ) {\n const isColorStyleKey = colorStyleKey[key]\n const [val, type] = isColorStyleKey ? [0, undefined] : getValue(valIn)\n let animateToValue = val\n const value = animated || new Animated.Value(val)\n const curInterpolation = animationsState.current.get(value)\n\n let interpolateArgs: any\n if (type) {\n interpolateArgs = getInterpolated(\n curInterpolation?.current ?? value['_value'],\n val,\n type\n )\n animationsState.current!.set(value, {\n interpolation: value.interpolate(interpolateArgs),\n current: val,\n })\n }\n\n if (isColorStyleKey) {\n animateToValue = curInterpolation?.animateToValue ? 0 : 1\n interpolateArgs = getColorInterpolated(\n curInterpolation?.current as string,\n // valIn is the next color\n valIn as string,\n animateToValue\n )\n animationsState.current!.set(value, {\n current: valIn,\n interpolation: value.interpolate(interpolateArgs),\n animateToValue: curInterpolation?.animateToValue ? 0 : 1,\n })\n }\n\n if (value) {\n const animationConfig = getAnimationConfig(\n key,\n animations,\n props.transition,\n animationState\n )\n\n let resolve\n const promise = new Promise<void>((res) => {\n resolve = res\n })\n completions.push(promise)\n\n runners.push(() => {\n value.stopAnimation()\n\n function getAnimation() {\n return Animated[animationConfig.type || 'spring'](value, {\n toValue: animateToValue,\n useNativeDriver: nativeDriver,\n ...animationConfig,\n })\n }\n\n const animation = animationConfig.delay\n ? Animated.sequence([\n Animated.delay(animationConfig.delay),\n getAnimation(),\n ])\n : getAnimation()\n\n animation.start(({ finished }) => {\n // always resolve during exit (element is leaving anyway)\n // for non-exit, only resolve on successful completion\n if (finished || isExiting) {\n resolve()\n }\n })\n })\n }\n\n if (process.env.NODE_ENV === 'development') {\n if (props['debug'] === 'verbose') {\n // prettier-ignore\n console.info(\n ' 💠 animate',\n key,\n `from (${value['_value']}) to`,\n valIn,\n `(${val})`,\n 'type',\n type,\n 'interpolate',\n interpolateArgs\n )\n }\n }\n return value\n }\n }, args)\n\n // track previous exiting state\n React.useEffect(() => {\n wasExitingRef.current = isExiting\n })\n\n useIsomorphicLayoutEffect(() => {\n res.runners.forEach((r) => r())\n\n // capture current cycle id\n const cycleId = exitCycleIdRef.current\n\n // handle zero-completion case immediately\n if (res.completions.length === 0) {\n onDidAnimate?.()\n if (isExiting && !exitCompletedRef.current) {\n exitCompletedRef.current = true\n sendExitComplete?.()\n }\n return\n }\n\n let cancel = false\n Promise.all(res.completions).then(() => {\n if (cancel) return\n // guard against stale cycle completion\n if (isExiting && cycleId !== exitCycleIdRef.current) return\n if (isExiting && exitCompletedRef.current) return\n\n onDidAnimate?.()\n if (isExiting) {\n exitCompletedRef.current = true\n sendExitComplete?.()\n }\n })\n return () => {\n cancel = true\n }\n }, args)\n\n // avoidReRenders: receive style changes imperatively from tamagui\n // and update Animated.Values directly without React re-renders\n // reuses the same update() + runner pattern as the useMemo path\n useStyleEmitter?.((nextStyle) => {\n for (const key in nextStyle) {\n const rawVal = nextStyle[key]\n const val = resolveDynamicValue(rawVal, isDark)\n if (val === undefined) continue\n\n if (key === 'transform' && Array.isArray(val)) {\n for (const [index, transform] of val.entries()) {\n if (!transform) continue\n const tkey = Object.keys(transform)[0]\n const currentTransform = animatedTranforms.current[index]?.[tkey]\n animatedTranforms.current[index] = {\n [tkey]: update(tkey, currentTransform, transform[tkey]),\n }\n }\n } else if (animatedStyleKey[key] != null || costlyToAnimateStyleKey[key]) {\n animateStyles.current[key] = update(key, animateStyles.current[key], val)\n }\n }\n\n // run the queued animations immediately\n res.runners.forEach((r) => r())\n\n function update(\n key: string,\n animated: Animated.Value | undefined,\n valIn: string | number\n ) {\n const isColor = colorStyleKey[key]\n const [numVal, type] = isColor ? [0, undefined] : getValue(valIn)\n let animateToValue = numVal\n const value = animated || new Animated.Value(numVal)\n const curInterpolation = animationsState.current.get(value)\n\n if (type) {\n animationsState.current.set(value, {\n interpolation: value.interpolate(\n getInterpolated(\n curInterpolation?.current ?? value['_value'],\n numVal,\n type\n )\n ),\n current: numVal,\n })\n }\n\n if (isColor) {\n animateToValue = curInterpolation?.animateToValue ? 0 : 1\n animationsState.current.set(value, {\n current: valIn,\n interpolation: value.interpolate(\n getColorInterpolated(\n curInterpolation?.current as string,\n valIn as string,\n animateToValue\n )\n ),\n animateToValue: curInterpolation?.animateToValue ? 0 : 1,\n })\n }\n\n const animationConfig = getAnimationConfig(\n key,\n animations,\n props.transition,\n 'default'\n )\n res.runners.push(() => {\n value.stopAnimation()\n const anim = Animated[animationConfig.type || 'spring'](value, {\n toValue: animateToValue,\n useNativeDriver: nativeDriver,\n ...animationConfig,\n })\n ;(animationConfig.delay\n ? Animated.sequence([Animated.delay(animationConfig.delay), anim])\n : anim\n ).start()\n })\n\n return value\n }\n })\n\n if (process.env.NODE_ENV === 'development') {\n if (props['debug'] === 'verbose') {\n console.info(`Animated`, { response: res, inputStyle: style, isExiting })\n }\n }\n\n return res\n },\n }\n}\n\nfunction getColorInterpolated(\n currentColor: string | undefined,\n nextColor: string,\n animateToValue: number\n) {\n const inputRange = [0, 1]\n const outputRange = [currentColor ? currentColor : nextColor, nextColor]\n if (animateToValue === 0) {\n // because we are animating from value 1 to 0, we need to put target color at the beginning\n outputRange.reverse()\n }\n return {\n inputRange,\n outputRange,\n }\n}\n\nfunction getInterpolated(current: number, next: number, postfix = 'deg') {\n if (next === current) {\n current = next - 0.000000001\n }\n const inputRange = [current, next]\n const outputRange = [`${current}${postfix}`, `${next}${postfix}`]\n if (next < current) {\n inputRange.reverse()\n outputRange.reverse()\n }\n return {\n inputRange,\n outputRange,\n }\n}\n\nfunction getAnimationConfig(\n key: string,\n animations: AnimationsConfig,\n transition?: TransitionProp,\n animationState: 'enter' | 'exit' | 'default' = 'default'\n): AnimationConfig {\n const normalized = normalizeTransition(transition)\n const shortKey = transformShorthands[key]\n\n // Check for property-specific animation\n const propAnimation = normalized.properties[key] ?? normalized.properties[shortKey]\n\n let animationType: string | null = null\n let extraConf: any = {}\n\n if (typeof propAnimation === 'string') {\n // Direct animation name: { x: 'quick' }\n animationType = propAnimation\n } else if (propAnimation && typeof propAnimation === 'object') {\n // Config object: { x: { type: 'quick', delay: 100 } }\n // Use effective animation based on state if no explicit type in config\n animationType =\n propAnimation.type || getEffectiveAnimation(normalized, animationState)\n extraConf = propAnimation\n } else {\n // Fall back to effective animation based on state (enter/exit/default)\n animationType = getEffectiveAnimation(normalized, animationState)\n }\n\n // Apply global delay if no property-specific delay\n if (normalized.delay && !extraConf.delay) {\n extraConf = { ...extraConf, delay: normalized.delay }\n }\n\n const found = animationType ? animations[animationType] : {}\n return {\n ...found,\n // Apply global spring config overrides (from transition={['bouncy', { stiffness: 1000 }]})\n ...normalized.config,\n // Property-specific config takes highest precedence\n ...extraConf,\n }\n}\n\n// try both combos\nconst transformShorthands = {\n x: 'translateX',\n y: 'translateY',\n translateX: 'x',\n translateY: 'y',\n}\n\nfunction getValue(input: number | string, isColor = false) {\n if (typeof input !== 'string') {\n return [input] as const\n }\n const [_, number, after] = input.match(/([-0-9]+)(deg|%|px)/) ?? []\n return [+number, after] as const\n}\n"
|
|
10
|
+
]
|
|
16
11
|
}
|
package/types/index.d.ts.map
CHANGED
package/types/polyfill.d.ts.map
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
"sources": [
|
|
5
5
|
"src/polyfill.ts"
|
|
6
6
|
],
|
|
7
|
+
"version": 3,
|
|
7
8
|
"sourcesContent": [
|
|
8
9
|
"// for SSR\nif (typeof requestAnimationFrame === 'undefined') {\n globalThis['requestAnimationFrame'] =\n typeof setImmediate === 'undefined' ? setTimeout : setImmediate\n}\n"
|
|
9
|
-
]
|
|
10
|
-
"version": 3
|
|
10
|
+
]
|
|
11
11
|
}
|