@onlynative/inertia 0.0.1-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 +90 -0
- package/dist/index.d.mts +185 -0
- package/dist/index.d.ts +185 -0
- package/dist/index.js +817 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +796 -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 +656 -0
- package/dist/motion/Image.js.map +1 -0
- package/dist/motion/Image.mjs +650 -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 +656 -0
- package/dist/motion/Pressable.js.map +1 -0
- package/dist/motion/Pressable.mjs +650 -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 +656 -0
- package/dist/motion/ScrollView.js.map +1 -0
- package/dist/motion/ScrollView.mjs +650 -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 +656 -0
- package/dist/motion/Text.js.map +1 -0
- package/dist/motion/Text.mjs +650 -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 +656 -0
- package/dist/motion/View.js.map +1 -0
- package/dist/motion/View.mjs +650 -0
- package/dist/motion/View.mjs.map +1 -0
- package/dist/types-CmbXx-G3.d.mts +185 -0
- package/dist/types-CmbXx-G3.d.ts +185 -0
- package/llms.txt +78 -0
- package/package.json +120 -0
- package/src/config/MotionConfig.tsx +30 -0
- package/src/config/MotionConfigContext.ts +53 -0
- package/src/config/index.ts +9 -0
- package/src/index.ts +49 -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 +850 -0
- package/src/motion/index.ts +26 -0
- package/src/presence/Presence.tsx +165 -0
- package/src/presence/PresenceContext.ts +28 -0
- package/src/presence/index.ts +6 -0
- package/src/transitions/easing.ts +29 -0
- package/src/transitions/index.ts +2 -0
- package/src/transitions/resolve.ts +265 -0
- package/src/types.ts +207 -0
- package/src/values/index.ts +1 -0
- package/src/values/useVariants.ts +60 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { MotionImage } from './Image'
|
|
2
|
+
import { MotionPressable } from './Pressable'
|
|
3
|
+
import { MotionScrollView } from './ScrollView'
|
|
4
|
+
import { MotionText } from './Text'
|
|
5
|
+
import { MotionView } from './View'
|
|
6
|
+
|
|
7
|
+
export { createMotionComponent } from './createMotionComponent'
|
|
8
|
+
export {
|
|
9
|
+
MotionView,
|
|
10
|
+
MotionText,
|
|
11
|
+
MotionImage,
|
|
12
|
+
MotionPressable,
|
|
13
|
+
MotionScrollView,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The `Motion.*` namespace. Each property is a primitive with its style prop
|
|
18
|
+
* inferred from the underlying RN component. There is no shared style fallback.
|
|
19
|
+
*/
|
|
20
|
+
export const Motion = {
|
|
21
|
+
View: MotionView,
|
|
22
|
+
Text: MotionText,
|
|
23
|
+
Image: MotionImage,
|
|
24
|
+
Pressable: MotionPressable,
|
|
25
|
+
ScrollView: MotionScrollView,
|
|
26
|
+
} as const
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Children,
|
|
3
|
+
isValidElement,
|
|
4
|
+
type Key,
|
|
5
|
+
type ReactElement,
|
|
6
|
+
type ReactNode,
|
|
7
|
+
useCallback,
|
|
8
|
+
useMemo,
|
|
9
|
+
useRef,
|
|
10
|
+
useState,
|
|
11
|
+
} from 'react'
|
|
12
|
+
import { PresenceContext, type PresenceContextValue } from './PresenceContext'
|
|
13
|
+
|
|
14
|
+
interface RenderEntry {
|
|
15
|
+
key: Key
|
|
16
|
+
element: ReactElement
|
|
17
|
+
isPresent: boolean
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Wrap a list of children with mount / unmount transitions. When a child is
|
|
22
|
+
* removed from the incoming list it stays in the snapshot until its exit
|
|
23
|
+
* animation completes; descendants consume the per-child `<PresenceContext>`
|
|
24
|
+
* to coordinate.
|
|
25
|
+
*
|
|
26
|
+
* Children must be `<Motion.*>` primitives (or any component that consumes
|
|
27
|
+
* `usePresence()` and calls `safeToRemove`). Plain elements without that
|
|
28
|
+
* contract will linger in the snapshot once removed; document that and pick
|
|
29
|
+
* the right primitive.
|
|
30
|
+
*
|
|
31
|
+
* Children also need explicit `key`s so removal is detectable across
|
|
32
|
+
* renders. Without a key, React falls back to positional identity and
|
|
33
|
+
* removal looks like a prop change — Presence has nothing to mark exiting.
|
|
34
|
+
*/
|
|
35
|
+
export function Presence({ children }: { children: ReactNode }) {
|
|
36
|
+
const incoming = useMemo(() => {
|
|
37
|
+
const out: ReactElement[] = []
|
|
38
|
+
Children.forEach(children, (child) => {
|
|
39
|
+
if (!isValidElement(child)) return
|
|
40
|
+
if (child.key === null) {
|
|
41
|
+
if (__DEV__) {
|
|
42
|
+
console.warn(
|
|
43
|
+
'[inertia] <Presence> children must have a `key`. Skipping a keyless child.',
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
out.push(child)
|
|
49
|
+
})
|
|
50
|
+
return out
|
|
51
|
+
}, [children])
|
|
52
|
+
|
|
53
|
+
// Snapshot of elements removed from `incoming` whose exit animation is
|
|
54
|
+
// still in flight. setExiting is called synchronously during render below
|
|
55
|
+
// (the documented pattern for derived-from-prop-change state), so React
|
|
56
|
+
// re-renders with the new snapshot before committing — no visual frame
|
|
57
|
+
// where the departing child has vanished.
|
|
58
|
+
const [exiting, setExiting] = useState<Map<Key, ReactElement>>(
|
|
59
|
+
() => new Map(),
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
// Tracks the previous render's `incoming` so we can diff. Updated
|
|
63
|
+
// synchronously alongside the setState call.
|
|
64
|
+
const prevIncomingRef = useRef<ReactElement[]>(incoming)
|
|
65
|
+
|
|
66
|
+
if (prevIncomingRef.current !== incoming) {
|
|
67
|
+
const prev = prevIncomingRef.current
|
|
68
|
+
prevIncomingRef.current = incoming
|
|
69
|
+
const incomingKeys = new Set(incoming.map((el) => el.key as Key))
|
|
70
|
+
let next: Map<Key, ReactElement> | null = null
|
|
71
|
+
const ensureMutable = () => {
|
|
72
|
+
if (!next) next = new Map(exiting)
|
|
73
|
+
return next
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Departures: in prev but not in current → snapshot for exit.
|
|
77
|
+
for (const oldEl of prev) {
|
|
78
|
+
const key = oldEl.key as Key
|
|
79
|
+
if (!incomingKeys.has(key) && !exiting.has(key)) {
|
|
80
|
+
ensureMutable().set(key, oldEl)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// Returns: was exiting and reappears → drop the snapshot. The live
|
|
84
|
+
// `incoming` entry takes over with the same key, so React reconciles
|
|
85
|
+
// the underlying Motion instance and the in-flight exit animation
|
|
86
|
+
// interrupts back toward `animate` values.
|
|
87
|
+
for (const el of incoming) {
|
|
88
|
+
const key = el.key as Key
|
|
89
|
+
if (exiting.has(key)) {
|
|
90
|
+
ensureMutable().delete(key)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (next) setExiting(next)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const handleRemove = useCallback((key: Key) => {
|
|
98
|
+
setExiting((prev) => {
|
|
99
|
+
if (!prev.has(key)) return prev
|
|
100
|
+
const next = new Map(prev)
|
|
101
|
+
next.delete(key)
|
|
102
|
+
return next
|
|
103
|
+
})
|
|
104
|
+
}, [])
|
|
105
|
+
|
|
106
|
+
// Single combined render list. Putting `incoming` and `exiting` entries in
|
|
107
|
+
// one array (rather than two `.map` calls inside a fragment) ensures React
|
|
108
|
+
// reconciles by `key` across positions — when an entry moves from
|
|
109
|
+
// present-list to exiting-list, the component instance persists.
|
|
110
|
+
const renderList: RenderEntry[] = []
|
|
111
|
+
for (const el of incoming) {
|
|
112
|
+
renderList.push({
|
|
113
|
+
key: el.key as Key,
|
|
114
|
+
element: el,
|
|
115
|
+
isPresent: true,
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
for (const [key, el] of exiting) {
|
|
119
|
+
if (!renderList.some((entry) => entry.key === key)) {
|
|
120
|
+
renderList.push({ key, element: el, isPresent: false })
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<>
|
|
126
|
+
{renderList.map(({ key, element, isPresent }) => (
|
|
127
|
+
<PresenceItem
|
|
128
|
+
key={key}
|
|
129
|
+
itemKey={key}
|
|
130
|
+
isPresent={isPresent}
|
|
131
|
+
onRemove={handleRemove}
|
|
132
|
+
>
|
|
133
|
+
{element}
|
|
134
|
+
</PresenceItem>
|
|
135
|
+
))}
|
|
136
|
+
</>
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function PresenceItem({
|
|
141
|
+
itemKey,
|
|
142
|
+
isPresent,
|
|
143
|
+
onRemove,
|
|
144
|
+
children,
|
|
145
|
+
}: {
|
|
146
|
+
itemKey: Key
|
|
147
|
+
isPresent: boolean
|
|
148
|
+
onRemove: (key: Key) => void
|
|
149
|
+
children: ReactNode
|
|
150
|
+
}) {
|
|
151
|
+
const value = useMemo<PresenceContextValue>(
|
|
152
|
+
() => ({
|
|
153
|
+
isPresent,
|
|
154
|
+
safeToRemove: () => onRemove(itemKey),
|
|
155
|
+
}),
|
|
156
|
+
[isPresent, itemKey, onRemove],
|
|
157
|
+
)
|
|
158
|
+
return (
|
|
159
|
+
<PresenceContext.Provider value={value}>
|
|
160
|
+
{children}
|
|
161
|
+
</PresenceContext.Provider>
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
declare const __DEV__: boolean
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Per-child contract between `<Presence>` and its descendant Motion
|
|
5
|
+
* primitives. `<Presence>` provides a fresh value to each rendered child;
|
|
6
|
+
* Motion primitives consume it to gate exit animations.
|
|
7
|
+
*
|
|
8
|
+
* - `isPresent`: `true` while the child is in the incoming children list.
|
|
9
|
+
* Flips to `false` when the parent removes it; the child remains rendered
|
|
10
|
+
* until `safeToRemove` is called.
|
|
11
|
+
* - `safeToRemove`: callback the child invokes when its exit animation has
|
|
12
|
+
* settled. `<Presence>` then drops the snapshot entry and unmounts.
|
|
13
|
+
*/
|
|
14
|
+
export interface PresenceContextValue {
|
|
15
|
+
isPresent: boolean
|
|
16
|
+
safeToRemove: () => void
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const PresenceContext = createContext<PresenceContextValue | null>(null)
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Read the surrounding `<Presence>` contract from a child component. Returns
|
|
23
|
+
* `null` when there is no `<Presence>` ancestor — useful for components that
|
|
24
|
+
* want to support both standalone and Presence-wrapped use without branching.
|
|
25
|
+
*/
|
|
26
|
+
export function usePresence(): PresenceContextValue | null {
|
|
27
|
+
return useContext(PresenceContext)
|
|
28
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { isWorkletFunction } from 'react-native-reanimated'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Reanimated 3.9+ validates that easing functions used in nested-transition
|
|
5
|
+
* contexts (variants, sequences, per-property maps) are worklets, and crashes
|
|
6
|
+
* with `[Reanimated] The easing function is not a worklet` otherwise. The
|
|
7
|
+
* library accepts plain functions on the public surface; this helper wraps
|
|
8
|
+
* them so consumers don't have to think about the worklet boundary.
|
|
9
|
+
*
|
|
10
|
+
* If the input is already a worklet (has been processed by the worklets babel
|
|
11
|
+
* plugin), it's returned as-is. Otherwise it's wrapped in a function whose
|
|
12
|
+
* body declares the `'worklet'` directive — when our source is processed by
|
|
13
|
+
* the consumer's worklets babel plugin (the default Expo/RN setup), the
|
|
14
|
+
* wrapper becomes a real worklet that captures the user fn via closure.
|
|
15
|
+
*
|
|
16
|
+
* The user fn must be pure: no JS-thread captured refs, no shared mutable
|
|
17
|
+
* state, no calls to non-worklet APIs.
|
|
18
|
+
*/
|
|
19
|
+
export function ensureWorkletEasing(
|
|
20
|
+
easing: ((t: number) => number) | undefined,
|
|
21
|
+
): ((t: number) => number) | undefined {
|
|
22
|
+
if (!easing) return undefined
|
|
23
|
+
if (isWorkletFunction(easing)) return easing
|
|
24
|
+
const wrapped = (t: number) => {
|
|
25
|
+
'worklet'
|
|
26
|
+
return easing(t)
|
|
27
|
+
}
|
|
28
|
+
return wrapped
|
|
29
|
+
}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Easing,
|
|
3
|
+
withDecay,
|
|
4
|
+
withDelay,
|
|
5
|
+
withRepeat,
|
|
6
|
+
withSequence,
|
|
7
|
+
withSpring,
|
|
8
|
+
withTiming,
|
|
9
|
+
} from 'react-native-reanimated'
|
|
10
|
+
import { ensureWorkletEasing } from './easing'
|
|
11
|
+
import {
|
|
12
|
+
type AnimatableValue,
|
|
13
|
+
type DecayTransition,
|
|
14
|
+
type RepeatConfig,
|
|
15
|
+
type SequenceStep,
|
|
16
|
+
type SpringTransition,
|
|
17
|
+
type TimingTransition,
|
|
18
|
+
type TransitionConfig,
|
|
19
|
+
} from '../types'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* UI-thread callback Reanimated invokes when an animation settles. Must be a
|
|
23
|
+
* worklet — callers either author one with `'worklet'` or build one via
|
|
24
|
+
* `runOnJS(...)` to bridge to JS-thread code.
|
|
25
|
+
*/
|
|
26
|
+
export type AnimationCallback = (
|
|
27
|
+
finished?: boolean,
|
|
28
|
+
current?: number | string,
|
|
29
|
+
) => void
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Per-step callback factory. Resolvers call this with the step's phase and
|
|
33
|
+
* sequence index (or `undefined` for non-sequence animations) and attach the
|
|
34
|
+
* resulting callback to the underlying `withSpring` / `withTiming` /
|
|
35
|
+
* `withDecay` call.
|
|
36
|
+
*/
|
|
37
|
+
export type CallbackFactory = (
|
|
38
|
+
phase: 'step' | 'animation',
|
|
39
|
+
step: number | undefined,
|
|
40
|
+
) => AnimationCallback | undefined
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Default spring physics, expressed in react-spring vocabulary. Conversion
|
|
44
|
+
* to Reanimated's raw `stiffness` / `damping` lives below; raw config never
|
|
45
|
+
* leaks past this module.
|
|
46
|
+
*/
|
|
47
|
+
const DEFAULT_SPRING: Required<
|
|
48
|
+
Pick<SpringTransition, 'tension' | 'friction' | 'mass'>
|
|
49
|
+
> = {
|
|
50
|
+
tension: 170,
|
|
51
|
+
friction: 26,
|
|
52
|
+
mass: 1,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const DEFAULT_TIMING_DURATION = 250
|
|
56
|
+
|
|
57
|
+
function springToReanimated(t: SpringTransition) {
|
|
58
|
+
return {
|
|
59
|
+
stiffness: t.tension ?? DEFAULT_SPRING.tension,
|
|
60
|
+
damping: t.friction ?? DEFAULT_SPRING.friction,
|
|
61
|
+
mass: t.mass ?? DEFAULT_SPRING.mass,
|
|
62
|
+
velocity: t.velocity,
|
|
63
|
+
restSpeedThreshold: t.restSpeedThreshold,
|
|
64
|
+
restDisplacementThreshold: t.restDisplacementThreshold,
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function buildSpring(
|
|
69
|
+
cfg: SpringTransition,
|
|
70
|
+
toValue: number | string,
|
|
71
|
+
cb?: AnimationCallback,
|
|
72
|
+
) {
|
|
73
|
+
return withSpring(toValue as number, springToReanimated(cfg), cb as never)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function buildTiming(
|
|
77
|
+
cfg: TimingTransition,
|
|
78
|
+
toValue: number | string,
|
|
79
|
+
cb?: AnimationCallback,
|
|
80
|
+
) {
|
|
81
|
+
return withTiming(
|
|
82
|
+
toValue as number,
|
|
83
|
+
{
|
|
84
|
+
duration: cfg.duration ?? DEFAULT_TIMING_DURATION,
|
|
85
|
+
easing: ensureWorkletEasing(cfg.easing) ?? Easing.inOut(Easing.ease),
|
|
86
|
+
},
|
|
87
|
+
cb as never,
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function buildDecay(cfg: DecayTransition, cb?: AnimationCallback) {
|
|
92
|
+
return withDecay(
|
|
93
|
+
{
|
|
94
|
+
velocity: cfg.velocity ?? 0,
|
|
95
|
+
deceleration: cfg.deceleration,
|
|
96
|
+
clamp: cfg.clamp,
|
|
97
|
+
},
|
|
98
|
+
cb as never,
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Build a single-step animation (no repeat / no delay / no sequence) for a
|
|
104
|
+
* given config + target. Pulled out so sequence steps can compose without
|
|
105
|
+
* recursing into repeat/delay handling per step. The callback is forwarded
|
|
106
|
+
* to Reanimated; for `no-animation` the callback is fired synchronously
|
|
107
|
+
* since there's nothing to wait for.
|
|
108
|
+
*/
|
|
109
|
+
function buildOne(
|
|
110
|
+
cfg: TransitionConfig,
|
|
111
|
+
toValue: number | string,
|
|
112
|
+
cb?: AnimationCallback,
|
|
113
|
+
): unknown {
|
|
114
|
+
if (cfg.type === 'no-animation') {
|
|
115
|
+
if (cb) cb(true, toValue)
|
|
116
|
+
return toValue
|
|
117
|
+
}
|
|
118
|
+
if (cfg.type === 'decay') return buildDecay(cfg, cb)
|
|
119
|
+
if (cfg.type === 'timing') return buildTiming(cfg, toValue, cb)
|
|
120
|
+
return buildSpring(cfg as SpringTransition, toValue, cb)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Wrap an animation in `withRepeat` per the unified `repeat` shape:
|
|
125
|
+
* - `number` → finite count, alternating direction
|
|
126
|
+
* - `'infinite'` → endless, alternating direction
|
|
127
|
+
* - `{ count, alternate }`→ explicit; `alternate` defaults to `true`
|
|
128
|
+
*/
|
|
129
|
+
function applyRepeat(animation: unknown, repeat: RepeatConfig | undefined) {
|
|
130
|
+
if (repeat === undefined) return animation
|
|
131
|
+
if (repeat === 'infinite') {
|
|
132
|
+
return withRepeat(animation as never, -1, true)
|
|
133
|
+
}
|
|
134
|
+
if (typeof repeat === 'number') {
|
|
135
|
+
return withRepeat(animation as never, repeat, true)
|
|
136
|
+
}
|
|
137
|
+
const count = repeat.count === 'infinite' ? -1 : repeat.count
|
|
138
|
+
const alternate = repeat.alternate ?? true
|
|
139
|
+
return withRepeat(animation as never, count, alternate)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function applyDelay(animation: unknown, delay: number | undefined) {
|
|
143
|
+
if (!delay || delay <= 0) return animation
|
|
144
|
+
return withDelay(delay, animation as never)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Build a Reanimated animation for a single property. Runs on the JS thread
|
|
149
|
+
* once per change and produces a baked `withSpring` / `withTiming` /
|
|
150
|
+
* `withDecay` (optionally wrapped in `withDelay` / `withRepeat`) call. The
|
|
151
|
+
* worklet body only consumes the result.
|
|
152
|
+
*
|
|
153
|
+
* `callback`, when provided, fires once when the underlying single-shot
|
|
154
|
+
* animation settles. Repeat-wrapped animations forward the callback to
|
|
155
|
+
* `withRepeat`, so it fires once per iteration as Reanimated does.
|
|
156
|
+
*/
|
|
157
|
+
export function resolveTransition(
|
|
158
|
+
config: TransitionConfig | undefined,
|
|
159
|
+
toValue: number | string,
|
|
160
|
+
callback?: AnimationCallback,
|
|
161
|
+
): unknown {
|
|
162
|
+
const cfg = config ?? ({ type: 'spring' } as SpringTransition)
|
|
163
|
+
const base = buildOne(cfg, toValue, callback)
|
|
164
|
+
const repeated = applyRepeat(base, repeatOf(cfg))
|
|
165
|
+
return applyDelay(repeated, delayOf(cfg))
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function repeatOf(cfg: TransitionConfig): RepeatConfig | undefined {
|
|
169
|
+
if (cfg.type === 'no-animation' || cfg.type === 'decay') return undefined
|
|
170
|
+
return cfg.repeat
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Return `cfg` minus its `repeat` field. Used when peeling top-level repeat
|
|
175
|
+
* off a base transition before passing it down to per-sequence-step
|
|
176
|
+
* resolution — the sequence as a whole is what should repeat, not each step.
|
|
177
|
+
*/
|
|
178
|
+
function stripRepeat(
|
|
179
|
+
cfg: TransitionConfig | undefined,
|
|
180
|
+
): TransitionConfig | undefined {
|
|
181
|
+
if (!cfg) return cfg
|
|
182
|
+
if (cfg.type === 'no-animation' || cfg.type === 'decay') return cfg
|
|
183
|
+
if (cfg.repeat === undefined) return cfg
|
|
184
|
+
const next = { ...cfg }
|
|
185
|
+
delete next.repeat
|
|
186
|
+
return next
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function delayOf(cfg: TransitionConfig): number | undefined {
|
|
190
|
+
if (cfg.type === 'no-animation') return undefined
|
|
191
|
+
return cfg.delay
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* True when the value is a `{ to, ...transitionOverride }` sequence step.
|
|
196
|
+
* Plain numbers and plain transition objects fail this check.
|
|
197
|
+
*/
|
|
198
|
+
function isStepObject<V>(
|
|
199
|
+
v: SequenceStep<V> | V,
|
|
200
|
+
): v is Extract<SequenceStep<V>, { to: V }> {
|
|
201
|
+
return (
|
|
202
|
+
typeof v === 'object' &&
|
|
203
|
+
v !== null &&
|
|
204
|
+
!Array.isArray(v) &&
|
|
205
|
+
'to' in (v as object)
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Resolve a per-property `animate` value into a Reanimated animation.
|
|
211
|
+
*
|
|
212
|
+
* Handles the three shapes of `AnimatableValue`:
|
|
213
|
+
* 1. plain value → single `resolveTransition` call
|
|
214
|
+
* 2. `{ to, ...over }` → single step with the override merged into `base`
|
|
215
|
+
* 3. array of either → `withSequence` of resolved steps, with the
|
|
216
|
+
* top-level `repeat` applied at the **sequence level** (not per step).
|
|
217
|
+
* Per-step `repeat` overrides remain step-local.
|
|
218
|
+
*/
|
|
219
|
+
export function resolveAnimatableValue<V extends number | string>(
|
|
220
|
+
value: AnimatableValue<V>,
|
|
221
|
+
base: TransitionConfig | undefined,
|
|
222
|
+
factory?: CallbackFactory,
|
|
223
|
+
): unknown {
|
|
224
|
+
if (Array.isArray(value)) {
|
|
225
|
+
const steps = value as ReadonlyArray<SequenceStep<V>>
|
|
226
|
+
const stepBase = stripRepeat(base)
|
|
227
|
+
const animations = steps.map((step, i) =>
|
|
228
|
+
resolveStep(step, stepBase, factory?.('step', i)),
|
|
229
|
+
)
|
|
230
|
+
const seq = withSequence(...(animations as never[]))
|
|
231
|
+
return applyRepeat(seq, base ? repeatOf(base) : undefined)
|
|
232
|
+
}
|
|
233
|
+
const step = value as SequenceStep<V>
|
|
234
|
+
const cb = factory?.('animation', undefined)
|
|
235
|
+
if (isStepObject<V>(step)) {
|
|
236
|
+
return resolveStep(step, base, cb)
|
|
237
|
+
}
|
|
238
|
+
return resolveTransition(base, step as V, cb)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function resolveStep<V extends number | string>(
|
|
242
|
+
step: SequenceStep<V>,
|
|
243
|
+
base: TransitionConfig | undefined,
|
|
244
|
+
cb?: AnimationCallback,
|
|
245
|
+
): unknown {
|
|
246
|
+
if (isStepObject<V>(step)) {
|
|
247
|
+
const { to, ...override } = step as { to: V } & Partial<TransitionConfig>
|
|
248
|
+
const merged = mergeTransition(base, override as Partial<TransitionConfig>)
|
|
249
|
+
return resolveTransition(merged, to, cb)
|
|
250
|
+
}
|
|
251
|
+
return resolveTransition(base, step as V, cb)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function mergeTransition(
|
|
255
|
+
base: TransitionConfig | undefined,
|
|
256
|
+
override: Partial<TransitionConfig>,
|
|
257
|
+
): TransitionConfig {
|
|
258
|
+
// If the override declares a `type`, it wins outright — mixing fields from
|
|
259
|
+
// a spring base into a timing override produces garbage. Otherwise inherit
|
|
260
|
+
// the base's type and shallow-merge the rest.
|
|
261
|
+
if (override.type && base && override.type !== base.type) {
|
|
262
|
+
return override as TransitionConfig
|
|
263
|
+
}
|
|
264
|
+
return { ...(base ?? { type: 'spring' }), ...override } as TransitionConfig
|
|
265
|
+
}
|