@rootnative/inertia 0.0.0-alpha.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/CHANGELOG.md +33 -0
- package/LICENSE +21 -0
- package/README.md +131 -0
- package/dist/gestureLayer/index.d.mts +119 -0
- package/dist/gestureLayer/index.d.ts +119 -0
- package/dist/gestureLayer/index.js +346 -0
- package/dist/gestureLayer/index.js.map +1 -0
- package/dist/gestureLayer/index.mjs +344 -0
- package/dist/gestureLayer/index.mjs.map +1 -0
- package/dist/index.d.mts +507 -0
- package/dist/index.d.ts +507 -0
- package/dist/index.js +1546 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1515 -0
- package/dist/index.mjs.map +1 -0
- package/dist/motion/Image.d.mts +12 -0
- package/dist/motion/Image.d.ts +12 -0
- package/dist/motion/Image.js +1138 -0
- package/dist/motion/Image.js.map +1 -0
- package/dist/motion/Image.mjs +1132 -0
- package/dist/motion/Image.mjs.map +1 -0
- package/dist/motion/Pressable.d.mts +15 -0
- package/dist/motion/Pressable.d.ts +15 -0
- package/dist/motion/Pressable.js +1138 -0
- package/dist/motion/Pressable.js.map +1 -0
- package/dist/motion/Pressable.mjs +1132 -0
- package/dist/motion/Pressable.mjs.map +1 -0
- package/dist/motion/ScrollView.d.mts +12 -0
- package/dist/motion/ScrollView.d.ts +12 -0
- package/dist/motion/ScrollView.js +1138 -0
- package/dist/motion/ScrollView.js.map +1 -0
- package/dist/motion/ScrollView.mjs +1132 -0
- package/dist/motion/ScrollView.mjs.map +1 -0
- package/dist/motion/Text.d.mts +11 -0
- package/dist/motion/Text.d.ts +11 -0
- package/dist/motion/Text.js +1138 -0
- package/dist/motion/Text.js.map +1 -0
- package/dist/motion/Text.mjs +1132 -0
- package/dist/motion/Text.mjs.map +1 -0
- package/dist/motion/View.d.mts +11 -0
- package/dist/motion/View.d.ts +11 -0
- package/dist/motion/View.js +1138 -0
- package/dist/motion/View.js.map +1 -0
- package/dist/motion/View.mjs +1132 -0
- package/dist/motion/View.mjs.map +1 -0
- package/dist/testing/index.d.mts +57 -0
- package/dist/testing/index.d.ts +57 -0
- package/dist/testing/index.js +19 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/index.mjs +16 -0
- package/dist/testing/index.mjs.map +1 -0
- package/dist/touch/index.d.mts +146 -0
- package/dist/touch/index.d.ts +146 -0
- package/dist/touch/index.js +166 -0
- package/dist/touch/index.js.map +1 -0
- package/dist/touch/index.mjs +164 -0
- package/dist/touch/index.mjs.map +1 -0
- package/dist/types-cU43dEmH.d.mts +339 -0
- package/dist/types-cU43dEmH.d.ts +339 -0
- package/dist/useGesture-BnY65PlQ.d.ts +84 -0
- package/dist/useGesture-DxtXdz-K.d.mts +84 -0
- package/jest-preset.js +33 -0
- package/jest-setup.js +213 -0
- package/llms.txt +142 -0
- package/package.json +151 -0
- package/src/__type-tests__/animate.test-d.tsx +88 -0
- package/src/__type-tests__/variants.test-d.tsx +67 -0
- package/src/config/MotionConfig.tsx +30 -0
- package/src/config/MotionConfigContext.ts +53 -0
- package/src/config/index.ts +9 -0
- package/src/gestureLayer/index.ts +21 -0
- package/src/gestureLayer/useGestureLayer.ts +285 -0
- package/src/gestures/focusVisibility.ts +61 -0
- package/src/gestures/index.ts +1 -0
- package/src/index.ts +72 -0
- package/src/layout/index.ts +16 -0
- package/src/layout/resolveLayout.ts +54 -0
- package/src/layout/sharedRegistry.ts +111 -0
- package/src/layout/useSharedLayout.ts +289 -0
- package/src/motion/Image.tsx +9 -0
- package/src/motion/Pressable.tsx +12 -0
- package/src/motion/ScrollView.tsx +9 -0
- package/src/motion/Text.tsx +8 -0
- package/src/motion/View.tsx +8 -0
- package/src/motion/createMotionComponent.tsx +1306 -0
- package/src/motion/index.ts +26 -0
- package/src/motion/installCheck.ts +65 -0
- package/src/presence/Presence.tsx +165 -0
- package/src/presence/PresenceContext.ts +28 -0
- package/src/presence/index.ts +6 -0
- package/src/testing/index.ts +78 -0
- package/src/touch/index.ts +18 -0
- package/src/touch/useTouchDrag.ts +289 -0
- package/src/transitions/easing.ts +53 -0
- package/src/transitions/index.ts +7 -0
- package/src/transitions/keys.ts +32 -0
- package/src/transitions/resolve.ts +169 -0
- package/src/transitions/resolveSequence.ts +87 -0
- package/src/transitions/runtime.ts +63 -0
- package/src/transitions/sig.ts +40 -0
- package/src/transitions/spring.ts +42 -0
- package/src/types.ts +383 -0
- package/src/values/index.ts +26 -0
- package/src/values/useAnimation.ts +69 -0
- package/src/values/useBooleanSpring.ts +33 -0
- package/src/values/useColorTransition.ts +72 -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/useShadow.ts +116 -0
- package/src/values/useSpring.ts +98 -0
- package/src/values/useTransform.ts +132 -0
- package/src/values/useVariants.ts +60 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// `isWorkletFunction` lives in `react-native-worklets` (the Reanimated 4 peer
|
|
2
|
+
// dep); Reanimated's own re-export is deprecated.
|
|
3
|
+
import { isWorkletFunction } from 'react-native-worklets'
|
|
4
|
+
import { type EasingInput } from '../types'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Reanimated 3.9+ validates that easing functions used in nested-transition
|
|
8
|
+
* contexts (variants, sequences, per-property maps) are worklets, and crashes
|
|
9
|
+
* with `[Reanimated] The easing function is not a worklet` otherwise. The
|
|
10
|
+
* library accepts plain functions on the public surface; this helper wraps
|
|
11
|
+
* them so consumers don't have to think about the worklet boundary.
|
|
12
|
+
*
|
|
13
|
+
* If the input is already a worklet (has been processed by the worklets babel
|
|
14
|
+
* plugin), it's returned as-is. Otherwise it's wrapped in a function whose
|
|
15
|
+
* body declares the `'worklet'` directive — when our source is processed by
|
|
16
|
+
* the consumer's worklets babel plugin (the default Expo/RN setup), the
|
|
17
|
+
* wrapper becomes a real worklet that captures the user fn via closure.
|
|
18
|
+
*
|
|
19
|
+
* Reanimated 4 changed `Easing.bezier(...)` to return an
|
|
20
|
+
* `EasingFunctionFactory` (`{ factory: () => EasingFunction }`) rather than
|
|
21
|
+
* the function itself. The helper accepts both shapes — `EasingFunction` and
|
|
22
|
+
* `EasingFunctionFactory` — and unwraps the factory automatically so
|
|
23
|
+
* consumers don't have to call `.factory()` manually.
|
|
24
|
+
*
|
|
25
|
+
* The user fn must be pure: no JS-thread captured refs, no shared mutable
|
|
26
|
+
* state, no calls to non-worklet APIs.
|
|
27
|
+
*/
|
|
28
|
+
export function ensureWorkletEasing(
|
|
29
|
+
easing: EasingInput | undefined,
|
|
30
|
+
): ((t: number) => number) | undefined {
|
|
31
|
+
if (!easing) return undefined
|
|
32
|
+
// Reanimated 4 `EasingFunctionFactory` — unwrap via `.factory()` before
|
|
33
|
+
// checking worklet status, so the wrapped fn (not the factory wrapper)
|
|
34
|
+
// ends up in the transition config.
|
|
35
|
+
const fn = isEasingFactory(easing) ? easing.factory() : easing
|
|
36
|
+
if (isWorkletFunction(fn)) return fn
|
|
37
|
+
const wrapped = (t: number) => {
|
|
38
|
+
'worklet'
|
|
39
|
+
return fn(t)
|
|
40
|
+
}
|
|
41
|
+
return wrapped
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function isEasingFactory(
|
|
45
|
+
value: EasingInput,
|
|
46
|
+
): value is { factory: () => (t: number) => number } {
|
|
47
|
+
return (
|
|
48
|
+
typeof value === 'object' &&
|
|
49
|
+
value !== null &&
|
|
50
|
+
'factory' in value &&
|
|
51
|
+
typeof (value as { factory: unknown }).factory === 'function'
|
|
52
|
+
)
|
|
53
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { resolveTransition } from './resolve'
|
|
2
|
+
export { resolveAnimatableValue } from './resolveSequence'
|
|
3
|
+
export { ensureWorkletEasing } from './easing'
|
|
4
|
+
export { isTopLevelTransition, TRANSITION_CONFIG_KEYS } from './keys'
|
|
5
|
+
export { buildReleaseAnimation } from './runtime'
|
|
6
|
+
export { stableSig } from './sig'
|
|
7
|
+
export { DEFAULT_SPRING, springToReanimated } from './spring'
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type TransitionConfig } from '../types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Field names that may appear on a `TransitionConfig` (spring / timing /
|
|
5
|
+
* decay / no-animation). Used as a structural discriminator: if every key on
|
|
6
|
+
* an object is in this set, the object is treated as a top-level transition;
|
|
7
|
+
* otherwise it's a per-property / per-layer transition map.
|
|
8
|
+
*
|
|
9
|
+
* Adding a new field to `TransitionConfig` requires adding the name here.
|
|
10
|
+
*/
|
|
11
|
+
export const TRANSITION_CONFIG_KEYS = new Set([
|
|
12
|
+
'type',
|
|
13
|
+
'tension',
|
|
14
|
+
'friction',
|
|
15
|
+
'mass',
|
|
16
|
+
'velocity',
|
|
17
|
+
'restSpeedThreshold',
|
|
18
|
+
'restDisplacementThreshold',
|
|
19
|
+
'duration',
|
|
20
|
+
'easing',
|
|
21
|
+
'delay',
|
|
22
|
+
'repeat',
|
|
23
|
+
'deceleration',
|
|
24
|
+
'clamp',
|
|
25
|
+
])
|
|
26
|
+
|
|
27
|
+
export function isTopLevelTransition(t: unknown): t is TransitionConfig {
|
|
28
|
+
if (t === null || typeof t !== 'object') return false
|
|
29
|
+
const keys = Object.keys(t as object)
|
|
30
|
+
if (keys.length === 0) return false
|
|
31
|
+
return keys.every((k) => TRANSITION_CONFIG_KEYS.has(k))
|
|
32
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Easing,
|
|
3
|
+
withDecay,
|
|
4
|
+
withDelay,
|
|
5
|
+
withRepeat,
|
|
6
|
+
withSpring,
|
|
7
|
+
withTiming,
|
|
8
|
+
} from 'react-native-reanimated'
|
|
9
|
+
import { ensureWorkletEasing } from './easing'
|
|
10
|
+
import { springToReanimated } from './spring'
|
|
11
|
+
import {
|
|
12
|
+
type DecayTransition,
|
|
13
|
+
type RepeatConfig,
|
|
14
|
+
type SpringTransition,
|
|
15
|
+
type TimingTransition,
|
|
16
|
+
type TransitionConfig,
|
|
17
|
+
} from '../types'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* UI-thread callback Reanimated invokes when an animation settles. Must be a
|
|
21
|
+
* worklet — callers either author one with `'worklet'` or build one via
|
|
22
|
+
* `runOnJS(...)` to bridge to JS-thread code.
|
|
23
|
+
*/
|
|
24
|
+
export type AnimationCallback = (
|
|
25
|
+
finished?: boolean,
|
|
26
|
+
current?: number | string,
|
|
27
|
+
) => void
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Per-step callback factory. Resolvers call this with the step's phase and
|
|
31
|
+
* sequence index (or `undefined` for non-sequence animations) and attach the
|
|
32
|
+
* resulting callback to the underlying `withSpring` / `withTiming` /
|
|
33
|
+
* `withDecay` call.
|
|
34
|
+
*/
|
|
35
|
+
export type CallbackFactory = (
|
|
36
|
+
phase: 'step' | 'animation',
|
|
37
|
+
step: number | undefined,
|
|
38
|
+
) => AnimationCallback | undefined
|
|
39
|
+
|
|
40
|
+
const DEFAULT_TIMING_DURATION = 250
|
|
41
|
+
|
|
42
|
+
function buildSpring(
|
|
43
|
+
cfg: SpringTransition,
|
|
44
|
+
toValue: number | string,
|
|
45
|
+
cb?: AnimationCallback,
|
|
46
|
+
) {
|
|
47
|
+
return withSpring(toValue as number, springToReanimated(cfg), cb as never)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function buildTiming(
|
|
51
|
+
cfg: TimingTransition,
|
|
52
|
+
toValue: number | string,
|
|
53
|
+
cb?: AnimationCallback,
|
|
54
|
+
) {
|
|
55
|
+
return withTiming(
|
|
56
|
+
toValue as number,
|
|
57
|
+
{
|
|
58
|
+
duration: cfg.duration ?? DEFAULT_TIMING_DURATION,
|
|
59
|
+
easing: ensureWorkletEasing(cfg.easing) ?? Easing.inOut(Easing.ease),
|
|
60
|
+
},
|
|
61
|
+
cb as never,
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function buildDecay(cfg: DecayTransition, cb?: AnimationCallback) {
|
|
66
|
+
return withDecay(
|
|
67
|
+
{
|
|
68
|
+
velocity: cfg.velocity ?? 0,
|
|
69
|
+
deceleration: cfg.deceleration,
|
|
70
|
+
clamp: cfg.clamp,
|
|
71
|
+
},
|
|
72
|
+
cb as never,
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Build a single-step animation (no repeat / no delay / no sequence) for a
|
|
78
|
+
* given config + target. Pulled out so sequence steps can compose without
|
|
79
|
+
* recursing into repeat/delay handling per step. The callback is forwarded
|
|
80
|
+
* to Reanimated; for `no-animation` the callback is fired synchronously
|
|
81
|
+
* since there's nothing to wait for.
|
|
82
|
+
*/
|
|
83
|
+
function buildOne(
|
|
84
|
+
cfg: TransitionConfig,
|
|
85
|
+
toValue: number | string,
|
|
86
|
+
cb?: AnimationCallback,
|
|
87
|
+
): unknown {
|
|
88
|
+
if (cfg.type === 'no-animation') {
|
|
89
|
+
if (cb) cb(true, toValue)
|
|
90
|
+
return toValue
|
|
91
|
+
}
|
|
92
|
+
if (cfg.type === 'decay') return buildDecay(cfg, cb)
|
|
93
|
+
if (cfg.type === 'timing') return buildTiming(cfg, toValue, cb)
|
|
94
|
+
return buildSpring(cfg as SpringTransition, toValue, cb)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Wrap an animation in `withRepeat` per the unified `repeat` shape:
|
|
99
|
+
* - `number` → finite count, alternating direction
|
|
100
|
+
* - `'infinite'` → endless, alternating direction
|
|
101
|
+
* - `{ count, alternate }`→ explicit; `alternate` defaults to `true`
|
|
102
|
+
*/
|
|
103
|
+
export function applyRepeat(
|
|
104
|
+
animation: unknown,
|
|
105
|
+
repeat: RepeatConfig | undefined,
|
|
106
|
+
) {
|
|
107
|
+
if (repeat === undefined) return animation
|
|
108
|
+
if (repeat === 'infinite') {
|
|
109
|
+
return withRepeat(animation as never, -1, true)
|
|
110
|
+
}
|
|
111
|
+
if (typeof repeat === 'number') {
|
|
112
|
+
return withRepeat(animation as never, repeat, true)
|
|
113
|
+
}
|
|
114
|
+
const count = repeat.count === 'infinite' ? -1 : repeat.count
|
|
115
|
+
const alternate = repeat.alternate ?? true
|
|
116
|
+
return withRepeat(animation as never, count, alternate)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function applyDelay(animation: unknown, delay: number | undefined) {
|
|
120
|
+
if (!delay || delay <= 0) return animation
|
|
121
|
+
return withDelay(delay, animation as never)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Build a Reanimated animation for a single property. Runs on the JS thread
|
|
126
|
+
* once per change and produces a baked `withSpring` / `withTiming` /
|
|
127
|
+
* `withDecay` (optionally wrapped in `withDelay` / `withRepeat`) call. The
|
|
128
|
+
* worklet body only consumes the result.
|
|
129
|
+
*
|
|
130
|
+
* `callback`, when provided, fires once when the underlying single-shot
|
|
131
|
+
* animation settles. Repeat-wrapped animations forward the callback to
|
|
132
|
+
* `withRepeat`, so it fires once per iteration as Reanimated does.
|
|
133
|
+
*/
|
|
134
|
+
export function resolveTransition(
|
|
135
|
+
config: TransitionConfig | undefined,
|
|
136
|
+
toValue: number | string,
|
|
137
|
+
callback?: AnimationCallback,
|
|
138
|
+
): unknown {
|
|
139
|
+
const cfg = config ?? ({ type: 'spring' } as SpringTransition)
|
|
140
|
+
const base = buildOne(cfg, toValue, callback)
|
|
141
|
+
const repeated = applyRepeat(base, repeatOf(cfg))
|
|
142
|
+
return applyDelay(repeated, delayOf(cfg))
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function repeatOf(cfg: TransitionConfig): RepeatConfig | undefined {
|
|
146
|
+
if (cfg.type === 'no-animation' || cfg.type === 'decay') return undefined
|
|
147
|
+
return cfg.repeat
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Return `cfg` minus its `repeat` field. Used when peeling top-level repeat
|
|
152
|
+
* off a base transition before passing it down to per-sequence-step
|
|
153
|
+
* resolution — the sequence as a whole is what should repeat, not each step.
|
|
154
|
+
*/
|
|
155
|
+
export function stripRepeat(
|
|
156
|
+
cfg: TransitionConfig | undefined,
|
|
157
|
+
): TransitionConfig | undefined {
|
|
158
|
+
if (!cfg) return cfg
|
|
159
|
+
if (cfg.type === 'no-animation' || cfg.type === 'decay') return cfg
|
|
160
|
+
if (cfg.repeat === undefined) return cfg
|
|
161
|
+
const next = { ...cfg }
|
|
162
|
+
delete next.repeat
|
|
163
|
+
return next
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function delayOf(cfg: TransitionConfig): number | undefined {
|
|
167
|
+
if (cfg.type === 'no-animation') return undefined
|
|
168
|
+
return cfg.delay
|
|
169
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { withSequence } from 'react-native-reanimated'
|
|
2
|
+
import {
|
|
3
|
+
applyRepeat,
|
|
4
|
+
repeatOf,
|
|
5
|
+
resolveTransition,
|
|
6
|
+
stripRepeat,
|
|
7
|
+
type AnimationCallback,
|
|
8
|
+
type CallbackFactory,
|
|
9
|
+
} from './resolve'
|
|
10
|
+
import {
|
|
11
|
+
type AnimatableValue,
|
|
12
|
+
type SequenceStep,
|
|
13
|
+
type TransitionConfig,
|
|
14
|
+
} from '../types'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* True when the value is a `{ to, ...transitionOverride }` sequence step.
|
|
18
|
+
* Plain numbers and plain transition objects fail this check.
|
|
19
|
+
*/
|
|
20
|
+
function isStepObject<V>(
|
|
21
|
+
v: SequenceStep<V> | V,
|
|
22
|
+
): v is Extract<SequenceStep<V>, { to: V }> {
|
|
23
|
+
return (
|
|
24
|
+
typeof v === 'object' &&
|
|
25
|
+
v !== null &&
|
|
26
|
+
!Array.isArray(v) &&
|
|
27
|
+
'to' in (v as object)
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Resolve a per-property `animate` value into a Reanimated animation.
|
|
33
|
+
*
|
|
34
|
+
* Handles the three shapes of `AnimatableValue`:
|
|
35
|
+
* 1. plain value → single `resolveTransition` call
|
|
36
|
+
* 2. `{ to, ...over }` → single step with the override merged into `base`
|
|
37
|
+
* 3. array of either → `withSequence` of resolved steps, with the
|
|
38
|
+
* top-level `repeat` applied at the **sequence level** (not per step).
|
|
39
|
+
* Per-step `repeat` overrides remain step-local.
|
|
40
|
+
*/
|
|
41
|
+
export function resolveAnimatableValue<V extends number | string>(
|
|
42
|
+
value: AnimatableValue<V>,
|
|
43
|
+
base: TransitionConfig | undefined,
|
|
44
|
+
factory?: CallbackFactory,
|
|
45
|
+
): unknown {
|
|
46
|
+
if (Array.isArray(value)) {
|
|
47
|
+
const steps = value as ReadonlyArray<SequenceStep<V>>
|
|
48
|
+
const stepBase = stripRepeat(base)
|
|
49
|
+
const animations = steps.map((step, i) =>
|
|
50
|
+
resolveStep(step, stepBase, factory?.('step', i)),
|
|
51
|
+
)
|
|
52
|
+
const seq = withSequence(...(animations as never[]))
|
|
53
|
+
return applyRepeat(seq, base ? repeatOf(base) : undefined)
|
|
54
|
+
}
|
|
55
|
+
const step = value as SequenceStep<V>
|
|
56
|
+
const cb = factory?.('animation', undefined)
|
|
57
|
+
if (isStepObject<V>(step)) {
|
|
58
|
+
return resolveStep(step, base, cb)
|
|
59
|
+
}
|
|
60
|
+
return resolveTransition(base, step as V, cb)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function resolveStep<V extends number | string>(
|
|
64
|
+
step: SequenceStep<V>,
|
|
65
|
+
base: TransitionConfig | undefined,
|
|
66
|
+
cb?: AnimationCallback,
|
|
67
|
+
): unknown {
|
|
68
|
+
if (isStepObject<V>(step)) {
|
|
69
|
+
const { to, ...override } = step as { to: V } & Partial<TransitionConfig>
|
|
70
|
+
const merged = mergeTransition(base, override as Partial<TransitionConfig>)
|
|
71
|
+
return resolveTransition(merged, to, cb)
|
|
72
|
+
}
|
|
73
|
+
return resolveTransition(base, step as V, cb)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function mergeTransition(
|
|
77
|
+
base: TransitionConfig | undefined,
|
|
78
|
+
override: Partial<TransitionConfig>,
|
|
79
|
+
): TransitionConfig {
|
|
80
|
+
// If the override declares a `type`, it wins outright — mixing fields from
|
|
81
|
+
// a spring base into a timing override produces garbage. Otherwise inherit
|
|
82
|
+
// the base's type and shallow-merge the rest.
|
|
83
|
+
if (override.type && base && override.type !== base.type) {
|
|
84
|
+
return override as TransitionConfig
|
|
85
|
+
}
|
|
86
|
+
return { ...(base ?? { type: 'spring' }), ...override } as TransitionConfig
|
|
87
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Easing,
|
|
3
|
+
withDecay,
|
|
4
|
+
withSpring,
|
|
5
|
+
withTiming,
|
|
6
|
+
} from 'react-native-reanimated'
|
|
7
|
+
import { springToReanimated } from './spring'
|
|
8
|
+
import { type TransitionConfig } from '../types'
|
|
9
|
+
|
|
10
|
+
const DEFAULT_TIMING_DURATION = 250
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Worklet-safe single-step animation builder. Mirrors a subset of
|
|
14
|
+
* `resolveTransition` for the UI-thread path where the transition config is
|
|
15
|
+
* picked at gesture-release time, not at render time.
|
|
16
|
+
*
|
|
17
|
+
* Supported: spring / timing / decay / no-animation, single-step only.
|
|
18
|
+
* Not supported: sequences, top-level repeat, easing-function
|
|
19
|
+
* auto-worklet-wrapping (pass an already-worklet easing if you need a custom
|
|
20
|
+
* one — most release transitions don't).
|
|
21
|
+
*
|
|
22
|
+
* Use this from gesture worklets (`useDrag` / `usePan` release callbacks, or
|
|
23
|
+
* any custom `Gesture.Pan().onEnd(() => ...)` worklet) to animate a shared
|
|
24
|
+
* value with an Inertia transition without the JS round-trip that would lose
|
|
25
|
+
* the release velocity.
|
|
26
|
+
*
|
|
27
|
+
* For decay transitions, `toValue` is ignored — decay decelerates from the
|
|
28
|
+
* SV's current position via its own physics. Pass `0` if you don't have one.
|
|
29
|
+
*/
|
|
30
|
+
export function buildReleaseAnimation(
|
|
31
|
+
transition: TransitionConfig,
|
|
32
|
+
toValue: number,
|
|
33
|
+
): unknown {
|
|
34
|
+
'worklet'
|
|
35
|
+
if (transition.type === 'no-animation') return toValue
|
|
36
|
+
if (transition.type === 'decay') {
|
|
37
|
+
const cfg: {
|
|
38
|
+
velocity: number
|
|
39
|
+
deceleration?: number
|
|
40
|
+
clamp?: [number, number]
|
|
41
|
+
} = { velocity: transition.velocity ?? 0 }
|
|
42
|
+
if (transition.deceleration !== undefined) {
|
|
43
|
+
cfg.deceleration = transition.deceleration
|
|
44
|
+
}
|
|
45
|
+
if (transition.clamp !== undefined) cfg.clamp = transition.clamp
|
|
46
|
+
return withDecay(cfg)
|
|
47
|
+
}
|
|
48
|
+
if (transition.type === 'timing') {
|
|
49
|
+
// Reanimated 4's `Easing.bezier(...)` returns an `EasingFunctionFactory`
|
|
50
|
+
// rather than the function itself. Unwrap inline so consumers calling
|
|
51
|
+
// `buildReleaseAnimation` from a gesture worklet don't have to.
|
|
52
|
+
const e = transition.easing
|
|
53
|
+
const easingFn =
|
|
54
|
+
e && typeof e === 'object' && 'factory' in e
|
|
55
|
+
? e.factory()
|
|
56
|
+
: (e ?? Easing.inOut(Easing.ease))
|
|
57
|
+
return withTiming(toValue, {
|
|
58
|
+
duration: transition.duration ?? DEFAULT_TIMING_DURATION,
|
|
59
|
+
easing: easingFn,
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
return withSpring(toValue, springToReanimated(transition))
|
|
63
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stable string signature for an arbitrary value — used as a dep-array
|
|
3
|
+
* member so a fresh object literal each render doesn't re-fire an effect
|
|
4
|
+
* unless something structurally changed. Functions serialize as `null`
|
|
5
|
+
* (their identity isn't useful in a sig); `undefined` collapses to an empty
|
|
6
|
+
* string so omitted props compare equal across renders.
|
|
7
|
+
*/
|
|
8
|
+
export function stableSig(value: unknown): string {
|
|
9
|
+
if (value === undefined) return ''
|
|
10
|
+
try {
|
|
11
|
+
return stableStringify(value)
|
|
12
|
+
} catch {
|
|
13
|
+
return String(value)
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* JSON.stringify with keys sorted at every level so a sig is invariant under
|
|
19
|
+
* property-declaration order. Functions and `undefined` both serialize as
|
|
20
|
+
* `null` — we accept the latter's information loss (rare in practice) in
|
|
21
|
+
* exchange for not crashing on circular function-bearing graphs.
|
|
22
|
+
*/
|
|
23
|
+
function stableStringify(v: unknown): string {
|
|
24
|
+
if (v === null || typeof v !== 'object') {
|
|
25
|
+
if (typeof v === 'function' || v === undefined) return 'null'
|
|
26
|
+
return JSON.stringify(v)
|
|
27
|
+
}
|
|
28
|
+
if (Array.isArray(v)) {
|
|
29
|
+
return '[' + v.map(stableStringify).join(',') + ']'
|
|
30
|
+
}
|
|
31
|
+
const obj = v as Record<string, unknown>
|
|
32
|
+
const keys = Object.keys(obj).sort()
|
|
33
|
+
return (
|
|
34
|
+
'{' +
|
|
35
|
+
keys
|
|
36
|
+
.map((k) => JSON.stringify(k) + ':' + stableStringify(obj[k]))
|
|
37
|
+
.join(',') +
|
|
38
|
+
'}'
|
|
39
|
+
)
|
|
40
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { type SpringTransition } from '../types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Default spring physics, expressed in react-spring vocabulary.
|
|
5
|
+
*
|
|
6
|
+
* `tension: 170` / `friction: 26` / `mass: 1` was picked over Reanimated's
|
|
7
|
+
* raw `stiffness: 100` / `damping: 10` default because the raw default
|
|
8
|
+
* overshoots noticeably for the small (~100px) translates that dominate
|
|
9
|
+
* UI work — buttons, sheets, popovers. These numbers settle in ~350ms with
|
|
10
|
+
* a single, almost-imperceptible overshoot, which matches the perceptual
|
|
11
|
+
* target the rest of the library is tuned against.
|
|
12
|
+
*/
|
|
13
|
+
export const DEFAULT_SPRING: Required<
|
|
14
|
+
Pick<SpringTransition, 'tension' | 'friction' | 'mass'>
|
|
15
|
+
> = {
|
|
16
|
+
tension: 170,
|
|
17
|
+
friction: 26,
|
|
18
|
+
mass: 1,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Convert public react-spring vocabulary (`tension` / `friction` / `mass`)
|
|
23
|
+
* to Reanimated's raw `stiffness` / `damping` / `mass`. This is the single
|
|
24
|
+
* place the mapping lives; resolvers, value hooks, and any future surface
|
|
25
|
+
* that needs a Reanimated spring config import from here.
|
|
26
|
+
*
|
|
27
|
+
* The mapping is identity (tension ≡ stiffness, friction ≡ damping) — the
|
|
28
|
+
* names differ but the underlying physics constants are the same. We don't
|
|
29
|
+
* surface the raw names publicly because the react-spring vocabulary is
|
|
30
|
+
* what designers and prior-art consumers expect.
|
|
31
|
+
*/
|
|
32
|
+
export function springToReanimated(t: SpringTransition) {
|
|
33
|
+
'worklet'
|
|
34
|
+
return {
|
|
35
|
+
stiffness: t.tension ?? DEFAULT_SPRING.tension,
|
|
36
|
+
damping: t.friction ?? DEFAULT_SPRING.friction,
|
|
37
|
+
mass: t.mass ?? DEFAULT_SPRING.mass,
|
|
38
|
+
velocity: t.velocity,
|
|
39
|
+
restSpeedThreshold: t.restSpeedThreshold,
|
|
40
|
+
restDisplacementThreshold: t.restDisplacementThreshold,
|
|
41
|
+
}
|
|
42
|
+
}
|