@rootnative/inertia 0.0.2 → 0.0.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/CHANGELOG.md +63 -5
- package/README.md +5 -3
- package/dist/{chunk-ZN3PNGHQ.mjs → chunk-4ASOELLA.mjs} +1 -1
- package/dist/{chunk-LYMLQ6WQ.mjs → chunk-55W7BNO2.mjs} +604 -106
- package/dist/chunk-5SRD2WCO.js +8 -0
- package/dist/{chunk-PTRF47DA.js → chunk-7A6WTXNK.js} +10 -3
- package/dist/{chunk-NKA6XR77.js → chunk-CENTULW6.js} +2 -2
- package/dist/{chunk-4PEHWDAZ.js → chunk-D5KAHWMC.js} +7 -7
- package/dist/{chunk-2DHKV2XY.mjs → chunk-J5Q46OPW.mjs} +1 -1
- package/dist/{chunk-RGNX6UZN.mjs → chunk-K3CLP74S.mjs} +2 -2
- package/dist/{chunk-IWLU3VDE.js → chunk-MYE5NMXW.js} +623 -123
- package/dist/{chunk-HFDZAHWP.mjs → chunk-NAOXQJ7U.mjs} +1 -1
- package/dist/{chunk-R63GIUNU.mjs → chunk-QKC3APP4.mjs} +1 -1
- package/dist/{chunk-V6BTXHZK.mjs → chunk-RF7J5FRM.mjs} +1 -1
- package/dist/{chunk-CY7Y64C3.mjs → chunk-TU2XFPAK.mjs} +10 -4
- package/dist/chunk-TVFNJ25Z.js +8 -0
- package/dist/chunk-VLDIFL3F.js +8 -0
- package/dist/{chunk-7AOERN53.js → chunk-WVRCKOPO.js} +3 -3
- package/dist/{chunk-TUYHTHVV.js → chunk-XMEVPEMH.js} +2 -2
- package/dist/{chunk-TUMNV4P7.mjs → chunk-YKGY22SB.mjs} +1 -1
- package/dist/gestureLayer/index.d.mts +2 -2
- package/dist/gestureLayer/index.d.ts +2 -2
- package/dist/gestureLayer/index.js +10 -10
- package/dist/gestureLayer/index.mjs +3 -3
- package/dist/index.d.mts +14 -12
- package/dist/index.d.ts +14 -12
- package/dist/index.js +68 -176
- package/dist/index.mjs +40 -147
- package/dist/motion/Image.d.mts +1 -1
- package/dist/motion/Image.d.ts +1 -1
- package/dist/motion/Image.js +5 -5
- package/dist/motion/Image.mjs +4 -4
- package/dist/motion/Pressable.d.mts +1 -1
- package/dist/motion/Pressable.d.ts +1 -1
- package/dist/motion/Pressable.js +5 -5
- package/dist/motion/Pressable.mjs +4 -4
- package/dist/motion/ScrollView.d.mts +1 -1
- package/dist/motion/ScrollView.d.ts +1 -1
- package/dist/motion/ScrollView.js +5 -5
- package/dist/motion/ScrollView.mjs +4 -4
- package/dist/motion/Text.d.mts +1 -1
- package/dist/motion/Text.d.ts +1 -1
- package/dist/motion/Text.js +5 -5
- package/dist/motion/Text.mjs +4 -4
- package/dist/motion/View.d.mts +1 -1
- package/dist/motion/View.d.ts +1 -1
- package/dist/motion/View.js +5 -5
- package/dist/motion/View.mjs +4 -4
- package/dist/touch/index.d.mts +1 -1
- package/dist/touch/index.d.ts +1 -1
- package/dist/touch/index.js +3 -3
- package/dist/touch/index.mjs +1 -1
- package/dist/{types-G1dBU2IQ.d.mts → types-37pN82yh.d.mts} +49 -14
- package/dist/{types-G1dBU2IQ.d.ts → types-37pN82yh.d.ts} +49 -14
- package/dist/{useGesture-D1e4ixJo.d.ts → useGesture-Cl4uzZIr.d.ts} +1 -1
- package/dist/{useGesture-RFY6kTL3.d.mts → useGesture-_IKkS8Dv.d.mts} +1 -1
- package/llms.txt +3 -1
- package/package.json +1 -1
- package/src/index.ts +2 -1
- package/src/internal/boxShadow.ts +177 -6
- package/src/internal/nonWorkletWarning.ts +4 -6
- package/src/internal/warnOnce.ts +28 -0
- package/src/layout/index.ts +11 -0
- package/src/layout/measureWindow.ts +135 -0
- package/src/layout/sharedRegistry.ts +156 -18
- package/src/layout/useSharedLayout.ts +256 -91
- package/src/motion/createMotionComponent.tsx +550 -55
- package/src/motion/installCheck.ts +1 -1
- package/src/presence/Presence.tsx +73 -10
- package/src/transitions/index.ts +1 -1
- package/src/types.ts +52 -12
- package/src/values/useAnimator.ts +25 -14
- package/src/values/useColorCascade.ts +34 -16
- package/src/values/useColorTransition.ts +1 -1
- package/dist/chunk-34Q4UM6V.js +0 -8
- package/dist/chunk-KYJROYCG.js +0 -8
- package/dist/chunk-X5J5M3K3.js +0 -8
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
} from 'react'
|
|
9
9
|
import { type LayoutChangeEvent } from 'react-native'
|
|
10
10
|
import {
|
|
11
|
+
cancelAnimation,
|
|
11
12
|
type SharedValue,
|
|
12
13
|
useSharedValue,
|
|
13
14
|
withSequence,
|
|
@@ -16,11 +17,14 @@ import {
|
|
|
16
17
|
} from 'react-native-reanimated'
|
|
17
18
|
import { DEFAULT_SPRING, springToReanimated } from '../transitions/spring'
|
|
18
19
|
import { type SpringTransition, type TransitionConfig } from '../types'
|
|
20
|
+
import { measureWindowRect } from './measureWindow'
|
|
19
21
|
import {
|
|
20
22
|
consumeLayout,
|
|
21
23
|
registerLayout,
|
|
22
24
|
releaseLayout,
|
|
25
|
+
type SharedLayoutSource,
|
|
23
26
|
type SharedRect,
|
|
27
|
+
type SharedStyleSnapshot,
|
|
24
28
|
} from './sharedRegistry'
|
|
25
29
|
|
|
26
30
|
/**
|
|
@@ -43,9 +47,29 @@ export interface SharedLayoutValues {
|
|
|
43
47
|
sy: SharedValue<number>
|
|
44
48
|
}
|
|
45
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Shared values carrying the *style* half of a shared-element transition — the
|
|
52
|
+
* part the rect FLIP can't express.
|
|
53
|
+
*
|
|
54
|
+
* `snapshot` parks the source element's values for the carried keys; `progress`
|
|
55
|
+
* runs 1 → 0 on the same transition as the FLIP. The host's worklet reads both
|
|
56
|
+
* and pulls each carried key from the snapshot toward its own resolved value,
|
|
57
|
+
* so at the first frame the arriving element wears the source's colors and by
|
|
58
|
+
* the last it wears its own.
|
|
59
|
+
*
|
|
60
|
+
* At rest `snapshot` is `null` and `progress` is 0, so the worklet's whole
|
|
61
|
+
* contribution is one comparison.
|
|
62
|
+
*/
|
|
63
|
+
export interface SharedLayoutStyleValues {
|
|
64
|
+
snapshot: SharedValue<SharedStyleSnapshot | null>
|
|
65
|
+
progress: SharedValue<number>
|
|
66
|
+
}
|
|
67
|
+
|
|
46
68
|
/** What the host component needs to wire into its rendered tree. */
|
|
47
69
|
export interface SharedLayoutBindings {
|
|
48
70
|
flip: SharedLayoutValues
|
|
71
|
+
/** Source-style carry for the same transition. See `SharedLayoutStyleValues`. */
|
|
72
|
+
carry: SharedLayoutStyleValues
|
|
49
73
|
/**
|
|
50
74
|
* Composite ref the consumer attaches to the rendered animated
|
|
51
75
|
* component. Forwards the underlying ref to the user-supplied `ref`
|
|
@@ -72,14 +96,32 @@ export interface SharedLayoutBindings {
|
|
|
72
96
|
* visually places the new element at the source position, then
|
|
73
97
|
* animate back to identity. `withSequence(snap, animate)` keeps the
|
|
74
98
|
* animation starting from the snapped delta rather than from zero.
|
|
99
|
+
* 5. Carry the source's style values for the keys the rect can't express
|
|
100
|
+
* (colors, `opacity`, `borderRadius`) by parking them in a snapshot and
|
|
101
|
+
* running a crossfade progress 1 → 0 on the same transition. The blend
|
|
102
|
+
* happens in the host's worklet; see `SharedLayoutStyleValues`.
|
|
103
|
+
*
|
|
104
|
+
* Coordinate space: rects are recorded in **window** coordinates when the host
|
|
105
|
+
* can be measured synchronously (Fabric), and in the parent-relative
|
|
106
|
+
* coordinates `onLayout` reports when it can't (Paper, or a detached node).
|
|
107
|
+
* Window coordinates are what let a source and target sitting under containers
|
|
108
|
+
* at different screen offsets FLIP correctly — the case the original
|
|
109
|
+
* parent-relative implementation got wrong. A source and target that ended up
|
|
110
|
+
* in *different* spaces are not comparable, so the transition is skipped rather
|
|
111
|
+
* than played wrongly. See `measureWindow.ts` for why there is no third,
|
|
112
|
+
* asynchronous option.
|
|
113
|
+
*
|
|
114
|
+
* The source is re-measured at *consume* time when it is still mounted (the
|
|
115
|
+
* usual case — a stack navigator keeps the previous screen alive underneath).
|
|
116
|
+
* That is what stops a scrolled list from offsetting every transition by its
|
|
117
|
+
* scroll distance: `onLayout` never fires for an ancestor's scroll, so the
|
|
118
|
+
* stored rect drifts while the element itself has not moved relative to its
|
|
119
|
+
* parent. See `SharedLayoutSource`.
|
|
75
120
|
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
* and target screens sit under containers at different screen offsets
|
|
81
|
-
* will be off by that offset; v1 documents this and leaves a precise
|
|
82
|
-
* window-coords path for v2.
|
|
121
|
+
* Known limitation: if the navigator has already begun translating the outgoing
|
|
122
|
+
* screen by the time the target first lays out, re-measuring catches the source
|
|
123
|
+
* mid-transition. The error is bounded by however far the transition has
|
|
124
|
+
* progressed, which is far smaller than an unbounded scroll offset.
|
|
83
125
|
*
|
|
84
126
|
* When `layoutId` is `undefined`, every callback is a no-op and the FLIP
|
|
85
127
|
* shared values stay at identity — the host's worklet then skips the
|
|
@@ -91,15 +133,33 @@ export function useSharedLayout(options: {
|
|
|
91
133
|
transition: TransitionConfig | undefined
|
|
92
134
|
shouldReduceMotion: boolean
|
|
93
135
|
userOnLayout: ((event: LayoutChangeEvent) => void) | undefined
|
|
136
|
+
/**
|
|
137
|
+
* Read this element's current values for the carried style keys. Supplied by
|
|
138
|
+
* the host (which owns the per-key shared values); `undefined` when the host
|
|
139
|
+
* has nothing to carry. Identity may change every render — it is read through
|
|
140
|
+
* a ref, so it never churns `onLayout`.
|
|
141
|
+
*/
|
|
142
|
+
readStyles?: () => SharedStyleSnapshot | undefined
|
|
94
143
|
}): SharedLayoutBindings {
|
|
95
|
-
const {
|
|
96
|
-
|
|
144
|
+
const {
|
|
145
|
+
layoutId,
|
|
146
|
+
userRef,
|
|
147
|
+
transition,
|
|
148
|
+
shouldReduceMotion,
|
|
149
|
+
userOnLayout,
|
|
150
|
+
readStyles,
|
|
151
|
+
} = options
|
|
97
152
|
|
|
98
153
|
const dx = useSharedValue(0)
|
|
99
154
|
const dy = useSharedValue(0)
|
|
100
155
|
const sx = useSharedValue(1)
|
|
101
156
|
const sy = useSharedValue(1)
|
|
102
157
|
|
|
158
|
+
// Style-carry pair. `null` / 0 at rest means the host worklet skips the
|
|
159
|
+
// whole branch on any element that isn't mid-transition.
|
|
160
|
+
const snapshot = useSharedValue<SharedStyleSnapshot | null>(null)
|
|
161
|
+
const carryProgress = useSharedValue(0)
|
|
162
|
+
|
|
103
163
|
// Most-recent rect for this primitive. Updated on every layout commit;
|
|
104
164
|
// read on unmount to populate the registry as the FLIP source for the
|
|
105
165
|
// next mount with the same id.
|
|
@@ -110,55 +170,100 @@ export function useSharedLayout(options: {
|
|
|
110
170
|
// refresh the registry but never re-trigger a FLIP from an old source.
|
|
111
171
|
const consumedRef = useRef(false)
|
|
112
172
|
|
|
173
|
+
// The host node, captured off the composite ref so this element can be
|
|
174
|
+
// measured in window coordinates — both for its own registry entry and when
|
|
175
|
+
// a later mount asks it to re-measure as a FLIP source.
|
|
176
|
+
const nodeRef = useRef<unknown>(null)
|
|
177
|
+
|
|
113
178
|
const transitionRef = useRef(transition)
|
|
114
179
|
transitionRef.current = transition
|
|
115
180
|
const reducedMotionRef = useRef(shouldReduceMotion)
|
|
116
181
|
reducedMotionRef.current = shouldReduceMotion
|
|
182
|
+
const readStylesRef = useRef(readStyles)
|
|
183
|
+
readStylesRef.current = readStyles
|
|
117
184
|
|
|
118
185
|
const setRef = useCallback(
|
|
119
186
|
(node: unknown) => {
|
|
187
|
+
nodeRef.current = node
|
|
120
188
|
if (typeof userRef === 'function') userRef(node)
|
|
121
189
|
else if (userRef) (userRef as MutableRefObject<unknown>).current = node
|
|
122
190
|
},
|
|
123
191
|
[userRef],
|
|
124
192
|
)
|
|
125
193
|
|
|
194
|
+
// Offered to the registry while mounted so a later mount can ask for a
|
|
195
|
+
// current measurement instead of trusting the one from our last layout.
|
|
196
|
+
const remeasure = useCallback(() => measureWindowRect(nodeRef.current), [])
|
|
197
|
+
|
|
198
|
+
// Same idea for the carried style keys: the host's values move without a
|
|
199
|
+
// layout pass, so a consumer should read them now rather than trust the
|
|
200
|
+
// snapshot from whenever we last laid out. Stable identity — the host's
|
|
201
|
+
// reader is reached through a ref.
|
|
202
|
+
const snapshotStyles = useCallback(() => readStylesRef.current?.(), [])
|
|
203
|
+
|
|
204
|
+
const startTransition = useCallback(
|
|
205
|
+
(
|
|
206
|
+
source: SharedLayoutSource,
|
|
207
|
+
sourceRect: SharedRect,
|
|
208
|
+
target: SharedRect,
|
|
209
|
+
) => {
|
|
210
|
+
// Mixed coordinate spaces are not comparable — the delta would be off by
|
|
211
|
+
// the parent's window offset. Skipping is the honest outcome; playing
|
|
212
|
+
// the animation anyway would fling the element in from the wrong place.
|
|
213
|
+
// The style carry goes with it: half a shared-element transition reads
|
|
214
|
+
// as a glitch, not as a graceful degradation.
|
|
215
|
+
if (sourceRect.space !== target.space) return
|
|
216
|
+
const shared = {
|
|
217
|
+
transition: transitionRef.current,
|
|
218
|
+
shouldReduceMotion: reducedMotionRef.current,
|
|
219
|
+
}
|
|
220
|
+
applyFlip({ source: sourceRect, target, dx, dy, sx, sy, ...shared })
|
|
221
|
+
applyStyleCarry({
|
|
222
|
+
snapshot,
|
|
223
|
+
progress: carryProgress,
|
|
224
|
+
// Prefer a live read for the same reason the rect is re-measured: a
|
|
225
|
+
// still-mounted source may have moved on since its last layout.
|
|
226
|
+
styles: source.readStyles?.() ?? source.styles,
|
|
227
|
+
...shared,
|
|
228
|
+
})
|
|
229
|
+
},
|
|
230
|
+
[dx, dy, sx, sy, snapshot, carryProgress],
|
|
231
|
+
)
|
|
232
|
+
|
|
126
233
|
const onLayout = useCallback(
|
|
127
234
|
(event: LayoutChangeEvent) => {
|
|
128
235
|
userOnLayout?.(event)
|
|
129
236
|
if (!layoutId) return
|
|
130
237
|
|
|
131
238
|
const { x, y, width, height } = event.nativeEvent.layout
|
|
132
|
-
const rect: SharedRect = { x, y, width, height }
|
|
133
|
-
lastRectRef.current = rect
|
|
134
239
|
|
|
135
240
|
// First-layout-only: read the registry BEFORE writing our own rect
|
|
136
241
|
// so a previously-released source rect can be consumed cleanly
|
|
137
242
|
// without being overwritten by the current rect first.
|
|
138
|
-
let source:
|
|
243
|
+
let source: SharedLayoutSource | undefined
|
|
139
244
|
if (!consumedRef.current) {
|
|
140
245
|
consumedRef.current = true
|
|
141
246
|
source = consumeLayout(layoutId)
|
|
142
247
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
248
|
+
|
|
249
|
+
// Window coordinates when the host can supply them synchronously,
|
|
250
|
+
// otherwise the parent-relative rect this event carries. See
|
|
251
|
+
// `measureWindow.ts` for why there is no third, asynchronous option.
|
|
252
|
+
const rect: SharedRect =
|
|
253
|
+
measureWindowRect(nodeRef.current) ??
|
|
254
|
+
({ x, y, width, height, space: 'parent' } as SharedRect)
|
|
255
|
+
|
|
256
|
+
lastRectRef.current = rect
|
|
257
|
+
registerLayout(layoutId, rect, remeasure, snapshotStyles)
|
|
258
|
+
|
|
259
|
+
if (!source) return
|
|
260
|
+
// Prefer a fresh measurement of the source over its stored rect: it may
|
|
261
|
+
// have scrolled since its last layout, and `onLayout` never reports an
|
|
262
|
+
// ancestor's scroll. Falls back to the stored rect once the source has
|
|
263
|
+
// unmounted or when it can't be measured.
|
|
264
|
+
startTransition(source, source.remeasure?.() ?? source.rect, rect)
|
|
157
265
|
},
|
|
158
|
-
|
|
159
|
-
// exhaustive-deps would flag them — including them is harmless and
|
|
160
|
-
// silences the warning.
|
|
161
|
-
[layoutId, userOnLayout, dx, dy, sx, sy],
|
|
266
|
+
[layoutId, userOnLayout, remeasure, snapshotStyles, startTransition],
|
|
162
267
|
)
|
|
163
268
|
|
|
164
269
|
// Reset the first-layout latch when the id changes — a new id is logically
|
|
@@ -167,33 +272,104 @@ export function useSharedLayout(options: {
|
|
|
167
272
|
consumedRef.current = false
|
|
168
273
|
}, [layoutId])
|
|
169
274
|
|
|
170
|
-
// On unmount, hand the latest rect
|
|
171
|
-
//
|
|
275
|
+
// On unmount, hand the latest rect — and a final snapshot of the carried
|
|
276
|
+
// style keys — to the registry under this id so the next mount can consume
|
|
277
|
+
// them. The styles are read by value here rather than left behind as a
|
|
278
|
+
// callback: after this the node is gone, and a later read would be measuring
|
|
279
|
+
// a component nobody owns.
|
|
172
280
|
useEffect(() => {
|
|
173
281
|
return () => {
|
|
174
282
|
if (!layoutId) return
|
|
175
283
|
const rect = lastRectRef.current
|
|
176
284
|
if (!rect) return
|
|
177
|
-
releaseLayout(layoutId, rect)
|
|
285
|
+
releaseLayout(layoutId, rect, snapshotStyles())
|
|
178
286
|
}
|
|
179
|
-
}, [layoutId])
|
|
287
|
+
}, [layoutId, snapshotStyles])
|
|
288
|
+
|
|
289
|
+
// Mark unmounted and stop any in-flight FLIP. The mounted flag is what keeps
|
|
290
|
+
// an asynchronous measurement callback from writing to shared values the
|
|
291
|
+
// component no longer owns; cancelling matches the guard the factory's own
|
|
292
|
+
// per-key values and the value-layer hooks got in `0.0.2` — these four were
|
|
293
|
+
// missed at the time.
|
|
294
|
+
// Stop any in-flight FLIP on unmount. Matches the guard the factory's own
|
|
295
|
+
// per-key values and the value-layer hooks got in `0.0.2` — these four were
|
|
296
|
+
// missed at the time.
|
|
297
|
+
useEffect(
|
|
298
|
+
() => () => {
|
|
299
|
+
cancelAnimation(dx)
|
|
300
|
+
cancelAnimation(dy)
|
|
301
|
+
cancelAnimation(sx)
|
|
302
|
+
cancelAnimation(sy)
|
|
303
|
+
// `snapshot` is assigned, never animated, so there is nothing to cancel
|
|
304
|
+
// on it — only the progress value drives.
|
|
305
|
+
cancelAnimation(carryProgress)
|
|
306
|
+
},
|
|
307
|
+
// The FLIP shared values are identity-stable per hook instance.
|
|
308
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
309
|
+
[],
|
|
310
|
+
)
|
|
180
311
|
|
|
181
312
|
return useMemo<SharedLayoutBindings>(
|
|
182
313
|
() => ({
|
|
183
314
|
flip: { dx, dy, sx, sy },
|
|
315
|
+
carry: { snapshot, progress: carryProgress },
|
|
184
316
|
setRef,
|
|
185
317
|
onLayout,
|
|
186
318
|
}),
|
|
187
|
-
[dx, dy, sx, sy, setRef, onLayout],
|
|
319
|
+
[dx, dy, sx, sy, snapshot, carryProgress, setRef, onLayout],
|
|
188
320
|
)
|
|
189
321
|
}
|
|
190
322
|
|
|
323
|
+
/**
|
|
324
|
+
* Build the `withSequence(snap, animate)` pair every leg of a shared-element
|
|
325
|
+
* transition uses: jump to `from` instantly, then animate to `to`. The snap leg
|
|
326
|
+
* is what makes the animation start from the source — a plain `withSpring(to)`
|
|
327
|
+
* off the resting base would animate from wherever the value already sat.
|
|
328
|
+
*
|
|
329
|
+
* Returned as a closure so the spring params are converted once and reused
|
|
330
|
+
* across all five legs (four transforms plus the style crossfade), which also
|
|
331
|
+
* guarantees the rect and the style land together.
|
|
332
|
+
*/
|
|
333
|
+
function legBuilder(
|
|
334
|
+
transition: TransitionConfig | undefined,
|
|
335
|
+
): (from: number, to: number) => number {
|
|
336
|
+
if (transition?.type === 'timing') {
|
|
337
|
+
const duration = transition.duration ?? 300
|
|
338
|
+
return (from, to) =>
|
|
339
|
+
withSequence(
|
|
340
|
+
withTiming(from, { duration: 0 }),
|
|
341
|
+
withTiming(to, { duration }),
|
|
342
|
+
)
|
|
343
|
+
}
|
|
344
|
+
// Spring path (default, and the fallback for `'decay'` which doesn't have a
|
|
345
|
+
// meaningful target value for a FLIP transition).
|
|
346
|
+
const springCfg: SpringTransition =
|
|
347
|
+
transition?.type === 'spring'
|
|
348
|
+
? { ...DEFAULT_SPRING, ...transition }
|
|
349
|
+
: { type: 'spring', ...DEFAULT_SPRING }
|
|
350
|
+
const springParams = springToReanimated(springCfg)
|
|
351
|
+
return (from, to) =>
|
|
352
|
+
withSequence(
|
|
353
|
+
withTiming(from, { duration: 0 }),
|
|
354
|
+
withSpring(to, springParams),
|
|
355
|
+
)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Whether a shared-element transition should play at all, or snap. Reduced
|
|
360
|
+
* motion and an explicit `'no-animation'` transition are the same answer for
|
|
361
|
+
* both the rect and the style halves.
|
|
362
|
+
*/
|
|
363
|
+
function shouldSnap(
|
|
364
|
+
transition: TransitionConfig | undefined,
|
|
365
|
+
shouldReduceMotion: boolean,
|
|
366
|
+
): boolean {
|
|
367
|
+
return shouldReduceMotion || transition?.type === 'no-animation'
|
|
368
|
+
}
|
|
369
|
+
|
|
191
370
|
/**
|
|
192
371
|
* Snap the FLIP shared values so the new element visually overlays its
|
|
193
|
-
* source rect, then animate back to identity.
|
|
194
|
-
* animate)` shape is what makes the spring start from the snapped delta —
|
|
195
|
-
* a plain `withSpring(0)` from a zero base would animate from-zero, not
|
|
196
|
-
* from-source.
|
|
372
|
+
* source rect, then animate back to identity.
|
|
197
373
|
*/
|
|
198
374
|
function applyFlip(args: {
|
|
199
375
|
source: SharedRect
|
|
@@ -223,9 +399,9 @@ function applyFlip(args: {
|
|
|
223
399
|
const scaleX = target.width > 0 ? source.width / target.width : 1
|
|
224
400
|
const scaleY = target.height > 0 ? source.height / target.height : 1
|
|
225
401
|
|
|
226
|
-
if (shouldReduceMotion) {
|
|
227
|
-
//
|
|
228
|
-
//
|
|
402
|
+
if (shouldSnap(transition, shouldReduceMotion)) {
|
|
403
|
+
// Skip the visual transition entirely. The element appears at its natural
|
|
404
|
+
// position; the source rect is discarded.
|
|
229
405
|
dx.value = 0
|
|
230
406
|
dy.value = 0
|
|
231
407
|
sx.value = 1
|
|
@@ -233,57 +409,46 @@ function applyFlip(args: {
|
|
|
233
409
|
return
|
|
234
410
|
}
|
|
235
411
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
}
|
|
412
|
+
const leg = legBuilder(transition)
|
|
413
|
+
dx.value = leg(deltaX, 0)
|
|
414
|
+
dy.value = leg(deltaY, 0)
|
|
415
|
+
sx.value = leg(scaleX, 1)
|
|
416
|
+
sy.value = leg(scaleY, 1)
|
|
417
|
+
}
|
|
243
418
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
419
|
+
/**
|
|
420
|
+
* Park the source element's carried style values and run the crossfade
|
|
421
|
+
* progress 1 → 0 on the same transition as the rect FLIP.
|
|
422
|
+
*
|
|
423
|
+
* The blend itself happens in the host's worklet, which pulls each carried key
|
|
424
|
+
* from `snapshot[key]` toward its own resolved value by `progress`. Driving
|
|
425
|
+
* only a scalar here is what keeps this cheap: one shared value moves, however
|
|
426
|
+
* many keys are being carried, and colors go through the same
|
|
427
|
+
* `interpolateColor` branch the gesture cascade already uses.
|
|
428
|
+
*
|
|
429
|
+
* A source with nothing to carry leaves both values alone — an element whose
|
|
430
|
+
* counterpart declared no carried keys behaves exactly as it did before style
|
|
431
|
+
* carry existed.
|
|
432
|
+
*/
|
|
433
|
+
function applyStyleCarry(args: {
|
|
434
|
+
snapshot: SharedValue<SharedStyleSnapshot | null>
|
|
435
|
+
progress: SharedValue<number>
|
|
436
|
+
styles: SharedStyleSnapshot | undefined
|
|
437
|
+
transition: TransitionConfig | undefined
|
|
438
|
+
shouldReduceMotion: boolean
|
|
439
|
+
}): void {
|
|
440
|
+
const { snapshot, progress, styles, transition, shouldReduceMotion } = args
|
|
441
|
+
if (!styles) return
|
|
442
|
+
|
|
443
|
+
if (shouldSnap(transition, shouldReduceMotion)) {
|
|
444
|
+
// Matching the rect path: the element arrives wearing its own style rather
|
|
445
|
+
// than crossfading into it. Clearing the snapshot too means the worklet's
|
|
446
|
+
// branch stays untaken instead of resting on a dead payload.
|
|
447
|
+
snapshot.value = null
|
|
448
|
+
progress.value = 0
|
|
262
449
|
return
|
|
263
450
|
}
|
|
264
451
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
const springCfg: SpringTransition =
|
|
268
|
-
transition?.type === 'spring'
|
|
269
|
-
? { ...DEFAULT_SPRING, ...transition }
|
|
270
|
-
: { type: 'spring', ...DEFAULT_SPRING }
|
|
271
|
-
const springParams = springToReanimated(springCfg)
|
|
272
|
-
|
|
273
|
-
dx.value = withSequence(
|
|
274
|
-
withTiming(deltaX, { duration: 0 }),
|
|
275
|
-
withSpring(0, springParams),
|
|
276
|
-
)
|
|
277
|
-
dy.value = withSequence(
|
|
278
|
-
withTiming(deltaY, { duration: 0 }),
|
|
279
|
-
withSpring(0, springParams),
|
|
280
|
-
)
|
|
281
|
-
sx.value = withSequence(
|
|
282
|
-
withTiming(scaleX, { duration: 0 }),
|
|
283
|
-
withSpring(1, springParams),
|
|
284
|
-
)
|
|
285
|
-
sy.value = withSequence(
|
|
286
|
-
withTiming(scaleY, { duration: 0 }),
|
|
287
|
-
withSpring(1, springParams),
|
|
288
|
-
)
|
|
452
|
+
snapshot.value = styles
|
|
453
|
+
progress.value = legBuilder(transition)(1, 0)
|
|
289
454
|
}
|