@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
|
@@ -14,7 +14,7 @@ import Animated, {
|
|
|
14
14
|
useSharedValue,
|
|
15
15
|
type SharedValue,
|
|
16
16
|
} from 'react-native-reanimated'
|
|
17
|
-
import { type LayoutChangeEvent } from 'react-native'
|
|
17
|
+
import { StyleSheet, type LayoutChangeEvent } from 'react-native'
|
|
18
18
|
import {
|
|
19
19
|
lookupNamedTransition,
|
|
20
20
|
resolveNamedTransitionProp,
|
|
@@ -22,9 +22,17 @@ import {
|
|
|
22
22
|
useShouldReduceMotion,
|
|
23
23
|
} from '../config'
|
|
24
24
|
import { isFocusVisible } from '../gestures'
|
|
25
|
+
import {
|
|
26
|
+
normalizeBoxShadow,
|
|
27
|
+
prepareBoxShadowAnimation,
|
|
28
|
+
type AnimatedBoxShadowLayer,
|
|
29
|
+
type SplitBoxShadow,
|
|
30
|
+
} from '../internal/boxShadow'
|
|
31
|
+
import { warnOnce } from '../internal/warnOnce'
|
|
25
32
|
import {
|
|
26
33
|
resolveLayoutTransition,
|
|
27
34
|
type LayoutProp,
|
|
35
|
+
type SharedStyleSnapshot,
|
|
28
36
|
useSharedLayout,
|
|
29
37
|
} from '../layout'
|
|
30
38
|
import { usePresence } from '../presence'
|
|
@@ -33,6 +41,7 @@ import {
|
|
|
33
41
|
resolveAnimatableValue,
|
|
34
42
|
resolveTransition,
|
|
35
43
|
stableSig,
|
|
44
|
+
type CallbackFactory,
|
|
36
45
|
} from '../transitions'
|
|
37
46
|
import { ensureReanimatedInstalled } from './installCheck'
|
|
38
47
|
import {
|
|
@@ -42,6 +51,7 @@ import {
|
|
|
42
51
|
type GestureLayerTransitions,
|
|
43
52
|
type GestureSubStates,
|
|
44
53
|
type MotionComponent,
|
|
54
|
+
type BoxShadowInput,
|
|
45
55
|
type MotionProps,
|
|
46
56
|
type PerPropertyTransition,
|
|
47
57
|
type Transition,
|
|
@@ -108,6 +118,42 @@ const COLOR_KEYS = [
|
|
|
108
118
|
// these keys directly.
|
|
109
119
|
const SHADOW_OFFSET_KEYS = ['shadowOffsetWidth', 'shadowOffsetHeight'] as const
|
|
110
120
|
|
|
121
|
+
// Keys whose shared value holds a structure rather than a scalar. `boxShadow`
|
|
122
|
+
// is the only one: its slot carries an array of layer objects, which
|
|
123
|
+
// Reanimated's animation drivers recurse into, animating each leaf number and
|
|
124
|
+
// color independently (`arrayOnStart` → `objectOnStart` in Reanimated's
|
|
125
|
+
// `animation/util.ts`).
|
|
126
|
+
//
|
|
127
|
+
// Passing the CSS string through instead would NOT work — a box-shadow string
|
|
128
|
+
// isn't a color, so it lands in Reanimated's prefix-number-suffix branch,
|
|
129
|
+
// which is built for values like '100%' and would pull a single number out of
|
|
130
|
+
// a four-value shadow. All string parsing therefore happens on the JS thread,
|
|
131
|
+
// once per change, in `internal/boxShadow.ts`.
|
|
132
|
+
const STRUCTURED_KEYS = ['boxShadow'] as const
|
|
133
|
+
|
|
134
|
+
// Keys a shared-element transition (`layoutId`) carries from the source
|
|
135
|
+
// element to the target, crossfading them alongside the rect FLIP. Everything
|
|
136
|
+
// here is either a scalar or a color, so the worklet's existing lerp /
|
|
137
|
+
// `interpolateColor` branches handle it with no new machinery.
|
|
138
|
+
//
|
|
139
|
+
// Transform keys are deliberately absent: they are the FLIP's job, and
|
|
140
|
+
// carrying them too would apply the same displacement twice.
|
|
141
|
+
//
|
|
142
|
+
// So is `shadowColor`, despite being a color key. Carrying one of the four
|
|
143
|
+
// native shadow props crossfades a shadow's *color* over geometry that snapped
|
|
144
|
+
// — a worse result than letting the whole shadow snap together. (It would also
|
|
145
|
+
// pull `shadowColor` into the active set on any `layoutId` element whose style
|
|
146
|
+
// declares it, tripping the `boxShadow`-with-native-`shadow*` warning for a
|
|
147
|
+
// consumer who never asked for either.)
|
|
148
|
+
const SHARED_STYLE_KEYS = [
|
|
149
|
+
'opacity',
|
|
150
|
+
'borderRadius',
|
|
151
|
+
'backgroundColor',
|
|
152
|
+
'borderColor',
|
|
153
|
+
'color',
|
|
154
|
+
'tintColor',
|
|
155
|
+
] as const
|
|
156
|
+
|
|
111
157
|
/**
|
|
112
158
|
* Per-effect transform-group coordinator. Counts how many transform-axis
|
|
113
159
|
* terminal callbacks are still pending; when the last one fires, the
|
|
@@ -121,6 +167,7 @@ const ALL_KEYS = [
|
|
|
121
167
|
...NUMERIC_TOP_LEVEL_KEYS,
|
|
122
168
|
...COLOR_KEYS,
|
|
123
169
|
...SHADOW_OFFSET_KEYS,
|
|
170
|
+
...STRUCTURED_KEYS,
|
|
124
171
|
] as const
|
|
125
172
|
type AnimatableKey = (typeof ALL_KEYS)[number]
|
|
126
173
|
type TransformKey = (typeof TRANSFORM_KEYS)[number]
|
|
@@ -129,6 +176,20 @@ type ShadowOffsetKey = (typeof SHADOW_OFFSET_KEYS)[number]
|
|
|
129
176
|
const TRANSFORM_KEY_SET = new Set<AnimatableKey>(TRANSFORM_KEYS)
|
|
130
177
|
const COLOR_KEY_SET = new Set<AnimatableKey>(COLOR_KEYS)
|
|
131
178
|
const SHADOW_OFFSET_KEY_SET = new Set<AnimatableKey>(SHADOW_OFFSET_KEYS)
|
|
179
|
+
const STRUCTURED_KEY_SET = new Set<AnimatableKey>(STRUCTURED_KEYS)
|
|
180
|
+
const SHARED_STYLE_KEY_SET = new Set<AnimatableKey>(SHARED_STYLE_KEYS)
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* What a per-key shared value can hold. Scalars for every key except
|
|
184
|
+
* `boxShadow`, whose slot carries the layer array (see `STRUCTURED_KEYS`).
|
|
185
|
+
*/
|
|
186
|
+
type AnimatableSlotValue = number | string | readonly AnimatedBoxShadowLayer[]
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Resting `boxShadow` — no shadow at all. Frozen and hoisted so every
|
|
190
|
+
* primitive shares one reference and an untouched slot never allocates.
|
|
191
|
+
*/
|
|
192
|
+
const NO_BOX_SHADOW: readonly AnimatedBoxShadowLayer[] = Object.freeze([])
|
|
132
193
|
|
|
133
194
|
const GESTURE_LAYER_NAMES = [
|
|
134
195
|
'hovered',
|
|
@@ -144,7 +205,7 @@ const GESTURE_LAYER_NAME_SET = new Set<string>(GESTURE_LAYER_NAMES)
|
|
|
144
205
|
// Reanimated's style merging treats it as a no-op when present.
|
|
145
206
|
const EXITING_POINTER_EVENTS_STYLE = { pointerEvents: 'none' } as const
|
|
146
207
|
|
|
147
|
-
const DEFAULT_RESTING: Record<AnimatableKey,
|
|
208
|
+
const DEFAULT_RESTING: Record<AnimatableKey, AnimatableSlotValue> = {
|
|
148
209
|
translateX: 0,
|
|
149
210
|
translateY: 0,
|
|
150
211
|
scale: 1,
|
|
@@ -171,6 +232,7 @@ const DEFAULT_RESTING: Record<AnimatableKey, number | string> = {
|
|
|
171
232
|
shadowColor: 'transparent',
|
|
172
233
|
shadowOffsetWidth: 0,
|
|
173
234
|
shadowOffsetHeight: 0,
|
|
235
|
+
boxShadow: NO_BOX_SHADOW,
|
|
174
236
|
}
|
|
175
237
|
|
|
176
238
|
// Both lookups run after the factory has resolved registered transition
|
|
@@ -398,6 +460,24 @@ export function createMotionComponent<C extends ComponentType<any>>(
|
|
|
398
460
|
// identity only changes on the renders that actually add a key, so the
|
|
399
461
|
// `useAnimatedStyle` worklet (which reads `.current` each frame) sees the
|
|
400
462
|
// expansion without churning frame-to-frame.
|
|
463
|
+
const hasLayoutId = layoutId !== undefined
|
|
464
|
+
|
|
465
|
+
// The static `style`, flattened at most once per render and only when
|
|
466
|
+
// something asks for it. Two readers: the shared-element key scan just
|
|
467
|
+
// below, and the resting-value pass further down. A plain `animate`-only
|
|
468
|
+
// instance triggers neither and never pays for the flatten.
|
|
469
|
+
let flatStyle: Record<string, unknown> | undefined
|
|
470
|
+
let flatStyleRead = false
|
|
471
|
+
const getFlatStyle = () => {
|
|
472
|
+
if (!flatStyleRead) {
|
|
473
|
+
flatStyleRead = true
|
|
474
|
+
flatStyle = StyleSheet.flatten(style as never) as
|
|
475
|
+
| Record<string, unknown>
|
|
476
|
+
| undefined
|
|
477
|
+
}
|
|
478
|
+
return flatStyle
|
|
479
|
+
}
|
|
480
|
+
|
|
401
481
|
const touched = new Set<AnimatableKey>()
|
|
402
482
|
collectTouchedKeys(touched, animateRecord)
|
|
403
483
|
if (initialRecord) collectTouchedKeys(touched, initialRecord)
|
|
@@ -420,9 +500,31 @@ export function createMotionComponent<C extends ComponentType<any>>(
|
|
|
420
500
|
}
|
|
421
501
|
if (exitRecord) collectTouchedKeys(touched, exitRecord)
|
|
422
502
|
|
|
503
|
+
// Shared-element style carry: a `layoutId` element crossfades the carried
|
|
504
|
+
// keys from its counterpart, so those keys have to be in the active set
|
|
505
|
+
// for the worklet to emit them at all.
|
|
506
|
+
//
|
|
507
|
+
// Only keys this element already has a value for join — from a record
|
|
508
|
+
// above, or from the static style. Never invented: activating a key with
|
|
509
|
+
// no source value rests it at `DEFAULT_RESTING`, and for `color` on a
|
|
510
|
+
// `Motion.Text` that inherits its colour from a parent, that default is
|
|
511
|
+
// `'transparent'` — invisible text on an element the consumer only asked
|
|
512
|
+
// to move. Same failure mode as the `0.0.3` P0, reached from the other
|
|
513
|
+
// side.
|
|
514
|
+
if (hasLayoutId) {
|
|
515
|
+
const flat = getFlatStyle()
|
|
516
|
+
if (flat) {
|
|
517
|
+
for (const key of SHARED_STYLE_KEYS) {
|
|
518
|
+
if (touched.has(key)) continue
|
|
519
|
+
if (styleValueFor(flat, key) !== undefined) touched.add(key)
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
423
524
|
const activeKeysRef = useRef<readonly AnimatableKey[] | null>(null)
|
|
424
525
|
const hasTransformRef = useRef<boolean>(false)
|
|
425
526
|
const hasShadowOffsetRef = useRef<boolean>(false)
|
|
527
|
+
const hasBoxShadowRef = useRef<boolean>(false)
|
|
426
528
|
// Expand the active set only when this render touched a key we haven't
|
|
427
529
|
// recorded yet. When nothing new appears we keep the existing array
|
|
428
530
|
// identity so the worklet's captured ref doesn't see a fresh value.
|
|
@@ -446,18 +548,111 @@ export function createMotionComponent<C extends ComponentType<any>>(
|
|
|
446
548
|
hasShadowOffsetRef.current = activeKeysRef.current.some((k) =>
|
|
447
549
|
SHADOW_OFFSET_KEY_SET.has(k),
|
|
448
550
|
)
|
|
551
|
+
hasBoxShadowRef.current = activeKeysRef.current.includes('boxShadow')
|
|
552
|
+
// Animating `boxShadow` alongside the native `shadow*` keys puts two
|
|
553
|
+
// shadow systems on one element; whichever the underlying view resolves
|
|
554
|
+
// last wins, and the result reads as a bug rather than a choice.
|
|
555
|
+
// Checked here rather than per render — the active key set is what
|
|
556
|
+
// decides it, and this block only runs when that set grows.
|
|
557
|
+
if (
|
|
558
|
+
__DEV__ &&
|
|
559
|
+
hasBoxShadowRef.current &&
|
|
560
|
+
activeKeysRef.current.some(
|
|
561
|
+
(k) => k !== 'boxShadow' && k.startsWith('shadow'),
|
|
562
|
+
)
|
|
563
|
+
) {
|
|
564
|
+
warnOnce(
|
|
565
|
+
'boxShadow-with-native-shadow',
|
|
566
|
+
'[inertia] `boxShadow` is animated alongside the native `shadow*` ' +
|
|
567
|
+
'keys on the same element, which applies two shadow systems at ' +
|
|
568
|
+
'once. Pick one: `boxShadow` for the cross-platform CSS form, or ' +
|
|
569
|
+
'the native `shadow*` keys.',
|
|
570
|
+
)
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// Which keys a record actually drives *right now*. Everything else in
|
|
575
|
+
// the active set is there only because a gesture sub-state, an `exit`
|
|
576
|
+
// target, or a variant branch that isn't current mentions it. Those must
|
|
577
|
+
// rest at whatever the static `style` says — the animated style merges
|
|
578
|
+
// AFTER `style`, so resting them at `DEFAULT_RESTING` silently stomps it
|
|
579
|
+
// (`borderColor` → 'transparent', `width` → 0, and so on for every key
|
|
580
|
+
// whose default isn't an identity). Transforms and `opacity` hid this
|
|
581
|
+
// bug for a long time: their defaults happen to be no-ops.
|
|
582
|
+
const drivenNow = new Set<AnimatableKey>()
|
|
583
|
+
collectTouchedKeys(drivenNow, animateRecord)
|
|
584
|
+
if (initialRecord) collectTouchedKeys(drivenNow, initialRecord)
|
|
585
|
+
if (isExiting && exitRecord) collectTouchedKeys(drivenNow, exitRecord)
|
|
586
|
+
|
|
587
|
+
// Monotonic, like the active set. Once a record has driven a key we stop
|
|
588
|
+
// syncing it to the style, so transitioning to a variant that doesn't
|
|
589
|
+
// mention the key leaves it where the previous variant put it — the
|
|
590
|
+
// existing semantic — instead of snapping back to the style value.
|
|
591
|
+
const everDrivenRef = useRef<Set<AnimatableKey>>(new Set())
|
|
592
|
+
for (const k of drivenNow) everDrivenRef.current.add(k)
|
|
593
|
+
|
|
594
|
+
// Resting values pulled off the static `style`, for active keys nothing
|
|
595
|
+
// has driven. Gated so the common `animate`-only instance never flattens
|
|
596
|
+
// a style it has no use for.
|
|
597
|
+
const activeKeys = activeKeysRef.current!
|
|
598
|
+
const everDriven = everDrivenRef.current
|
|
599
|
+
const styleResting: Partial<Record<AnimatableKey, AnimatableSlotValue>> =
|
|
600
|
+
{}
|
|
601
|
+
// `boxShadow`'s resting value is two halves (animated layers + static
|
|
602
|
+
// inset flags), so it can't ride the scalar map above.
|
|
603
|
+
let styleRestingShadow: SplitBoxShadow | null = null
|
|
604
|
+
if (activeKeys.some((k) => !everDriven.has(k))) {
|
|
605
|
+
const flat = getFlatStyle()
|
|
606
|
+
if (flat) {
|
|
607
|
+
for (const key of activeKeys) {
|
|
608
|
+
if (everDriven.has(key)) continue
|
|
609
|
+
if (key === 'boxShadow') {
|
|
610
|
+
const raw = flat.boxShadow as BoxShadowInput | undefined
|
|
611
|
+
if (raw !== undefined) {
|
|
612
|
+
styleRestingShadow = normalizeBoxShadow(raw)
|
|
613
|
+
styleResting.boxShadow = styleRestingShadow.layers
|
|
614
|
+
}
|
|
615
|
+
continue
|
|
616
|
+
}
|
|
617
|
+
const v = styleValueFor(flat, key)
|
|
618
|
+
if (v !== undefined) styleResting[key] = v
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// Seed the `boxShadow` slot once, at mount, on the same precedence chain
|
|
624
|
+
// every other key uses (`initial` → `animate` → static style → default).
|
|
625
|
+
// Held in a ref rather than recomputed per render because resolving it
|
|
626
|
+
// may parse a CSS string, and the value is only ever read on the mount
|
|
627
|
+
// pass.
|
|
628
|
+
const boxShadowSeedRef = useRef<SplitBoxShadow | null>(null)
|
|
629
|
+
if (boxShadowSeedRef.current === null) {
|
|
630
|
+
const source =
|
|
631
|
+
initial === false
|
|
632
|
+
? animateRecord.boxShadow
|
|
633
|
+
: (initialRecord?.boxShadow ?? animateRecord.boxShadow)
|
|
634
|
+
boxShadowSeedRef.current =
|
|
635
|
+
source !== undefined
|
|
636
|
+
? normalizeBoxShadow(source)
|
|
637
|
+
: (styleRestingShadow ?? {
|
|
638
|
+
layers: [...NO_BOX_SHADOW],
|
|
639
|
+
insets: null,
|
|
640
|
+
})
|
|
449
641
|
}
|
|
450
642
|
|
|
451
643
|
const sharedValues = useAnimatableSharedValues((key) => {
|
|
644
|
+
if (key === 'boxShadow') return boxShadowSeedRef.current!.layers
|
|
452
645
|
// Shadow offset synthetics seed from the corresponding axis on the
|
|
453
646
|
// `shadowOffset: { width, height }` source — the consumer doesn't write
|
|
454
647
|
// `shadowOffsetWidth` / `shadowOffsetHeight` directly. Fall back to the
|
|
455
|
-
// generic resting default when neither initial
|
|
648
|
+
// static style, then the generic resting default, when neither initial
|
|
649
|
+
// nor animate touched it.
|
|
456
650
|
if (SHADOW_OFFSET_KEY_SET.has(key)) {
|
|
457
651
|
const axis = shadowOffsetAxisFor(key as ShadowOffsetKey)
|
|
458
652
|
if (initial === false) {
|
|
459
653
|
return (
|
|
460
654
|
shadowOffsetAxisValue(animateRecord.shadowOffset, axis) ??
|
|
655
|
+
styleResting[key] ??
|
|
461
656
|
DEFAULT_RESTING[key]
|
|
462
657
|
)
|
|
463
658
|
}
|
|
@@ -469,20 +664,52 @@ export function createMotionComponent<C extends ComponentType<any>>(
|
|
|
469
664
|
axis,
|
|
470
665
|
) ??
|
|
471
666
|
shadowOffsetAxisValue(animateRecord.shadowOffset, axis) ??
|
|
667
|
+
styleResting[key] ??
|
|
472
668
|
DEFAULT_RESTING[key]
|
|
473
669
|
)
|
|
474
670
|
}
|
|
475
671
|
if (initial === false) {
|
|
476
672
|
const a = animateRecord[key]
|
|
477
|
-
return restValue(a) ?? DEFAULT_RESTING[key]
|
|
673
|
+
return restValue(a) ?? styleResting[key] ?? DEFAULT_RESTING[key]
|
|
478
674
|
}
|
|
479
675
|
return (
|
|
480
676
|
initialRecord?.[key] ??
|
|
481
677
|
restValue(animateRecord[key]) ??
|
|
678
|
+
styleResting[key] ??
|
|
482
679
|
DEFAULT_RESTING[key]
|
|
483
680
|
)
|
|
484
681
|
})
|
|
485
682
|
|
|
683
|
+
// Keep never-driven keys tracking the *live* static style. The seed above
|
|
684
|
+
// only runs at mount, but the animated style emits these keys on every
|
|
685
|
+
// frame — so without this a `style` that changes afterwards (a theme
|
|
686
|
+
// swap, a conditional colour) would stay invisible for any key some
|
|
687
|
+
// gesture / exit / variant branch happens to mention. Direct assignment,
|
|
688
|
+
// not an animation: this is a static value, not a target.
|
|
689
|
+
// Static `inset` flags for the layers currently in the `boxShadow` slot,
|
|
690
|
+
// parallel to `sharedValues.boxShadow`. Kept out of the animated payload
|
|
691
|
+
// because Reanimated would drive the boolean down its numeric path and
|
|
692
|
+
// hand native a number mid-flight; see `AnimatedBoxShadowLayer`. `null`
|
|
693
|
+
// whenever no layer is inset, which lets the worklet emit the animated
|
|
694
|
+
// array with no per-frame reassembly at all.
|
|
695
|
+
const boxShadowInsets = useSharedValue<boolean[] | null>(
|
|
696
|
+
boxShadowSeedRef.current.insets,
|
|
697
|
+
)
|
|
698
|
+
|
|
699
|
+
const styleRestingSig = stableSig(styleResting)
|
|
700
|
+
useEffect(() => {
|
|
701
|
+
for (const key of activeKeysRef.current!) {
|
|
702
|
+
if (everDrivenRef.current.has(key)) continue
|
|
703
|
+
sharedValues[key].value = (styleResting[key] ??
|
|
704
|
+
DEFAULT_RESTING[key]) as never
|
|
705
|
+
if (key === 'boxShadow') {
|
|
706
|
+
boxShadowInsets.value = styleRestingShadow?.insets ?? null
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
// `styleResting` is rebuilt each render; its signature is the real dep.
|
|
710
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
711
|
+
}, [styleRestingSig])
|
|
712
|
+
|
|
486
713
|
// One progress SV per gesture layer, allocated unconditionally for hook
|
|
487
714
|
// stability. Each layer's progress animates 0↔1 with its own transition
|
|
488
715
|
// when its activation flips; the worklet reads them when compositing.
|
|
@@ -568,20 +795,71 @@ export function createMotionComponent<C extends ComponentType<any>>(
|
|
|
568
795
|
}
|
|
569
796
|
}
|
|
570
797
|
|
|
798
|
+
// Per-key transition resolution, hoisted out of the loop below so the
|
|
799
|
+
// transform-group count can ask about a key's config before the loop
|
|
800
|
+
// reaches it. Reduced motion overrides every per-key transition (and
|
|
801
|
+
// any nested sequence-step transition) with `no-animation`, which the
|
|
802
|
+
// resolver turns into a direct value assignment. Sequences still
|
|
803
|
+
// iterate but each step settles instantly, matching the "snap to final
|
|
804
|
+
// state" expectation.
|
|
805
|
+
const configFor = (key: AnimatableKey): TransitionConfig | undefined =>
|
|
806
|
+
shouldReduceMotion
|
|
807
|
+
? ({ type: 'no-animation' } as const)
|
|
808
|
+
: transitionFor(
|
|
809
|
+
SHADOW_OFFSET_KEY_SET.has(key)
|
|
810
|
+
? ('shadowOffset' as keyof typeof baseRecord)
|
|
811
|
+
: key,
|
|
812
|
+
transition,
|
|
813
|
+
)
|
|
814
|
+
|
|
571
815
|
// Count transform axes participating in this effect run so the factory
|
|
572
816
|
// can coalesce their terminal callbacks into a single transform-group
|
|
573
817
|
// event. `undefined` when no transform axis is animating, which lets
|
|
574
818
|
// the factory skip the coalescing branch entirely.
|
|
819
|
+
//
|
|
820
|
+
// Endless axes are excluded. A `repeat: 'infinite'` axis never reaches
|
|
821
|
+
// the terminal `'animation'` phase, so counting it would pin
|
|
822
|
+
// `remaining` permanently above zero — a sibling axis with a finite
|
|
823
|
+
// transition would settle, decrement, and have its completion silently
|
|
824
|
+
// swallowed. An axis that never finishes simply isn't part of the group
|
|
825
|
+
// that does.
|
|
575
826
|
let transformPending = 0
|
|
576
827
|
for (const k of ALL_KEYS) {
|
|
577
|
-
if (TRANSFORM_KEY_SET.has(k)
|
|
578
|
-
|
|
579
|
-
|
|
828
|
+
if (!TRANSFORM_KEY_SET.has(k) || baseRecord[k] === undefined) continue
|
|
829
|
+
if (!reachesTerminalPhase(configFor(k))) continue
|
|
830
|
+
transformPending++
|
|
580
831
|
}
|
|
581
832
|
const transformGroup: TransformGroup | undefined =
|
|
582
833
|
transformPending > 0 ? { remaining: transformPending } : undefined
|
|
583
834
|
|
|
584
835
|
for (const key of ALL_KEYS) {
|
|
836
|
+
// Structured keys don't go through `resolveAnimatableValue` — their
|
|
837
|
+
// target is already an array, which that resolver would read as a
|
|
838
|
+
// keyframe sequence.
|
|
839
|
+
if (key === 'boxShadow') {
|
|
840
|
+
const shadowTarget = baseRecord.boxShadow
|
|
841
|
+
if (shadowTarget === undefined) continue
|
|
842
|
+
const cfg = configFor('boxShadow')
|
|
843
|
+
if (isExiting && reachesTerminalPhase(cfg)) pending++
|
|
844
|
+
driveBoxShadow(
|
|
845
|
+
sharedValues.boxShadow,
|
|
846
|
+
boxShadowInsets,
|
|
847
|
+
shadowTarget,
|
|
848
|
+
cfg,
|
|
849
|
+
makeKeyCallbackFactory(
|
|
850
|
+
'boxShadow',
|
|
851
|
+
sharedValues.boxShadow,
|
|
852
|
+
// The declared target, not the padded one — the padding is an
|
|
853
|
+
// implementation detail of getting Reanimated to interpolate.
|
|
854
|
+
shadowTarget,
|
|
855
|
+
onAnimationEndRef,
|
|
856
|
+
{ stepCount: 1, totalIterations: totalIterationsOf(cfg) },
|
|
857
|
+
isExiting ? onSettle : undefined,
|
|
858
|
+
undefined,
|
|
859
|
+
),
|
|
860
|
+
)
|
|
861
|
+
continue
|
|
862
|
+
}
|
|
585
863
|
// Shadow offset synthetics read their target from the nested
|
|
586
864
|
// `shadowOffset: { width, height }` source on `baseRecord` — the
|
|
587
865
|
// animate / exit record never has `shadowOffsetWidth` etc. on it
|
|
@@ -595,20 +873,15 @@ export function createMotionComponent<C extends ComponentType<any>>(
|
|
|
595
873
|
)
|
|
596
874
|
: baseRecord[key]
|
|
597
875
|
if (target === undefined) continue
|
|
598
|
-
|
|
599
|
-
//
|
|
600
|
-
//
|
|
601
|
-
//
|
|
602
|
-
//
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
? ('shadowOffset' as keyof typeof baseRecord)
|
|
608
|
-
: key,
|
|
609
|
-
transition,
|
|
610
|
-
)
|
|
611
|
-
if (isExiting) pending++
|
|
876
|
+
const cfg = configFor(key)
|
|
877
|
+
// <Presence> waits on this counter before unmounting, so only count
|
|
878
|
+
// keys that can actually settle. An endless exit animation (a
|
|
879
|
+
// `repeat: 'infinite'` transition inherited by `exit`, e.g. a pulsing
|
|
880
|
+
// element inside <Presence>) would otherwise never call
|
|
881
|
+
// `safeToRemove` and the child would stay mounted forever. Finite
|
|
882
|
+
// keys still gate the unmount; if every exit key is endless, the
|
|
883
|
+
// post-loop `pending === 0` release fires immediately.
|
|
884
|
+
if (isExiting && reachesTerminalPhase(cfg)) pending++
|
|
612
885
|
const factory = makeKeyCallbackFactory(
|
|
613
886
|
key,
|
|
614
887
|
sharedValues[key],
|
|
@@ -691,20 +964,44 @@ export function createMotionComponent<C extends ComponentType<any>>(
|
|
|
691
964
|
// transform array so they compose with the user's animate transforms —
|
|
692
965
|
// multiple `translateX` entries sum, multiple `scaleX` entries multiply,
|
|
693
966
|
// which is exactly the FLIP semantic.
|
|
967
|
+
//
|
|
968
|
+
// The style half rides the same wiring: `readStyles` lets a later mount
|
|
969
|
+
// (or our own unmount) snapshot the carried keys straight off the shared
|
|
970
|
+
// values, and the worklet below crossfades a consumed snapshot out over
|
|
971
|
+
// whatever this element resolves to. Reached through a ref inside the
|
|
972
|
+
// hook, so building it inline here costs nothing downstream.
|
|
973
|
+
const readSharedStyles = () => {
|
|
974
|
+
if (!hasLayoutId) return undefined
|
|
975
|
+
const out: SharedStyleSnapshot = {}
|
|
976
|
+
let any = false
|
|
977
|
+
for (const key of activeKeysRef.current!) {
|
|
978
|
+
if (!SHARED_STYLE_KEY_SET.has(key)) continue
|
|
979
|
+
const v = sharedValues[key].value
|
|
980
|
+
// Structured slots can't be carried, and a key nothing ever gave a
|
|
981
|
+
// value to has nothing to hand over.
|
|
982
|
+
if (typeof v !== 'number' && typeof v !== 'string') continue
|
|
983
|
+
out[key] = v
|
|
984
|
+
any = true
|
|
985
|
+
}
|
|
986
|
+
return any ? out : undefined
|
|
987
|
+
}
|
|
988
|
+
|
|
694
989
|
const sharedLayout = useSharedLayout({
|
|
695
990
|
layoutId,
|
|
696
991
|
userRef: ref,
|
|
697
992
|
transition: isTopLevelTransition(transition) ? transition : undefined,
|
|
698
993
|
shouldReduceMotion,
|
|
699
994
|
userOnLayout,
|
|
995
|
+
readStyles: readSharedStyles,
|
|
700
996
|
})
|
|
701
997
|
const flip = sharedLayout.flip
|
|
702
|
-
const
|
|
998
|
+
const carry = sharedLayout.carry
|
|
703
999
|
|
|
704
1000
|
const animatedStyle = useAnimatedStyle(() => {
|
|
705
1001
|
const activeKeys = activeKeysRef.current!
|
|
706
1002
|
const hasTransform = hasTransformRef.current
|
|
707
1003
|
const hasShadowOffset = hasShadowOffsetRef.current
|
|
1004
|
+
const hasBoxShadow = hasBoxShadowRef.current
|
|
708
1005
|
const out: Record<string, unknown> = {}
|
|
709
1006
|
const transform: Array<Record<string, unknown>> = []
|
|
710
1007
|
// shadow-offset reassembly buffers. The two synthetic axis SVs feed in
|
|
@@ -720,6 +1017,12 @@ export function createMotionComponent<C extends ComponentType<any>>(
|
|
|
720
1017
|
const pfv = focusVisibleProgress.value
|
|
721
1018
|
const pp = pressedProgress.value
|
|
722
1019
|
|
|
1020
|
+
// Shared-element style carry. Both reads are cheap and the pair rests
|
|
1021
|
+
// at (null, 0), so a primitive that isn't mid-transition — which is
|
|
1022
|
+
// every primitive, almost all of the time — pays one comparison.
|
|
1023
|
+
const carried = carry.snapshot.value
|
|
1024
|
+
const pc = carry.progress.value
|
|
1025
|
+
|
|
723
1026
|
const layers = gestureSV.value
|
|
724
1027
|
// Locals are suffixed `Layer` so they don't shadow the outer `pressed` /
|
|
725
1028
|
// `focused` / `focusVisible` / `hovered` JS-state booleans — Reanimated's
|
|
@@ -731,6 +1034,11 @@ export function createMotionComponent<C extends ComponentType<any>>(
|
|
|
731
1034
|
const pressedLayer = layers ? layers.pressed : null
|
|
732
1035
|
|
|
733
1036
|
for (const key of activeKeys) {
|
|
1037
|
+
// `boxShadow` is emitted after the loop: its value is a layer array,
|
|
1038
|
+
// which none of the scalar compositing below can act on. Gesture
|
|
1039
|
+
// sub-states are rejected for this key upstream, so there is no
|
|
1040
|
+
// layer contribution to skip past either.
|
|
1041
|
+
if (key === 'boxShadow') continue
|
|
734
1042
|
let v = sharedValues[key].value
|
|
735
1043
|
const isColor = COLOR_KEY_SET.has(key)
|
|
736
1044
|
|
|
@@ -769,6 +1077,23 @@ export function createMotionComponent<C extends ComponentType<any>>(
|
|
|
769
1077
|
: (v as number) + ((t as number) - (v as number)) * pp
|
|
770
1078
|
}
|
|
771
1079
|
|
|
1080
|
+
// Shared-element carry sits above every gesture layer: the element
|
|
1081
|
+
// is arriving from somewhere else, and where it came from outranks
|
|
1082
|
+
// how it is being touched right now. `pc` runs 1 → 0, so the first
|
|
1083
|
+
// frame shows the source's value and the last shows this element's
|
|
1084
|
+
// own — including whatever the layers underneath contributed.
|
|
1085
|
+
//
|
|
1086
|
+
// Transform keys never appear in a snapshot (the FLIP owns them), so
|
|
1087
|
+
// this can't double-apply displacement.
|
|
1088
|
+
if (carried !== null && pc > 0) {
|
|
1089
|
+
const s = carried[key]
|
|
1090
|
+
if (s !== undefined) {
|
|
1091
|
+
v = isColor
|
|
1092
|
+
? interpolateColor(pc, [0, 1], [v as string, s as string])
|
|
1093
|
+
: (v as number) + ((s as number) - (v as number)) * pc
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
|
|
772
1097
|
if (TRANSFORM_KEY_SET.has(key)) {
|
|
773
1098
|
transform.push(
|
|
774
1099
|
ROTATION_KEYS.has(key) ? { [key]: `${v}deg` } : { [key]: v },
|
|
@@ -797,12 +1122,28 @@ export function createMotionComponent<C extends ComponentType<any>>(
|
|
|
797
1122
|
if (hasShadowOffset) {
|
|
798
1123
|
out.shadowOffset = { width: shadowOffsetW, height: shadowOffsetH }
|
|
799
1124
|
}
|
|
1125
|
+
if (hasBoxShadow) {
|
|
1126
|
+
const layers = sharedValues.boxShadow
|
|
1127
|
+
.value as readonly AnimatedBoxShadowLayer[]
|
|
1128
|
+
const insets = boxShadowInsets.value
|
|
1129
|
+
// Fast path — no inset layer anywhere, so the animated array is
|
|
1130
|
+
// already exactly what RN wants and crosses over untouched.
|
|
1131
|
+
if (insets === null) {
|
|
1132
|
+
out.boxShadow = layers
|
|
1133
|
+
} else {
|
|
1134
|
+
const withInset = []
|
|
1135
|
+
for (let i = 0; i < layers.length; i++) {
|
|
1136
|
+
withInset.push({ ...layers[i], inset: insets[i] })
|
|
1137
|
+
}
|
|
1138
|
+
out.boxShadow = withInset
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
800
1141
|
return out
|
|
801
1142
|
})
|
|
802
1143
|
|
|
803
1144
|
// Exiting children are tap-deaf: the next press should fall through to
|
|
804
1145
|
// whatever is underneath, not re-trigger a soon-to-unmount node. This is
|
|
805
|
-
// the moti #297 fix and a
|
|
1146
|
+
// the moti #297 fix and a `0.0.1` acceptance criterion. RN 0.71+ deprecates
|
|
806
1147
|
// `pointerEvents` as a prop in favor of the style key, so we merge it
|
|
807
1148
|
// alongside the animated style instead of spreading as a prop.
|
|
808
1149
|
const mergedStyle = useMemo(
|
|
@@ -890,7 +1231,7 @@ function hasMotionProps(props: Record<string, unknown>): boolean {
|
|
|
890
1231
|
return false
|
|
891
1232
|
}
|
|
892
1233
|
|
|
893
|
-
type SharedValueMap = Record<AnimatableKey, SharedValue<
|
|
1234
|
+
type SharedValueMap = Record<AnimatableKey, SharedValue<AnimatableSlotValue>>
|
|
894
1235
|
|
|
895
1236
|
/**
|
|
896
1237
|
* Allocate one shared value per animatable key in `ALL_KEYS` and return a
|
|
@@ -911,36 +1252,39 @@ type SharedValueMap = Record<AnimatableKey, SharedValue<number | string>>
|
|
|
911
1252
|
* `withSpring` / `withTiming` call.
|
|
912
1253
|
*/
|
|
913
1254
|
function useAnimatableSharedValues(
|
|
914
|
-
init: (key: AnimatableKey) =>
|
|
1255
|
+
init: (key: AnimatableKey) => AnimatableSlotValue,
|
|
915
1256
|
): SharedValueMap {
|
|
916
|
-
const translateX = useSharedValue<
|
|
917
|
-
const translateY = useSharedValue<
|
|
918
|
-
const scale = useSharedValue<
|
|
919
|
-
const scaleX = useSharedValue<
|
|
920
|
-
const scaleY = useSharedValue<
|
|
921
|
-
const rotate = useSharedValue<
|
|
922
|
-
const rotateX = useSharedValue<
|
|
923
|
-
const rotateY = useSharedValue<
|
|
924
|
-
const opacity = useSharedValue<
|
|
925
|
-
const width = useSharedValue<
|
|
926
|
-
const height = useSharedValue<
|
|
927
|
-
const borderRadius = useSharedValue<
|
|
928
|
-
const shadowOpacity = useSharedValue<
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
const
|
|
1257
|
+
const translateX = useSharedValue<AnimatableSlotValue>(init('translateX'))
|
|
1258
|
+
const translateY = useSharedValue<AnimatableSlotValue>(init('translateY'))
|
|
1259
|
+
const scale = useSharedValue<AnimatableSlotValue>(init('scale'))
|
|
1260
|
+
const scaleX = useSharedValue<AnimatableSlotValue>(init('scaleX'))
|
|
1261
|
+
const scaleY = useSharedValue<AnimatableSlotValue>(init('scaleY'))
|
|
1262
|
+
const rotate = useSharedValue<AnimatableSlotValue>(init('rotate'))
|
|
1263
|
+
const rotateX = useSharedValue<AnimatableSlotValue>(init('rotateX'))
|
|
1264
|
+
const rotateY = useSharedValue<AnimatableSlotValue>(init('rotateY'))
|
|
1265
|
+
const opacity = useSharedValue<AnimatableSlotValue>(init('opacity'))
|
|
1266
|
+
const width = useSharedValue<AnimatableSlotValue>(init('width'))
|
|
1267
|
+
const height = useSharedValue<AnimatableSlotValue>(init('height'))
|
|
1268
|
+
const borderRadius = useSharedValue<AnimatableSlotValue>(init('borderRadius'))
|
|
1269
|
+
const shadowOpacity = useSharedValue<AnimatableSlotValue>(
|
|
1270
|
+
init('shadowOpacity'),
|
|
1271
|
+
)
|
|
1272
|
+
const shadowRadius = useSharedValue<AnimatableSlotValue>(init('shadowRadius'))
|
|
1273
|
+
const elevation = useSharedValue<AnimatableSlotValue>(init('elevation'))
|
|
1274
|
+
const backgroundColor = useSharedValue<AnimatableSlotValue>(
|
|
932
1275
|
init('backgroundColor'),
|
|
933
1276
|
)
|
|
934
|
-
const borderColor = useSharedValue<
|
|
935
|
-
const color = useSharedValue<
|
|
936
|
-
const tintColor = useSharedValue<
|
|
937
|
-
const shadowColor = useSharedValue<
|
|
938
|
-
const shadowOffsetWidth = useSharedValue<
|
|
1277
|
+
const borderColor = useSharedValue<AnimatableSlotValue>(init('borderColor'))
|
|
1278
|
+
const color = useSharedValue<AnimatableSlotValue>(init('color'))
|
|
1279
|
+
const tintColor = useSharedValue<AnimatableSlotValue>(init('tintColor'))
|
|
1280
|
+
const shadowColor = useSharedValue<AnimatableSlotValue>(init('shadowColor'))
|
|
1281
|
+
const shadowOffsetWidth = useSharedValue<AnimatableSlotValue>(
|
|
939
1282
|
init('shadowOffsetWidth'),
|
|
940
1283
|
)
|
|
941
|
-
const shadowOffsetHeight = useSharedValue<
|
|
1284
|
+
const shadowOffsetHeight = useSharedValue<AnimatableSlotValue>(
|
|
942
1285
|
init('shadowOffsetHeight'),
|
|
943
1286
|
)
|
|
1287
|
+
const boxShadow = useSharedValue<AnimatableSlotValue>(init('boxShadow'))
|
|
944
1288
|
|
|
945
1289
|
const ref = useRef<SharedValueMap | null>(null)
|
|
946
1290
|
if (ref.current === null) {
|
|
@@ -967,6 +1311,7 @@ function useAnimatableSharedValues(
|
|
|
967
1311
|
shadowColor,
|
|
968
1312
|
shadowOffsetWidth,
|
|
969
1313
|
shadowOffsetHeight,
|
|
1314
|
+
boxShadow,
|
|
970
1315
|
}
|
|
971
1316
|
}
|
|
972
1317
|
|
|
@@ -1005,8 +1350,11 @@ function useAnimatableSharedValues(
|
|
|
1005
1350
|
*/
|
|
1006
1351
|
function makeKeyCallbackFactory(
|
|
1007
1352
|
key: string,
|
|
1008
|
-
sharedValue: SharedValue<
|
|
1009
|
-
|
|
1353
|
+
sharedValue: SharedValue<AnimatableSlotValue>,
|
|
1354
|
+
// Widened past the scalar surface for `boxShadow`, whose declared target is
|
|
1355
|
+
// a CSS string or a layer array. Reaches the consumer as
|
|
1356
|
+
// `AnimationCallbackInfo.target`, which is `unknown`.
|
|
1357
|
+
target: number | string | BoxShadowInput | undefined,
|
|
1010
1358
|
onAnimationEndRef: {
|
|
1011
1359
|
current: ((info: AnimationCallbackInfo<unknown>) => void) | undefined
|
|
1012
1360
|
},
|
|
@@ -1026,7 +1374,9 @@ function makeKeyCallbackFactory(
|
|
|
1026
1374
|
rawPhase: 'step' | 'animation',
|
|
1027
1375
|
step: number | undefined,
|
|
1028
1376
|
finished: boolean,
|
|
1029
|
-
value
|
|
1377
|
+
// Widened for `boxShadow`, whose settled value is a layer array. Surfaces
|
|
1378
|
+
// on `AnimationCallbackInfo.value`, which is `unknown` either way.
|
|
1379
|
+
value: AnimatableSlotValue | undefined,
|
|
1030
1380
|
) => {
|
|
1031
1381
|
const isLastIteration = state.iteration >= meta.totalIterations - 1
|
|
1032
1382
|
let phase: 'step' | 'sequence' | 'repeat' | 'animation'
|
|
@@ -1112,18 +1462,25 @@ function makeKeyCallbackFactory(
|
|
|
1112
1462
|
* nested object decomposes into the `shadowOffsetWidth` / `shadowOffsetHeight`
|
|
1113
1463
|
* synthetic axes downstream; consumers don't see the synthetics.
|
|
1114
1464
|
*
|
|
1115
|
-
*
|
|
1116
|
-
* (no sequences, no `{ to }` step objects, no array keyframes).
|
|
1117
|
-
* forms on the nested axes can land
|
|
1465
|
+
* Contract since `0.0.1`: `shadowOffset` accepts a single `{ width, height }`
|
|
1466
|
+
* literal (no sequences, no `{ to }` step objects, no array keyframes).
|
|
1467
|
+
* Sequence forms on the nested axes can land later if real consumers ask.
|
|
1118
1468
|
*/
|
|
1469
|
+
type ScalarKey = Exclude<AnimatableKey, (typeof STRUCTURED_KEYS)[number]>
|
|
1470
|
+
|
|
1119
1471
|
type InternalAnimateRecord = Partial<
|
|
1120
|
-
Record<
|
|
1472
|
+
Record<ScalarKey, AnimatableValue<number | string>>
|
|
1121
1473
|
> & {
|
|
1122
1474
|
shadowOffset?: { width?: number; height?: number }
|
|
1475
|
+
// Structured keys depart from `AnimatableValue` — see `STRUCTURED_KEYS`.
|
|
1476
|
+
// Kept out of the mapped half above so the two don't intersect into an
|
|
1477
|
+
// uninhabitable `AnimatableValue<number | string> & BoxShadowInput`.
|
|
1478
|
+
boxShadow?: BoxShadowInput
|
|
1123
1479
|
}
|
|
1124
1480
|
|
|
1125
|
-
type InternalInitialRecord = Partial<Record<
|
|
1481
|
+
type InternalInitialRecord = Partial<Record<ScalarKey, number | string>> & {
|
|
1126
1482
|
shadowOffset?: { width?: number; height?: number }
|
|
1483
|
+
boxShadow?: BoxShadowInput
|
|
1127
1484
|
}
|
|
1128
1485
|
|
|
1129
1486
|
/**
|
|
@@ -1146,6 +1503,107 @@ function shadowOffsetAxisValue(
|
|
|
1146
1503
|
return source?.[axis]
|
|
1147
1504
|
}
|
|
1148
1505
|
|
|
1506
|
+
/**
|
|
1507
|
+
* Drive the `boxShadow` slot toward a new target.
|
|
1508
|
+
*
|
|
1509
|
+
* Two things make this different from the scalar path:
|
|
1510
|
+
*
|
|
1511
|
+
* 1. **Both endpoints are padded to a common layer count first.** Reanimated's
|
|
1512
|
+
* array driver iterates the *current* value's indices and pairs each with
|
|
1513
|
+
* `toValue[i]`, so a target with a different layer count either strands
|
|
1514
|
+
* leaves at `toValue: undefined` or never animates the extras. When the
|
|
1515
|
+
* padding changes the slot's shape, we snap it to the padded base before
|
|
1516
|
+
* starting the animation — otherwise the animation would begin from a
|
|
1517
|
+
* differently-shaped value and the pairing would be lost again.
|
|
1518
|
+
* 2. **`inset` travels separately**, in its own slot, because Reanimated
|
|
1519
|
+
* would drive the boolean down the numeric path.
|
|
1520
|
+
*/
|
|
1521
|
+
function driveBoxShadow(
|
|
1522
|
+
slot: SharedValue<AnimatableSlotValue>,
|
|
1523
|
+
insetSlot: SharedValue<boolean[] | null>,
|
|
1524
|
+
target: BoxShadowInput,
|
|
1525
|
+
cfg: TransitionConfig | undefined,
|
|
1526
|
+
factory: CallbackFactory | undefined,
|
|
1527
|
+
): void {
|
|
1528
|
+
const currentLayers = Array.isArray(slot.value)
|
|
1529
|
+
? (slot.value as readonly AnimatedBoxShadowLayer[])
|
|
1530
|
+
: NO_BOX_SHADOW
|
|
1531
|
+
const { from, to, insets } = prepareBoxShadowAnimation(
|
|
1532
|
+
{ layers: [...currentLayers], insets: insetSlot.value },
|
|
1533
|
+
target,
|
|
1534
|
+
)
|
|
1535
|
+
insetSlot.value = insets
|
|
1536
|
+
if (from.length !== currentLayers.length) slot.value = from
|
|
1537
|
+
// `resolveTransition` is typed for the scalar surface it was written for;
|
|
1538
|
+
// Reanimated itself accepts the structured target and recurses into it.
|
|
1539
|
+
slot.value = resolveTransition(
|
|
1540
|
+
cfg,
|
|
1541
|
+
to as unknown as number,
|
|
1542
|
+
factory?.('animation', undefined),
|
|
1543
|
+
) as AnimatableSlotValue
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
/**
|
|
1547
|
+
* Read an animatable key's resting value out of a **flattened style object**.
|
|
1548
|
+
*
|
|
1549
|
+
* Used for keys that are in the active set but that no record drives — they
|
|
1550
|
+
* have to rest wherever the static `style` put them, because the animated
|
|
1551
|
+
* style merges after `style` and would otherwise override it.
|
|
1552
|
+
*
|
|
1553
|
+
* Three shapes need unwrapping: transform keys live inside the `transform`
|
|
1554
|
+
* array rather than at the top level, `shadowOffset*` synthetics decompose
|
|
1555
|
+
* from the nested object, and rotations are unit-suffixed strings on a style
|
|
1556
|
+
* but plain degrees in our shared values.
|
|
1557
|
+
*
|
|
1558
|
+
* Type-narrow deliberately: only numbers are accepted for numeric slots, so a
|
|
1559
|
+
* `width: '100%'` is left to `DEFAULT_RESTING` rather than seeded into a slot
|
|
1560
|
+
* that a later `withTiming` would try to interpolate as a number.
|
|
1561
|
+
*/
|
|
1562
|
+
function styleValueFor(
|
|
1563
|
+
flat: Record<string, unknown>,
|
|
1564
|
+
key: AnimatableKey,
|
|
1565
|
+
): number | string | undefined {
|
|
1566
|
+
if (SHADOW_OFFSET_KEY_SET.has(key)) {
|
|
1567
|
+
return shadowOffsetAxisValue(
|
|
1568
|
+
flat.shadowOffset as { width?: number; height?: number } | undefined,
|
|
1569
|
+
shadowOffsetAxisFor(key as ShadowOffsetKey),
|
|
1570
|
+
)
|
|
1571
|
+
}
|
|
1572
|
+
if (TRANSFORM_KEY_SET.has(key)) {
|
|
1573
|
+
const list = flat.transform
|
|
1574
|
+
if (!Array.isArray(list)) return undefined
|
|
1575
|
+
for (const entry of list) {
|
|
1576
|
+
if (!entry || typeof entry !== 'object') continue
|
|
1577
|
+
const v = (entry as Record<string, unknown>)[key]
|
|
1578
|
+
if (v === undefined) continue
|
|
1579
|
+
if (ROTATION_KEYS.has(key)) return parseAngleDegrees(v)
|
|
1580
|
+
return typeof v === 'number' ? v : undefined
|
|
1581
|
+
}
|
|
1582
|
+
return undefined
|
|
1583
|
+
}
|
|
1584
|
+
const v = flat[key]
|
|
1585
|
+
if (COLOR_KEY_SET.has(key)) {
|
|
1586
|
+
return typeof v === 'string' || typeof v === 'number' ? v : undefined
|
|
1587
|
+
}
|
|
1588
|
+
return typeof v === 'number' ? v : undefined
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
/**
|
|
1592
|
+
* Style rotations are unit-suffixed strings (`'45deg'`, `'0.5rad'`); the shared
|
|
1593
|
+
* value behind a rotation key holds plain degrees. Anything we can't convert
|
|
1594
|
+
* confidently returns `undefined` so the caller falls back to the resting
|
|
1595
|
+
* default instead of guessing at a unit.
|
|
1596
|
+
*/
|
|
1597
|
+
function parseAngleDegrees(v: unknown): number | undefined {
|
|
1598
|
+
if (typeof v === 'number') return v
|
|
1599
|
+
if (typeof v !== 'string') return undefined
|
|
1600
|
+
const deg = /^(-?\d*\.?\d+)deg$/.exec(v)
|
|
1601
|
+
if (deg) return Number(deg[1])
|
|
1602
|
+
const rad = /^(-?\d*\.?\d+)rad$/.exec(v)
|
|
1603
|
+
if (rad) return (Number(rad[1]) * 180) / Math.PI
|
|
1604
|
+
return undefined
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1149
1607
|
/**
|
|
1150
1608
|
* Populate `touched` with the `AnimatableKey`s mentioned in `record`. Direct
|
|
1151
1609
|
* matches (e.g. `opacity`, `width`) come from the key iteration; the nested
|
|
@@ -1190,6 +1648,22 @@ function totalIterationsOf(cfg: TransitionConfig | undefined): number {
|
|
|
1190
1648
|
return r.count
|
|
1191
1649
|
}
|
|
1192
1650
|
|
|
1651
|
+
/**
|
|
1652
|
+
* Whether an animation built from `cfg` will ever reach its terminal
|
|
1653
|
+
* `'animation'` phase. Everything finite does; `repeat: 'infinite'` never
|
|
1654
|
+
* does, because `dispatch` only promotes a callback to the terminal phase once
|
|
1655
|
+
* `iteration >= totalIterations - 1`, and that comparison is unreachable
|
|
1656
|
+
* against `Infinity`.
|
|
1657
|
+
*
|
|
1658
|
+
* Anything counting pending completions must exclude endless animations, or
|
|
1659
|
+
* the counter never drains: one endless transform axis swallowed every sibling
|
|
1660
|
+
* axis's `onAnimationEnd`, and one endless `exit` key left a `<Presence>` child
|
|
1661
|
+
* mounted forever because `safeToRemove` was gated behind it.
|
|
1662
|
+
*/
|
|
1663
|
+
function reachesTerminalPhase(cfg: TransitionConfig | undefined): boolean {
|
|
1664
|
+
return Number.isFinite(totalIterationsOf(cfg))
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1193
1667
|
/**
|
|
1194
1668
|
* Pull a single end-value out of an `AnimatableValue` for the
|
|
1195
1669
|
* `AnimationCallbackInfo.target` field. Plain numbers/strings come through;
|
|
@@ -1304,6 +1778,27 @@ function resolveGestureLayers(
|
|
|
1304
1778
|
if (!subState) continue
|
|
1305
1779
|
const resolved: Record<string, number | string> = {}
|
|
1306
1780
|
for (const key of ALL_KEYS) {
|
|
1781
|
+
// Structured keys have no place in the layer cascade: compositing them
|
|
1782
|
+
// would mean per-layer, per-field interpolation on the UI thread for
|
|
1783
|
+
// every primitive, whether or not it animates a shadow. Rejected for
|
|
1784
|
+
// now rather than half-supported — a silently-ignored `boxShadow` in a
|
|
1785
|
+
// `gesture` sub-state is exactly the class of bug this library exists
|
|
1786
|
+
// to avoid, so say so.
|
|
1787
|
+
if (STRUCTURED_KEY_SET.has(key)) {
|
|
1788
|
+
if (
|
|
1789
|
+
__DEV__ &&
|
|
1790
|
+
(subState as Record<string, unknown>)[key] !== undefined
|
|
1791
|
+
) {
|
|
1792
|
+
warnOnce(
|
|
1793
|
+
`gesture-structured-${layer}-${key}`,
|
|
1794
|
+
`[inertia] \`${key}\` is not supported inside \`gesture.${layer}\` ` +
|
|
1795
|
+
'and will be ignored. Drive it from `animate` (optionally via a ' +
|
|
1796
|
+
'variant keyed off the same state), or interpolate it yourself ' +
|
|
1797
|
+
'with `useShadow`.',
|
|
1798
|
+
)
|
|
1799
|
+
}
|
|
1800
|
+
continue
|
|
1801
|
+
}
|
|
1307
1802
|
// Shadow offset synthetics decompose from the nested `shadowOffset:
|
|
1308
1803
|
// { width, height }` source on the sub-state, the same as on `animate`.
|
|
1309
1804
|
if (SHADOW_OFFSET_KEY_SET.has(key)) {
|