@onlynative/inertia 0.0.1-alpha.2 → 0.0.1-alpha.4
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/README.md +44 -3
- package/dist/index.d.mts +259 -3
- package/dist/index.d.ts +259 -3
- package/dist/index.js +1866 -161
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1864 -165
- package/dist/index.mjs.map +1 -1
- package/dist/motion/Image.d.mts +1 -1
- package/dist/motion/Image.d.ts +1 -1
- package/dist/motion/Image.js +1696 -146
- package/dist/motion/Image.js.map +1 -1
- package/dist/motion/Image.mjs +1698 -148
- package/dist/motion/Image.mjs.map +1 -1
- package/dist/motion/Pressable.d.mts +1 -1
- package/dist/motion/Pressable.d.ts +1 -1
- package/dist/motion/Pressable.js +1696 -146
- package/dist/motion/Pressable.js.map +1 -1
- package/dist/motion/Pressable.mjs +1698 -148
- package/dist/motion/Pressable.mjs.map +1 -1
- package/dist/motion/ScrollView.d.mts +1 -1
- package/dist/motion/ScrollView.d.ts +1 -1
- package/dist/motion/ScrollView.js +1696 -146
- package/dist/motion/ScrollView.js.map +1 -1
- package/dist/motion/ScrollView.mjs +1698 -148
- package/dist/motion/ScrollView.mjs.map +1 -1
- package/dist/motion/Text.d.mts +1 -1
- package/dist/motion/Text.d.ts +1 -1
- package/dist/motion/Text.js +1696 -146
- package/dist/motion/Text.js.map +1 -1
- package/dist/motion/Text.mjs +1698 -148
- package/dist/motion/Text.mjs.map +1 -1
- package/dist/motion/View.d.mts +1 -1
- package/dist/motion/View.d.ts +1 -1
- package/dist/motion/View.js +1696 -146
- package/dist/motion/View.js.map +1 -1
- package/dist/motion/View.mjs +1698 -148
- package/dist/motion/View.mjs.map +1 -1
- package/dist/{types-DeZZzE_e.d.mts → types-CjztO3RW.d.mts} +89 -20
- package/dist/{types-DeZZzE_e.d.ts → types-CjztO3RW.d.ts} +89 -20
- package/llms.txt +54 -6
- package/package.json +1 -1
- package/src/__type-tests__/animate.test-d.tsx +88 -0
- package/src/index.ts +16 -1
- package/src/layout/index.ts +1 -0
- package/src/layout/resolveLayout.ts +54 -0
- package/src/motion/createMotionComponent.tsx +292 -153
- package/src/motion/installCheck.ts +69 -0
- package/src/transitions/easing.ts +3 -1
- package/src/transitions/index.ts +3 -0
- package/src/transitions/keys.ts +32 -0
- package/src/transitions/resolve.ts +1 -24
- package/src/transitions/sig.ts +40 -0
- package/src/transitions/spring.ts +41 -0
- package/src/types.ts +96 -18
- package/src/values/index.ts +14 -0
- package/src/values/useAnimation.ts +69 -0
- package/src/values/useGesture.ts +144 -0
- package/src/values/useMotionValue.ts +33 -0
- package/src/values/useScroll.ts +72 -0
- package/src/values/useSpring.ts +93 -0
- package/src/values/useTransform.ts +132 -0
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
useState,
|
|
8
8
|
} from 'react'
|
|
9
9
|
import Animated, {
|
|
10
|
+
interpolateColor,
|
|
10
11
|
runOnJS,
|
|
11
12
|
useAnimatedStyle,
|
|
12
13
|
useSharedValue,
|
|
@@ -14,12 +15,20 @@ import Animated, {
|
|
|
14
15
|
} from 'react-native-reanimated'
|
|
15
16
|
import { useShouldReduceMotion } from '../config'
|
|
16
17
|
import { isFocusVisible } from '../gestures'
|
|
18
|
+
import { resolveLayoutTransition, type LayoutProp } from '../layout'
|
|
17
19
|
import { usePresence } from '../presence'
|
|
18
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
isTopLevelTransition,
|
|
22
|
+
resolveAnimatableValue,
|
|
23
|
+
resolveTransition,
|
|
24
|
+
stableSig,
|
|
25
|
+
} from '../transitions'
|
|
26
|
+
import { ensureReanimatedInstalled } from './installCheck'
|
|
19
27
|
import {
|
|
20
28
|
type AnimatableValue,
|
|
21
29
|
type AnimateStyle,
|
|
22
30
|
type AnimationCallbackInfo,
|
|
31
|
+
type GestureLayerTransitions,
|
|
23
32
|
type GestureSubStates,
|
|
24
33
|
type MotionComponent,
|
|
25
34
|
type MotionProps,
|
|
@@ -42,8 +51,17 @@ const TRANSFORM_KEYS = [
|
|
|
42
51
|
'scaleX',
|
|
43
52
|
'scaleY',
|
|
44
53
|
'rotate',
|
|
54
|
+
'rotateX',
|
|
55
|
+
'rotateY',
|
|
45
56
|
] as const
|
|
46
57
|
|
|
58
|
+
// Rotation keys land in the transform array as `{ rotate: '45deg' }` / `{
|
|
59
|
+
// rotateX: '45deg' }` etc. — Reanimated needs the unit-suffixed string form.
|
|
60
|
+
// We hold the underlying shared value as a plain number (degrees) and wrap
|
|
61
|
+
// in the worklet so the resolver pipeline stays uniform with the other
|
|
62
|
+
// numeric transform keys.
|
|
63
|
+
const ROTATION_KEYS = new Set<string>(['rotate', 'rotateX', 'rotateY'])
|
|
64
|
+
|
|
47
65
|
const NUMERIC_TOP_LEVEL_KEYS = [
|
|
48
66
|
'opacity',
|
|
49
67
|
'width',
|
|
@@ -84,6 +102,16 @@ type AnimatableKey = (typeof ALL_KEYS)[number]
|
|
|
84
102
|
type TransformKey = (typeof TRANSFORM_KEYS)[number]
|
|
85
103
|
|
|
86
104
|
const TRANSFORM_KEY_SET = new Set<AnimatableKey>(TRANSFORM_KEYS)
|
|
105
|
+
const COLOR_KEY_SET = new Set<AnimatableKey>(COLOR_KEYS)
|
|
106
|
+
|
|
107
|
+
const GESTURE_LAYER_NAMES = [
|
|
108
|
+
'hovered',
|
|
109
|
+
'focused',
|
|
110
|
+
'focusVisible',
|
|
111
|
+
'pressed',
|
|
112
|
+
] as const
|
|
113
|
+
type GestureLayerName = (typeof GESTURE_LAYER_NAMES)[number]
|
|
114
|
+
const GESTURE_LAYER_NAME_SET = new Set<string>(GESTURE_LAYER_NAMES)
|
|
87
115
|
|
|
88
116
|
// Stable style object applied while a Motion primitive is mid-exit so taps
|
|
89
117
|
// fall through. Hoisted so every render shares the same reference and
|
|
@@ -97,6 +125,8 @@ const DEFAULT_RESTING: Record<AnimatableKey, number | string> = {
|
|
|
97
125
|
scaleX: 1,
|
|
98
126
|
scaleY: 1,
|
|
99
127
|
rotate: 0,
|
|
128
|
+
rotateX: 0,
|
|
129
|
+
rotateY: 0,
|
|
100
130
|
opacity: 1,
|
|
101
131
|
width: 0,
|
|
102
132
|
height: 0,
|
|
@@ -111,38 +141,30 @@ const DEFAULT_RESTING: Record<AnimatableKey, number | string> = {
|
|
|
111
141
|
tintColor: 'transparent',
|
|
112
142
|
}
|
|
113
143
|
|
|
114
|
-
const TRANSITION_KEYS = new Set([
|
|
115
|
-
'type',
|
|
116
|
-
'tension',
|
|
117
|
-
'friction',
|
|
118
|
-
'mass',
|
|
119
|
-
'velocity',
|
|
120
|
-
'restSpeedThreshold',
|
|
121
|
-
'restDisplacementThreshold',
|
|
122
|
-
'duration',
|
|
123
|
-
'easing',
|
|
124
|
-
'delay',
|
|
125
|
-
'repeat',
|
|
126
|
-
'deceleration',
|
|
127
|
-
'clamp',
|
|
128
|
-
])
|
|
129
|
-
|
|
130
|
-
function isTopLevelTransition(t: unknown): t is TransitionConfig {
|
|
131
|
-
if (t === null || typeof t !== 'object') return false
|
|
132
|
-
const keys = Object.keys(t as object)
|
|
133
|
-
if (keys.length === 0) return false
|
|
134
|
-
return keys.every((k) => TRANSITION_KEYS.has(k))
|
|
135
|
-
}
|
|
136
|
-
|
|
137
144
|
function transitionFor<S>(
|
|
138
145
|
prop: keyof S,
|
|
139
146
|
transition: Transition<S> | undefined,
|
|
140
147
|
): TransitionConfig | undefined {
|
|
141
148
|
if (!transition) return undefined
|
|
142
149
|
if (isTopLevelTransition(transition)) return transition
|
|
150
|
+
// Gesture-layer keys (`pressed`, `hovered`, …) live on the same map as
|
|
151
|
+
// per-property keys; skip them when looking up a property transition so a
|
|
152
|
+
// user who wires `transition.pressed` doesn't accidentally apply that to a
|
|
153
|
+
// style key named `pressed` (none currently exist, but keep the lookup
|
|
154
|
+
// honest).
|
|
155
|
+
if (GESTURE_LAYER_NAME_SET.has(prop as string)) return undefined
|
|
143
156
|
return (transition as PerPropertyTransition<S>)[prop]
|
|
144
157
|
}
|
|
145
158
|
|
|
159
|
+
function gestureLayerTransitionFor<S>(
|
|
160
|
+
layer: GestureLayerName,
|
|
161
|
+
transition: Transition<S> | undefined,
|
|
162
|
+
): TransitionConfig | undefined {
|
|
163
|
+
if (!transition) return undefined
|
|
164
|
+
if (isTopLevelTransition(transition)) return transition
|
|
165
|
+
return (transition as GestureLayerTransitions)[layer]
|
|
166
|
+
}
|
|
167
|
+
|
|
146
168
|
/**
|
|
147
169
|
* Factory that wraps a React Native primitive as a `Motion.*` component.
|
|
148
170
|
*
|
|
@@ -154,10 +176,17 @@ function transitionFor<S>(
|
|
|
154
176
|
* borderRadius) and color properties (backgroundColor, borderColor, color,
|
|
155
177
|
* tintColor) applied via Reanimated shared values + `useAnimatedStyle`.
|
|
156
178
|
*/
|
|
179
|
+
// `ComponentType<any>` is React's canonical "accept any component" idiom.
|
|
180
|
+
// `unknown` doesn't work — props need to widen to whatever `C` actually accepts
|
|
181
|
+
// at the call site. The two `any` uses below are deliberate.
|
|
182
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
157
183
|
export function createMotionComponent<C extends ComponentType<any>>(
|
|
158
184
|
Component: C,
|
|
159
185
|
): MotionComponent<C> {
|
|
186
|
+
ensureReanimatedInstalled()
|
|
187
|
+
|
|
160
188
|
const AnimatedComponent = Animated.createAnimatedComponent(
|
|
189
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
161
190
|
Component as ComponentType<any>,
|
|
162
191
|
)
|
|
163
192
|
|
|
@@ -172,10 +201,11 @@ export function createMotionComponent<C extends ComponentType<any>>(
|
|
|
172
201
|
variants,
|
|
173
202
|
controller,
|
|
174
203
|
gesture,
|
|
204
|
+
layout,
|
|
175
205
|
onAnimationEnd,
|
|
176
206
|
style,
|
|
177
207
|
...rest
|
|
178
|
-
} = props as Props & { style?: unknown }
|
|
208
|
+
} = props as Props & { style?: unknown; layout?: LayoutProp }
|
|
179
209
|
|
|
180
210
|
// <Presence> contract: when an ancestor flips `isPresent` to false the
|
|
181
211
|
// child stays rendered until `safeToRemove` is called, giving the exit
|
|
@@ -220,10 +250,10 @@ export function createMotionComponent<C extends ComponentType<any>>(
|
|
|
220
250
|
>)
|
|
221
251
|
: undefined
|
|
222
252
|
|
|
223
|
-
// Gesture sub-state activation tracked as JS state
|
|
224
|
-
// the
|
|
225
|
-
//
|
|
226
|
-
//
|
|
253
|
+
// Gesture sub-state activation tracked as JS state. Activation flips drive
|
|
254
|
+
// the per-layer progress shared values (0↔1); they intentionally do NOT
|
|
255
|
+
// re-run the value-driving effect — gesture sub-state targets live on the
|
|
256
|
+
// worklet's composition chain, not on the base `animate` SV.
|
|
227
257
|
const [pressed, setPressed] = useState(false)
|
|
228
258
|
const [focused, setFocused] = useState(false)
|
|
229
259
|
const [focusVisible, setFocusVisible] = useState(false)
|
|
@@ -233,7 +263,8 @@ export function createMotionComponent<C extends ComponentType<any>>(
|
|
|
233
263
|
// variants in play the union across all variants is what matters — a key
|
|
234
264
|
// touched by any variant must be active so the worklet picks it up when
|
|
235
265
|
// the controller transitions. Gesture sub-states join the same union so
|
|
236
|
-
// pressed/focused/hovered targets can drive any key they
|
|
266
|
+
// pressed/focused/focusVisible/hovered targets can drive any key they
|
|
267
|
+
// declare even when the base `animate` doesn't touch it.
|
|
237
268
|
const activeKeysRef = useRef<readonly AnimatableKey[] | null>(null)
|
|
238
269
|
if (activeKeysRef.current === null) {
|
|
239
270
|
const touched = new Set<AnimatableKey>()
|
|
@@ -285,24 +316,46 @@ export function createMotionComponent<C extends ComponentType<any>>(
|
|
|
285
316
|
)
|
|
286
317
|
})
|
|
287
318
|
|
|
288
|
-
//
|
|
289
|
-
//
|
|
290
|
-
//
|
|
291
|
-
//
|
|
319
|
+
// One progress SV per gesture layer, allocated unconditionally for hook
|
|
320
|
+
// stability. Each layer's progress animates 0↔1 with its own transition
|
|
321
|
+
// when its activation flips; the worklet reads them when compositing.
|
|
322
|
+
// Initial value is 0 — even if a sub-state is somehow active on mount,
|
|
323
|
+
// the activation effect below will animate it to 1 on the next tick.
|
|
324
|
+
const pressedProgress = useSharedValue(0)
|
|
325
|
+
const focusedProgress = useSharedValue(0)
|
|
326
|
+
const focusVisibleProgress = useSharedValue(0)
|
|
327
|
+
const hoveredProgress = useSharedValue(0)
|
|
328
|
+
|
|
329
|
+
// Mirror gesture targets into a UI-runtime-resident shared value so the
|
|
330
|
+
// animated-style worklet can read the latest layer values without having
|
|
331
|
+
// to capture `gesture` directly (which would re-register the worklet on
|
|
332
|
+
// every render where the consumer passes a fresh literal). The signature
|
|
333
|
+
// dependency means we only push to the SV when targets actually change —
|
|
334
|
+
// the SV ref itself is stable across renders.
|
|
292
335
|
//
|
|
293
|
-
//
|
|
294
|
-
//
|
|
295
|
-
|
|
336
|
+
// The resolved value is a layer-keyed map of primitive endpoints (numbers
|
|
337
|
+
// or color strings); sequence/`{ to }` step shapes on a sub-state collapse
|
|
338
|
+
// to their final endpoint via `targetEndValue` because a gesture layer
|
|
339
|
+
// describes a steady target, not a keyframe sequence.
|
|
340
|
+
const gestureSV = useSharedValue<ResolvedGestureLayers | null>(
|
|
341
|
+
resolveGestureLayers(gesture),
|
|
342
|
+
)
|
|
343
|
+
const gestureTargetsSig = stableSig(gesture)
|
|
344
|
+
useEffect(() => {
|
|
345
|
+
gestureSV.value = resolveGestureLayers(gesture)
|
|
346
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
347
|
+
}, [gestureTargetsSig])
|
|
348
|
+
|
|
349
|
+
// The base record drives the per-key shared values. Gesture sub-state
|
|
350
|
+
// targets are intentionally NOT merged here — they layer on top in the
|
|
351
|
+
// worklet. Exit values still take precedence over `animate` while exiting
|
|
352
|
+
// because the base SV is what <Presence> waits on to settle.
|
|
353
|
+
const baseRecord =
|
|
296
354
|
isExiting && exitRecord
|
|
297
355
|
? { ...animateRecord, ...exitRecord }
|
|
298
|
-
:
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
focusVisible,
|
|
302
|
-
hovered,
|
|
303
|
-
})
|
|
304
|
-
const mergedSig =
|
|
305
|
-
stableSig(mergedRecord) +
|
|
356
|
+
: animateRecord
|
|
357
|
+
const baseSig =
|
|
358
|
+
stableSig(baseRecord) +
|
|
306
359
|
(isExiting ? '|exit' : '') +
|
|
307
360
|
(shouldReduceMotion ? '|rm' : '')
|
|
308
361
|
const transitionSig = stableSig(transition)
|
|
@@ -337,7 +390,7 @@ export function createMotionComponent<C extends ComponentType<any>>(
|
|
|
337
390
|
// the factory skip the coalescing branch entirely.
|
|
338
391
|
let transformPending = 0
|
|
339
392
|
for (const k of ALL_KEYS) {
|
|
340
|
-
if (TRANSFORM_KEY_SET.has(k) &&
|
|
393
|
+
if (TRANSFORM_KEY_SET.has(k) && baseRecord[k] !== undefined) {
|
|
341
394
|
transformPending++
|
|
342
395
|
}
|
|
343
396
|
}
|
|
@@ -345,7 +398,7 @@ export function createMotionComponent<C extends ComponentType<any>>(
|
|
|
345
398
|
transformPending > 0 ? { remaining: transformPending } : undefined
|
|
346
399
|
|
|
347
400
|
for (const key of ALL_KEYS) {
|
|
348
|
-
const target =
|
|
401
|
+
const target = baseRecord[key]
|
|
349
402
|
if (target === undefined) continue
|
|
350
403
|
// Reduced-motion overrides every per-key transition (and any nested
|
|
351
404
|
// sequence-step transition) with `no-animation`, which the resolver
|
|
@@ -382,18 +435,120 @@ export function createMotionComponent<C extends ComponentType<any>>(
|
|
|
382
435
|
safeToRemoveRef.current?.()
|
|
383
436
|
}
|
|
384
437
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
385
|
-
}, [
|
|
438
|
+
}, [baseSig, transitionSig])
|
|
439
|
+
|
|
440
|
+
// Per-layer progress: when a sub-state activation flips, animate its
|
|
441
|
+
// progress SV 0↔1 with the layer's own transition (or the parent
|
|
442
|
+
// transition / library default, in priority order). On exit we snap every
|
|
443
|
+
// layer to 0 instantly so the unmount-bound base SV isn't fighting a
|
|
444
|
+
// stale layer contribution mid-fade.
|
|
445
|
+
//
|
|
446
|
+
// The `declared` flag short-circuits the effect when the consumer hasn't
|
|
447
|
+
// wired the corresponding sub-state — so a Motion primitive without a
|
|
448
|
+
// `gesture` prop (or with only some sub-states declared) makes zero extra
|
|
449
|
+
// `withSpring` / `withTiming` calls on mount.
|
|
450
|
+
useGestureLayerProgress(
|
|
451
|
+
pressedProgress,
|
|
452
|
+
pressed,
|
|
453
|
+
gesture?.pressed != null,
|
|
454
|
+
'pressed',
|
|
455
|
+
transition,
|
|
456
|
+
isExiting,
|
|
457
|
+
shouldReduceMotion,
|
|
458
|
+
)
|
|
459
|
+
useGestureLayerProgress(
|
|
460
|
+
focusedProgress,
|
|
461
|
+
focused,
|
|
462
|
+
gesture?.focused != null,
|
|
463
|
+
'focused',
|
|
464
|
+
transition,
|
|
465
|
+
isExiting,
|
|
466
|
+
shouldReduceMotion,
|
|
467
|
+
)
|
|
468
|
+
useGestureLayerProgress(
|
|
469
|
+
focusVisibleProgress,
|
|
470
|
+
focusVisible,
|
|
471
|
+
gesture?.focusVisible != null,
|
|
472
|
+
'focusVisible',
|
|
473
|
+
transition,
|
|
474
|
+
isExiting,
|
|
475
|
+
shouldReduceMotion,
|
|
476
|
+
)
|
|
477
|
+
useGestureLayerProgress(
|
|
478
|
+
hoveredProgress,
|
|
479
|
+
hovered,
|
|
480
|
+
gesture?.hovered != null,
|
|
481
|
+
'hovered',
|
|
482
|
+
transition,
|
|
483
|
+
isExiting,
|
|
484
|
+
shouldReduceMotion,
|
|
485
|
+
)
|
|
386
486
|
|
|
387
487
|
const animatedStyle = useAnimatedStyle(() => {
|
|
388
488
|
const activeKeys = activeKeysRef.current!
|
|
389
489
|
const hasTransform = hasTransformRef.current
|
|
390
490
|
const out: Record<string, unknown> = {}
|
|
391
491
|
const transform: Array<Record<string, unknown>> = []
|
|
492
|
+
|
|
493
|
+
// Read each progress SV exactly once so the chain below sees a coherent
|
|
494
|
+
// snapshot for this frame. Reading them on the UI thread is cheap.
|
|
495
|
+
const ph = hoveredProgress.value
|
|
496
|
+
const pf = focusedProgress.value
|
|
497
|
+
const pfv = focusVisibleProgress.value
|
|
498
|
+
const pp = pressedProgress.value
|
|
499
|
+
|
|
500
|
+
const layers = gestureSV.value
|
|
501
|
+
// Locals are suffixed `Layer` so they don't shadow the outer `pressed` /
|
|
502
|
+
// `focused` / `focusVisible` / `hovered` JS-state booleans — Reanimated's
|
|
503
|
+
// worklet closure tracker would otherwise pick those up as captured
|
|
504
|
+
// dependencies and re-register the worklet on every activation flip.
|
|
505
|
+
const hoveredLayer = layers ? layers.hovered : null
|
|
506
|
+
const focusedLayer = layers ? layers.focused : null
|
|
507
|
+
const focusVisibleLayer = layers ? layers.focusVisible : null
|
|
508
|
+
const pressedLayer = layers ? layers.pressed : null
|
|
509
|
+
|
|
392
510
|
for (const key of activeKeys) {
|
|
393
|
-
|
|
511
|
+
let v = sharedValues[key].value
|
|
512
|
+
const isColor = COLOR_KEY_SET.has(key)
|
|
513
|
+
|
|
514
|
+
// Composite gesture layers in priority order (lowest first). Each
|
|
515
|
+
// active layer pulls the value toward its pre-resolved primitive
|
|
516
|
+
// endpoint by `progress`; numeric keys lerp, color keys go through
|
|
517
|
+
// Reanimated's RGBA `interpolateColor`. We skip layers with progress
|
|
518
|
+
// 0 to avoid an `interpolateColor(0, ...)` call that would parse the
|
|
519
|
+
// target color string for no visible effect.
|
|
520
|
+
if (hoveredLayer && ph > 0 && hoveredLayer[key] !== undefined) {
|
|
521
|
+
const t = hoveredLayer[key]
|
|
522
|
+
v = isColor
|
|
523
|
+
? interpolateColor(ph, [0, 1], [v as string, t as string])
|
|
524
|
+
: (v as number) + ((t as number) - (v as number)) * ph
|
|
525
|
+
}
|
|
526
|
+
if (focusedLayer && pf > 0 && focusedLayer[key] !== undefined) {
|
|
527
|
+
const t = focusedLayer[key]
|
|
528
|
+
v = isColor
|
|
529
|
+
? interpolateColor(pf, [0, 1], [v as string, t as string])
|
|
530
|
+
: (v as number) + ((t as number) - (v as number)) * pf
|
|
531
|
+
}
|
|
532
|
+
if (
|
|
533
|
+
focusVisibleLayer &&
|
|
534
|
+
pfv > 0 &&
|
|
535
|
+
focusVisibleLayer[key] !== undefined
|
|
536
|
+
) {
|
|
537
|
+
const t = focusVisibleLayer[key]
|
|
538
|
+
v = isColor
|
|
539
|
+
? interpolateColor(pfv, [0, 1], [v as string, t as string])
|
|
540
|
+
: (v as number) + ((t as number) - (v as number)) * pfv
|
|
541
|
+
}
|
|
542
|
+
if (pressedLayer && pp > 0 && pressedLayer[key] !== undefined) {
|
|
543
|
+
const t = pressedLayer[key]
|
|
544
|
+
v = isColor
|
|
545
|
+
? interpolateColor(pp, [0, 1], [v as string, t as string])
|
|
546
|
+
: (v as number) + ((t as number) - (v as number)) * pp
|
|
547
|
+
}
|
|
548
|
+
|
|
394
549
|
if (TRANSFORM_KEY_SET.has(key)) {
|
|
395
550
|
transform.push(
|
|
396
|
-
key
|
|
551
|
+
ROTATION_KEYS.has(key) ? { [key]: `${v}deg` } : { [key]: v },
|
|
397
552
|
)
|
|
398
553
|
} else {
|
|
399
554
|
out[key] = v
|
|
@@ -425,11 +580,24 @@ export function createMotionComponent<C extends ComponentType<any>>(
|
|
|
425
580
|
setHovered,
|
|
426
581
|
)
|
|
427
582
|
|
|
583
|
+
// Resolve the `layout` prop into a Reanimated `LinearTransition` builder.
|
|
584
|
+
// Memoized on the value's stable signature so a fresh `layout={true}` or
|
|
585
|
+
// `layout={{ ... }}` literal each render doesn't rebuild the builder. When
|
|
586
|
+
// reduced motion is active we pass `undefined` — see `resolveLayout` for
|
|
587
|
+
// why we don't pass a duration-0 builder instead.
|
|
588
|
+
const layoutSig = stableSig(layout)
|
|
589
|
+
const layoutTransition = useMemo(
|
|
590
|
+
() => (shouldReduceMotion ? undefined : resolveLayoutTransition(layout)),
|
|
591
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
592
|
+
[layoutSig, shouldReduceMotion],
|
|
593
|
+
)
|
|
594
|
+
|
|
428
595
|
return (
|
|
429
596
|
<AnimatedComponent
|
|
430
597
|
ref={ref as never}
|
|
431
598
|
{...(rest as object)}
|
|
432
599
|
{...gestureHandlers}
|
|
600
|
+
layout={layoutTransition}
|
|
433
601
|
style={mergedStyle}
|
|
434
602
|
/>
|
|
435
603
|
)
|
|
@@ -469,6 +637,8 @@ function useAnimatableSharedValues(
|
|
|
469
637
|
const scaleX = useSharedValue<number | string>(init('scaleX'))
|
|
470
638
|
const scaleY = useSharedValue<number | string>(init('scaleY'))
|
|
471
639
|
const rotate = useSharedValue<number | string>(init('rotate'))
|
|
640
|
+
const rotateX = useSharedValue<number | string>(init('rotateX'))
|
|
641
|
+
const rotateY = useSharedValue<number | string>(init('rotateY'))
|
|
472
642
|
const opacity = useSharedValue<number | string>(init('opacity'))
|
|
473
643
|
const width = useSharedValue<number | string>(init('width'))
|
|
474
644
|
const height = useSharedValue<number | string>(init('height'))
|
|
@@ -489,6 +659,8 @@ function useAnimatableSharedValues(
|
|
|
489
659
|
scaleX,
|
|
490
660
|
scaleY,
|
|
491
661
|
rotate,
|
|
662
|
+
rotateX,
|
|
663
|
+
rotateY,
|
|
492
664
|
opacity,
|
|
493
665
|
width,
|
|
494
666
|
height,
|
|
@@ -735,113 +907,73 @@ function restValue(
|
|
|
735
907
|
return undefined
|
|
736
908
|
}
|
|
737
909
|
|
|
738
|
-
function stableSig(value: unknown): string {
|
|
739
|
-
if (value === undefined) return ''
|
|
740
|
-
try {
|
|
741
|
-
return stableStringify(value)
|
|
742
|
-
} catch {
|
|
743
|
-
return String(value)
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
|
|
747
910
|
/**
|
|
748
|
-
*
|
|
749
|
-
*
|
|
750
|
-
*
|
|
751
|
-
*
|
|
911
|
+
* Per-layer resolved targets: each declared gesture sub-state collapses to a
|
|
912
|
+
* map of primitive endpoints (numbers or color strings), already passed
|
|
913
|
+
* through `targetEndValue` so the worklet can use them directly without
|
|
914
|
+
* inspecting `AnimatableValue` shapes on the UI thread.
|
|
752
915
|
*/
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
916
|
+
type ResolvedGestureLayers = {
|
|
917
|
+
pressed?: Record<string, number | string>
|
|
918
|
+
focused?: Record<string, number | string>
|
|
919
|
+
focusVisible?: Record<string, number | string>
|
|
920
|
+
hovered?: Record<string, number | string>
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
function resolveGestureLayers(
|
|
924
|
+
gesture: GestureSubStates<unknown> | undefined,
|
|
925
|
+
): ResolvedGestureLayers | null {
|
|
926
|
+
if (!gesture) return null
|
|
927
|
+
const out: ResolvedGestureLayers = {}
|
|
928
|
+
for (const layer of GESTURE_LAYER_NAMES) {
|
|
929
|
+
const subState = gesture[layer]
|
|
930
|
+
if (!subState) continue
|
|
931
|
+
const resolved: Record<string, number | string> = {}
|
|
932
|
+
for (const key of ALL_KEYS) {
|
|
933
|
+
const raw = (subState as Record<string, unknown>)[key]
|
|
934
|
+
if (raw === undefined) continue
|
|
935
|
+
const t = targetEndValue(raw as AnimatableValue<number | string>)
|
|
936
|
+
if (t !== undefined) resolved[key] = t
|
|
937
|
+
}
|
|
938
|
+
out[layer] = resolved
|
|
760
939
|
}
|
|
761
|
-
|
|
762
|
-
const keys = Object.keys(obj).sort()
|
|
763
|
-
return (
|
|
764
|
-
'{' +
|
|
765
|
-
keys
|
|
766
|
-
.map((k) => JSON.stringify(k) + ':' + stableStringify(obj[k]))
|
|
767
|
-
.join(',') +
|
|
768
|
-
'}'
|
|
769
|
-
)
|
|
940
|
+
return out
|
|
770
941
|
}
|
|
771
942
|
|
|
772
943
|
/**
|
|
773
|
-
*
|
|
774
|
-
*
|
|
775
|
-
*
|
|
776
|
-
*
|
|
777
|
-
*
|
|
778
|
-
*
|
|
944
|
+
* Drive a single gesture layer's progress shared value 0↔1 with its own
|
|
945
|
+
* transition. Resolution priority for the layer config:
|
|
946
|
+
* `transition.<layerName>` → top-level `transition` → library default spring.
|
|
947
|
+
* On exit, snap to 0 instantly so the unmount-bound base SV finishes its exit
|
|
948
|
+
* animation without a stale layer pulling the value off-target.
|
|
949
|
+
*
|
|
950
|
+
* The hook is invoked unconditionally (one call per layer) so hook order
|
|
951
|
+
* stays stable even when `gesture` adds or removes sub-states across renders.
|
|
779
952
|
*/
|
|
780
|
-
function
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
}
|
|
796
|
-
const subStates = [
|
|
797
|
-
gesture.hovered,
|
|
798
|
-
gesture.focused,
|
|
799
|
-
gesture.focusVisible,
|
|
800
|
-
gesture.pressed,
|
|
801
|
-
] as Array<
|
|
802
|
-
Partial<Record<AnimatableKey, AnimatableValue<number | string>>> | undefined
|
|
803
|
-
>
|
|
804
|
-
for (const sub of subStates) {
|
|
805
|
-
if (!sub) continue
|
|
806
|
-
for (const k of ALL_KEYS) {
|
|
807
|
-
if (k in sub && !(k in merged)) {
|
|
808
|
-
merged[k] = DEFAULT_RESTING[k]
|
|
809
|
-
}
|
|
953
|
+
function useGestureLayerProgress<S>(
|
|
954
|
+
progress: SharedValue<number>,
|
|
955
|
+
active: boolean,
|
|
956
|
+
declared: boolean,
|
|
957
|
+
layer: GestureLayerName,
|
|
958
|
+
transition: Transition<S> | undefined,
|
|
959
|
+
isExiting: boolean,
|
|
960
|
+
shouldReduceMotion: boolean,
|
|
961
|
+
): void {
|
|
962
|
+
const layerCfgSig = stableSig(gestureLayerTransitionFor(layer, transition))
|
|
963
|
+
useEffect(() => {
|
|
964
|
+
if (!declared) return
|
|
965
|
+
if (isExiting) {
|
|
966
|
+
progress.value = 0
|
|
967
|
+
return
|
|
810
968
|
}
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
}
|
|
820
|
-
if (active.focused && gesture.focused) {
|
|
821
|
-
Object.assign(
|
|
822
|
-
merged,
|
|
823
|
-
gesture.focused as Partial<
|
|
824
|
-
Record<AnimatableKey, AnimatableValue<number | string>>
|
|
825
|
-
>,
|
|
826
|
-
)
|
|
827
|
-
}
|
|
828
|
-
if (active.focusVisible && gesture.focusVisible) {
|
|
829
|
-
Object.assign(
|
|
830
|
-
merged,
|
|
831
|
-
gesture.focusVisible as Partial<
|
|
832
|
-
Record<AnimatableKey, AnimatableValue<number | string>>
|
|
833
|
-
>,
|
|
834
|
-
)
|
|
835
|
-
}
|
|
836
|
-
if (active.pressed && gesture.pressed) {
|
|
837
|
-
Object.assign(
|
|
838
|
-
merged,
|
|
839
|
-
gesture.pressed as Partial<
|
|
840
|
-
Record<AnimatableKey, AnimatableValue<number | string>>
|
|
841
|
-
>,
|
|
842
|
-
)
|
|
843
|
-
}
|
|
844
|
-
return merged
|
|
969
|
+
const target = active ? 1 : 0
|
|
970
|
+
const cfg = shouldReduceMotion
|
|
971
|
+
? ({ type: 'no-animation' } as const)
|
|
972
|
+
: (gestureLayerTransitionFor(layer, transition) ??
|
|
973
|
+
({ type: 'spring' } as const))
|
|
974
|
+
progress.value = resolveTransition(cfg, target) as never
|
|
975
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
976
|
+
}, [active, declared, isExiting, shouldReduceMotion, layerCfgSig])
|
|
845
977
|
}
|
|
846
978
|
|
|
847
979
|
type GestureHandlers = Record<string, (event: unknown) => void>
|
|
@@ -863,6 +995,13 @@ function useGestureHandlers(
|
|
|
863
995
|
setFocusVisible: (next: boolean) => void,
|
|
864
996
|
setHovered: (next: boolean) => void,
|
|
865
997
|
): GestureHandlers {
|
|
998
|
+
// Deps key on declared-ness, not object identity — a fresh `gesture={...}`
|
|
999
|
+
// literal each render must not rebuild handlers if the same sub-states are
|
|
1000
|
+
// declared.
|
|
1001
|
+
const hasPressed = gesture?.pressed ? 1 : 0
|
|
1002
|
+
const hasFocused = gesture?.focused ? 1 : 0
|
|
1003
|
+
const hasFocusVisible = gesture?.focusVisible ? 1 : 0
|
|
1004
|
+
const hasHovered = gesture?.hovered ? 1 : 0
|
|
866
1005
|
return useMemo(() => {
|
|
867
1006
|
if (!gesture) return {}
|
|
868
1007
|
const handlers: GestureHandlers = {}
|
|
@@ -903,10 +1042,10 @@ function useGestureHandlers(
|
|
|
903
1042
|
return handlers
|
|
904
1043
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
905
1044
|
}, [
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
1045
|
+
hasPressed,
|
|
1046
|
+
hasFocused,
|
|
1047
|
+
hasFocusVisible,
|
|
1048
|
+
hasHovered,
|
|
910
1049
|
rest.onTouchStart,
|
|
911
1050
|
rest.onTouchEnd,
|
|
912
1051
|
rest.onTouchCancel,
|