@hanzogui/animations-react-native 2.0.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/LICENSE +21 -0
- package/dist/cjs/createAnimations.cjs +389 -0
- package/dist/cjs/createAnimations.native.js +477 -0
- package/dist/cjs/createAnimations.native.js.map +1 -0
- package/dist/cjs/index.cjs +19 -0
- package/dist/cjs/index.native.js +22 -0
- package/dist/cjs/index.native.js.map +1 -0
- package/dist/cjs/polyfill.cjs +1 -0
- package/dist/cjs/polyfill.native.js +4 -0
- package/dist/cjs/polyfill.native.js.map +1 -0
- package/dist/esm/createAnimations.mjs +350 -0
- package/dist/esm/createAnimations.mjs.map +1 -0
- package/dist/esm/createAnimations.native.js +435 -0
- package/dist/esm/createAnimations.native.js.map +1 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/index.mjs +2 -0
- package/dist/esm/index.mjs.map +1 -0
- package/dist/esm/index.native.js +2 -0
- package/dist/esm/index.native.js.map +1 -0
- package/dist/esm/polyfill.mjs +2 -0
- package/dist/esm/polyfill.mjs.map +1 -0
- package/dist/esm/polyfill.native.js +2 -0
- package/dist/esm/polyfill.native.js.map +1 -0
- package/package.json +56 -0
- package/src/createAnimations.tsx +682 -0
- package/src/index.ts +3 -0
- package/src/polyfill.ts +5 -0
- package/types/createAnimations.d.ts +23 -0
- package/types/createAnimations.d.ts.map +11 -0
- package/types/index.d.ts +4 -0
- package/types/index.d.ts.map +11 -0
- package/types/polyfill.d.ts +2 -0
- package/types/polyfill.d.ts.map +11 -0
|
@@ -0,0 +1,682 @@
|
|
|
1
|
+
import { getEffectiveAnimation, normalizeTransition } from '@hanzogui/animation-helpers'
|
|
2
|
+
import { isWeb, useIsomorphicLayoutEffect } from '@hanzogui/constants'
|
|
3
|
+
import { ResetPresence, usePresence } from '@hanzogui/use-presence'
|
|
4
|
+
import type {
|
|
5
|
+
AnimatedNumberStrategy,
|
|
6
|
+
AnimationDriver,
|
|
7
|
+
TransitionProp,
|
|
8
|
+
UniversalAnimatedNumber,
|
|
9
|
+
UseAnimatedNumberReaction,
|
|
10
|
+
UseAnimatedNumberStyle,
|
|
11
|
+
} from '@hanzogui/web'
|
|
12
|
+
import { useEvent, useThemeWithState } from '@hanzogui/web'
|
|
13
|
+
import React from 'react'
|
|
14
|
+
import { Animated, type Text, type View } from 'react-native'
|
|
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
|
+
|
|
20
|
+
// Helper to resolve dynamic theme values like {dynamic: {dark: "value", light: undefined}}
|
|
21
|
+
const resolveDynamicValue = (value: any, isDark: boolean): any => {
|
|
22
|
+
if (value && typeof value === 'object' && 'dynamic' in value) {
|
|
23
|
+
const dynamicValue = isDark ? value.dynamic.dark : value.dynamic.light
|
|
24
|
+
return dynamicValue
|
|
25
|
+
}
|
|
26
|
+
return value
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type AnimationsConfig<A extends object = any> = { [Key in keyof A]: AnimationConfig }
|
|
30
|
+
|
|
31
|
+
type SpringConfig = { type?: 'spring' } & Partial<
|
|
32
|
+
Pick<
|
|
33
|
+
Animated.SpringAnimationConfig,
|
|
34
|
+
| 'delay'
|
|
35
|
+
| 'bounciness'
|
|
36
|
+
| 'damping'
|
|
37
|
+
| 'friction'
|
|
38
|
+
| 'mass'
|
|
39
|
+
| 'overshootClamping'
|
|
40
|
+
| 'speed'
|
|
41
|
+
| 'stiffness'
|
|
42
|
+
| 'tension'
|
|
43
|
+
| 'velocity'
|
|
44
|
+
>
|
|
45
|
+
>
|
|
46
|
+
|
|
47
|
+
type TimingConfig = { type: 'timing' } & Partial<Animated.TimingAnimationConfig>
|
|
48
|
+
|
|
49
|
+
type AnimationConfig = SpringConfig | TimingConfig
|
|
50
|
+
|
|
51
|
+
const animatedStyleKey = {
|
|
52
|
+
transform: true,
|
|
53
|
+
opacity: true,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const colorStyleKey = {
|
|
57
|
+
backgroundColor: true,
|
|
58
|
+
color: true,
|
|
59
|
+
borderColor: true,
|
|
60
|
+
borderLeftColor: true,
|
|
61
|
+
borderRightColor: true,
|
|
62
|
+
borderTopColor: true,
|
|
63
|
+
borderBottomColor: true,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// these style keys are costly to animate and only work with native driver on Fabric
|
|
67
|
+
const costlyToAnimateStyleKey = {
|
|
68
|
+
borderRadius: true,
|
|
69
|
+
borderTopLeftRadius: true,
|
|
70
|
+
borderTopRightRadius: true,
|
|
71
|
+
borderBottomLeftRadius: true,
|
|
72
|
+
borderBottomRightRadius: true,
|
|
73
|
+
borderWidth: true,
|
|
74
|
+
borderLeftWidth: true,
|
|
75
|
+
borderRightWidth: true,
|
|
76
|
+
borderTopWidth: true,
|
|
77
|
+
borderBottomWidth: true,
|
|
78
|
+
...colorStyleKey,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
type CreateAnimationsOptions = {
|
|
82
|
+
// override native driver detection (default: auto-detect Fabric)
|
|
83
|
+
useNativeDriver?: boolean
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export const AnimatedView: Animated.AnimatedComponent<typeof View> = Animated.View
|
|
87
|
+
export const AnimatedText: Animated.AnimatedComponent<typeof Text> = Animated.Text
|
|
88
|
+
|
|
89
|
+
export function useAnimatedNumber(
|
|
90
|
+
initial: number
|
|
91
|
+
): UniversalAnimatedNumber<Animated.Value> {
|
|
92
|
+
const state = React.useRef(
|
|
93
|
+
null as any as {
|
|
94
|
+
val: Animated.Value
|
|
95
|
+
composite: Animated.CompositeAnimation | null
|
|
96
|
+
strategy: AnimatedNumberStrategy
|
|
97
|
+
}
|
|
98
|
+
)
|
|
99
|
+
if (!state.current) {
|
|
100
|
+
state.current = {
|
|
101
|
+
composite: null,
|
|
102
|
+
val: new Animated.Value(initial),
|
|
103
|
+
strategy: { type: 'spring' },
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
getInstance() {
|
|
109
|
+
return state.current.val
|
|
110
|
+
},
|
|
111
|
+
getValue() {
|
|
112
|
+
return state.current.val['_value']
|
|
113
|
+
},
|
|
114
|
+
stop() {
|
|
115
|
+
state.current.composite?.stop()
|
|
116
|
+
state.current.composite = null
|
|
117
|
+
},
|
|
118
|
+
setValue(next: number, { type, ...config } = { type: 'spring' }, onFinish) {
|
|
119
|
+
const val = state.current.val
|
|
120
|
+
|
|
121
|
+
const handleFinish = onFinish
|
|
122
|
+
? ({ finished }) => (finished ? onFinish() : null)
|
|
123
|
+
: undefined
|
|
124
|
+
|
|
125
|
+
if (type === 'direct') {
|
|
126
|
+
val.setValue(next)
|
|
127
|
+
} else if (type === 'spring') {
|
|
128
|
+
state.current.composite?.stop()
|
|
129
|
+
const composite = Animated.spring(val, {
|
|
130
|
+
...config,
|
|
131
|
+
toValue: next,
|
|
132
|
+
useNativeDriver: isFabric,
|
|
133
|
+
})
|
|
134
|
+
composite.start(handleFinish)
|
|
135
|
+
state.current.composite = composite
|
|
136
|
+
} else {
|
|
137
|
+
state.current.composite?.stop()
|
|
138
|
+
const composite = Animated.timing(val, {
|
|
139
|
+
...config,
|
|
140
|
+
toValue: next,
|
|
141
|
+
useNativeDriver: isFabric,
|
|
142
|
+
})
|
|
143
|
+
composite.start(handleFinish)
|
|
144
|
+
state.current.composite = composite
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
type RNAnimatedNum = UniversalAnimatedNumber<Animated.Value>
|
|
151
|
+
|
|
152
|
+
export const useAnimatedNumberReaction: UseAnimatedNumberReaction<RNAnimatedNum> = (
|
|
153
|
+
{ value },
|
|
154
|
+
onValue
|
|
155
|
+
) => {
|
|
156
|
+
const onChange = useEvent((current) => {
|
|
157
|
+
onValue(current.value)
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
React.useEffect(() => {
|
|
161
|
+
const id = value.getInstance().addListener(onChange)
|
|
162
|
+
return () => {
|
|
163
|
+
value.getInstance().removeListener(id)
|
|
164
|
+
}
|
|
165
|
+
}, [value, onChange])
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export const useAnimatedNumberStyle: UseAnimatedNumberStyle<RNAnimatedNum> = (
|
|
169
|
+
value,
|
|
170
|
+
getStyle
|
|
171
|
+
) => {
|
|
172
|
+
return getStyle(value.getInstance())
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function createAnimations<A extends AnimationsConfig>(
|
|
176
|
+
animations: A,
|
|
177
|
+
options?: CreateAnimationsOptions
|
|
178
|
+
): AnimationDriver<A> {
|
|
179
|
+
const nativeDriver = options?.useNativeDriver ?? isFabric
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
isReactNative: true,
|
|
183
|
+
inputStyle: 'value',
|
|
184
|
+
outputStyle: 'inline',
|
|
185
|
+
avoidReRenders: true,
|
|
186
|
+
animations,
|
|
187
|
+
needsCustomComponent: true,
|
|
188
|
+
View: AnimatedView,
|
|
189
|
+
Text: AnimatedText,
|
|
190
|
+
useAnimatedNumber,
|
|
191
|
+
useAnimatedNumberReaction,
|
|
192
|
+
useAnimatedNumberStyle,
|
|
193
|
+
usePresence,
|
|
194
|
+
ResetPresence,
|
|
195
|
+
useAnimations: ({
|
|
196
|
+
props,
|
|
197
|
+
onDidAnimate,
|
|
198
|
+
style,
|
|
199
|
+
componentState,
|
|
200
|
+
presence,
|
|
201
|
+
useStyleEmitter,
|
|
202
|
+
}) => {
|
|
203
|
+
const isDisabled = isWeb && componentState.unmounted === true
|
|
204
|
+
const isExiting = presence?.[0] === false
|
|
205
|
+
const sendExitComplete = presence?.[1]
|
|
206
|
+
const [, themeState] = useThemeWithState({})
|
|
207
|
+
// Check scheme first, then fall back to checking theme name for 'dark'
|
|
208
|
+
const isDark = themeState?.scheme === 'dark' || themeState?.name?.startsWith('dark')
|
|
209
|
+
|
|
210
|
+
/** store Animated value of each key e.g: color: AnimatedValue */
|
|
211
|
+
const animateStyles = React.useRef<Record<string, Animated.Value>>({})
|
|
212
|
+
const animatedTranforms = React.useRef<{ [key: string]: Animated.Value }[]>([])
|
|
213
|
+
const animationsState = React.useRef(
|
|
214
|
+
new WeakMap<
|
|
215
|
+
Animated.Value,
|
|
216
|
+
{
|
|
217
|
+
interpolation: Animated.AnimatedInterpolation<any>
|
|
218
|
+
current?: number | string | undefined
|
|
219
|
+
// only for colors
|
|
220
|
+
animateToValue?: number
|
|
221
|
+
}
|
|
222
|
+
>()
|
|
223
|
+
)
|
|
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
|
+
|
|
244
|
+
const animateOnly = (props.animateOnly as string[]) || []
|
|
245
|
+
const hasTransitionOnly = !!props.animateOnly
|
|
246
|
+
|
|
247
|
+
// Track if we just finished entering (transition from entering to not entering)
|
|
248
|
+
// must be declared before args array that uses justFinishedEntering
|
|
249
|
+
const isEntering = !!componentState.unmounted
|
|
250
|
+
const wasEnteringRef = React.useRef(isEntering)
|
|
251
|
+
const justFinishedEntering = wasEnteringRef.current && !isEntering
|
|
252
|
+
React.useEffect(() => {
|
|
253
|
+
wasEnteringRef.current = isEntering
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
const args = [
|
|
257
|
+
JSON.stringify(style),
|
|
258
|
+
componentState,
|
|
259
|
+
isExiting,
|
|
260
|
+
!!onDidAnimate,
|
|
261
|
+
isDark,
|
|
262
|
+
justFinishedEntering,
|
|
263
|
+
hasTransitionOnly,
|
|
264
|
+
]
|
|
265
|
+
|
|
266
|
+
const res = React.useMemo(() => {
|
|
267
|
+
const runners: Function[] = []
|
|
268
|
+
const completions: Promise<void>[] = []
|
|
269
|
+
|
|
270
|
+
// Determine animation state for enter/exit transitions
|
|
271
|
+
// Use 'enter' if we're entering OR if we just finished entering
|
|
272
|
+
const animationState: 'enter' | 'exit' | 'default' = isExiting
|
|
273
|
+
? 'exit'
|
|
274
|
+
: isEntering || justFinishedEntering
|
|
275
|
+
? 'enter'
|
|
276
|
+
: 'default'
|
|
277
|
+
|
|
278
|
+
const nonAnimatedStyle = {}
|
|
279
|
+
|
|
280
|
+
for (const key in style) {
|
|
281
|
+
const rawVal = style[key]
|
|
282
|
+
// Resolve dynamic theme values (like $theme-dark)
|
|
283
|
+
const val = resolveDynamicValue(rawVal, isDark)
|
|
284
|
+
if (val === undefined) continue
|
|
285
|
+
|
|
286
|
+
if (isDisabled) {
|
|
287
|
+
continue
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (animatedStyleKey[key] == null && !costlyToAnimateStyleKey[key]) {
|
|
291
|
+
nonAnimatedStyle[key] = val
|
|
292
|
+
continue
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (hasTransitionOnly && !animateOnly.includes(key)) {
|
|
296
|
+
nonAnimatedStyle[key] = val
|
|
297
|
+
continue
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (key !== 'transform') {
|
|
301
|
+
animateStyles.current[key] = update(key, animateStyles.current[key], val)
|
|
302
|
+
continue
|
|
303
|
+
}
|
|
304
|
+
// key: 'transform'
|
|
305
|
+
// for now just support one transform key
|
|
306
|
+
if (!val) continue
|
|
307
|
+
if (typeof val === 'string') {
|
|
308
|
+
console.warn(`Warning: Hanzo GUI can't animate string transforms yet!`)
|
|
309
|
+
continue
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
for (const [index, transform] of val.entries()) {
|
|
313
|
+
if (!transform) continue
|
|
314
|
+
// tkey: e.g: 'translateX'
|
|
315
|
+
const tkey = Object.keys(transform)[0]
|
|
316
|
+
const currentTransform = animatedTranforms.current[index]?.[tkey]
|
|
317
|
+
animatedTranforms.current[index] = {
|
|
318
|
+
[tkey]: update(tkey, currentTransform, transform[tkey]),
|
|
319
|
+
}
|
|
320
|
+
animatedTranforms.current = [...animatedTranforms.current]
|
|
321
|
+
}
|
|
322
|
+
}
|
|
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
|
+
|
|
336
|
+
const animatedStyle = {
|
|
337
|
+
...Object.fromEntries(
|
|
338
|
+
Object.entries(animateStyles.current).map(([k, v]) => [
|
|
339
|
+
k,
|
|
340
|
+
animationsState.current!.get(v)?.interpolation || v,
|
|
341
|
+
])
|
|
342
|
+
),
|
|
343
|
+
...animatedTransformStyle,
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return {
|
|
347
|
+
runners,
|
|
348
|
+
completions,
|
|
349
|
+
style: [nonAnimatedStyle, animatedStyle],
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function update(
|
|
353
|
+
key: string,
|
|
354
|
+
animated: Animated.Value | undefined,
|
|
355
|
+
valIn: string | number
|
|
356
|
+
) {
|
|
357
|
+
const isColorStyleKey = colorStyleKey[key]
|
|
358
|
+
const [val, type] = isColorStyleKey ? [0, undefined] : getValue(valIn)
|
|
359
|
+
let animateToValue = val
|
|
360
|
+
const value = animated || new Animated.Value(val)
|
|
361
|
+
const curInterpolation = animationsState.current.get(value)
|
|
362
|
+
|
|
363
|
+
let interpolateArgs: any
|
|
364
|
+
if (type) {
|
|
365
|
+
interpolateArgs = getInterpolated(
|
|
366
|
+
curInterpolation?.current ?? value['_value'],
|
|
367
|
+
val,
|
|
368
|
+
type
|
|
369
|
+
)
|
|
370
|
+
animationsState.current!.set(value, {
|
|
371
|
+
interpolation: value.interpolate(interpolateArgs),
|
|
372
|
+
current: val,
|
|
373
|
+
})
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (isColorStyleKey) {
|
|
377
|
+
animateToValue = curInterpolation?.animateToValue ? 0 : 1
|
|
378
|
+
interpolateArgs = getColorInterpolated(
|
|
379
|
+
curInterpolation?.current as string,
|
|
380
|
+
// valIn is the next color
|
|
381
|
+
valIn as string,
|
|
382
|
+
animateToValue
|
|
383
|
+
)
|
|
384
|
+
animationsState.current!.set(value, {
|
|
385
|
+
current: valIn,
|
|
386
|
+
interpolation: value.interpolate(interpolateArgs),
|
|
387
|
+
animateToValue: curInterpolation?.animateToValue ? 0 : 1,
|
|
388
|
+
})
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (value) {
|
|
392
|
+
const animationConfig = getAnimationConfig(
|
|
393
|
+
key,
|
|
394
|
+
animations,
|
|
395
|
+
props.transition,
|
|
396
|
+
animationState
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
let resolve
|
|
400
|
+
const promise = new Promise<void>((res) => {
|
|
401
|
+
resolve = res
|
|
402
|
+
})
|
|
403
|
+
completions.push(promise)
|
|
404
|
+
|
|
405
|
+
runners.push(() => {
|
|
406
|
+
value.stopAnimation()
|
|
407
|
+
|
|
408
|
+
function getAnimation() {
|
|
409
|
+
return Animated[animationConfig.type || 'spring'](value, {
|
|
410
|
+
toValue: animateToValue,
|
|
411
|
+
useNativeDriver: nativeDriver,
|
|
412
|
+
...animationConfig,
|
|
413
|
+
})
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const animation = animationConfig.delay
|
|
417
|
+
? Animated.sequence([
|
|
418
|
+
Animated.delay(animationConfig.delay),
|
|
419
|
+
getAnimation(),
|
|
420
|
+
])
|
|
421
|
+
: getAnimation()
|
|
422
|
+
|
|
423
|
+
animation.start(({ finished }) => {
|
|
424
|
+
// always resolve during exit (element is leaving anyway)
|
|
425
|
+
// for non-exit, only resolve on successful completion
|
|
426
|
+
if (finished || isExiting) {
|
|
427
|
+
resolve()
|
|
428
|
+
}
|
|
429
|
+
})
|
|
430
|
+
})
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (process.env.NODE_ENV === 'development') {
|
|
434
|
+
if (props['debug'] === 'verbose') {
|
|
435
|
+
// prettier-ignore
|
|
436
|
+
console.info(
|
|
437
|
+
' 💠 animate',
|
|
438
|
+
key,
|
|
439
|
+
`from (${value['_value']}) to`,
|
|
440
|
+
valIn,
|
|
441
|
+
`(${val})`,
|
|
442
|
+
'type',
|
|
443
|
+
type,
|
|
444
|
+
'interpolate',
|
|
445
|
+
interpolateArgs
|
|
446
|
+
)
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
return value
|
|
450
|
+
}
|
|
451
|
+
}, args)
|
|
452
|
+
|
|
453
|
+
// track previous exiting state
|
|
454
|
+
React.useEffect(() => {
|
|
455
|
+
wasExitingRef.current = isExiting
|
|
456
|
+
})
|
|
457
|
+
|
|
458
|
+
useIsomorphicLayoutEffect(() => {
|
|
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
|
+
|
|
474
|
+
let cancel = false
|
|
475
|
+
Promise.all(res.completions).then(() => {
|
|
476
|
+
if (cancel) return
|
|
477
|
+
// guard against stale cycle completion
|
|
478
|
+
if (isExiting && cycleId !== exitCycleIdRef.current) return
|
|
479
|
+
if (isExiting && exitCompletedRef.current) return
|
|
480
|
+
|
|
481
|
+
onDidAnimate?.()
|
|
482
|
+
if (isExiting) {
|
|
483
|
+
exitCompletedRef.current = true
|
|
484
|
+
sendExitComplete?.()
|
|
485
|
+
}
|
|
486
|
+
})
|
|
487
|
+
return () => {
|
|
488
|
+
cancel = true
|
|
489
|
+
}
|
|
490
|
+
}, args)
|
|
491
|
+
|
|
492
|
+
// avoidReRenders: receive style changes imperatively from gui
|
|
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
|
+
|
|
580
|
+
if (process.env.NODE_ENV === 'development') {
|
|
581
|
+
if (props['debug'] === 'verbose') {
|
|
582
|
+
console.info(`Animated`, { response: res, inputStyle: style, isExiting })
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
return res
|
|
587
|
+
},
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function getColorInterpolated(
|
|
592
|
+
currentColor: string | undefined,
|
|
593
|
+
nextColor: string,
|
|
594
|
+
animateToValue: number
|
|
595
|
+
) {
|
|
596
|
+
const inputRange = [0, 1]
|
|
597
|
+
const outputRange = [currentColor ? currentColor : nextColor, nextColor]
|
|
598
|
+
if (animateToValue === 0) {
|
|
599
|
+
// because we are animating from value 1 to 0, we need to put target color at the beginning
|
|
600
|
+
outputRange.reverse()
|
|
601
|
+
}
|
|
602
|
+
return {
|
|
603
|
+
inputRange,
|
|
604
|
+
outputRange,
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function getInterpolated(current: number, next: number, postfix = 'deg') {
|
|
609
|
+
if (next === current) {
|
|
610
|
+
current = next - 0.000000001
|
|
611
|
+
}
|
|
612
|
+
const inputRange = [current, next]
|
|
613
|
+
const outputRange = [`${current}${postfix}`, `${next}${postfix}`]
|
|
614
|
+
if (next < current) {
|
|
615
|
+
inputRange.reverse()
|
|
616
|
+
outputRange.reverse()
|
|
617
|
+
}
|
|
618
|
+
return {
|
|
619
|
+
inputRange,
|
|
620
|
+
outputRange,
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function getAnimationConfig(
|
|
625
|
+
key: string,
|
|
626
|
+
animations: AnimationsConfig,
|
|
627
|
+
transition?: TransitionProp,
|
|
628
|
+
animationState: 'enter' | 'exit' | 'default' = 'default'
|
|
629
|
+
): AnimationConfig {
|
|
630
|
+
const normalized = normalizeTransition(transition)
|
|
631
|
+
const shortKey = transformShorthands[key]
|
|
632
|
+
|
|
633
|
+
// Check for property-specific animation
|
|
634
|
+
const propAnimation = normalized.properties[key] ?? normalized.properties[shortKey]
|
|
635
|
+
|
|
636
|
+
let animationType: string | null = null
|
|
637
|
+
let extraConf: any = {}
|
|
638
|
+
|
|
639
|
+
if (typeof propAnimation === 'string') {
|
|
640
|
+
// Direct animation name: { x: 'quick' }
|
|
641
|
+
animationType = propAnimation
|
|
642
|
+
} else if (propAnimation && typeof propAnimation === 'object') {
|
|
643
|
+
// Config object: { x: { type: 'quick', delay: 100 } }
|
|
644
|
+
// Use effective animation based on state if no explicit type in config
|
|
645
|
+
animationType =
|
|
646
|
+
propAnimation.type || getEffectiveAnimation(normalized, animationState)
|
|
647
|
+
extraConf = propAnimation
|
|
648
|
+
} else {
|
|
649
|
+
// Fall back to effective animation based on state (enter/exit/default)
|
|
650
|
+
animationType = getEffectiveAnimation(normalized, animationState)
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// Apply global delay if no property-specific delay
|
|
654
|
+
if (normalized.delay && !extraConf.delay) {
|
|
655
|
+
extraConf = { ...extraConf, delay: normalized.delay }
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
const found = animationType ? animations[animationType] : {}
|
|
659
|
+
return {
|
|
660
|
+
...found,
|
|
661
|
+
// Apply global spring config overrides (from transition={['bouncy', { stiffness: 1000 }]})
|
|
662
|
+
...normalized.config,
|
|
663
|
+
// Property-specific config takes highest precedence
|
|
664
|
+
...extraConf,
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// try both combos
|
|
669
|
+
const transformShorthands = {
|
|
670
|
+
x: 'translateX',
|
|
671
|
+
y: 'translateY',
|
|
672
|
+
translateX: 'x',
|
|
673
|
+
translateY: 'y',
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function getValue(input: number | string, isColor = false) {
|
|
677
|
+
if (typeof input !== 'string') {
|
|
678
|
+
return [input] as const
|
|
679
|
+
}
|
|
680
|
+
const [_, number, after] = input.match(/([-0-9]+)(deg|%|px)/) ?? []
|
|
681
|
+
return [+number, after] as const
|
|
682
|
+
}
|
package/src/index.ts
ADDED