@tamagui/animations-react-native 1.0.1-beta.98 → 1.0.1-rc.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.
@@ -1,115 +1,332 @@
1
- import { PresenceContext, usePresence } from '@tamagui/animate-presence'
2
- import { AnimationDriver, AnimationProp } from '@tamagui/core'
3
- import { useCallback, useContext, useMemo, useRef, useState } from 'react'
1
+ import {
2
+ AnimatedNumberStrategy,
3
+ AnimationDriver,
4
+ AnimationProp,
5
+ UniversalAnimatedNumber,
6
+ isWeb,
7
+ useEvent,
8
+ useIsomorphicLayoutEffect,
9
+ useSafeRef,
10
+ } from '@tamagui/core'
11
+ import { usePresence } from '@tamagui/use-presence'
12
+ import { useEffect, useMemo } from 'react'
4
13
  import { Animated } from 'react-native'
5
14
 
6
15
  type AnimationsConfig<A extends Object = any> = {
7
16
  [Key in keyof A]: AnimationConfig
8
17
  }
9
18
 
10
- type AnimationConfig = {}
11
- // | ({ type: 'timing'; loop?: number; repeat?: number; repeatReverse?: boolean } & WithTimingConfig)
12
- // | ({ type: 'spring'; loop?: number; repeat?: number; repeatReverse?: boolean } & WithSpringConfig)
13
- // | ({ type: 'decay'; loop?: number; repeat?: number; repeatReverse?: boolean } & WithDecayConfig)
19
+ type AnimationConfig = Partial<
20
+ Pick<
21
+ Animated.SpringAnimationConfig,
22
+ | 'delay'
23
+ | 'bounciness'
24
+ | 'damping'
25
+ | 'friction'
26
+ | 'mass'
27
+ | 'overshootClamping'
28
+ | 'speed'
29
+ | 'stiffness'
30
+ | 'tension'
31
+ | 'velocity'
32
+ >
33
+ >
14
34
 
