@rootnative/inertia 0.0.0-alpha.0 → 0.0.0-alpha.2
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/CHANGELOG.md +10 -0
- package/README.md +2 -0
- package/dist/gestureLayer/index.d.mts +48 -7
- package/dist/gestureLayer/index.d.ts +48 -7
- package/dist/gestureLayer/index.js +107 -30
- package/dist/gestureLayer/index.mjs +107 -30
- package/dist/index.d.mts +127 -16
- package/dist/index.d.ts +127 -16
- package/dist/index.js +177 -62
- package/dist/index.mjs +175 -63
- package/dist/motion/Image.d.mts +1 -1
- package/dist/motion/Image.d.ts +1 -1
- package/dist/motion/Image.js +90 -50
- package/dist/motion/Image.mjs +90 -50
- package/dist/motion/Pressable.d.mts +1 -1
- package/dist/motion/Pressable.d.ts +1 -1
- package/dist/motion/Pressable.js +90 -50
- package/dist/motion/Pressable.mjs +90 -50
- package/dist/motion/ScrollView.d.mts +1 -1
- package/dist/motion/ScrollView.d.ts +1 -1
- package/dist/motion/ScrollView.js +90 -50
- package/dist/motion/ScrollView.mjs +90 -50
- package/dist/motion/Text.d.mts +1 -1
- package/dist/motion/Text.d.ts +1 -1
- package/dist/motion/Text.js +90 -50
- package/dist/motion/Text.mjs +90 -50
- package/dist/motion/View.d.mts +1 -1
- package/dist/motion/View.d.ts +1 -1
- package/dist/motion/View.js +90 -50
- package/dist/motion/View.mjs +90 -50
- package/dist/testing/index.js +0 -2
- package/dist/testing/index.mjs +0 -2
- package/dist/touch/index.d.mts +1 -1
- package/dist/touch/index.d.ts +1 -1
- package/dist/touch/index.js +0 -2
- package/dist/touch/index.mjs +0 -2
- package/dist/{types-cU43dEmH.d.mts → types-BzEgiUdJ.d.mts} +58 -9
- package/dist/{types-cU43dEmH.d.ts → types-BzEgiUdJ.d.ts} +58 -9
- package/dist/{useGesture-BnY65PlQ.d.ts → useGesture-BRLgiNOC.d.ts} +6 -4
- package/dist/{useGesture-DxtXdz-K.d.mts → useGesture-C0GBS7d2.d.mts} +6 -4
- package/llms.txt +9 -5
- package/package.json +2 -1
- package/src/config/MotionConfig.tsx +55 -9
- package/src/config/MotionConfigContext.ts +21 -1
- package/src/config/index.ts +7 -1
- package/src/config/namedTransitions.ts +80 -0
- package/src/gestureLayer/index.ts +1 -0
- package/src/gestureLayer/useGestureLayer.ts +92 -8
- package/src/index.ts +20 -2
- package/src/motion/createMotionComponent.tsx +36 -6
- package/src/transitions/cubicBezier.ts +126 -0
- package/src/transitions/index.ts +1 -0
- package/src/types.ts +63 -8
- package/src/values/useAnimation.ts +12 -6
- package/src/values/useBooleanSpring.ts +5 -3
- package/src/values/useGesture.ts +25 -7
- package/src/values/useMotionValue.ts +11 -0
- package/src/values/useScroll.ts +8 -1
- package/src/values/useSpring.ts +46 -9
- package/dist/gestureLayer/index.js.map +0 -1
- package/dist/gestureLayer/index.mjs.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/index.mjs.map +0 -1
- package/dist/motion/Image.js.map +0 -1
- package/dist/motion/Image.mjs.map +0 -1
- package/dist/motion/Pressable.js.map +0 -1
- package/dist/motion/Pressable.mjs.map +0 -1
- package/dist/motion/ScrollView.js.map +0 -1
- package/dist/motion/ScrollView.mjs.map +0 -1
- package/dist/motion/Text.js.map +0 -1
- package/dist/motion/Text.mjs.map +0 -1
- package/dist/motion/View.js.map +0 -1
- package/dist/motion/View.mjs.map +0 -1
- package/dist/testing/index.js.map +0 -1
- package/dist/testing/index.mjs.map +0 -1
- package/dist/touch/index.js.map +0 -1
- package/dist/touch/index.mjs.map +0 -1
- package/src/__type-tests__/animate.test-d.tsx +0 -88
- package/src/__type-tests__/variants.test-d.tsx +0 -67
|
@@ -4,11 +4,24 @@ import {
|
|
|
4
4
|
useAnimatedStyle,
|
|
5
5
|
useSharedValue,
|
|
6
6
|
type AnimatedStyle,
|
|
7
|
+
type SharedValue,
|
|
7
8
|
} from 'react-native-reanimated'
|
|
8
|
-
import {
|
|
9
|
-
|
|
9
|
+
import {
|
|
10
|
+
resolveNamedTransitionProp,
|
|
11
|
+
useNamedTransitions,
|
|
12
|
+
useShouldReduceMotion,
|
|
13
|
+
} from '../config'
|
|
14
|
+
import {
|
|
15
|
+
isTopLevelTransition,
|
|
16
|
+
resolveTransition,
|
|
17
|
+
stableSig,
|
|
18
|
+
} from '../transitions'
|
|
10
19
|
import { useGesture, type UseGestureHandlers } from '../values/useGesture'
|
|
11
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
type GestureLayerTransitions,
|
|
22
|
+
type TransitionConfig,
|
|
23
|
+
type TransitionInput,
|
|
24
|
+
} from '../types'
|
|
12
25
|
|
|
13
26
|
/**
|
|
14
27
|
* A single gesture-layer style — a flat map of style keys to a value. Numeric
|
|
@@ -57,10 +70,32 @@ export interface UseGestureLayerOptions {
|
|
|
57
70
|
/**
|
|
58
71
|
* Transition forwarded to the underlying `useGesture` hook. Either a single
|
|
59
72
|
* `TransitionConfig` for every gesture layer, or a `GestureLayerTransitions`
|
|
60
|
-
* map for per-layer fades.
|
|
61
|
-
*
|
|
73
|
+
* map for per-layer fades. A `TransitionName` registered on the nearest
|
|
74
|
+
* `<MotionConfig transitions>` is accepted in both positions. Reduced
|
|
75
|
+
* motion collapses every transition to `no-animation`.
|
|
62
76
|
*/
|
|
63
|
-
transition?:
|
|
77
|
+
transition?: TransitionInput | GestureLayerTransitions
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The per-state 0↔1 progress shared values behind a `useGestureLayer` call.
|
|
82
|
+
* The gesture entries are the same shared values `useGesture` owns; `disabled`
|
|
83
|
+
* is driven by `options.disabled`. All five are stable across renders — feed
|
|
84
|
+
* them into any `useAnimatedStyle` / `useDerivedValue` (or hooks like
|
|
85
|
+
* `useShadow({ progress })`) to derive extra styles from the same gesture
|
|
86
|
+
* wiring without a second `useGesture` call.
|
|
87
|
+
*/
|
|
88
|
+
export interface GestureLayerProgress {
|
|
89
|
+
/** 0↔1 progress for the hovered layer (web only — stays at 0 on native). */
|
|
90
|
+
hovered: SharedValue<number>
|
|
91
|
+
/** 0↔1 progress for the focused layer (any focus modality). */
|
|
92
|
+
focused: SharedValue<number>
|
|
93
|
+
/** 0↔1 progress for the focusVisible layer (keyboard focus only). */
|
|
94
|
+
focusVisible: SharedValue<number>
|
|
95
|
+
/** 0↔1 progress for the pressed layer. */
|
|
96
|
+
pressed: SharedValue<number>
|
|
97
|
+
/** 0↔1 progress for the disabled override (driven by `options.disabled`). */
|
|
98
|
+
disabled: SharedValue<number>
|
|
64
99
|
}
|
|
65
100
|
|
|
66
101
|
export interface UseGestureLayerResult {
|
|
@@ -71,6 +106,12 @@ export interface UseGestureLayerResult {
|
|
|
71
106
|
style: AnimatedStyle<Record<string, unknown>>
|
|
72
107
|
/** Handlers to spread on the receiving `Pressable`. */
|
|
73
108
|
handlers: UseGestureHandlers
|
|
109
|
+
/**
|
|
110
|
+
* Per-state progress shared values — the inputs the composed `style` is
|
|
111
|
+
* derived from. Read-only by convention: writing to them fights the
|
|
112
|
+
* handlers.
|
|
113
|
+
*/
|
|
114
|
+
states: GestureLayerProgress
|
|
74
115
|
}
|
|
75
116
|
|
|
76
117
|
/**
|
|
@@ -94,6 +135,13 @@ export interface UseGestureLayerResult {
|
|
|
94
135
|
* keys — when active, it lerps the composed value toward the `disabled`
|
|
95
136
|
* target.
|
|
96
137
|
*
|
|
138
|
+
* Alongside `style` and `handlers`, the hook returns `states` — the per-state
|
|
139
|
+
* 0↔1 progress shared values the style is derived from (the four gesture
|
|
140
|
+
* layers plus the disabled override). Feed them into other progress-driven
|
|
141
|
+
* hooks (`useShadow`, `useColorTransition`) or a custom `useAnimatedStyle`
|
|
142
|
+
* to derive extra styles from the same gesture wiring — no second
|
|
143
|
+
* `useGesture` call, no duplicated handlers.
|
|
144
|
+
*
|
|
97
145
|
* Reach for this when you want MD3 / iOS-translucent state-layer overlays
|
|
98
146
|
* without rewriting the worklet by hand for every consumer; reach for plain
|
|
99
147
|
* `useGesture()` when you need a composition model this hook doesn't
|
|
@@ -123,23 +171,41 @@ export interface UseGestureLayerResult {
|
|
|
123
171
|
* )
|
|
124
172
|
* }
|
|
125
173
|
* ```
|
|
174
|
+
*
|
|
175
|
+
* @example Elevation crossfade driven by the hover progress
|
|
176
|
+
* ```tsx
|
|
177
|
+
* const { style, handlers, states } = useGestureLayer(layers, options)
|
|
178
|
+
* const shadow = useShadow({ from: restShadow, to: raisedShadow, progress: states.hovered })
|
|
179
|
+
* // <Animated.View style={[style, shadow]} />
|
|
180
|
+
* ```
|
|
126
181
|
*/
|
|
127
182
|
export function useGestureLayer(
|
|
128
183
|
states: GestureLayerStates,
|
|
129
184
|
options: UseGestureLayerOptions = {},
|
|
130
185
|
): UseGestureLayerResult {
|
|
131
|
-
const { disabled: isDisabled = false, transition } = options
|
|
186
|
+
const { disabled: isDisabled = false, transition: transitionInput } = options
|
|
132
187
|
const shouldReduceMotion = useShouldReduceMotion()
|
|
188
|
+
// Resolve registered names here (not just in useGesture) because the
|
|
189
|
+
// `disabled` layer reads the top-level transition locally. Identity is
|
|
190
|
+
// preserved when no names are present; when a name resolves, the effect
|
|
191
|
+
// below keys on the structural signature so a per-render resolve of the
|
|
192
|
+
// map form doesn't re-fire it.
|
|
193
|
+
const transition = resolveNamedTransitionProp(
|
|
194
|
+
transitionInput,
|
|
195
|
+
useNamedTransitions(),
|
|
196
|
+
)
|
|
133
197
|
const gesture = useGesture(transition)
|
|
134
198
|
const disabledProgress = useSharedValue(0)
|
|
135
199
|
|
|
200
|
+
const transitionSig = stableSig(transition)
|
|
136
201
|
useEffect(() => {
|
|
137
202
|
const target = isDisabled ? 1 : 0
|
|
138
203
|
const cfg = shouldReduceMotion
|
|
139
204
|
? ({ type: 'no-animation' } as const)
|
|
140
205
|
: (disabledTransition(transition) ?? ({ type: 'spring' } as const))
|
|
141
206
|
disabledProgress.value = resolveTransition(cfg, target) as never
|
|
142
|
-
|
|
207
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
208
|
+
}, [isDisabled, shouldReduceMotion, transitionSig, disabledProgress])
|
|
143
209
|
|
|
144
210
|
// JS-thread precompute: union of keys across all layers, per-key type
|
|
145
211
|
// (number vs color), and a rest-fallback table. The worklet body reads
|
|
@@ -270,9 +336,27 @@ export function useGestureLayer(
|
|
|
270
336
|
return out
|
|
271
337
|
})
|
|
272
338
|
|
|
339
|
+
const progress = useMemo<GestureLayerProgress>(
|
|
340
|
+
() => ({
|
|
341
|
+
hovered: gesture.hovered,
|
|
342
|
+
focused: gesture.focused,
|
|
343
|
+
focusVisible: gesture.focusVisible,
|
|
344
|
+
pressed: gesture.pressed,
|
|
345
|
+
disabled: disabledProgress,
|
|
346
|
+
}),
|
|
347
|
+
[
|
|
348
|
+
gesture.hovered,
|
|
349
|
+
gesture.focused,
|
|
350
|
+
gesture.focusVisible,
|
|
351
|
+
gesture.pressed,
|
|
352
|
+
disabledProgress,
|
|
353
|
+
],
|
|
354
|
+
)
|
|
355
|
+
|
|
273
356
|
return {
|
|
274
357
|
style: style as AnimatedStyle<Record<string, unknown>>,
|
|
275
358
|
handlers: gesture.handlers,
|
|
359
|
+
states: progress,
|
|
276
360
|
}
|
|
277
361
|
}
|
|
278
362
|
|
package/src/index.ts
CHANGED
|
@@ -18,12 +18,23 @@ export {
|
|
|
18
18
|
MotionScrollView,
|
|
19
19
|
createMotionComponent,
|
|
20
20
|
} from './motion'
|
|
21
|
-
export {
|
|
22
|
-
|
|
21
|
+
export {
|
|
22
|
+
MotionConfig,
|
|
23
|
+
resolveNamedTransition,
|
|
24
|
+
useMotionConfig,
|
|
25
|
+
useNamedTransitions,
|
|
26
|
+
useShouldReduceMotion,
|
|
27
|
+
} from './config'
|
|
28
|
+
export type {
|
|
29
|
+
MotionConfigProps,
|
|
30
|
+
MotionConfigValue,
|
|
31
|
+
ReducedMotion,
|
|
32
|
+
} from './config'
|
|
23
33
|
export { Presence, usePresence } from './presence'
|
|
24
34
|
export type { PresenceContextValue } from './presence'
|
|
25
35
|
export {
|
|
26
36
|
buildReleaseAnimation,
|
|
37
|
+
cubicBezier,
|
|
27
38
|
resolveTransition,
|
|
28
39
|
resolveAnimatableValue,
|
|
29
40
|
ensureWorkletEasing,
|
|
@@ -56,17 +67,24 @@ export type {
|
|
|
56
67
|
AnimateStyle,
|
|
57
68
|
AnimationCallbackInfo,
|
|
58
69
|
DecayTransition,
|
|
70
|
+
EasingFunction,
|
|
71
|
+
EasingFunctionFactory,
|
|
72
|
+
EasingInput,
|
|
59
73
|
GestureSubStates,
|
|
60
74
|
MotionComponent,
|
|
61
75
|
MotionProps,
|
|
76
|
+
NamedTransitions,
|
|
62
77
|
NoAnimationTransition,
|
|
63
78
|
PerPropertyTransition,
|
|
79
|
+
RegisteredTransitions,
|
|
64
80
|
RepeatConfig,
|
|
65
81
|
SequenceStep,
|
|
66
82
|
SpringTransition,
|
|
67
83
|
TimingTransition,
|
|
68
84
|
Transition,
|
|
69
85
|
TransitionConfig,
|
|
86
|
+
TransitionInput,
|
|
87
|
+
TransitionName,
|
|
70
88
|
VariantController,
|
|
71
89
|
VariantsMap,
|
|
72
90
|
} from './types'
|
|
@@ -14,7 +14,12 @@ import Animated, {
|
|
|
14
14
|
type SharedValue,
|
|
15
15
|
} from 'react-native-reanimated'
|
|
16
16
|
import { type LayoutChangeEvent } from 'react-native'
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
lookupNamedTransition,
|
|
19
|
+
resolveNamedTransitionProp,
|
|
20
|
+
useNamedTransitions,
|
|
21
|
+
useShouldReduceMotion,
|
|
22
|
+
} from '../config'
|
|
18
23
|
import { isFocusVisible } from '../gestures'
|
|
19
24
|
import {
|
|
20
25
|
resolveLayoutTransition,
|
|
@@ -167,11 +172,16 @@ const DEFAULT_RESTING: Record<AnimatableKey, number | string> = {
|
|
|
167
172
|
shadowOffsetHeight: 0,
|
|
168
173
|
}
|
|
169
174
|
|
|
175
|
+
// Both lookups run after the factory has resolved registered transition
|
|
176
|
+
// names into concrete configs (`resolveNamedTransitionProp`), so the
|
|
177
|
+
// `TransitionInput` values on the map forms are narrowed to `TransitionConfig`
|
|
178
|
+
// here — a string can no longer reach these call sites at runtime.
|
|
170
179
|
function transitionFor<S>(
|
|
171
180
|
prop: keyof S,
|
|
172
181
|
transition: Transition<S> | undefined,
|
|
173
182
|
): TransitionConfig | undefined {
|
|
174
183
|
if (!transition) return undefined
|
|
184
|
+
if (typeof transition === 'string') return undefined
|
|
175
185
|
if (isTopLevelTransition(transition)) return transition
|
|
176
186
|
// Gesture-layer keys (`pressed`, `hovered`, …) live on the same map as
|
|
177
187
|
// per-property keys; skip them when looking up a property transition so a
|
|
@@ -179,7 +189,9 @@ function transitionFor<S>(
|
|
|
179
189
|
// style key named `pressed` (none currently exist, but keep the lookup
|
|
180
190
|
// honest).
|
|
181
191
|
if (GESTURE_LAYER_NAME_SET.has(prop as string)) return undefined
|
|
182
|
-
return (transition as PerPropertyTransition<S>)[prop]
|
|
192
|
+
return (transition as PerPropertyTransition<S>)[prop] as
|
|
193
|
+
| TransitionConfig
|
|
194
|
+
| undefined
|
|
183
195
|
}
|
|
184
196
|
|
|
185
197
|
function gestureLayerTransitionFor<S>(
|
|
@@ -187,8 +199,11 @@ function gestureLayerTransitionFor<S>(
|
|
|
187
199
|
transition: Transition<S> | undefined,
|
|
188
200
|
): TransitionConfig | undefined {
|
|
189
201
|
if (!transition) return undefined
|
|
202
|
+
if (typeof transition === 'string') return undefined
|
|
190
203
|
if (isTopLevelTransition(transition)) return transition
|
|
191
|
-
return (transition as GestureLayerTransitions)[layer]
|
|
204
|
+
return (transition as GestureLayerTransitions)[layer] as
|
|
205
|
+
| TransitionConfig
|
|
206
|
+
| undefined
|
|
192
207
|
}
|
|
193
208
|
|
|
194
209
|
/**
|
|
@@ -223,11 +238,11 @@ export function createMotionComponent<C extends ComponentType<any>>(
|
|
|
223
238
|
initial,
|
|
224
239
|
animate,
|
|
225
240
|
exit,
|
|
226
|
-
transition,
|
|
241
|
+
transition: transitionProp,
|
|
227
242
|
variants,
|
|
228
243
|
controller,
|
|
229
244
|
gesture,
|
|
230
|
-
layout,
|
|
245
|
+
layout: layoutProp,
|
|
231
246
|
layoutId,
|
|
232
247
|
onAnimationEnd,
|
|
233
248
|
style,
|
|
@@ -235,11 +250,26 @@ export function createMotionComponent<C extends ComponentType<any>>(
|
|
|
235
250
|
...rest
|
|
236
251
|
} = props as Props & {
|
|
237
252
|
style?: unknown
|
|
238
|
-
layout?: LayoutProp
|
|
253
|
+
layout?: LayoutProp | string
|
|
239
254
|
layoutId?: string
|
|
240
255
|
onLayout?: (event: LayoutChangeEvent) => void
|
|
241
256
|
}
|
|
242
257
|
|
|
258
|
+
// Resolve registered transition names (from the nearest <MotionConfig
|
|
259
|
+
// transitions>) into concrete configs before anything downstream touches
|
|
260
|
+
// the props. Resolution is JS-thread and identity-preserving when no
|
|
261
|
+
// names are present; when names resolve, the registry entries are stable
|
|
262
|
+
// objects, so every signature-keyed memo below stays warm.
|
|
263
|
+
const namedTransitions = useNamedTransitions()
|
|
264
|
+
const transition = resolveNamedTransitionProp(
|
|
265
|
+
transitionProp as Transition<Record<string, unknown>> | undefined,
|
|
266
|
+
namedTransitions,
|
|
267
|
+
)
|
|
268
|
+
const layout: LayoutProp =
|
|
269
|
+
typeof layoutProp === 'string'
|
|
270
|
+
? lookupNamedTransition(layoutProp, namedTransitions)
|
|
271
|
+
: layoutProp
|
|
272
|
+
|
|
243
273
|
// Function-form `style={(state) => ...}` is the Pressable render-prop API.
|
|
244
274
|
// Inertia drives press/focus state through `gesture.*` and merges its own
|
|
245
275
|
// animated style; a function passed here lands inside a style array where
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { Easing } from 'react-native-reanimated'
|
|
2
|
+
import { type EasingInput } from '../types'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The CSS easing keywords and their canonical cubic-bezier control points
|
|
6
|
+
* (W3C css-easing-1). `linear` is special-cased to Reanimated's identity
|
|
7
|
+
* easing rather than a degenerate bezier.
|
|
8
|
+
*/
|
|
9
|
+
const CSS_KEYWORDS: Record<string, readonly [number, number, number, number]> =
|
|
10
|
+
{
|
|
11
|
+
ease: [0.25, 0.1, 0.25, 1],
|
|
12
|
+
'ease-in': [0.42, 0, 1, 1],
|
|
13
|
+
'ease-out': [0, 0, 0.58, 1],
|
|
14
|
+
'ease-in-out': [0.42, 0, 0.58, 1],
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const CSS_FUNCTION = /^cubic-bezier\((.*)\)$/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Build a `timing.easing` value from cubic-bezier control points — as four
|
|
21
|
+
* numbers, as a W3C CSS `cubic-bezier(...)` string, or as a CSS easing
|
|
22
|
+
* keyword. Design systems store easing tokens in the CSS format; this helper
|
|
23
|
+
* makes any such token file directly consumable without hand-translating to
|
|
24
|
+
* `Easing.bezier` calls:
|
|
25
|
+
*
|
|
26
|
+
* ```ts
|
|
27
|
+
* cubicBezier(0.2, 0, 0, 1) // number form
|
|
28
|
+
* cubicBezier('cubic-bezier(0.2, 0, 0, 1)') // CSS token form
|
|
29
|
+
* cubicBezier('ease-out') // CSS keywords, incl. 'linear'
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* The return value is exactly what `timing.easing` accepts (Reanimated's
|
|
33
|
+
* bezier factory; the resolver worklet-wraps it via `ensureWorkletEasing`
|
|
34
|
+
* like every other easing input), so it works in the `transition` prop, in
|
|
35
|
+
* named transitions registered on `<MotionConfig transitions>`, and in the
|
|
36
|
+
* value-layer hooks:
|
|
37
|
+
*
|
|
38
|
+
* ```tsx
|
|
39
|
+
* <MotionConfig
|
|
40
|
+
* transitions={{
|
|
41
|
+
* 'state-hover': {
|
|
42
|
+
* type: 'timing',
|
|
43
|
+
* duration: 150,
|
|
44
|
+
* easing: cubicBezier(theme.motion.easingStandard),
|
|
45
|
+
* },
|
|
46
|
+
* }}
|
|
47
|
+
* >
|
|
48
|
+
* ```
|
|
49
|
+
*
|
|
50
|
+
* Invalid input **throws** rather than warning: easing tokens are constructed
|
|
51
|
+
* at theme/module setup, and a malformed token should fail loudly there, not
|
|
52
|
+
* silently animate with the wrong curve. Per the CSS spec, `x1` / `x2` must
|
|
53
|
+
* be within `[0, 1]` (`y1` / `y2` are unrestricted). The stepping keywords
|
|
54
|
+
* (`step-start` / `step-end`) and the CSS `linear(...)` function are not
|
|
55
|
+
* easing curves this helper produces — they're intentionally unsupported.
|
|
56
|
+
*/
|
|
57
|
+
export function cubicBezier(
|
|
58
|
+
x1: number,
|
|
59
|
+
y1: number,
|
|
60
|
+
x2: number,
|
|
61
|
+
y2: number,
|
|
62
|
+
): EasingInput
|
|
63
|
+
export function cubicBezier(css: string): EasingInput
|
|
64
|
+
export function cubicBezier(
|
|
65
|
+
first: number | string,
|
|
66
|
+
y1?: number,
|
|
67
|
+
x2?: number,
|
|
68
|
+
y2?: number,
|
|
69
|
+
): EasingInput {
|
|
70
|
+
if (typeof first === 'string') return fromCss(first)
|
|
71
|
+
return bezier(first, y1 as number, x2 as number, y2 as number, undefined)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function fromCss(input: string): EasingInput {
|
|
75
|
+
const token = input.trim().toLowerCase()
|
|
76
|
+
if (token === 'linear') return Easing.linear
|
|
77
|
+
const keyword = CSS_KEYWORDS[token]
|
|
78
|
+
if (keyword) return bezier(...keyword, input)
|
|
79
|
+
const match = CSS_FUNCTION.exec(token)
|
|
80
|
+
if (!match) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
`[inertia] cubicBezier: unsupported easing token ${JSON.stringify(input)}. ` +
|
|
83
|
+
`Expected four numbers, a 'cubic-bezier(x1, y1, x2, y2)' string, or ` +
|
|
84
|
+
`one of the CSS keywords 'linear' | 'ease' | 'ease-in' | 'ease-out' ` +
|
|
85
|
+
`| 'ease-in-out'.`,
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
const parts = match[1]!.split(',').map((p) => Number(p.trim()))
|
|
89
|
+
if (parts.length !== 4 || parts.some((n) => !Number.isFinite(n))) {
|
|
90
|
+
throw new Error(
|
|
91
|
+
`[inertia] cubicBezier: could not parse ${JSON.stringify(input)} — ` +
|
|
92
|
+
`expected exactly four finite numbers inside cubic-bezier(...).`,
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
return bezier(parts[0]!, parts[1]!, parts[2]!, parts[3]!, input)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function bezier(
|
|
99
|
+
x1: number,
|
|
100
|
+
y1: number,
|
|
101
|
+
x2: number,
|
|
102
|
+
y2: number,
|
|
103
|
+
source: string | undefined,
|
|
104
|
+
): EasingInput {
|
|
105
|
+
const describe = () =>
|
|
106
|
+
source !== undefined
|
|
107
|
+
? JSON.stringify(source)
|
|
108
|
+
: `cubicBezier(${x1}, ${y1}, ${x2}, ${y2})`
|
|
109
|
+
for (const n of [x1, y1, x2, y2]) {
|
|
110
|
+
if (typeof n !== 'number' || !Number.isFinite(n)) {
|
|
111
|
+
throw new Error(
|
|
112
|
+
`[inertia] cubicBezier: ${describe()} — every control point must be ` +
|
|
113
|
+
`a finite number.`,
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// CSS (and the underlying bezier solver) require the curve to be a function
|
|
118
|
+
// of time: x1 / x2 within [0, 1]. y values are unrestricted (overshoot).
|
|
119
|
+
if (x1 < 0 || x1 > 1 || x2 < 0 || x2 > 1) {
|
|
120
|
+
throw new Error(
|
|
121
|
+
`[inertia] cubicBezier: ${describe()} — x1 and x2 must be within ` +
|
|
122
|
+
`[0, 1] (got x1=${x1}, x2=${x2}).`,
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
return Easing.bezier(x1, y1, x2, y2)
|
|
126
|
+
}
|
package/src/transitions/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { resolveTransition } from './resolve'
|
|
2
2
|
export { resolveAnimatableValue } from './resolveSequence'
|
|
3
|
+
export { cubicBezier } from './cubicBezier'
|
|
3
4
|
export { ensureWorkletEasing } from './easing'
|
|
4
5
|
export { isTopLevelTransition, TRANSITION_CONFIG_KEYS } from './keys'
|
|
5
6
|
export { buildReleaseAnimation } from './runtime'
|
package/src/types.ts
CHANGED
|
@@ -77,6 +77,57 @@ export type TransitionConfig =
|
|
|
77
77
|
| DecayTransition
|
|
78
78
|
| NoAnimationTransition
|
|
79
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Augmentation point for typed named transitions. Empty by default — the
|
|
82
|
+
* registry is data fed to `<MotionConfig transitions={...}>` at runtime, so
|
|
83
|
+
* out of the box any string is accepted wherever a `TransitionName` is.
|
|
84
|
+
*
|
|
85
|
+
* Consumers who want their registered names to autocomplete (and typos to be
|
|
86
|
+
* compile errors) augment this interface with their design system's names:
|
|
87
|
+
*
|
|
88
|
+
* ```ts
|
|
89
|
+
* declare module '@rootnative/inertia' {
|
|
90
|
+
* interface RegisteredTransitions {
|
|
91
|
+
* 'state-hover': TransitionConfig
|
|
92
|
+
* 'selection': TransitionConfig
|
|
93
|
+
* }
|
|
94
|
+
* }
|
|
95
|
+
* ```
|
|
96
|
+
*
|
|
97
|
+
* The value type is ignored — only the keys matter. Inertia itself never
|
|
98
|
+
* declares a name here: presets are consumer data, not library surface.
|
|
99
|
+
*/
|
|
100
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
101
|
+
export interface RegisteredTransitions {}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* A named transition registered on the nearest `<MotionConfig transitions>`.
|
|
105
|
+
* Accepted anywhere a `TransitionConfig` is: the `transition` prop (top-level
|
|
106
|
+
* and per-property), the `layout` prop, and the value-layer hooks. Resolution
|
|
107
|
+
* happens on the JS thread against the nearest provider; unknown names warn
|
|
108
|
+
* in dev and fall back to the library default spring.
|
|
109
|
+
*
|
|
110
|
+
* Plain `string` until `RegisteredTransitions` is augmented; with an
|
|
111
|
+
* augmentation in place it narrows to the registered keys.
|
|
112
|
+
*/
|
|
113
|
+
export type TransitionName = keyof RegisteredTransitions extends never
|
|
114
|
+
? string
|
|
115
|
+
: keyof RegisteredTransitions & string
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The registry shape accepted by `<MotionConfig transitions={...}>`: named
|
|
119
|
+
* `TransitionConfig`s. Names are consumer vocabulary (design tokens, semantic
|
|
120
|
+
* roles); Inertia ships no presets.
|
|
121
|
+
*/
|
|
122
|
+
export type NamedTransitions = Partial<Record<TransitionName, TransitionConfig>>
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* A transition given either inline (`TransitionConfig`) or by registered name
|
|
126
|
+
* (`TransitionName`). This is the input type everywhere the public surface
|
|
127
|
+
* accepts a single transition.
|
|
128
|
+
*/
|
|
129
|
+
export type TransitionInput = TransitionConfig | TransitionName
|
|
130
|
+
|
|
80
131
|
/**
|
|
81
132
|
* Repeat config — one shape, not three flags. Default `alternate: true`.
|
|
82
133
|
*/
|
|
@@ -91,7 +142,7 @@ export type RepeatConfig =
|
|
|
91
142
|
* here.
|
|
92
143
|
*/
|
|
93
144
|
export type PerPropertyTransition<S> = {
|
|
94
|
-
[K in keyof S]?:
|
|
145
|
+
[K in keyof S]?: TransitionInput
|
|
95
146
|
}
|
|
96
147
|
|
|
97
148
|
/**
|
|
@@ -104,14 +155,14 @@ export type PerPropertyTransition<S> = {
|
|
|
104
155
|
* collide with the primitive's inferred style keys.
|
|
105
156
|
*/
|
|
106
157
|
export interface GestureLayerTransitions {
|
|
107
|
-
pressed?:
|
|
108
|
-
focused?:
|
|
109
|
-
focusVisible?:
|
|
110
|
-
hovered?:
|
|
158
|
+
pressed?: TransitionInput
|
|
159
|
+
focused?: TransitionInput
|
|
160
|
+
focusVisible?: TransitionInput
|
|
161
|
+
hovered?: TransitionInput
|
|
111
162
|
}
|
|
112
163
|
|
|
113
164
|
export type Transition<S> =
|
|
114
|
-
|
|
|
165
|
+
| TransitionInput
|
|
115
166
|
| (PerPropertyTransition<S> & GestureLayerTransitions)
|
|
116
167
|
|
|
117
168
|
/**
|
|
@@ -293,7 +344,9 @@ export interface MotionProps<C, V extends VariantsMap<C> = VariantsMap<C>> {
|
|
|
293
344
|
gesture?: GestureSubStates<C>
|
|
294
345
|
/**
|
|
295
346
|
* Per-property or top-level transition config. Per-property entries take
|
|
296
|
-
* precedence over the top-level transition.
|
|
347
|
+
* precedence over the top-level transition. Anywhere a config object is
|
|
348
|
+
* accepted (top-level, per-property, per gesture layer) a `TransitionName`
|
|
349
|
+
* registered on the nearest `<MotionConfig transitions>` is accepted too.
|
|
297
350
|
*/
|
|
298
351
|
transition?: Transition<AnimateStyle<C>>
|
|
299
352
|
/**
|
|
@@ -305,6 +358,8 @@ export interface MotionProps<C, V extends VariantsMap<C> = VariantsMap<C>> {
|
|
|
305
358
|
* - `true` — animate with the library's default spring.
|
|
306
359
|
* - `TransitionConfig` — spring (react-spring vocab) or timing config; the
|
|
307
360
|
* resolver bridges to Reanimated's `LinearTransition` builder.
|
|
361
|
+
* - `TransitionName` — a name registered on the nearest
|
|
362
|
+
* `<MotionConfig transitions>`.
|
|
308
363
|
* - omitted / `false` — no layout animation (default).
|
|
309
364
|
*
|
|
310
365
|
* Only `'spring'` / `'timing'` / `'no-animation'` map to layout transitions
|
|
@@ -316,7 +371,7 @@ export interface MotionProps<C, V extends VariantsMap<C> = VariantsMap<C>> {
|
|
|
316
371
|
* own layout changes, `layoutId` animates from a different element's
|
|
317
372
|
* last measured rect to this element's current rect.
|
|
318
373
|
*/
|
|
319
|
-
layout?: boolean |
|
|
374
|
+
layout?: boolean | TransitionInput
|
|
320
375
|
/**
|
|
321
376
|
* Shared-element transition id. When a Motion primitive with `layoutId`
|
|
322
377
|
* unmounts, its last on-screen rect is recorded under that id; the next
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import { useEffect } from 'react'
|
|
2
2
|
import { useSharedValue, type SharedValue } from 'react-native-reanimated'
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
resolveNamedTransition,
|
|
5
|
+
useNamedTransitions,
|
|
6
|
+
useShouldReduceMotion,
|
|
7
|
+
} from '../config'
|
|
4
8
|
import { resolveTransition, stableSig } from '../transitions'
|
|
5
|
-
import { type
|
|
9
|
+
import { type TransitionInput } from '../types'
|
|
6
10
|
|
|
7
11
|
/**
|
|
8
12
|
* Drive a `SharedValue<number>` toward `target` with **any** transition shape
|
|
9
|
-
* — spring, timing, decay, or no-animation
|
|
13
|
+
* — spring, timing, decay, or no-animation — or a `TransitionName` registered
|
|
14
|
+
* on the nearest `<MotionConfig transitions>`. The general-purpose value-layer
|
|
10
15
|
* hook: reach for it when you need raw `useSharedValue + useEffect + withX`
|
|
11
16
|
* outside the declarative `animate` flow.
|
|
12
17
|
*
|
|
@@ -50,16 +55,17 @@ import { type TransitionConfig } from '../types'
|
|
|
50
55
|
*/
|
|
51
56
|
export function useAnimation(
|
|
52
57
|
target: number,
|
|
53
|
-
transition?:
|
|
58
|
+
transition?: TransitionInput,
|
|
54
59
|
): SharedValue<number> {
|
|
55
60
|
const output = useSharedValue<number>(target)
|
|
56
61
|
const shouldReduceMotion = useShouldReduceMotion()
|
|
57
|
-
const
|
|
62
|
+
const resolved = resolveNamedTransition(transition, useNamedTransitions())
|
|
63
|
+
const cfgSig = stableSig(resolved)
|
|
58
64
|
|
|
59
65
|
useEffect(() => {
|
|
60
66
|
const cfg = shouldReduceMotion
|
|
61
67
|
? ({ type: 'no-animation' } as const)
|
|
62
|
-
: (
|
|
68
|
+
: (resolved ?? ({ type: 'spring' } as const))
|
|
63
69
|
output.value = resolveTransition(cfg, target) as never
|
|
64
70
|
// `output` is identity-stable per hook instance.
|
|
65
71
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type SharedValue } from 'react-native-reanimated'
|
|
2
2
|
import { useSpring } from './useSpring'
|
|
3
|
-
import { type SpringTransition } from '../types'
|
|
3
|
+
import { type SpringTransition, type TransitionName } from '../types'
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Toggle a 0↔1 progress value with a spring whenever `active` flips.
|
|
@@ -23,11 +23,13 @@ import { type SpringTransition } from '../types'
|
|
|
23
23
|
*
|
|
24
24
|
* The spring config follows the same react-spring vocabulary as the rest of
|
|
25
25
|
* the library (`tension` / `friction` / `mass`); omit it to take the
|
|
26
|
-
* library's defaults
|
|
26
|
+
* library's defaults, or pass a `TransitionName` registered on the nearest
|
|
27
|
+
* `<MotionConfig transitions>` (spring-typed names only — see `useSpring`
|
|
28
|
+
* for the non-spring fallback behaviour).
|
|
27
29
|
*/
|
|
28
30
|
export function useBooleanSpring(
|
|
29
31
|
active: boolean,
|
|
30
|
-
springConfig?: SpringTransition,
|
|
32
|
+
springConfig?: SpringTransition | TransitionName,
|
|
31
33
|
): SharedValue<number> {
|
|
32
34
|
return useSpring(active ? 1 : 0, springConfig)
|
|
33
35
|
}
|