@onlynative/inertia 0.0.1-alpha.4 → 0.0.1-alpha.6
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/dist/index.d.mts +30 -4
- package/dist/index.d.ts +30 -4
- package/dist/index.js +123 -24
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +123 -25
- package/dist/index.mjs.map +1 -1
- package/dist/motion/Image.d.mts +1 -1
- package/dist/motion/Image.d.ts +1 -1
- package/dist/motion/Image.js +100 -24
- package/dist/motion/Image.js.map +1 -1
- package/dist/motion/Image.mjs +100 -24
- package/dist/motion/Image.mjs.map +1 -1
- package/dist/motion/Pressable.d.mts +1 -1
- package/dist/motion/Pressable.d.ts +1 -1
- package/dist/motion/Pressable.js +100 -24
- package/dist/motion/Pressable.js.map +1 -1
- package/dist/motion/Pressable.mjs +100 -24
- package/dist/motion/Pressable.mjs.map +1 -1
- package/dist/motion/ScrollView.d.mts +1 -1
- package/dist/motion/ScrollView.d.ts +1 -1
- package/dist/motion/ScrollView.js +100 -24
- package/dist/motion/ScrollView.js.map +1 -1
- package/dist/motion/ScrollView.mjs +100 -24
- package/dist/motion/ScrollView.mjs.map +1 -1
- package/dist/motion/Text.d.mts +1 -1
- package/dist/motion/Text.d.ts +1 -1
- package/dist/motion/Text.js +100 -24
- package/dist/motion/Text.js.map +1 -1
- package/dist/motion/Text.mjs +100 -24
- package/dist/motion/Text.mjs.map +1 -1
- package/dist/motion/View.d.mts +1 -1
- package/dist/motion/View.d.ts +1 -1
- package/dist/motion/View.js +100 -24
- package/dist/motion/View.js.map +1 -1
- package/dist/motion/View.mjs +100 -24
- package/dist/motion/View.mjs.map +1 -1
- package/dist/{types-CjztO3RW.d.mts → types-NmNeJjo1.d.mts} +23 -2
- package/dist/{types-CjztO3RW.d.ts → types-NmNeJjo1.d.ts} +23 -2
- package/jest-preset.js +33 -0
- package/jest-setup.js +209 -0
- package/package.json +5 -1
- package/src/index.ts +1 -0
- package/src/motion/createMotionComponent.tsx +172 -26
- package/src/transitions/easing.ts +25 -3
- package/src/transitions/index.ts +1 -0
- package/src/transitions/runtime.ts +63 -0
- package/src/transitions/spring.ts +1 -0
- package/src/types.ts +23 -1
|
@@ -67,6 +67,9 @@ const NUMERIC_TOP_LEVEL_KEYS = [
|
|
|
67
67
|
'width',
|
|
68
68
|
'height',
|
|
69
69
|
'borderRadius',
|
|
70
|
+
'shadowOpacity',
|
|
71
|
+
'shadowRadius',
|
|
72
|
+
'elevation',
|
|
70
73
|
] as const
|
|
71
74
|
|
|
72
75
|
// Color-valued keys. Reanimated's value setter detects color strings and
|
|
@@ -83,8 +86,17 @@ const COLOR_KEYS = [
|
|
|
83
86
|
'borderColor',
|
|
84
87
|
'color',
|
|
85
88
|
'tintColor',
|
|
89
|
+
'shadowColor',
|
|
86
90
|
] as const
|
|
87
91
|
|
|
92
|
+
// Synthetic axes for the `shadowOffset: { width, height }` nested-object style
|
|
93
|
+
// prop. RN doesn't surface these as top-level keys; the worklet recomposes
|
|
94
|
+
// `shadowOffset` from the two synthetic SVs before emitting the style. The
|
|
95
|
+
// consumer's animate value (`shadowOffset: { width, height }`) decomposes into
|
|
96
|
+
// these at the activation / value-driving boundary; consumers don't write
|
|
97
|
+
// these keys directly.
|
|
98
|
+
const SHADOW_OFFSET_KEYS = ['shadowOffsetWidth', 'shadowOffsetHeight'] as const
|
|
99
|
+
|
|
88
100
|
/**
|
|
89
101
|
* Per-effect transform-group coordinator. Counts how many transform-axis
|
|
90
102
|
* terminal callbacks are still pending; when the last one fires, the
|
|
@@ -97,12 +109,15 @@ const ALL_KEYS = [
|
|
|
97
109
|
...TRANSFORM_KEYS,
|
|
98
110
|
...NUMERIC_TOP_LEVEL_KEYS,
|
|
99
111
|
...COLOR_KEYS,
|
|
112
|
+
...SHADOW_OFFSET_KEYS,
|
|
100
113
|
] as const
|
|
101
114
|
type AnimatableKey = (typeof ALL_KEYS)[number]
|
|
102
115
|
type TransformKey = (typeof TRANSFORM_KEYS)[number]
|
|
116
|
+
type ShadowOffsetKey = (typeof SHADOW_OFFSET_KEYS)[number]
|
|
103
117
|
|
|
104
118
|
const TRANSFORM_KEY_SET = new Set<AnimatableKey>(TRANSFORM_KEYS)
|
|
105
119
|
const COLOR_KEY_SET = new Set<AnimatableKey>(COLOR_KEYS)
|
|
120
|
+
const SHADOW_OFFSET_KEY_SET = new Set<AnimatableKey>(SHADOW_OFFSET_KEYS)
|
|
106
121
|
|
|
107
122
|
const GESTURE_LAYER_NAMES = [
|
|
108
123
|
'hovered',
|
|
@@ -131,6 +146,9 @@ const DEFAULT_RESTING: Record<AnimatableKey, number | string> = {
|
|
|
131
146
|
width: 0,
|
|
132
147
|
height: 0,
|
|
133
148
|
borderRadius: 0,
|
|
149
|
+
shadowOpacity: 0,
|
|
150
|
+
shadowRadius: 0,
|
|
151
|
+
elevation: 0,
|
|
134
152
|
// 'transparent' is the only safe universal default for colors: it works as
|
|
135
153
|
// an initial seed for any color animation (no jarring opaque flash on mount
|
|
136
154
|
// when `initial` is omitted) and rgba(0,0,0,0) interpolates cleanly into
|
|
@@ -139,6 +157,9 @@ const DEFAULT_RESTING: Record<AnimatableKey, number | string> = {
|
|
|
139
157
|
borderColor: 'transparent',
|
|
140
158
|
color: 'transparent',
|
|
141
159
|
tintColor: 'transparent',
|
|
160
|
+
shadowColor: 'transparent',
|
|
161
|
+
shadowOffsetWidth: 0,
|
|
162
|
+
shadowOffsetHeight: 0,
|
|
142
163
|
}
|
|
143
164
|
|
|
144
165
|
function transitionFor<S>(
|
|
@@ -237,18 +258,12 @@ export function createMotionComponent<C extends ComponentType<any>>(
|
|
|
237
258
|
variantKey,
|
|
238
259
|
)
|
|
239
260
|
|
|
240
|
-
const animateRecord = (resolvedAnimate ?? {}) as
|
|
241
|
-
Record<AnimatableKey, AnimatableValue<number | string>>
|
|
242
|
-
>
|
|
261
|
+
const animateRecord = (resolvedAnimate ?? {}) as InternalAnimateRecord
|
|
243
262
|
const initialRecord =
|
|
244
263
|
initial && initial !== false
|
|
245
|
-
? (initial as
|
|
264
|
+
? (initial as InternalInitialRecord)
|
|
246
265
|
: undefined
|
|
247
|
-
const exitRecord = exit
|
|
248
|
-
? (exit as Partial<
|
|
249
|
-
Record<AnimatableKey, AnimatableValue<number | string>>
|
|
250
|
-
>)
|
|
251
|
-
: undefined
|
|
266
|
+
const exitRecord = exit ? (exit as InternalAnimateRecord) : undefined
|
|
252
267
|
|
|
253
268
|
// Gesture sub-state activation tracked as JS state. Activation flips drive
|
|
254
269
|
// the per-layer progress shared values (0↔1); they intentionally do NOT
|
|
@@ -268,16 +283,12 @@ export function createMotionComponent<C extends ComponentType<any>>(
|
|
|
268
283
|
const activeKeysRef = useRef<readonly AnimatableKey[] | null>(null)
|
|
269
284
|
if (activeKeysRef.current === null) {
|
|
270
285
|
const touched = new Set<AnimatableKey>()
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
if (initialRecord && k in initialRecord) touched.add(k)
|
|
274
|
-
}
|
|
286
|
+
collectTouchedKeys(touched, animateRecord)
|
|
287
|
+
if (initialRecord) collectTouchedKeys(touched, initialRecord)
|
|
275
288
|
if (variants) {
|
|
276
289
|
for (const variant of Object.values(variants) as object[]) {
|
|
277
290
|
if (!variant) continue
|
|
278
|
-
|
|
279
|
-
if (k in variant) touched.add(k)
|
|
280
|
-
}
|
|
291
|
+
collectTouchedKeys(touched, variant as Record<string, unknown>)
|
|
281
292
|
}
|
|
282
293
|
}
|
|
283
294
|
if (gesture) {
|
|
@@ -288,23 +299,43 @@ export function createMotionComponent<C extends ComponentType<any>>(
|
|
|
288
299
|
gesture.hovered,
|
|
289
300
|
] as Array<object | undefined>) {
|
|
290
301
|
if (!subState) continue
|
|
291
|
-
|
|
292
|
-
if (k in subState) touched.add(k)
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
if (exitRecord) {
|
|
297
|
-
for (const k of ALL_KEYS) {
|
|
298
|
-
if (k in exitRecord) touched.add(k)
|
|
302
|
+
collectTouchedKeys(touched, subState as Record<string, unknown>)
|
|
299
303
|
}
|
|
300
304
|
}
|
|
305
|
+
if (exitRecord) collectTouchedKeys(touched, exitRecord)
|
|
301
306
|
activeKeysRef.current = ALL_KEYS.filter((k) => touched.has(k))
|
|
302
307
|
}
|
|
303
308
|
const hasTransformRef = useRef<boolean>(
|
|
304
309
|
activeKeysRef.current.some((k) => TRANSFORM_KEY_SET.has(k)),
|
|
305
310
|
)
|
|
311
|
+
const hasShadowOffsetRef = useRef<boolean>(
|
|
312
|
+
activeKeysRef.current.some((k) => SHADOW_OFFSET_KEY_SET.has(k)),
|
|
313
|
+
)
|
|
306
314
|
|
|
307
315
|
const sharedValues = useAnimatableSharedValues((key) => {
|
|
316
|
+
// Shadow offset synthetics seed from the corresponding axis on the
|
|
317
|
+
// `shadowOffset: { width, height }` source — the consumer doesn't write
|
|
318
|
+
// `shadowOffsetWidth` / `shadowOffsetHeight` directly. Fall back to the
|
|
319
|
+
// generic resting default when neither initial nor animate touched it.
|
|
320
|
+
if (SHADOW_OFFSET_KEY_SET.has(key)) {
|
|
321
|
+
const axis = shadowOffsetAxisFor(key as ShadowOffsetKey)
|
|
322
|
+
if (initial === false) {
|
|
323
|
+
return (
|
|
324
|
+
shadowOffsetAxisValue(animateRecord.shadowOffset, axis) ??
|
|
325
|
+
DEFAULT_RESTING[key]
|
|
326
|
+
)
|
|
327
|
+
}
|
|
328
|
+
return (
|
|
329
|
+
shadowOffsetAxisValue(
|
|
330
|
+
initialRecord?.shadowOffset as
|
|
331
|
+
| { width?: number; height?: number }
|
|
332
|
+
| undefined,
|
|
333
|
+
axis,
|
|
334
|
+
) ??
|
|
335
|
+
shadowOffsetAxisValue(animateRecord.shadowOffset, axis) ??
|
|
336
|
+
DEFAULT_RESTING[key]
|
|
337
|
+
)
|
|
338
|
+
}
|
|
308
339
|
if (initial === false) {
|
|
309
340
|
const a = animateRecord[key]
|
|
310
341
|
return restValue(a) ?? DEFAULT_RESTING[key]
|
|
@@ -398,7 +429,18 @@ export function createMotionComponent<C extends ComponentType<any>>(
|
|
|
398
429
|
transformPending > 0 ? { remaining: transformPending } : undefined
|
|
399
430
|
|
|
400
431
|
for (const key of ALL_KEYS) {
|
|
401
|
-
|
|
432
|
+
// Shadow offset synthetics read their target from the nested
|
|
433
|
+
// `shadowOffset: { width, height }` source on `baseRecord` — the
|
|
434
|
+
// animate / exit record never has `shadowOffsetWidth` etc. on it
|
|
435
|
+
// directly. The synthetic transition follows the same `shadowOffset`
|
|
436
|
+
// top-level transition entry (no per-axis split).
|
|
437
|
+
const target: AnimatableValue<number | string> | undefined =
|
|
438
|
+
SHADOW_OFFSET_KEY_SET.has(key)
|
|
439
|
+
? shadowOffsetAxisValue(
|
|
440
|
+
baseRecord.shadowOffset,
|
|
441
|
+
shadowOffsetAxisFor(key as ShadowOffsetKey),
|
|
442
|
+
)
|
|
443
|
+
: baseRecord[key]
|
|
402
444
|
if (target === undefined) continue
|
|
403
445
|
// Reduced-motion overrides every per-key transition (and any nested
|
|
404
446
|
// sequence-step transition) with `no-animation`, which the resolver
|
|
@@ -407,7 +449,12 @@ export function createMotionComponent<C extends ComponentType<any>>(
|
|
|
407
449
|
// state" expectation.
|
|
408
450
|
const cfg = shouldReduceMotion
|
|
409
451
|
? ({ type: 'no-animation' } as const)
|
|
410
|
-
: transitionFor(
|
|
452
|
+
: transitionFor(
|
|
453
|
+
SHADOW_OFFSET_KEY_SET.has(key)
|
|
454
|
+
? ('shadowOffset' as keyof typeof baseRecord)
|
|
455
|
+
: key,
|
|
456
|
+
transition,
|
|
457
|
+
)
|
|
411
458
|
if (isExiting) pending++
|
|
412
459
|
const factory = makeKeyCallbackFactory(
|
|
413
460
|
key,
|
|
@@ -487,8 +534,14 @@ export function createMotionComponent<C extends ComponentType<any>>(
|
|
|
487
534
|
const animatedStyle = useAnimatedStyle(() => {
|
|
488
535
|
const activeKeys = activeKeysRef.current!
|
|
489
536
|
const hasTransform = hasTransformRef.current
|
|
537
|
+
const hasShadowOffset = hasShadowOffsetRef.current
|
|
490
538
|
const out: Record<string, unknown> = {}
|
|
491
539
|
const transform: Array<Record<string, unknown>> = []
|
|
540
|
+
// shadow-offset reassembly buffers. The two synthetic axis SVs feed in
|
|
541
|
+
// here and the recomposed `{ width, height }` object lands on `out`
|
|
542
|
+
// after the loop so RN gets a single `shadowOffset` style prop.
|
|
543
|
+
let shadowOffsetW = 0
|
|
544
|
+
let shadowOffsetH = 0
|
|
492
545
|
|
|
493
546
|
// Read each progress SV exactly once so the chain below sees a coherent
|
|
494
547
|
// snapshot for this frame. Reading them on the UI thread is cheap.
|
|
@@ -550,11 +603,18 @@ export function createMotionComponent<C extends ComponentType<any>>(
|
|
|
550
603
|
transform.push(
|
|
551
604
|
ROTATION_KEYS.has(key) ? { [key]: `${v}deg` } : { [key]: v },
|
|
552
605
|
)
|
|
606
|
+
} else if (key === 'shadowOffsetWidth') {
|
|
607
|
+
shadowOffsetW = v as number
|
|
608
|
+
} else if (key === 'shadowOffsetHeight') {
|
|
609
|
+
shadowOffsetH = v as number
|
|
553
610
|
} else {
|
|
554
611
|
out[key] = v
|
|
555
612
|
}
|
|
556
613
|
}
|
|
557
614
|
if (hasTransform) out.transform = transform
|
|
615
|
+
if (hasShadowOffset) {
|
|
616
|
+
out.shadowOffset = { width: shadowOffsetW, height: shadowOffsetH }
|
|
617
|
+
}
|
|
558
618
|
return out
|
|
559
619
|
})
|
|
560
620
|
|
|
@@ -643,12 +703,22 @@ function useAnimatableSharedValues(
|
|
|
643
703
|
const width = useSharedValue<number | string>(init('width'))
|
|
644
704
|
const height = useSharedValue<number | string>(init('height'))
|
|
645
705
|
const borderRadius = useSharedValue<number | string>(init('borderRadius'))
|
|
706
|
+
const shadowOpacity = useSharedValue<number | string>(init('shadowOpacity'))
|
|
707
|
+
const shadowRadius = useSharedValue<number | string>(init('shadowRadius'))
|
|
708
|
+
const elevation = useSharedValue<number | string>(init('elevation'))
|
|
646
709
|
const backgroundColor = useSharedValue<number | string>(
|
|
647
710
|
init('backgroundColor'),
|
|
648
711
|
)
|
|
649
712
|
const borderColor = useSharedValue<number | string>(init('borderColor'))
|
|
650
713
|
const color = useSharedValue<number | string>(init('color'))
|
|
651
714
|
const tintColor = useSharedValue<number | string>(init('tintColor'))
|
|
715
|
+
const shadowColor = useSharedValue<number | string>(init('shadowColor'))
|
|
716
|
+
const shadowOffsetWidth = useSharedValue<number | string>(
|
|
717
|
+
init('shadowOffsetWidth'),
|
|
718
|
+
)
|
|
719
|
+
const shadowOffsetHeight = useSharedValue<number | string>(
|
|
720
|
+
init('shadowOffsetHeight'),
|
|
721
|
+
)
|
|
652
722
|
|
|
653
723
|
const ref = useRef<SharedValueMap | null>(null)
|
|
654
724
|
if (ref.current === null) {
|
|
@@ -665,10 +735,16 @@ function useAnimatableSharedValues(
|
|
|
665
735
|
width,
|
|
666
736
|
height,
|
|
667
737
|
borderRadius,
|
|
738
|
+
shadowOpacity,
|
|
739
|
+
shadowRadius,
|
|
740
|
+
elevation,
|
|
668
741
|
backgroundColor,
|
|
669
742
|
borderColor,
|
|
670
743
|
color,
|
|
671
744
|
tintColor,
|
|
745
|
+
shadowColor,
|
|
746
|
+
shadowOffsetWidth,
|
|
747
|
+
shadowOffsetHeight,
|
|
672
748
|
}
|
|
673
749
|
}
|
|
674
750
|
return ref.current
|
|
@@ -791,6 +867,65 @@ function makeKeyCallbackFactory(
|
|
|
791
867
|
}
|
|
792
868
|
}
|
|
793
869
|
|
|
870
|
+
/**
|
|
871
|
+
* Internal shape of the `animate` / `exit` record after the cast, widened to
|
|
872
|
+
* include the `shadowOffset: { width, height }` nested-object source. The
|
|
873
|
+
* nested object decomposes into the `shadowOffsetWidth` / `shadowOffsetHeight`
|
|
874
|
+
* synthetic axes downstream; consumers don't see the synthetics.
|
|
875
|
+
*
|
|
876
|
+
* v0.1 contract: `shadowOffset` accepts a single `{ width, height }` literal
|
|
877
|
+
* (no sequences, no `{ to }` step objects, no array keyframes). Sequence
|
|
878
|
+
* forms on the nested axes can land in v0.2 if real consumers ask for them.
|
|
879
|
+
*/
|
|
880
|
+
type InternalAnimateRecord = Partial<
|
|
881
|
+
Record<AnimatableKey, AnimatableValue<number | string>>
|
|
882
|
+
> & {
|
|
883
|
+
shadowOffset?: { width?: number; height?: number }
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
type InternalInitialRecord = Partial<Record<AnimatableKey, number | string>> & {
|
|
887
|
+
shadowOffset?: { width?: number; height?: number }
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
/**
|
|
891
|
+
* Resolve a `shadowOffsetWidth` / `shadowOffsetHeight` synthetic key to the
|
|
892
|
+
* axis it represents on the nested-object source.
|
|
893
|
+
*/
|
|
894
|
+
function shadowOffsetAxisFor(key: ShadowOffsetKey): 'width' | 'height' {
|
|
895
|
+
return key === 'shadowOffsetWidth' ? 'width' : 'height'
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
/**
|
|
899
|
+
* Read a single axis off a `shadowOffset: { width, height }` source. Returns
|
|
900
|
+
* `undefined` when the source is absent or the axis isn't set, so callers can
|
|
901
|
+
* fall back to the next source in the precedence chain.
|
|
902
|
+
*/
|
|
903
|
+
function shadowOffsetAxisValue(
|
|
904
|
+
source: { width?: number; height?: number } | undefined,
|
|
905
|
+
axis: 'width' | 'height',
|
|
906
|
+
): number | undefined {
|
|
907
|
+
return source?.[axis]
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
/**
|
|
911
|
+
* Populate `touched` with the `AnimatableKey`s mentioned in `record`. Direct
|
|
912
|
+
* matches (e.g. `opacity`, `width`) come from the key iteration; the nested
|
|
913
|
+
* `shadowOffset` source decomposes into the two `shadowOffset*` synthetics.
|
|
914
|
+
*/
|
|
915
|
+
function collectTouchedKeys(
|
|
916
|
+
touched: Set<AnimatableKey>,
|
|
917
|
+
record: Record<string, unknown>,
|
|
918
|
+
): void {
|
|
919
|
+
for (const k of ALL_KEYS) {
|
|
920
|
+
if (k in record) touched.add(k)
|
|
921
|
+
}
|
|
922
|
+
if ('shadowOffset' in record && record.shadowOffset) {
|
|
923
|
+
const so = record.shadowOffset as { width?: unknown; height?: unknown }
|
|
924
|
+
if (so.width !== undefined) touched.add('shadowOffsetWidth')
|
|
925
|
+
if (so.height !== undefined) touched.add('shadowOffsetHeight')
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
|
|
794
929
|
/**
|
|
795
930
|
* Number of sequence steps in an animatable value. `1` for plain values and
|
|
796
931
|
* single-step `{ to }` objects; the array length for keyframe arrays.
|
|
@@ -930,6 +1065,17 @@ function resolveGestureLayers(
|
|
|
930
1065
|
if (!subState) continue
|
|
931
1066
|
const resolved: Record<string, number | string> = {}
|
|
932
1067
|
for (const key of ALL_KEYS) {
|
|
1068
|
+
// Shadow offset synthetics decompose from the nested `shadowOffset:
|
|
1069
|
+
// { width, height }` source on the sub-state, the same as on `animate`.
|
|
1070
|
+
if (SHADOW_OFFSET_KEY_SET.has(key)) {
|
|
1071
|
+
const axis = shadowOffsetAxisFor(key as ShadowOffsetKey)
|
|
1072
|
+
const so = (
|
|
1073
|
+
subState as { shadowOffset?: { width?: number; height?: number } }
|
|
1074
|
+
).shadowOffset
|
|
1075
|
+
const v = shadowOffsetAxisValue(so, axis)
|
|
1076
|
+
if (v !== undefined) resolved[key] = v
|
|
1077
|
+
continue
|
|
1078
|
+
}
|
|
933
1079
|
const raw = (subState as Record<string, unknown>)[key]
|
|
934
1080
|
if (raw === undefined) continue
|
|
935
1081
|
const t = targetEndValue(raw as AnimatableValue<number | string>)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// `isWorkletFunction` lives in `react-native-worklets` (the Reanimated 4 peer
|
|
2
2
|
// dep); Reanimated's own re-export is deprecated.
|
|
3
3
|
import { isWorkletFunction } from 'react-native-worklets'
|
|
4
|
+
import { type EasingInput } from '../types'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Reanimated 3.9+ validates that easing functions used in nested-transition
|
|
@@ -15,17 +16,38 @@ import { isWorkletFunction } from 'react-native-worklets'
|
|
|
15
16
|
* the consumer's worklets babel plugin (the default Expo/RN setup), the
|
|
16
17
|
* wrapper becomes a real worklet that captures the user fn via closure.
|
|
17
18
|
*
|
|
19
|
+
* Reanimated 4 changed `Easing.bezier(...)` to return an
|
|
20
|
+
* `EasingFunctionFactory` (`{ factory: () => EasingFunction }`) rather than
|
|
21
|
+
* the function itself. The helper accepts both shapes — `EasingFunction` and
|
|
22
|
+
* `EasingFunctionFactory` — and unwraps the factory automatically so
|
|
23
|
+
* consumers don't have to call `.factory()` manually.
|
|
24
|
+
*
|
|
18
25
|
* The user fn must be pure: no JS-thread captured refs, no shared mutable
|
|
19
26
|
* state, no calls to non-worklet APIs.
|
|
20
27
|
*/
|
|
21
28
|
export function ensureWorkletEasing(
|
|
22
|
-
easing:
|
|
29
|
+
easing: EasingInput | undefined,
|
|
23
30
|
): ((t: number) => number) | undefined {
|
|
24
31
|
if (!easing) return undefined
|
|
25
|
-
|
|
32
|
+
// Reanimated 4 `EasingFunctionFactory` — unwrap via `.factory()` before
|
|
33
|
+
// checking worklet status, so the wrapped fn (not the factory wrapper)
|
|
34
|
+
// ends up in the transition config.
|
|
35
|
+
const fn = isEasingFactory(easing) ? easing.factory() : easing
|
|
36
|
+
if (isWorkletFunction(fn)) return fn
|
|
26
37
|
const wrapped = (t: number) => {
|
|
27
38
|
'worklet'
|
|
28
|
-
return
|
|
39
|
+
return fn(t)
|
|
29
40
|
}
|
|
30
41
|
return wrapped
|
|
31
42
|
}
|
|
43
|
+
|
|
44
|
+
function isEasingFactory(
|
|
45
|
+
value: EasingInput,
|
|
46
|
+
): value is { factory: () => (t: number) => number } {
|
|
47
|
+
return (
|
|
48
|
+
typeof value === 'object' &&
|
|
49
|
+
value !== null &&
|
|
50
|
+
'factory' in value &&
|
|
51
|
+
typeof (value as { factory: unknown }).factory === 'function'
|
|
52
|
+
)
|
|
53
|
+
}
|
package/src/transitions/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { resolveTransition, resolveAnimatableValue } from './resolve'
|
|
2
2
|
export { ensureWorkletEasing } from './easing'
|
|
3
3
|
export { isTopLevelTransition, TRANSITION_CONFIG_KEYS } from './keys'
|
|
4
|
+
export { buildReleaseAnimation } from './runtime'
|
|
4
5
|
export { stableSig } from './sig'
|
|
5
6
|
export { DEFAULT_SPRING, springToReanimated } from './spring'
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Easing,
|
|
3
|
+
withDecay,
|
|
4
|
+
withSpring,
|
|
5
|
+
withTiming,
|
|
6
|
+
} from 'react-native-reanimated'
|
|
7
|
+
import { springToReanimated } from './spring'
|
|
8
|
+
import { type TransitionConfig } from '../types'
|
|
9
|
+
|
|
10
|
+
const DEFAULT_TIMING_DURATION = 250
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Worklet-safe single-step animation builder. Mirrors a subset of
|
|
14
|
+
* `resolveTransition` for the UI-thread path where the transition config is
|
|
15
|
+
* picked at gesture-release time, not at render time.
|
|
16
|
+
*
|
|
17
|
+
* Supported: spring / timing / decay / no-animation, single-step only.
|
|
18
|
+
* Not supported: sequences, top-level repeat, easing-function
|
|
19
|
+
* auto-worklet-wrapping (pass an already-worklet easing if you need a custom
|
|
20
|
+
* one — most release transitions don't).
|
|
21
|
+
*
|
|
22
|
+
* Use this from gesture worklets (`useDrag` / `usePan` release callbacks, or
|
|
23
|
+
* any custom `Gesture.Pan().onEnd(() => ...)` worklet) to animate a shared
|
|
24
|
+
* value with an Inertia transition without the JS round-trip that would lose
|
|
25
|
+
* the release velocity.
|
|
26
|
+
*
|
|
27
|
+
* For decay transitions, `toValue` is ignored — decay decelerates from the
|
|
28
|
+
* SV's current position via its own physics. Pass `0` if you don't have one.
|
|
29
|
+
*/
|
|
30
|
+
export function buildReleaseAnimation(
|
|
31
|
+
transition: TransitionConfig,
|
|
32
|
+
toValue: number,
|
|
33
|
+
): unknown {
|
|
34
|
+
'worklet'
|
|
35
|
+
if (transition.type === 'no-animation') return toValue
|
|
36
|
+
if (transition.type === 'decay') {
|
|
37
|
+
const cfg: {
|
|
38
|
+
velocity: number
|
|
39
|
+
deceleration?: number
|
|
40
|
+
clamp?: [number, number]
|
|
41
|
+
} = { velocity: transition.velocity ?? 0 }
|
|
42
|
+
if (transition.deceleration !== undefined) {
|
|
43
|
+
cfg.deceleration = transition.deceleration
|
|
44
|
+
}
|
|
45
|
+
if (transition.clamp !== undefined) cfg.clamp = transition.clamp
|
|
46
|
+
return withDecay(cfg)
|
|
47
|
+
}
|
|
48
|
+
if (transition.type === 'timing') {
|
|
49
|
+
// Reanimated 4's `Easing.bezier(...)` returns an `EasingFunctionFactory`
|
|
50
|
+
// rather than the function itself. Unwrap inline so consumers calling
|
|
51
|
+
// `buildReleaseAnimation` from a gesture worklet don't have to.
|
|
52
|
+
const e = transition.easing
|
|
53
|
+
const easingFn =
|
|
54
|
+
e && typeof e === 'object' && 'factory' in e
|
|
55
|
+
? e.factory()
|
|
56
|
+
: (e ?? Easing.inOut(Easing.ease))
|
|
57
|
+
return withTiming(toValue, {
|
|
58
|
+
duration: transition.duration ?? DEFAULT_TIMING_DURATION,
|
|
59
|
+
easing: easingFn,
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
return withSpring(toValue, springToReanimated(transition))
|
|
63
|
+
}
|
|
@@ -30,6 +30,7 @@ export const DEFAULT_SPRING: Required<
|
|
|
30
30
|
* what designers and prior-art consumers expect.
|
|
31
31
|
*/
|
|
32
32
|
export function springToReanimated(t: SpringTransition) {
|
|
33
|
+
'worklet'
|
|
33
34
|
return {
|
|
34
35
|
stiffness: t.tension ?? DEFAULT_SPRING.tension,
|
|
35
36
|
damping: t.friction ?? DEFAULT_SPRING.friction,
|
package/src/types.ts
CHANGED
|
@@ -32,10 +32,24 @@ export interface SpringTransition {
|
|
|
32
32
|
repeat?: RepeatConfig
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Easing input accepted by `TimingTransition`. Either a plain easing function
|
|
37
|
+
* `(t: number) => number` (the pre-Reanimated-4 shape and the shape consumers
|
|
38
|
+
* author by hand) or an `EasingFunctionFactory` (the Reanimated 4 shape
|
|
39
|
+
* returned by `Easing.bezier(...)` and similar builders). The resolver
|
|
40
|
+
* unwraps the factory automatically — consumers don't have to call
|
|
41
|
+
* `.factory()` themselves.
|
|
42
|
+
*/
|
|
43
|
+
export type EasingFunction = (t: number) => number
|
|
44
|
+
export interface EasingFunctionFactory {
|
|
45
|
+
factory: () => EasingFunction
|
|
46
|
+
}
|
|
47
|
+
export type EasingInput = EasingFunction | EasingFunctionFactory
|
|
48
|
+
|
|
35
49
|
export interface TimingTransition {
|
|
36
50
|
type: 'timing'
|
|
37
51
|
duration?: number
|
|
38
|
-
easing?:
|
|
52
|
+
easing?: EasingInput
|
|
39
53
|
delay?: number
|
|
40
54
|
repeat?: RepeatConfig
|
|
41
55
|
}
|
|
@@ -192,6 +206,14 @@ export type VariantsMap<C> = Record<string, AnimateStyle<C>>
|
|
|
192
206
|
* Configure per-layer fade timing via `transition.<stateName>` on the parent
|
|
193
207
|
* primitive (see `GestureLayerTransitions`); without it, layers default to
|
|
194
208
|
* the parent transition or the library default spring.
|
|
209
|
+
*
|
|
210
|
+
* **Priority cascade is the only composition mode on this prop.** Non-priority
|
|
211
|
+
* blends — clamped-max (`Math.max(hover*α, focus*β, press*γ)`, as used by MD3
|
|
212
|
+
* state-layer haloes), additive accumulation, or any per-key custom blend —
|
|
213
|
+
* are not expressible declaratively. Drop to `useGesture()` for those: it
|
|
214
|
+
* returns the four progress shared values and a handler bag, and you write
|
|
215
|
+
* a `useAnimatedStyle` block with whatever composition you need. The hook's
|
|
216
|
+
* JSDoc shows the clamped-max halo pattern in full.
|
|
195
217
|
*/
|
|
196
218
|
export interface GestureSubStates<C> {
|
|
197
219
|
pressed?: AnimateStyle<C>
|