15
- export function createAnimations<A extends AnimationsConfig>(animations: A): AnimationDriver<A> {
16
- const AnimatedView = Animated.View
17
- const AnimatedText = Animated.Text
35
+ const animatedStyleKey = {
36
+ transform: true,
37
+ opacity: true,
38
+ }
39
+
40
+ export const AnimatedView = Animated.View
41
+ export const AnimatedText = Animated.Text
42
+
43
+ export function useAnimatedNumber(initial: number): UniversalAnimatedNumber<Animated.Value> {
44
+ const state = useSafeRef(
45
+ null as any as {
46
+ val: Animated.Value
47
+ composite: Animated.CompositeAnimation | null
48
+ strategy: AnimatedNumberStrategy
49
+ }
50
+ )
51
+ if (!state.current) {
52
+ state.current = {
53
+ composite: null,
54
+ val: new Animated.Value(initial),
55
+ strategy: { type: 'spring' },
56
+ }
57
+ }
58
+
59
+ return {
60
+ getInstance() {
61
+ return state.current.val
62
+ },
63
+ getValue() {
64
+ return state.current.val['_value']
65
+ },
66
+ stop() {
67
+ state.current.composite?.stop()
68
+ state.current.composite = null
69
+ },
70
+ setValue(next: number, { type, ...config } = { type: 'spring' }) {
71
+ const val = state.current.val
72
+ if (type === 'direct') {
73
+ val.setValue(next)
74
+ } else if (type === 'spring') {
75
+ state.current.composite?.stop()
76
+ const composite = Animated.spring(val, {
77
+ ...config,
78
+ toValue: next,
79
+ useNativeDriver: !isWeb,
80
+ })
81
+ composite.start()
82
+ state.current.composite = composite
83
+ } else {
84
+ state.current.composite?.stop()
85
+ const composite = Animated.timing(val, {
86
+ ...config,
87
+ toValue: next,
88
+ useNativeDriver: !isWeb,
89
+ })
90
+ composite.start()
91
+ state.current.composite = composite
92
+ }
93
+ },
94
+ }
95
+ }
18
96
 
97
+ export function useAnimatedNumberReaction(
98
+ value: UniversalAnimatedNumber<Animated.Value>,
99
+ cb: (current: number) => void
100
+ ) {
101
+ const onChange = useEvent((current) => {
102
+ cb(current.value)
103
+ })
104
+
105
+ useEffect(() => {
106
+ const id = value.getInstance().addListener(onChange)
107
+ return () => {
108
+ value.getInstance().removeListener(id)
109
+ }
110
+ }, [value, onChange])
111
+ }
112
+
113
+ export function useAnimatedNumberStyle<V extends UniversalAnimatedNumber<Animated.Value>>(
114
+ value: V,
115
+ getStyle: (value: any) => any
116
+ ) {
117
+ return getStyle(value.getInstance())
118
+ }
119
+
120
+ export function createAnimations<A extends AnimationsConfig>(animations: A): AnimationDriver<A> {
19
121
  AnimatedView['displayName'] = 'AnimatedView'
20
122
  AnimatedText['displayName'] = 'AnimatedText'
21
123
 
22
124
  return {
23
- avoidClasses: true,
125
+ isReactNative: true,
24
126
  animations,
25
127
  View: AnimatedView,
26
128
  Text: AnimatedText,
27
- useAnimations: (props, helpers) => {
28
- const { pseudos, onDidAnimate, delay, getStyle, state, staticConfig } = helpers
29
- const [isPresent, sendExitComplete] = usePresence()
30
- const presence = useContext(PresenceContext)
31
-
32
- const exitStyle = presence?.exitVariant
33
- ? staticConfig.variantsParsed?.[presence.exitVariant]?.true || pseudos.exitStyle
34
- : pseudos.exitStyle
35
-
36
- const onDidAnimateCb = useCallback<NonNullable<typeof onDidAnimate>>(
37
- (...args) => {
38
- onDidAnimate?.(...args)
39
- },
40
- [onDidAnimate]
41
- )
42
-
43
- const isExiting = isPresent === false
44
- const isEntering = !state.mounted
45
-
46
- const all = getStyle({
47
- isExiting,
48
- isEntering,
49
- exitVariant: presence?.exitVariant,
50
- enterVariant: presence?.enterVariant,
51
- })
52
-
53
- const animatedValues = useRef<Record<string, Animated.Value>>({})
54
-
55
- // TODO loop and create values, run them if they change
56
-
57
- const [animatedStyles, nonAnimatedStyle] = [{}, {}]
58
- const animatedStyleKey = {
59
- transform: true,
60
- opacity: true,
129
+ useAnimatedNumber,
130
+ useAnimatedNumberReaction,
131
+ useAnimatedNumberStyle,
132
+ usePresence,
133
+ useAnimations: ({ props, onDidAnimate, style, state, presence }) => {
134
+ const isExiting = presence?.[0] === false
135
+ const sendExitComplete = presence?.[1]
136
+ const mergedStyles = style
137
+ const animateStyles = useSafeRef<Record<string, Animated.Value>>({})
138
+ const animatedTranforms = useSafeRef<{ [key: string]: Animated.Value }[]>([])
139
+ const animationsState = useSafeRef<
140
+ WeakMap<
141
+ Animated.Value,
142
+ {
143
+ interopolation: Animated.AnimatedInterpolation
144
+ current?: number | undefined
145
+ }
146
+ >
147
+ >(null as any)
148
+ if (!animationsState.current) {
149
+ animationsState.current = new WeakMap()
61
150
  }
62
- for (const key of Object.keys(all)) {
63
- if (animatedStyleKey[key]) {
64
- animatedStyles[key] = all[key]
65
- } else {
66
- nonAnimatedStyle[key] = all[key]
151
+
152
+ const runners: Function[] = []
153
+ const completions: Promise<void>[] = []
154
+
155
+ // const args = [JSON.stringify(mergedStyles)]
156
+ const args = [JSON.stringify(mergedStyles), JSON.stringify(state), isExiting, !!onDidAnimate]
157
+
158
+ const res = useMemo(() => {
159
+ const nonAnimatedStyle = {}
160
+ for (const key in mergedStyles) {
161
+ const val = mergedStyles[key]
162
+ if (!animatedStyleKey[key]) {
163
+ nonAnimatedStyle[key] = val
164
+ continue
165
+ }
166
+ if (key !== 'transform') {
167
+ animateStyles.current[key] = update(key, animateStyles.current[key], val)
168
+ continue
169
+ }
170
+ // key: 'transform'
171
+ // for now just support one transform key
172
+ if (!val) continue
173
+ for (const [index, transform] of val.entries()) {
174
+ if (!transform) continue
175
+ const tkey = Object.keys(transform)[0]
176
+ const currentTransform = animatedTranforms.current[index]?.[tkey]
177
+ animatedTranforms.current[index] = {
178
+ [tkey]: update(tkey, currentTransform, transform[tkey]),
179
+ }
180
+ animatedTranforms.current = [...animatedTranforms.current]
181
+ }
182
+ }
183
+
184
+ const animatedStyle = {
185
+ ...Object.fromEntries(
186
+ Object.entries({
187
+ ...animateStyles.current,
188
+ }).map(([k, v]) => [k, animationsState.current!.get(v)?.interopolation || v])
189
+ ),
190
+ transform: animatedTranforms.current.map((r) => {
191
+ const key = Object.keys(r)[0]
192
+ const val = animationsState.current!.get(r[key])?.interopolation || r[key]
193
+ return { [key]: val }
194
+ }),
67
195
  }
68
- }
69
196
 
70
- const animatedStyle = animatedStyleKey
71
-
72
- const args = [
73
- JSON.stringify(all),
74
- state.mounted,
75
- state.hover,
76
- state.press,
77
- state.pressIn,
78
- state.focus,
79
- delay,
80
- isPresent,
81
- onDidAnimate,
82
- onDidAnimateCb,
83
- presence?.exitVariant,
84
- presence?.enterVariant,
85
- ]
86
-
87
- // const callback = (
88
- // isExiting: boolean,
89
- // exitingStyleProps: Record<string, boolean>,
90
- // key: string,
91
- // value: any
92
- // ) => {
93
- // return (completed, current) => {
94
- // onDidAnimateCb(key, completed, current, {
95
- // attemptedValue: value,
96
- // })
97
- // if (isExiting) {
98
- // exitingStyleProps[key] = false
99
- // const areStylesExiting = Object.values(exitingStyleProps).some(Boolean)
100
- // // if this is true, then we've finished our exit animations
101
- // if (!areStylesExiting) {
102
- // sendExitComplete?.()
103
- // }
104
- // }
105
- // }
106
- // }
107
-
108
- return useMemo(() => {
109
197
  return {
110
198
  style: [nonAnimatedStyle, animatedStyle],
111
199
  }
200
+
201
+ function update(key: string, animated: Animated.Value | undefined, valIn: string | number) {
202
+ const [val, type] = getValue(valIn)
203
+ const value = animated || new Animated.Value(val)
204
+ let interpolateArgs: any
205
+ if (type) {
206
+ const curInterpolation = animationsState.current.get(value)
207
+ interpolateArgs = getInterpolated(
208
+ curInterpolation?.current ?? value['_value'],
209
+ val,
210
+ type
211
+ )
212
+ animationsState.current!.set(value, {
213
+ interopolation: value.interpolate(interpolateArgs),
214
+ current: val,
215
+ })
216
+ }
217
+ if (value) {
218
+ const animationConfig = getAnimationConfig(key, animations, props.animation)
219
+
220
+ let resolve
221
+ const promise = new Promise<void>((res) => {
222
+ resolve = res
223
+ })
224
+ completions.push(promise)
225
+
226
+ runners.push(() => {
227
+ value.stopAnimation()
228
+ Animated.spring(value, {
229
+ toValue: val,
230
+ useNativeDriver: !isWeb,
231
+ ...animationConfig,
232
+ }).start(({ finished }) => {
233
+ if (finished) {
234
+ resolve()
235
+ }
236
+ })
237
+ })
238
+ }
239
+ if (process.env.NODE_ENV === 'development') {
240
+ if (props['debug']) {
241
+ // prettier-ignore
242
+ // eslint-disable-next-line no-console
243
+ console.log(' 💠 animate', key, `from ${value['_value']} to`, valIn, `(${val})`, 'type', type, 'interpolate', interpolateArgs)
244
+ }
245
+ }
246
+ return value
247
+ }
248
+ // eslint-disable-next-line react-hooks/exhaustive-deps
112
249
  }, args)
250
+
251
+ useIsomorphicLayoutEffect(() => {
252
+ runners.forEach((r) => r())
253
+ let cancel = false
254
+ Promise.all(completions).then(() => {
255
+ if (cancel) return
256
+ onDidAnimate?.()
257
+ if (isExiting) {
258
+ sendExitComplete?.()
259
+ }
260
+ })
261
+ return () => {
262
+ cancel = true
263
+ }
264
+ }, args)
265
+
266
+ if (process.env.NODE_ENV === 'development') {
267
+ if (props['debug']) {
268
+ // eslint-disable-next-line no-console
269
+ console.log(`Returning animated`, res)
270
+ }
271
+ }
272
+
273
+ return res
113
274
  },
114
275
  }
115
276
  }
277
+
278
+ function getInterpolated(current: number, next: number, postfix = 'deg') {
279
+ if (next === current) {
280
+ current = next - 0.000000001
281
+ }
282
+ const inputRange = [current, next]
283
+ const outputRange = [`${current}${postfix}`, `${next}${postfix}`]
284
+ if (next < current) {
285
+ inputRange.reverse()
286
+ outputRange.reverse()
287
+ }
288
+ return {
289
+ inputRange,
290
+ outputRange,
291
+ }
292
+ }
293
+
294
+ function getAnimationConfig(key: string, animations: AnimationsConfig, animation?: AnimationProp) {
295
+ if (typeof animation === 'string') {
296
+ return animations[animation]
297
+ }
298
+ let type = ''
299
+ let extraConf: any
300
+ if (Array.isArray(animation)) {
301
+ type = animation[0] as string
302
+ const conf = animation[1] && animation[1][key]
303
+ if (conf) {
304
+ if (typeof conf === 'string') {
305
+ type = conf
306
+ } else {
307
+ type = (conf as any).type || type
308
+ extraConf = conf
309
+ }
310
+ }
311
+ } else {
312
+ const val = animation?.[key]
313
+ type = val?.type
314
+ extraConf = val
315
+ }
316
+ const found = animations[type]
317
+ if (!found) {
318
+ throw new Error(`No animation of type "${type}" for key "${key}"`)
319
+ }
320
+ return {
321
+ ...found,
322
+ ...extraConf,
323
+ }
324
+ }
325
+
326
+ function getValue(input: number | string) {
327
+ if (typeof input !== 'string') {
328
+ return [input] as const
329
+ }
330
+ const [_, number, after] = input.match(/([-0-9]+)(deg|%|px)/) ?? []
331
+ return [+number, after] as const
332
+ }
package/src/index.tsx CHANGED
@@ -1 +1,3 @@
1
+ import './polyfill'
2
+
1
3
  export * from './createAnimations'
@@ -0,0 +1,4 @@
1
+ // for SSR
2
+ if (typeof requestAnimationFrame === 'undefined') {
3
+ globalThis['requestAnimationFrame'] = setImmediate
4
+ }
@@ -1,8 +1,14 @@
1
- import { AnimationDriver } from '@tamagui/core';
1
+ import { AnimationDriver, UniversalAnimatedNumber } from '@tamagui/core';
2
+ import { Animated } from 'react-native';
2
3
  declare type AnimationsConfig<A extends Object = any> = {
3
4
  [Key in keyof A]: AnimationConfig;
4
5
  };
5
- declare type AnimationConfig = {};
6
+ declare type AnimationConfig = Partial<Pick<Animated.SpringAnimationConfig, 'delay' | 'bounciness' | 'damping' | 'friction' | 'mass' | 'overshootClamping' | 'speed' | 'stiffness' | 'tension' | 'velocity'>>;
7
+ export declare const AnimatedView: Animated.AnimatedComponent<typeof import("react-native").View>;
8
+ export declare const AnimatedText: Animated.AnimatedComponent<typeof import("react-native").Text>;
9
+ export declare function useAnimatedNumber(initial: number): UniversalAnimatedNumber<Animated.Value>;
10
+ export declare function useAnimatedNumberReaction(value: UniversalAnimatedNumber<Animated.Value>, cb: (current: number) => void): void;
11
+ export declare function useAnimatedNumberStyle<V extends UniversalAnimatedNumber<Animated.Value>>(value: V, getStyle: (value: any) => any): any;
6
12
  export declare function createAnimations<A extends AnimationsConfig>(animations: A): AnimationDriver<A>;
7
13
  export {};
8
14
  //# sourceMappingURL=createAnimations.d.ts.map
package/types/index.d.ts CHANGED
@@ -1,2 +1,3 @@
1
+ import './polyfill';
1
2
  export * from './createAnimations';
2
3
  //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ //# sourceMappingURL=polyfill.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"createAnimations.d.ts","sourceRoot":"","sources":["../src/createAnimations.tsx"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAiB,MAAM,eAAe,CAAA;AAI9D,aAAK,gBAAgB,CAAC,CAAC,SAAS,MAAM,GAAG,GAAG,IAAI;KAC7C,GAAG,IAAI,MAAM,CAAC,GAAG,eAAe;CAClC,CAAA;AAED,aAAK,eAAe,GAAG,EAAE,CAAA;AAKzB,wBAAgB,gBAAgB,CAAC,CAAC,SAAS,gBAAgB,EAAE,UAAU,EAAE,CAAC,GAAG,eAAe,CAAC,CAAC,CAAC,CAoG9F"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"AAAA,cAAc,oBAAoB,CAAA"}