react-native-smooth-clip-view 0.2.8 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +86 -21
- package/android/CMakeLists.txt +7 -0
- package/android/build.gradle +14 -2
- package/android/proguard-rules.pro +3 -3
- package/android/src/main/cpp/SmoothClipAndroid.h +10 -0
- package/android/src/main/cpp/SmoothClipBindings.cpp +896 -6
- package/android/src/main/cpp/SmoothClipRegistry.cpp +1067 -46
- package/android/src/main/cpp/SmoothClipTrace.h +25 -0
- package/android/src/main/java/com/smoothclipview/ClipGeometryNormalizer.kt +265 -0
- package/android/src/main/java/com/smoothclipview/SmoothClipBindings.kt +21 -0
- package/android/src/main/java/com/smoothclipview/SmoothClipModule.kt +283 -0
- package/android/src/main/java/com/smoothclipview/SmoothClipView.kt +385 -24
- package/android/src/main/java/com/smoothclipview/SmoothClipViewManager.kt +138 -24
- package/cpp/SmoothClipAnimationCurve.h +151 -122
- package/cpp/SmoothClipRegistry.h +102 -4
- package/cpp/SmoothClipSharedGeometry.h +146 -7
- package/cpp/SmoothClipVelocityTracker.h +13 -9
- package/ios/SmoothClipGeometry.h +184 -6
- package/ios/SmoothClipRegistry.mm +1033 -25
- package/ios/SmoothClipRegistrySnapshot.h +13 -0
- package/ios/SmoothClipTurboModule.cpp +857 -2
- package/ios/SmoothClipTurboModule.h +310 -0
- package/ios/SmoothClipView.mm +714 -183
- package/ios/SmoothClipViewRegistry.h +1 -0
- package/ios/reactnativesmoothclipview.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
- package/ios/reactnativesmoothclipview.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
- package/lib/module/NativeSmoothClipModule.js.map +1 -1
- package/lib/module/SmoothClipView.ios.js +22 -6
- package/lib/module/SmoothClipView.ios.js.map +1 -1
- package/lib/module/SmoothClipView.js +1 -46
- package/lib/module/SmoothClipView.js.map +1 -1
- package/lib/module/SmoothClipViewNativeComponent.ts +27 -1
- package/lib/module/capabilities.js +4 -0
- package/lib/module/capabilities.js.map +1 -0
- package/lib/module/capabilities.native.js +36 -0
- package/lib/module/capabilities.native.js.map +1 -0
- package/lib/module/capabilityTypes.js +11 -0
- package/lib/module/capabilityTypes.js.map +1 -0
- package/lib/module/driverState.js +0 -28
- package/lib/module/driverState.js.map +1 -1
- package/lib/module/drivers.js +1 -281
- package/lib/module/drivers.js.map +1 -1
- package/lib/module/drivers.native.js +340 -75
- package/lib/module/drivers.native.js.map +1 -1
- package/lib/module/geometry.js +147 -17
- package/lib/module/geometry.js.map +1 -1
- package/lib/module/groupDriverTypes.js +4 -0
- package/lib/module/groupDriverTypes.js.map +1 -0
- package/lib/module/groupDrivers.android.js +4 -0
- package/lib/module/groupDrivers.android.js.map +1 -0
- package/lib/module/groupDrivers.ios.js +4 -0
- package/lib/module/groupDrivers.ios.js.map +1 -0
- package/lib/module/groupDrivers.js +4 -0
- package/lib/module/groupDrivers.js.map +1 -0
- package/lib/module/groupDrivers.native.js +799 -0
- package/lib/module/groupDrivers.native.js.map +1 -0
- package/lib/module/index.js +3 -2
- package/lib/module/index.js.map +1 -1
- package/lib/module/presentationProtocol.js +15 -0
- package/lib/module/presentationProtocol.js.map +1 -0
- package/lib/typescript/src/NativeSmoothClipModule.d.ts +25 -0
- package/lib/typescript/src/NativeSmoothClipModule.d.ts.map +1 -1
- package/lib/typescript/src/SmoothClipView.d.ts +2 -10
- package/lib/typescript/src/SmoothClipView.d.ts.map +1 -1
- package/lib/typescript/src/SmoothClipView.ios.d.ts +133 -2
- package/lib/typescript/src/SmoothClipView.ios.d.ts.map +1 -1
- package/lib/typescript/src/SmoothClipViewNativeComponent.d.ts +8 -0
- package/lib/typescript/src/SmoothClipViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/capabilities.d.ts +3 -0
- package/lib/typescript/src/capabilities.d.ts.map +1 -0
- package/lib/typescript/src/capabilities.native.d.ts +3 -0
- package/lib/typescript/src/capabilities.native.d.ts.map +1 -0
- package/lib/typescript/src/capabilityTypes.d.ts +10 -0
- package/lib/typescript/src/capabilityTypes.d.ts.map +1 -0
- package/lib/typescript/src/driverState.d.ts +1 -6
- package/lib/typescript/src/driverState.d.ts.map +1 -1
- package/lib/typescript/src/driverTypes.d.ts +39 -11
- package/lib/typescript/src/driverTypes.d.ts.map +1 -1
- package/lib/typescript/src/drivers.d.ts +2 -4
- package/lib/typescript/src/drivers.d.ts.map +1 -1
- package/lib/typescript/src/drivers.native.d.ts.map +1 -1
- package/lib/typescript/src/geometry.d.ts +36 -2
- package/lib/typescript/src/geometry.d.ts.map +1 -1
- package/lib/typescript/src/groupDriverTypes.d.ts +66 -0
- package/lib/typescript/src/groupDriverTypes.d.ts.map +1 -0
- package/lib/typescript/src/groupDrivers.android.d.ts +2 -0
- package/lib/typescript/src/groupDrivers.android.d.ts.map +1 -0
- package/lib/typescript/src/groupDrivers.d.ts +2 -0
- package/lib/typescript/src/groupDrivers.d.ts.map +1 -0
- package/lib/typescript/src/groupDrivers.ios.d.ts +2 -0
- package/lib/typescript/src/groupDrivers.ios.d.ts.map +1 -0
- package/lib/typescript/src/groupDrivers.native.d.ts +4 -0
- package/lib/typescript/src/groupDrivers.native.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +6 -3
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/presentationProtocol.d.ts +4 -0
- package/lib/typescript/src/presentationProtocol.d.ts.map +1 -0
- package/package.json +10 -11
- package/src/NativeSmoothClipModule.ts +275 -0
- package/src/SmoothClipView.ios.tsx +30 -11
- package/src/SmoothClipView.tsx +2 -76
- package/src/SmoothClipViewNativeComponent.ts +27 -1
- package/src/capabilities.native.ts +69 -0
- package/src/capabilities.ts +2 -0
- package/src/capabilityTypes.ts +17 -0
- package/src/driverState.ts +1 -41
- package/src/driverTypes.ts +56 -11
- package/src/drivers.native.ts +921 -151
- package/src/drivers.ts +2 -423
- package/src/geometry.ts +231 -27
- package/src/groupDriverTypes.ts +117 -0
- package/src/groupDrivers.android.ts +1 -0
- package/src/groupDrivers.ios.ts +1 -0
- package/src/groupDrivers.native.ts +1189 -0
- package/src/groupDrivers.ts +1 -0
- package/src/index.ts +30 -2
- package/src/presentationProtocol.ts +28 -0
- package/lib/module/SmoothClipView.web.js +0 -54
- package/lib/module/SmoothClipView.web.js.map +0 -1
- package/lib/module/index.web.js +0 -8
- package/lib/module/index.web.js.map +0 -1
- package/lib/module/webClipPath.js +0 -25
- package/lib/module/webClipPath.js.map +0 -1
- package/lib/typescript/src/SmoothClipView.web.d.ts +0 -10
- package/lib/typescript/src/SmoothClipView.web.d.ts.map +0 -1
- package/lib/typescript/src/index.web.d.ts +0 -9
- package/lib/typescript/src/index.web.d.ts.map +0 -1
- package/lib/typescript/src/webClipPath.d.ts +0 -7
- package/lib/typescript/src/webClipPath.d.ts.map +0 -1
- package/src/SmoothClipView.web.tsx +0 -66
- package/src/index.web.ts +0 -23
- package/src/webClipPath.ts +0 -30
package/src/drivers.native.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
// The shared
|
|
1
|
+
// The shared native driver: iOS and Android both execute this file, via the
|
|
2
2
|
// one-line re-exports in drivers.ios.ts / drivers.android.ts that Metro's
|
|
3
3
|
// platform resolution picks up from index.ts's `./drivers` import. Only the
|
|
4
|
-
// `./smoothClipNative` import below is platform-split.
|
|
5
|
-
// suffix) is the web/Reanimated fallback, not this driver's sibling.
|
|
4
|
+
// `./smoothClipNative` import below is platform-split.
|
|
6
5
|
import { useEffect, useRef } from 'react';
|
|
6
|
+
import { Platform } from 'react-native';
|
|
7
7
|
import { useSharedValue, type SharedValue } from 'react-native-reanimated';
|
|
8
8
|
import { isRNRuntime, scheduleOnRN, scheduleOnUI } from 'react-native-worklets';
|
|
9
9
|
import NativeSmoothClipModule from './smoothClipNative';
|
|
10
|
+
import { getSmoothClipCapabilities } from './capabilities';
|
|
10
11
|
import type {
|
|
11
12
|
ClipReduceMotion,
|
|
12
13
|
KeyframedClipAnimation,
|
|
@@ -29,9 +30,11 @@ import {
|
|
|
29
30
|
synchronizeNativeCompletion,
|
|
30
31
|
} from './nativeCompletion';
|
|
31
32
|
import {
|
|
33
|
+
canonicalizeClipPresentation,
|
|
32
34
|
clipPresentationEquals,
|
|
33
35
|
createClipPresentation,
|
|
34
36
|
isFiniteClipPresentation,
|
|
37
|
+
type CanonicalSmoothClipPresentation,
|
|
35
38
|
type ClipGeometry,
|
|
36
39
|
type SmoothClipPresentation,
|
|
37
40
|
} from './geometry';
|
|
@@ -44,7 +47,63 @@ import {
|
|
|
44
47
|
const INTERACTIVE = 0;
|
|
45
48
|
const NATIVE = 1;
|
|
46
49
|
|
|
47
|
-
|
|
50
|
+
// Only Android consumes the Reanimated start stamp (its frame-clock anchor —
|
|
51
|
+
// docs/android-frame-clock-anchor.md). iOS's TurboModule reads its declared
|
|
52
|
+
// parameters positionally and drops the trailing argument, so capturing the
|
|
53
|
+
// stamp there is a wasted native _getAnimationTimestamp() call per animateTo.
|
|
54
|
+
// Resolved once at module scope; the worklet captures the primitive.
|
|
55
|
+
const NEEDS_START_STAMP = Platform.OS === 'android';
|
|
56
|
+
const PRESENTATION_PROTOCOL_VERSION =
|
|
57
|
+
getSmoothClipCapabilities().presentationProtocolVersion;
|
|
58
|
+
|
|
59
|
+
type NativeV2HostFunctions = Readonly<{
|
|
60
|
+
setClipPresentationV2(
|
|
61
|
+
driverId: number,
|
|
62
|
+
x: number,
|
|
63
|
+
y: number,
|
|
64
|
+
width: number,
|
|
65
|
+
height: number,
|
|
66
|
+
topLeftRadius: number,
|
|
67
|
+
topRightRadius: number,
|
|
68
|
+
bottomRightRadius: number,
|
|
69
|
+
bottomLeftRadius: number,
|
|
70
|
+
curveCode: number,
|
|
71
|
+
contentTranslateX: number,
|
|
72
|
+
contentTranslateY: number,
|
|
73
|
+
contentScale: number,
|
|
74
|
+
takeOwnership: boolean,
|
|
75
|
+
overridePendingAnimation: boolean,
|
|
76
|
+
recordVelocity?: boolean
|
|
77
|
+
): void;
|
|
78
|
+
beginInteractionV2(driverId: number): readonly number[];
|
|
79
|
+
snapshotCurrentV2(driverId: number): readonly number[];
|
|
80
|
+
animateTimingV2(...args: readonly unknown[]): number;
|
|
81
|
+
animateSpringV2(...args: readonly unknown[]): number;
|
|
82
|
+
animateKeyframesV2(...args: readonly unknown[]): number;
|
|
83
|
+
animateTimingFromV2(...args: readonly unknown[]): number;
|
|
84
|
+
animateSpringFromV2(...args: readonly unknown[]): number;
|
|
85
|
+
animateKeyframesFromV2(...args: readonly unknown[]): number;
|
|
86
|
+
cancelAnimationV2(
|
|
87
|
+
driverId: number,
|
|
88
|
+
animationId: number,
|
|
89
|
+
behavior: number
|
|
90
|
+
): readonly number[];
|
|
91
|
+
}>;
|
|
92
|
+
|
|
93
|
+
const NativeSmoothClipModuleV2 =
|
|
94
|
+
NativeSmoothClipModule as unknown as Partial<NativeV2HostFunctions>;
|
|
95
|
+
|
|
96
|
+
// Widened like the animate* functions below: one trailing argument beyond the
|
|
97
|
+
// TurboModule spec — whether this write should record an `'inherit'` velocity
|
|
98
|
+
// sample. Android's JSI bindings read it (absent means record, the pre-flag
|
|
99
|
+
// behavior); the iOS TurboModule reads its declared parameters positionally
|
|
100
|
+
// and drops it, so iOS always records.
|
|
101
|
+
type WithRecordVelocity<F> = F extends (...args: infer A) => infer R
|
|
102
|
+
? (...args: [...A, recordVelocity?: boolean]) => R
|
|
103
|
+
: never;
|
|
104
|
+
const setPresentationHostFunction: WithRecordVelocity<
|
|
105
|
+
typeof NativeSmoothClipModule.setClipPresentation
|
|
106
|
+
> = NativeSmoothClipModule.setClipPresentation;
|
|
48
107
|
const beginInteractionHostFunction = NativeSmoothClipModule.beginInteraction;
|
|
49
108
|
|
|
50
109
|
// The animate* host functions take one argument beyond the TurboModule spec:
|
|
@@ -68,15 +127,35 @@ const animateKeyframesHostFunction: WithStartStamp<
|
|
|
68
127
|
const rejectAnimationHostFunction = NativeSmoothClipModule.rejectAnimation;
|
|
69
128
|
const cancelAnimationHostFunction = NativeSmoothClipModule.cancelAnimation;
|
|
70
129
|
const destroyDriverHostFunction = NativeSmoothClipModule.destroyDriver;
|
|
130
|
+
const setPresentationV2HostFunction =
|
|
131
|
+
NativeSmoothClipModuleV2.setClipPresentationV2;
|
|
132
|
+
const beginInteractionV2HostFunction =
|
|
133
|
+
NativeSmoothClipModuleV2.beginInteractionV2;
|
|
134
|
+
const snapshotCurrentV2HostFunction =
|
|
135
|
+
NativeSmoothClipModuleV2.snapshotCurrentV2;
|
|
136
|
+
const animateTimingV2HostFunction = NativeSmoothClipModuleV2.animateTimingV2;
|
|
137
|
+
const animateSpringV2HostFunction = NativeSmoothClipModuleV2.animateSpringV2;
|
|
138
|
+
const animateKeyframesV2HostFunction =
|
|
139
|
+
NativeSmoothClipModuleV2.animateKeyframesV2;
|
|
140
|
+
const animateTimingFromV2HostFunction =
|
|
141
|
+
NativeSmoothClipModuleV2.animateTimingFromV2;
|
|
142
|
+
const animateSpringFromV2HostFunction =
|
|
143
|
+
NativeSmoothClipModuleV2.animateSpringFromV2;
|
|
144
|
+
const animateKeyframesFromV2HostFunction =
|
|
145
|
+
NativeSmoothClipModuleV2.animateKeyframesFromV2;
|
|
146
|
+
const cancelAnimationV2HostFunction =
|
|
147
|
+
NativeSmoothClipModuleV2.cancelAnimationV2;
|
|
71
148
|
|
|
72
|
-
// Workaround for react-native-worklets 0.10
|
|
149
|
+
// Workaround for react-native-worklets 0.10.0 (fixed upstream in >=0.10.1,
|
|
150
|
+
// which drops the registry/proxy design; kept while the peer range still
|
|
151
|
+
// allows 0.10.0 consumers): the RN-side serialization cache
|
|
73
152
|
// hands out one remote-function registry id per function forever, but the
|
|
74
153
|
// registry entry is deleted as soon as any transient UI-side proxy of that
|
|
75
154
|
// function is garbage collected. A later scheduleOnRN for the same function
|
|
76
155
|
// then reads `undefined` from the registry and aborts in Value::getObject.
|
|
77
156
|
// Pinning one UI-side reference for the app lifetime keeps the proxy — and
|
|
78
|
-
// with it the registry entry — alive. Remove once
|
|
79
|
-
//
|
|
157
|
+
// with it the registry entry — alive. Remove once the peer range requires
|
|
158
|
+
// worklets >=0.10.1.
|
|
80
159
|
scheduleOnUI((resolver: typeof resolveReactRequest) => {
|
|
81
160
|
'worklet';
|
|
82
161
|
(globalThis as Record<string, unknown>).__smoothClipPinnedResolver = resolver;
|
|
@@ -94,8 +173,8 @@ function reduceMotionCode(value: ClipReduceMotion): number {
|
|
|
94
173
|
}
|
|
95
174
|
|
|
96
175
|
function animationIsFinite(
|
|
97
|
-
|
|
98
|
-
|
|
176
|
+
animation: SmoothClipAnimation,
|
|
177
|
+
strictV2: boolean
|
|
99
178
|
): boolean {
|
|
100
179
|
'worklet';
|
|
101
180
|
if (
|
|
@@ -110,62 +189,67 @@ function animationIsFinite(
|
|
|
110
189
|
const [x1, , x2] = animation.controlPoints;
|
|
111
190
|
return (
|
|
112
191
|
Number.isFinite(animation.duration) &&
|
|
192
|
+
(!strictV2 || animation.duration >= 0) &&
|
|
113
193
|
animation.controlPoints.every(Number.isFinite) &&
|
|
114
194
|
// A cubic-bezier easing is only defined for x within [0,1] (the CSS /
|
|
115
195
|
// CoreAnimation / Reanimated Easing.bezier contract). Out-of-range x
|
|
116
196
|
// makes the Android parameter solve meaningless while CoreAnimation
|
|
117
197
|
// silently clamps — reject like any other invalid input instead of
|
|
118
198
|
// diverging per platform.
|
|
119
|
-
x1 >= 0 &&
|
|
120
|
-
x1 <= 1 &&
|
|
121
|
-
x2 >= 0 &&
|
|
122
|
-
x2 <= 1
|
|
199
|
+
(!strictV2 || (x1 >= 0 && x1 <= 1 && x2 >= 0 && x2 <= 1))
|
|
123
200
|
);
|
|
124
201
|
}
|
|
125
202
|
if (animation.type === 'spring') {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
203
|
+
const mass = animation.mass ?? 1;
|
|
204
|
+
const stiffness = animation.stiffness ?? 100;
|
|
205
|
+
const damping = animation.damping ?? 10;
|
|
206
|
+
const initialVelocity =
|
|
130
207
|
animation.initialVelocity === 'inherit'
|
|
131
208
|
? 0
|
|
132
|
-
: (animation.initialVelocity ?? 0)
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}
|
|
138
|
-
let previousOffset = -1;
|
|
139
|
-
for (const frame of animation.frames) {
|
|
140
|
-
if (
|
|
141
|
-
!Number.isFinite(frame.offset) ||
|
|
142
|
-
frame.offset < 0 ||
|
|
143
|
-
frame.offset > 1 ||
|
|
144
|
-
frame.offset <= previousOffset ||
|
|
145
|
-
!isFiniteClipPresentation(frame.presentation)
|
|
146
|
-
) {
|
|
147
|
-
return false;
|
|
148
|
-
}
|
|
149
|
-
previousOffset = frame.offset;
|
|
209
|
+
: (animation.initialVelocity ?? 0);
|
|
210
|
+
return (
|
|
211
|
+
[mass, stiffness, damping, initialVelocity].every(Number.isFinite) &&
|
|
212
|
+
(!strictV2 || (mass > 0 && stiffness > 0 && damping >= 0))
|
|
213
|
+
);
|
|
150
214
|
}
|
|
215
|
+
// Keyframe frames validate inside flattenFiniteKeyframes — one traversal
|
|
216
|
+
// both checks and flattens, instead of walking every frame twice. The type
|
|
217
|
+
// check is statically always-true for the TS union but load-bearing for JS
|
|
218
|
+
// callers: an unrecognized type must reject here, or it would skip the
|
|
219
|
+
// keyframe gate entirely and hand native a null frames array.
|
|
151
220
|
return (
|
|
152
|
-
animation.
|
|
153
|
-
previousOffset === 1 &&
|
|
154
|
-
clipPresentationEquals(
|
|
155
|
-
animation.frames[animation.frames.length - 1]?.presentation ?? null,
|
|
156
|
-
target
|
|
157
|
-
)
|
|
221
|
+
animation.type === 'keyframes' && (!strictV2 || animation.duration >= 0)
|
|
158
222
|
);
|
|
159
223
|
}
|
|
160
224
|
|
|
161
225
|
function presentationFromNative(
|
|
162
226
|
values: ReadonlyArray<number>,
|
|
163
|
-
fallback:
|
|
227
|
+
fallback: CanonicalSmoothClipPresentation,
|
|
164
228
|
offset = 0
|
|
165
|
-
):
|
|
229
|
+
): CanonicalSmoothClipPresentation {
|
|
166
230
|
'worklet';
|
|
231
|
+
if (values.length >= offset + 12) {
|
|
232
|
+
const presentation = canonicalizeClipPresentation({
|
|
233
|
+
clip: {
|
|
234
|
+
x: values[offset] as number,
|
|
235
|
+
y: values[offset + 1] as number,
|
|
236
|
+
width: values[offset + 2] as number,
|
|
237
|
+
height: values[offset + 3] as number,
|
|
238
|
+
radius: 0,
|
|
239
|
+
topLeftRadius: values[offset + 4] as number,
|
|
240
|
+
topRightRadius: values[offset + 5] as number,
|
|
241
|
+
bottomRightRadius: values[offset + 6] as number,
|
|
242
|
+
bottomLeftRadius: values[offset + 7] as number,
|
|
243
|
+
curve: values[offset + 8] === 1 ? 'continuous' : 'circular',
|
|
244
|
+
},
|
|
245
|
+
contentTranslateX: values[offset + 9] as number,
|
|
246
|
+
contentTranslateY: values[offset + 10] as number,
|
|
247
|
+
contentScale: values[offset + 11] as number,
|
|
248
|
+
});
|
|
249
|
+
return presentation ?? fallback;
|
|
250
|
+
}
|
|
167
251
|
if (values.length < offset + 7) return fallback;
|
|
168
|
-
const presentation = {
|
|
252
|
+
const presentation = canonicalizeClipPresentation({
|
|
169
253
|
clip: {
|
|
170
254
|
x: values[offset] as number,
|
|
171
255
|
y: values[offset + 1] as number,
|
|
@@ -175,24 +259,133 @@ function presentationFromNative(
|
|
|
175
259
|
},
|
|
176
260
|
contentTranslateX: values[offset + 5] as number,
|
|
177
261
|
contentTranslateY: values[offset + 6] as number,
|
|
178
|
-
};
|
|
179
|
-
return
|
|
262
|
+
});
|
|
263
|
+
return presentation ?? fallback;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function curveCode(presentation: CanonicalSmoothClipPresentation): number {
|
|
267
|
+
'worklet';
|
|
268
|
+
return presentation.clip.curve === 'continuous' ? 1 : 0;
|
|
180
269
|
}
|
|
181
270
|
|
|
182
|
-
function
|
|
271
|
+
function isV1Compatible(
|
|
272
|
+
presentation: CanonicalSmoothClipPresentation
|
|
273
|
+
): boolean {
|
|
183
274
|
'worklet';
|
|
275
|
+
const clip = presentation.clip;
|
|
276
|
+
return (
|
|
277
|
+
clip.curve === 'circular' &&
|
|
278
|
+
clip.topLeftRadius === clip.radius &&
|
|
279
|
+
clip.topRightRadius === clip.radius &&
|
|
280
|
+
clip.bottomRightRadius === clip.radius &&
|
|
281
|
+
clip.bottomLeftRadius === clip.radius &&
|
|
282
|
+
presentation.contentScale === 1
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function springScaleIsProvablyPositive(
|
|
287
|
+
start: CanonicalSmoothClipPresentation,
|
|
288
|
+
target: CanonicalSmoothClipPresentation,
|
|
289
|
+
animation: SpringClipAnimation
|
|
290
|
+
): boolean {
|
|
291
|
+
'worklet';
|
|
292
|
+
if (start.contentScale === target.contentScale) return true;
|
|
293
|
+
|
|
294
|
+
// A zero-velocity critically/over-damped step is monotonic, so its sampled
|
|
295
|
+
// scale remains between two already-positive endpoints. Inherited or
|
|
296
|
+
// under-damped configurations can overshoot; callers must compile those
|
|
297
|
+
// plans to exact keyframes instead of risking a non-positive transform.
|
|
298
|
+
const mass = animation.mass ?? 1;
|
|
299
|
+
const stiffness = animation.stiffness ?? 100;
|
|
300
|
+
const damping = animation.damping ?? 10;
|
|
301
|
+
return (
|
|
302
|
+
animation.initialVelocity === 0 && damping * damping >= 4 * mass * stiffness
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Validates and flattens the keyframe list in one traversal (the flat number
|
|
307
|
+
// array is the wire format the native side reads back positionally). Returns
|
|
308
|
+
// null when any frame is invalid — same rejection contract the two-pass
|
|
309
|
+
// validate-then-flatten had.
|
|
310
|
+
function flattenFiniteKeyframes(
|
|
311
|
+
target: CanonicalSmoothClipPresentation,
|
|
312
|
+
animation: KeyframedClipAnimation
|
|
313
|
+
): {
|
|
314
|
+
frames: number[];
|
|
315
|
+
canonicalFrames: CanonicalSmoothClipPresentation[];
|
|
316
|
+
} | null {
|
|
317
|
+
'worklet';
|
|
318
|
+
if (!Number.isFinite(animation.duration) || animation.frames.length < 2) {
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
184
321
|
const values: number[] = [];
|
|
322
|
+
const canonicalFrames: CanonicalSmoothClipPresentation[] = [];
|
|
323
|
+
let previousOffset = -1;
|
|
185
324
|
for (const frame of animation.frames) {
|
|
186
|
-
const {
|
|
325
|
+
const { offset } = frame;
|
|
326
|
+
const presentation = canonicalizeClipPresentation(frame.presentation);
|
|
327
|
+
if (
|
|
328
|
+
!Number.isFinite(offset) ||
|
|
329
|
+
offset < 0 ||
|
|
330
|
+
offset > 1 ||
|
|
331
|
+
offset <= previousOffset ||
|
|
332
|
+
presentation === null ||
|
|
333
|
+
presentation.clip.curve !== target.clip.curve
|
|
334
|
+
) {
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
previousOffset = offset;
|
|
338
|
+
canonicalFrames.push(presentation);
|
|
339
|
+
const { clip, contentTranslateX, contentTranslateY, contentScale } =
|
|
340
|
+
presentation;
|
|
187
341
|
values.push(
|
|
188
|
-
|
|
342
|
+
offset,
|
|
189
343
|
clip.x,
|
|
190
344
|
clip.y,
|
|
191
345
|
clip.width,
|
|
192
346
|
clip.height,
|
|
193
|
-
clip.
|
|
347
|
+
clip.topLeftRadius,
|
|
348
|
+
clip.topRightRadius,
|
|
349
|
+
clip.bottomRightRadius,
|
|
350
|
+
clip.bottomLeftRadius,
|
|
351
|
+
curveCode(presentation),
|
|
194
352
|
contentTranslateX,
|
|
195
|
-
contentTranslateY
|
|
353
|
+
contentTranslateY,
|
|
354
|
+
contentScale
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
if (
|
|
358
|
+
animation.frames[0]?.offset !== 0 ||
|
|
359
|
+
previousOffset !== 1 ||
|
|
360
|
+
!clipPresentationEquals(
|
|
361
|
+
canonicalFrames[canonicalFrames.length - 1] ?? null,
|
|
362
|
+
target
|
|
363
|
+
)
|
|
364
|
+
) {
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
return { frames: values, canonicalFrames };
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function flattenV1Keyframes(
|
|
371
|
+
frames: readonly CanonicalSmoothClipPresentation[],
|
|
372
|
+
offsets: readonly number[]
|
|
373
|
+
): number[] {
|
|
374
|
+
'worklet';
|
|
375
|
+
const values: number[] = [];
|
|
376
|
+
for (let index = 0; index < frames.length; index += 1) {
|
|
377
|
+
const presentation = frames[index];
|
|
378
|
+
const offset = offsets[index];
|
|
379
|
+
if (presentation === undefined || offset === undefined) continue;
|
|
380
|
+
values.push(
|
|
381
|
+
offset,
|
|
382
|
+
presentation.clip.x,
|
|
383
|
+
presentation.clip.y,
|
|
384
|
+
presentation.clip.width,
|
|
385
|
+
presentation.clip.height,
|
|
386
|
+
presentation.clip.radius,
|
|
387
|
+
presentation.contentTranslateX,
|
|
388
|
+
presentation.contentTranslateY
|
|
196
389
|
);
|
|
197
390
|
}
|
|
198
391
|
return values;
|
|
@@ -209,11 +402,18 @@ export function useSmoothClipDriver(
|
|
|
209
402
|
initialValue: ClipGeometry | SmoothClipPresentation,
|
|
210
403
|
options: SmoothClipDriverOptions = {}
|
|
211
404
|
): SmoothClipDriver {
|
|
212
|
-
const
|
|
405
|
+
const requestedInitialPresentation =
|
|
213
406
|
'clip' in initialValue
|
|
214
407
|
? initialValue
|
|
215
408
|
: createClipPresentation(initialValue);
|
|
216
|
-
const
|
|
409
|
+
const initialPresentation = canonicalizeClipPresentation(
|
|
410
|
+
requestedInitialPresentation
|
|
411
|
+
);
|
|
412
|
+
if (initialPresentation === null) {
|
|
413
|
+
throw new Error('[SmoothClipView] Initial presentation must be finite.');
|
|
414
|
+
}
|
|
415
|
+
const presentation =
|
|
416
|
+
useSharedValue<SmoothClipPresentation>(initialPresentation);
|
|
217
417
|
const ownership = useSharedValue(INTERACTIVE);
|
|
218
418
|
const suppressDeliveries = useSharedValue(0);
|
|
219
419
|
const activeAnimationId = useSharedValue(0);
|
|
@@ -236,8 +436,12 @@ export function useSmoothClipDriver(
|
|
|
236
436
|
if (driverRef.current === null) {
|
|
237
437
|
const driverId = allocateDriverId();
|
|
238
438
|
const reduceMotion = reduceMotionCode(options.reduceMotion ?? 'system');
|
|
239
|
-
|
|
240
|
-
|
|
439
|
+
// Captured once, like reduceMotion. Only the per-frame setScalars stream
|
|
440
|
+
// consults it; set(), the fused animateTo `from` seed, and the effect's
|
|
441
|
+
// declarative deliveries always record, so every once-per-call channel
|
|
442
|
+
// keeps its pre-flag 'inherit' behavior.
|
|
443
|
+
const velocityTracking = options.velocityTracking === true;
|
|
444
|
+
const seedPresentation = (next: CanonicalSmoothClipPresentation) => {
|
|
241
445
|
'worklet';
|
|
242
446
|
if (clipPresentationEquals(presentation.value, next)) return;
|
|
243
447
|
suppressDeliveries.value += 1;
|
|
@@ -251,12 +455,16 @@ export function useSmoothClipDriver(
|
|
|
251
455
|
if (suppressDeliveries.value > 0) suppressDeliveries.value -= 1;
|
|
252
456
|
};
|
|
253
457
|
|
|
254
|
-
const beginOnUI = ():
|
|
458
|
+
const beginOnUI = (): CanonicalSmoothClipPresentation => {
|
|
255
459
|
'worklet';
|
|
256
|
-
|
|
460
|
+
const fallback =
|
|
461
|
+
canonicalizeClipPresentation(presentation.value) ?? initialPresentation;
|
|
462
|
+
if (disposed.value !== 0) return fallback;
|
|
257
463
|
const current = presentationFromNative(
|
|
258
|
-
|
|
259
|
-
|
|
464
|
+
PRESENTATION_PROTOCOL_VERSION === 2 && beginInteractionV2HostFunction
|
|
465
|
+
? beginInteractionV2HostFunction(driverId)
|
|
466
|
+
: beginInteractionHostFunction(driverId),
|
|
467
|
+
fallback
|
|
260
468
|
);
|
|
261
469
|
activeAnimationId.value = 0;
|
|
262
470
|
ownership.value = INTERACTIVE;
|
|
@@ -267,24 +475,55 @@ export function useSmoothClipDriver(
|
|
|
267
475
|
|
|
268
476
|
const setOnUI = (next: SmoothClipPresentation): void => {
|
|
269
477
|
'worklet';
|
|
270
|
-
|
|
478
|
+
const canonical = canonicalizeClipPresentation(next);
|
|
479
|
+
if (disposed.value !== 0 || canonical === null) return;
|
|
271
480
|
activeAnimationId.value = 0;
|
|
272
481
|
ownership.value = INTERACTIVE;
|
|
273
482
|
scalarsStale.value = 0;
|
|
274
|
-
const { clip, contentTranslateX, contentTranslateY } =
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
483
|
+
const { clip, contentTranslateX, contentTranslateY, contentScale } =
|
|
484
|
+
canonical;
|
|
485
|
+
// Always records: only the per-frame setScalars hot path is gated by
|
|
486
|
+
// velocityTracking. set() already pays full validation and a SharedValue
|
|
487
|
+
// seed per call, so the sample recording is noise here.
|
|
488
|
+
if (
|
|
489
|
+
PRESENTATION_PROTOCOL_VERSION === 2 &&
|
|
490
|
+
setPresentationV2HostFunction
|
|
491
|
+
) {
|
|
492
|
+
setPresentationV2HostFunction(
|
|
493
|
+
driverId,
|
|
494
|
+
clip.x,
|
|
495
|
+
clip.y,
|
|
496
|
+
clip.width,
|
|
497
|
+
clip.height,
|
|
498
|
+
clip.topLeftRadius,
|
|
499
|
+
clip.topRightRadius,
|
|
500
|
+
clip.bottomRightRadius,
|
|
501
|
+
clip.bottomLeftRadius,
|
|
502
|
+
curveCode(canonical),
|
|
503
|
+
contentTranslateX,
|
|
504
|
+
contentTranslateY,
|
|
505
|
+
contentScale,
|
|
506
|
+
true,
|
|
507
|
+
false,
|
|
508
|
+
true
|
|
509
|
+
);
|
|
510
|
+
} else if (isV1Compatible(canonical)) {
|
|
511
|
+
setPresentationHostFunction(
|
|
512
|
+
driverId,
|
|
513
|
+
clip.x,
|
|
514
|
+
clip.y,
|
|
515
|
+
clip.width,
|
|
516
|
+
clip.height,
|
|
517
|
+
clip.radius,
|
|
518
|
+
contentTranslateX,
|
|
519
|
+
contentTranslateY,
|
|
520
|
+
true,
|
|
521
|
+
false
|
|
522
|
+
);
|
|
523
|
+
} else {
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
seedPresentation(canonical);
|
|
288
527
|
};
|
|
289
528
|
|
|
290
529
|
const setScalarsOnUI = (
|
|
@@ -295,9 +534,24 @@ export function useSmoothClipDriver(
|
|
|
295
534
|
radius: number,
|
|
296
535
|
contentTranslateX: number,
|
|
297
536
|
contentTranslateY: number,
|
|
298
|
-
overridePendingAnimation = false
|
|
537
|
+
overridePendingAnimation = false,
|
|
538
|
+
forceRecordVelocity = false
|
|
299
539
|
): void => {
|
|
300
540
|
'worklet';
|
|
541
|
+
// Resolved in the body, not as a default-parameter initializer: the
|
|
542
|
+
// worklets Babel plugin only captures closure variables referenced
|
|
543
|
+
// inside the function body, so `= velocityTracking` up in the parameter
|
|
544
|
+
// list would throw "Property 'velocityTracking' doesn't exist" on the
|
|
545
|
+
// UI runtime.
|
|
546
|
+
const recordVelocity = forceRecordVelocity || velocityTracking;
|
|
547
|
+
// Number.isFinite (not a fused arithmetic gate): it also rejects
|
|
548
|
+
// non-number types, which arithmetic would coerce past the gate and
|
|
549
|
+
// into a UI-runtime throw from the binding's asNumber. The gate must
|
|
550
|
+
// stay on this side of the bridge: the native binding drops non-finite
|
|
551
|
+
// writes silently, and flipping the ownership SharedValues below for a
|
|
552
|
+
// write native never applied would let the deliver listener record
|
|
553
|
+
// values native has not observed — a later identical declarative write
|
|
554
|
+
// then dedupes against them and is swallowed for good.
|
|
301
555
|
if (
|
|
302
556
|
disposed.value !== 0 ||
|
|
303
557
|
!Number.isFinite(x) ||
|
|
@@ -323,7 +577,96 @@ export function useSmoothClipDriver(
|
|
|
323
577
|
contentTranslateX,
|
|
324
578
|
contentTranslateY,
|
|
325
579
|
true,
|
|
326
|
-
overridePendingAnimation
|
|
580
|
+
overridePendingAnimation,
|
|
581
|
+
recordVelocity
|
|
582
|
+
);
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
const setPresentationScalarsOnUI = (
|
|
586
|
+
x: number,
|
|
587
|
+
y: number,
|
|
588
|
+
width: number,
|
|
589
|
+
height: number,
|
|
590
|
+
topLeftRadius: number,
|
|
591
|
+
topRightRadius: number,
|
|
592
|
+
bottomRightRadius: number,
|
|
593
|
+
bottomLeftRadius: number,
|
|
594
|
+
nextCurveCode: number,
|
|
595
|
+
contentTranslateX: number,
|
|
596
|
+
contentTranslateY: number,
|
|
597
|
+
contentScale: number,
|
|
598
|
+
overridePendingAnimation = false,
|
|
599
|
+
forceRecordVelocity = false
|
|
600
|
+
): void => {
|
|
601
|
+
'worklet';
|
|
602
|
+
const values = [
|
|
603
|
+
x,
|
|
604
|
+
y,
|
|
605
|
+
width,
|
|
606
|
+
height,
|
|
607
|
+
topLeftRadius,
|
|
608
|
+
topRightRadius,
|
|
609
|
+
bottomRightRadius,
|
|
610
|
+
bottomLeftRadius,
|
|
611
|
+
nextCurveCode,
|
|
612
|
+
contentTranslateX,
|
|
613
|
+
contentTranslateY,
|
|
614
|
+
contentScale,
|
|
615
|
+
];
|
|
616
|
+
if (
|
|
617
|
+
disposed.value !== 0 ||
|
|
618
|
+
!values.every(Number.isFinite) ||
|
|
619
|
+
(nextCurveCode !== 0 && nextCurveCode !== 1) ||
|
|
620
|
+
contentScale <= 0
|
|
621
|
+
) {
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
const recordVelocity = forceRecordVelocity || velocityTracking;
|
|
625
|
+
if (
|
|
626
|
+
PRESENTATION_PROTOCOL_VERSION !== 2 ||
|
|
627
|
+
!setPresentationV2HostFunction
|
|
628
|
+
) {
|
|
629
|
+
if (
|
|
630
|
+
nextCurveCode === 0 &&
|
|
631
|
+
contentScale === 1 &&
|
|
632
|
+
topLeftRadius === topRightRadius &&
|
|
633
|
+
topLeftRadius === bottomRightRadius &&
|
|
634
|
+
topLeftRadius === bottomLeftRadius
|
|
635
|
+
) {
|
|
636
|
+
setScalarsOnUI(
|
|
637
|
+
x,
|
|
638
|
+
y,
|
|
639
|
+
width,
|
|
640
|
+
height,
|
|
641
|
+
topLeftRadius,
|
|
642
|
+
contentTranslateX,
|
|
643
|
+
contentTranslateY,
|
|
644
|
+
overridePendingAnimation,
|
|
645
|
+
forceRecordVelocity
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
if (activeAnimationId.value !== 0) activeAnimationId.value = 0;
|
|
651
|
+
if (ownership.value !== INTERACTIVE) ownership.value = INTERACTIVE;
|
|
652
|
+
if (scalarsStale.value === 0) scalarsStale.value = 1;
|
|
653
|
+
setPresentationV2HostFunction(
|
|
654
|
+
driverId,
|
|
655
|
+
x,
|
|
656
|
+
y,
|
|
657
|
+
width,
|
|
658
|
+
height,
|
|
659
|
+
topLeftRadius,
|
|
660
|
+
topRightRadius,
|
|
661
|
+
bottomRightRadius,
|
|
662
|
+
bottomLeftRadius,
|
|
663
|
+
nextCurveCode,
|
|
664
|
+
contentTranslateX,
|
|
665
|
+
contentTranslateY,
|
|
666
|
+
contentScale,
|
|
667
|
+
true,
|
|
668
|
+
overridePendingAnimation,
|
|
669
|
+
recordVelocity
|
|
327
670
|
);
|
|
328
671
|
};
|
|
329
672
|
|
|
@@ -336,9 +679,89 @@ export function useSmoothClipDriver(
|
|
|
336
679
|
// invalid stale call cannot mint a native rejection id/completion against
|
|
337
680
|
// a driver whose listener and registry entry are already gone.
|
|
338
681
|
if (disposed.value !== 0) return 0;
|
|
682
|
+
const canonicalTarget = canonicalizeClipPresentation(target);
|
|
683
|
+
const canonicalFrom =
|
|
684
|
+
animation.from === undefined
|
|
685
|
+
? undefined
|
|
686
|
+
: canonicalizeClipPresentation(animation.from);
|
|
687
|
+
const keyframeResult =
|
|
688
|
+
animation.type === 'keyframes'
|
|
689
|
+
? canonicalTarget === null
|
|
690
|
+
? null
|
|
691
|
+
: flattenFiniteKeyframes(canonicalTarget, animation)
|
|
692
|
+
: null;
|
|
693
|
+
if (
|
|
694
|
+
canonicalTarget === null ||
|
|
695
|
+
(animation.from !== undefined && canonicalFrom === null) ||
|
|
696
|
+
(animation.type === 'keyframes' && keyframeResult === null)
|
|
697
|
+
) {
|
|
698
|
+
return rejectAnimationHostFunction(driverId);
|
|
699
|
+
}
|
|
700
|
+
const resolvedFrom = canonicalFrom ?? undefined;
|
|
701
|
+
const v1Compatible =
|
|
702
|
+
isV1Compatible(canonicalTarget) &&
|
|
703
|
+
(resolvedFrom === undefined || isV1Compatible(resolvedFrom)) &&
|
|
704
|
+
(animation.type !== 'keyframes' ||
|
|
705
|
+
(keyframeResult?.canonicalFrames.every(isV1Compatible) ?? false));
|
|
706
|
+
// V1-representable plans deliberately stay on the original seven-scalar
|
|
707
|
+
// bridge even when V2 is installed. Besides preserving bridge shape,
|
|
708
|
+
// this retains V1's finite-value/clamping and host-normalization rules.
|
|
709
|
+
const useV2 = !v1Compatible;
|
|
710
|
+
if (!animationIsFinite(animation, useV2)) {
|
|
711
|
+
return rejectAnimationHostFunction(driverId);
|
|
712
|
+
}
|
|
713
|
+
const hasV2Protocol =
|
|
714
|
+
PRESENTATION_PROTOCOL_VERSION === 2 &&
|
|
715
|
+
snapshotCurrentV2HostFunction !== undefined &&
|
|
716
|
+
animateTimingV2HostFunction !== undefined &&
|
|
717
|
+
animateSpringV2HostFunction !== undefined &&
|
|
718
|
+
animateKeyframesV2HostFunction !== undefined;
|
|
719
|
+
const hasAtomicFromMethod =
|
|
720
|
+
resolvedFrom === undefined ||
|
|
721
|
+
(animation.type === 'timing'
|
|
722
|
+
? animateTimingFromV2HostFunction !== undefined
|
|
723
|
+
: animation.type === 'spring'
|
|
724
|
+
? animateSpringFromV2HostFunction !== undefined
|
|
725
|
+
: animateKeyframesFromV2HostFunction !== undefined);
|
|
726
|
+
// This is intentionally before sampling or any ownership/group/
|
|
727
|
+
// presentation mutation. New JS against old native must fail closed,
|
|
728
|
+
// and an explicit V2 start must never degrade to set()+animate().
|
|
729
|
+
if (useV2 && (!hasV2Protocol || !hasAtomicFromMethod)) {
|
|
730
|
+
return rejectAnimationHostFunction(driverId);
|
|
731
|
+
}
|
|
339
732
|
if (
|
|
340
|
-
|
|
341
|
-
|
|
733
|
+
useV2 &&
|
|
734
|
+
resolvedFrom !== undefined &&
|
|
735
|
+
animation.type === 'keyframes' &&
|
|
736
|
+
!clipPresentationEquals(
|
|
737
|
+
keyframeResult?.canonicalFrames[0] ?? null,
|
|
738
|
+
resolvedFrom
|
|
739
|
+
)
|
|
740
|
+
) {
|
|
741
|
+
return rejectAnimationHostFunction(driverId);
|
|
742
|
+
}
|
|
743
|
+
const currentPresentation =
|
|
744
|
+
canonicalizeClipPresentation(presentation.value) ?? initialPresentation;
|
|
745
|
+
const sampledStart =
|
|
746
|
+
resolvedFrom ??
|
|
747
|
+
(useV2
|
|
748
|
+
? presentationFromNative(
|
|
749
|
+
snapshotCurrentV2HostFunction!(driverId),
|
|
750
|
+
currentPresentation
|
|
751
|
+
)
|
|
752
|
+
: currentPresentation);
|
|
753
|
+
// setPresentationScalars intentionally leaves the public SharedValue
|
|
754
|
+
// stale to keep the hot path allocation-free. Validate an implicit
|
|
755
|
+
// retarget against the sampled native-visible start, not that stale JS
|
|
756
|
+
// value; otherwise a streamed curve transition cannot settle natively on
|
|
757
|
+
// its new (now stable) curve.
|
|
758
|
+
if (canonicalTarget.clip.curve !== sampledStart.clip.curve) {
|
|
759
|
+
return rejectAnimationHostFunction(driverId);
|
|
760
|
+
}
|
|
761
|
+
if (
|
|
762
|
+
useV2 &&
|
|
763
|
+
animation.type === 'spring' &&
|
|
764
|
+
!springScaleIsProvablyPositive(sampledStart, canonicalTarget, animation)
|
|
342
765
|
) {
|
|
343
766
|
return rejectAnimationHostFunction(driverId);
|
|
344
767
|
}
|
|
@@ -348,13 +771,18 @@ export function useSmoothClipDriver(
|
|
|
348
771
|
// rejectAnimation, which would mint an id and a completion for a driver
|
|
349
772
|
// whose JS state is already detached. This is the "unsupported dispatch"
|
|
350
773
|
// arm of the documented 0 contract.
|
|
351
|
-
const from =
|
|
352
|
-
if (from !== undefined) {
|
|
774
|
+
const from = resolvedFrom;
|
|
775
|
+
if (from !== undefined && !useV2) {
|
|
353
776
|
// Fused hot write: exactly setScalars(from…) issued immediately
|
|
354
777
|
// before the handoff, so native's latest value — the animation
|
|
355
778
|
// start below — is the caller's explicit presentation. Also
|
|
356
779
|
// re-grabs from a running animation, which the implicit
|
|
357
|
-
// interactive-start path would silently skip.
|
|
780
|
+
// interactive-start path would silently skip. Always records a
|
|
781
|
+
// velocity sample regardless of velocityTracking: this is one write
|
|
782
|
+
// per animateTo, not the per-frame stream the flag exists to keep
|
|
783
|
+
// cheap, and an unrecorded seed would invalidate the history a
|
|
784
|
+
// set()-driven drag just recorded (the tracker's coalesce and
|
|
785
|
+
// identical-re-record rules were designed for exactly this write).
|
|
358
786
|
setScalarsOnUI(
|
|
359
787
|
from.clip.x,
|
|
360
788
|
from.clip.y,
|
|
@@ -363,6 +791,7 @@ export function useSmoothClipDriver(
|
|
|
363
791
|
from.clip.radius,
|
|
364
792
|
from.contentTranslateX,
|
|
365
793
|
from.contentTranslateY,
|
|
794
|
+
true,
|
|
366
795
|
true
|
|
367
796
|
);
|
|
368
797
|
}
|
|
@@ -374,12 +803,18 @@ export function useSmoothClipDriver(
|
|
|
374
803
|
const scalarsWereStale = scalarsStale.value !== 0;
|
|
375
804
|
const hasInteractiveStart =
|
|
376
805
|
ownership.value === INTERACTIVE && !scalarsWereStale;
|
|
377
|
-
const start =
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
806
|
+
const start =
|
|
807
|
+
canonicalizeClipPresentation(presentation.value) ?? initialPresentation;
|
|
808
|
+
// V1 preserves the old optimistic SharedValue ownership transition.
|
|
809
|
+
// V2 commits it only after native has atomically accepted the plan.
|
|
810
|
+
if (!useV2) {
|
|
811
|
+
ownership.value = NATIVE;
|
|
812
|
+
presentation.value = canonicalTarget;
|
|
813
|
+
scalarsStale.value = 0;
|
|
814
|
+
}
|
|
815
|
+
const dispatchStart = useV2 && from !== undefined ? from : start;
|
|
816
|
+
const startClip = dispatchStart.clip;
|
|
817
|
+
const targetClip = canonicalTarget.clip;
|
|
383
818
|
// Reanimated stamps a parallel animation's t0 on this same runtime as
|
|
384
819
|
// `__frameTimestamp || _getAnimationTimestamp()` (valueSetter.ts).
|
|
385
820
|
// Capturing the identical value here and handing it to the integrator
|
|
@@ -390,19 +825,95 @@ export function useSmoothClipDriver(
|
|
|
390
825
|
// its rAF flush where __frameTimestamp is cleared, keeps the wall
|
|
391
826
|
// clock — a lead lasting the whole animation. NaN when the globals are
|
|
392
827
|
// absent (tests, non-worklets runtimes): native then falls back to its
|
|
393
|
-
// own clock plus the min() anchor, the pre-stamp behavior.
|
|
828
|
+
// own clock plus the min() anchor, the pre-stamp behavior. On iOS the
|
|
829
|
+
// stamp is always NaN: Core Animation anchors at commit time and the
|
|
830
|
+
// TurboModule discards the argument, so the capture would only cost.
|
|
394
831
|
const workletGlobal = globalThis as {
|
|
395
832
|
__frameTimestamp?: number;
|
|
396
833
|
_getAnimationTimestamp?: () => number;
|
|
397
834
|
};
|
|
398
|
-
const startedAtMs =
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
835
|
+
const startedAtMs = !NEEDS_START_STAMP
|
|
836
|
+
? Number.NaN
|
|
837
|
+
: workletGlobal.__frameTimestamp ||
|
|
838
|
+
(typeof workletGlobal._getAnimationTimestamp === 'function'
|
|
839
|
+
? workletGlobal._getAnimationTimestamp()
|
|
840
|
+
: Number.NaN);
|
|
403
841
|
let animationId: number;
|
|
404
842
|
|
|
405
|
-
if (animation.type === 'timing') {
|
|
843
|
+
if (animation.type === 'timing' && useV2) {
|
|
844
|
+
const [x1, y1, x2, y2] = animation.controlPoints;
|
|
845
|
+
animationId =
|
|
846
|
+
from !== undefined
|
|
847
|
+
? animateTimingFromV2HostFunction!(
|
|
848
|
+
driverId,
|
|
849
|
+
startClip.x,
|
|
850
|
+
startClip.y,
|
|
851
|
+
startClip.width,
|
|
852
|
+
startClip.height,
|
|
853
|
+
startClip.topLeftRadius,
|
|
854
|
+
startClip.topRightRadius,
|
|
855
|
+
startClip.bottomRightRadius,
|
|
856
|
+
startClip.bottomLeftRadius,
|
|
857
|
+
curveCode(dispatchStart),
|
|
858
|
+
dispatchStart.contentTranslateX,
|
|
859
|
+
dispatchStart.contentTranslateY,
|
|
860
|
+
dispatchStart.contentScale,
|
|
861
|
+
targetClip.x,
|
|
862
|
+
targetClip.y,
|
|
863
|
+
targetClip.width,
|
|
864
|
+
targetClip.height,
|
|
865
|
+
targetClip.topLeftRadius,
|
|
866
|
+
targetClip.topRightRadius,
|
|
867
|
+
targetClip.bottomRightRadius,
|
|
868
|
+
targetClip.bottomLeftRadius,
|
|
869
|
+
curveCode(canonicalTarget),
|
|
870
|
+
canonicalTarget.contentTranslateX,
|
|
871
|
+
canonicalTarget.contentTranslateY,
|
|
872
|
+
canonicalTarget.contentScale,
|
|
873
|
+
animation.duration,
|
|
874
|
+
x1,
|
|
875
|
+
y1,
|
|
876
|
+
x2,
|
|
877
|
+
y2,
|
|
878
|
+
reduceMotion,
|
|
879
|
+
startedAtMs
|
|
880
|
+
)
|
|
881
|
+
: animateTimingV2HostFunction!(
|
|
882
|
+
driverId,
|
|
883
|
+
hasInteractiveStart,
|
|
884
|
+
startClip.x,
|
|
885
|
+
startClip.y,
|
|
886
|
+
startClip.width,
|
|
887
|
+
startClip.height,
|
|
888
|
+
startClip.topLeftRadius,
|
|
889
|
+
startClip.topRightRadius,
|
|
890
|
+
startClip.bottomRightRadius,
|
|
891
|
+
startClip.bottomLeftRadius,
|
|
892
|
+
curveCode(dispatchStart),
|
|
893
|
+
dispatchStart.contentTranslateX,
|
|
894
|
+
dispatchStart.contentTranslateY,
|
|
895
|
+
dispatchStart.contentScale,
|
|
896
|
+
targetClip.x,
|
|
897
|
+
targetClip.y,
|
|
898
|
+
targetClip.width,
|
|
899
|
+
targetClip.height,
|
|
900
|
+
targetClip.topLeftRadius,
|
|
901
|
+
targetClip.topRightRadius,
|
|
902
|
+
targetClip.bottomRightRadius,
|
|
903
|
+
targetClip.bottomLeftRadius,
|
|
904
|
+
curveCode(canonicalTarget),
|
|
905
|
+
canonicalTarget.contentTranslateX,
|
|
906
|
+
canonicalTarget.contentTranslateY,
|
|
907
|
+
canonicalTarget.contentScale,
|
|
908
|
+
animation.duration,
|
|
909
|
+
x1,
|
|
910
|
+
y1,
|
|
911
|
+
x2,
|
|
912
|
+
y2,
|
|
913
|
+
reduceMotion,
|
|
914
|
+
startedAtMs
|
|
915
|
+
);
|
|
916
|
+
} else if (animation.type === 'timing') {
|
|
406
917
|
const [x1, y1, x2, y2] = animation.controlPoints;
|
|
407
918
|
animationId = animateTimingHostFunction(
|
|
408
919
|
driverId,
|
|
@@ -419,8 +930,8 @@ export function useSmoothClipDriver(
|
|
|
419
930
|
targetClip.width,
|
|
420
931
|
targetClip.height,
|
|
421
932
|
targetClip.radius,
|
|
422
|
-
|
|
423
|
-
|
|
933
|
+
canonicalTarget.contentTranslateX,
|
|
934
|
+
canonicalTarget.contentTranslateY,
|
|
424
935
|
Math.max(0, animation.duration),
|
|
425
936
|
x1,
|
|
426
937
|
y1,
|
|
@@ -429,6 +940,87 @@ export function useSmoothClipDriver(
|
|
|
429
940
|
reduceMotion,
|
|
430
941
|
startedAtMs
|
|
431
942
|
);
|
|
943
|
+
} else if (animation.type === 'spring' && useV2) {
|
|
944
|
+
const inheritVelocity =
|
|
945
|
+
animation.initialVelocity === undefined ||
|
|
946
|
+
animation.initialVelocity === 'inherit';
|
|
947
|
+
const mass = animation.mass ?? 1;
|
|
948
|
+
const stiffness = animation.stiffness ?? 100;
|
|
949
|
+
const damping = animation.damping ?? 10;
|
|
950
|
+
const initialVelocity = inheritVelocity
|
|
951
|
+
? 0
|
|
952
|
+
: (animation.initialVelocity as number);
|
|
953
|
+
animationId =
|
|
954
|
+
from !== undefined
|
|
955
|
+
? animateSpringFromV2HostFunction!(
|
|
956
|
+
driverId,
|
|
957
|
+
startClip.x,
|
|
958
|
+
startClip.y,
|
|
959
|
+
startClip.width,
|
|
960
|
+
startClip.height,
|
|
961
|
+
startClip.topLeftRadius,
|
|
962
|
+
startClip.topRightRadius,
|
|
963
|
+
startClip.bottomRightRadius,
|
|
964
|
+
startClip.bottomLeftRadius,
|
|
965
|
+
curveCode(dispatchStart),
|
|
966
|
+
dispatchStart.contentTranslateX,
|
|
967
|
+
dispatchStart.contentTranslateY,
|
|
968
|
+
dispatchStart.contentScale,
|
|
969
|
+
targetClip.x,
|
|
970
|
+
targetClip.y,
|
|
971
|
+
targetClip.width,
|
|
972
|
+
targetClip.height,
|
|
973
|
+
targetClip.topLeftRadius,
|
|
974
|
+
targetClip.topRightRadius,
|
|
975
|
+
targetClip.bottomRightRadius,
|
|
976
|
+
targetClip.bottomLeftRadius,
|
|
977
|
+
curveCode(canonicalTarget),
|
|
978
|
+
canonicalTarget.contentTranslateX,
|
|
979
|
+
canonicalTarget.contentTranslateY,
|
|
980
|
+
canonicalTarget.contentScale,
|
|
981
|
+
mass,
|
|
982
|
+
stiffness,
|
|
983
|
+
damping,
|
|
984
|
+
initialVelocity,
|
|
985
|
+
inheritVelocity,
|
|
986
|
+
reduceMotion,
|
|
987
|
+
startedAtMs
|
|
988
|
+
)
|
|
989
|
+
: animateSpringV2HostFunction!(
|
|
990
|
+
driverId,
|
|
991
|
+
hasInteractiveStart,
|
|
992
|
+
startClip.x,
|
|
993
|
+
startClip.y,
|
|
994
|
+
startClip.width,
|
|
995
|
+
startClip.height,
|
|
996
|
+
startClip.topLeftRadius,
|
|
997
|
+
startClip.topRightRadius,
|
|
998
|
+
startClip.bottomRightRadius,
|
|
999
|
+
startClip.bottomLeftRadius,
|
|
1000
|
+
curveCode(dispatchStart),
|
|
1001
|
+
dispatchStart.contentTranslateX,
|
|
1002
|
+
dispatchStart.contentTranslateY,
|
|
1003
|
+
dispatchStart.contentScale,
|
|
1004
|
+
targetClip.x,
|
|
1005
|
+
targetClip.y,
|
|
1006
|
+
targetClip.width,
|
|
1007
|
+
targetClip.height,
|
|
1008
|
+
targetClip.topLeftRadius,
|
|
1009
|
+
targetClip.topRightRadius,
|
|
1010
|
+
targetClip.bottomRightRadius,
|
|
1011
|
+
targetClip.bottomLeftRadius,
|
|
1012
|
+
curveCode(canonicalTarget),
|
|
1013
|
+
canonicalTarget.contentTranslateX,
|
|
1014
|
+
canonicalTarget.contentTranslateY,
|
|
1015
|
+
canonicalTarget.contentScale,
|
|
1016
|
+
mass,
|
|
1017
|
+
stiffness,
|
|
1018
|
+
damping,
|
|
1019
|
+
initialVelocity,
|
|
1020
|
+
inheritVelocity,
|
|
1021
|
+
reduceMotion,
|
|
1022
|
+
startedAtMs
|
|
1023
|
+
);
|
|
432
1024
|
} else if (animation.type === 'spring') {
|
|
433
1025
|
const inheritVelocity =
|
|
434
1026
|
animation.initialVelocity === undefined ||
|
|
@@ -448,8 +1040,8 @@ export function useSmoothClipDriver(
|
|
|
448
1040
|
targetClip.width,
|
|
449
1041
|
targetClip.height,
|
|
450
1042
|
targetClip.radius,
|
|
451
|
-
|
|
452
|
-
|
|
1043
|
+
canonicalTarget.contentTranslateX,
|
|
1044
|
+
canonicalTarget.contentTranslateY,
|
|
453
1045
|
Math.max(0.0001, animation.mass ?? 1),
|
|
454
1046
|
Math.max(0.0001, animation.stiffness ?? 100),
|
|
455
1047
|
Math.max(0, animation.damping ?? 10),
|
|
@@ -458,6 +1050,74 @@ export function useSmoothClipDriver(
|
|
|
458
1050
|
reduceMotion,
|
|
459
1051
|
startedAtMs
|
|
460
1052
|
);
|
|
1053
|
+
} else if (useV2) {
|
|
1054
|
+
const frames = (keyframeResult as NonNullable<typeof keyframeResult>)
|
|
1055
|
+
.frames;
|
|
1056
|
+
animationId =
|
|
1057
|
+
from !== undefined
|
|
1058
|
+
? animateKeyframesFromV2HostFunction!(
|
|
1059
|
+
driverId,
|
|
1060
|
+
startClip.x,
|
|
1061
|
+
startClip.y,
|
|
1062
|
+
startClip.width,
|
|
1063
|
+
startClip.height,
|
|
1064
|
+
startClip.topLeftRadius,
|
|
1065
|
+
startClip.topRightRadius,
|
|
1066
|
+
startClip.bottomRightRadius,
|
|
1067
|
+
startClip.bottomLeftRadius,
|
|
1068
|
+
curveCode(dispatchStart),
|
|
1069
|
+
dispatchStart.contentTranslateX,
|
|
1070
|
+
dispatchStart.contentTranslateY,
|
|
1071
|
+
dispatchStart.contentScale,
|
|
1072
|
+
targetClip.x,
|
|
1073
|
+
targetClip.y,
|
|
1074
|
+
targetClip.width,
|
|
1075
|
+
targetClip.height,
|
|
1076
|
+
targetClip.topLeftRadius,
|
|
1077
|
+
targetClip.topRightRadius,
|
|
1078
|
+
targetClip.bottomRightRadius,
|
|
1079
|
+
targetClip.bottomLeftRadius,
|
|
1080
|
+
curveCode(canonicalTarget),
|
|
1081
|
+
canonicalTarget.contentTranslateX,
|
|
1082
|
+
canonicalTarget.contentTranslateY,
|
|
1083
|
+
canonicalTarget.contentScale,
|
|
1084
|
+
animation.duration,
|
|
1085
|
+
frames,
|
|
1086
|
+
reduceMotion,
|
|
1087
|
+
startedAtMs
|
|
1088
|
+
)
|
|
1089
|
+
: animateKeyframesV2HostFunction!(
|
|
1090
|
+
driverId,
|
|
1091
|
+
hasInteractiveStart,
|
|
1092
|
+
startClip.x,
|
|
1093
|
+
startClip.y,
|
|
1094
|
+
startClip.width,
|
|
1095
|
+
startClip.height,
|
|
1096
|
+
startClip.topLeftRadius,
|
|
1097
|
+
startClip.topRightRadius,
|
|
1098
|
+
startClip.bottomRightRadius,
|
|
1099
|
+
startClip.bottomLeftRadius,
|
|
1100
|
+
curveCode(dispatchStart),
|
|
1101
|
+
dispatchStart.contentTranslateX,
|
|
1102
|
+
dispatchStart.contentTranslateY,
|
|
1103
|
+
dispatchStart.contentScale,
|
|
1104
|
+
targetClip.x,
|
|
1105
|
+
targetClip.y,
|
|
1106
|
+
targetClip.width,
|
|
1107
|
+
targetClip.height,
|
|
1108
|
+
targetClip.topLeftRadius,
|
|
1109
|
+
targetClip.topRightRadius,
|
|
1110
|
+
targetClip.bottomRightRadius,
|
|
1111
|
+
targetClip.bottomLeftRadius,
|
|
1112
|
+
curveCode(canonicalTarget),
|
|
1113
|
+
canonicalTarget.contentTranslateX,
|
|
1114
|
+
canonicalTarget.contentTranslateY,
|
|
1115
|
+
canonicalTarget.contentScale,
|
|
1116
|
+
animation.duration,
|
|
1117
|
+
frames,
|
|
1118
|
+
reduceMotion,
|
|
1119
|
+
startedAtMs
|
|
1120
|
+
);
|
|
461
1121
|
} else {
|
|
462
1122
|
animationId = animateKeyframesHostFunction(
|
|
463
1123
|
driverId,
|
|
@@ -474,10 +1134,16 @@ export function useSmoothClipDriver(
|
|
|
474
1134
|
targetClip.width,
|
|
475
1135
|
targetClip.height,
|
|
476
1136
|
targetClip.radius,
|
|
477
|
-
|
|
478
|
-
|
|
1137
|
+
canonicalTarget.contentTranslateX,
|
|
1138
|
+
canonicalTarget.contentTranslateY,
|
|
479
1139
|
Math.max(0, animation.duration),
|
|
480
|
-
|
|
1140
|
+
// Non-null here: the validation gate above rejected null before any
|
|
1141
|
+
// side effect.
|
|
1142
|
+
flattenV1Keyframes(
|
|
1143
|
+
(keyframeResult as NonNullable<typeof keyframeResult>)
|
|
1144
|
+
.canonicalFrames,
|
|
1145
|
+
animation.frames.map((frame) => frame.offset)
|
|
1146
|
+
),
|
|
481
1147
|
reduceMotion,
|
|
482
1148
|
startedAtMs
|
|
483
1149
|
);
|
|
@@ -491,12 +1157,19 @@ export function useSmoothClipDriver(
|
|
|
491
1157
|
// effect, with a fresh id and one finished:false completion once the
|
|
492
1158
|
// native entry exists — in the pre-seed window rejectAnimation has no
|
|
493
1159
|
// entry and returns the bare 0 sentinel with no completion instead.
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
1160
|
+
if (!useV2) {
|
|
1161
|
+
ownership.value = INTERACTIVE;
|
|
1162
|
+
presentation.value = start;
|
|
1163
|
+
if (scalarsWereStale) scalarsStale.value = 1;
|
|
1164
|
+
activeAnimationId.value = 0;
|
|
1165
|
+
}
|
|
498
1166
|
return rejectAnimationHostFunction(driverId);
|
|
499
1167
|
}
|
|
1168
|
+
if (useV2) {
|
|
1169
|
+
ownership.value = NATIVE;
|
|
1170
|
+
presentation.value = canonicalTarget;
|
|
1171
|
+
scalarsStale.value = 0;
|
|
1172
|
+
}
|
|
500
1173
|
activeAnimationId.value = animationId;
|
|
501
1174
|
return animationId;
|
|
502
1175
|
};
|
|
@@ -504,19 +1177,28 @@ export function useSmoothClipDriver(
|
|
|
504
1177
|
const cancelOnUI = (
|
|
505
1178
|
animationId = 0,
|
|
506
1179
|
behavior: 'current' | 'target' = 'current'
|
|
507
|
-
):
|
|
1180
|
+
): CanonicalSmoothClipPresentation => {
|
|
508
1181
|
'worklet';
|
|
509
1182
|
// Past the hook's cleanup the native entry is gone; the call would fail
|
|
510
1183
|
// defined (handled=false) but it is the last driver.ui path that still
|
|
511
1184
|
// crosses into native after the tombstone. Guard it like the rest.
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
1185
|
+
const fallback =
|
|
1186
|
+
canonicalizeClipPresentation(presentation.value) ?? initialPresentation;
|
|
1187
|
+
if (disposed.value !== 0) return fallback;
|
|
1188
|
+
const values =
|
|
1189
|
+
PRESENTATION_PROTOCOL_VERSION === 2 && cancelAnimationV2HostFunction
|
|
1190
|
+
? cancelAnimationV2HostFunction(
|
|
1191
|
+
driverId,
|
|
1192
|
+
animationId,
|
|
1193
|
+
behavior === 'target' ? 1 : 0
|
|
1194
|
+
)
|
|
1195
|
+
: cancelAnimationHostFunction(
|
|
1196
|
+
driverId,
|
|
1197
|
+
animationId,
|
|
1198
|
+
behavior === 'target' ? 1 : 0
|
|
1199
|
+
);
|
|
1200
|
+
if (values.length < 8 || values[0] !== 1) return fallback;
|
|
1201
|
+
const current = presentationFromNative(values, fallback, 1);
|
|
520
1202
|
activeAnimationId.value = 0;
|
|
521
1203
|
ownership.value = INTERACTIVE;
|
|
522
1204
|
scalarsStale.value = 0;
|
|
@@ -559,6 +1241,37 @@ export function useSmoothClipDriver(
|
|
|
559
1241
|
contentTranslateY
|
|
560
1242
|
);
|
|
561
1243
|
},
|
|
1244
|
+
setPresentationScalars(
|
|
1245
|
+
x,
|
|
1246
|
+
y,
|
|
1247
|
+
width,
|
|
1248
|
+
height,
|
|
1249
|
+
topLeftRadius,
|
|
1250
|
+
topRightRadius,
|
|
1251
|
+
bottomRightRadius,
|
|
1252
|
+
bottomLeftRadius,
|
|
1253
|
+
nextCurveCode,
|
|
1254
|
+
contentTranslateX,
|
|
1255
|
+
contentTranslateY,
|
|
1256
|
+
contentScale
|
|
1257
|
+
) {
|
|
1258
|
+
'worklet';
|
|
1259
|
+
if (isRNRuntime()) return uiOnly();
|
|
1260
|
+
setPresentationScalarsOnUI(
|
|
1261
|
+
x,
|
|
1262
|
+
y,
|
|
1263
|
+
width,
|
|
1264
|
+
height,
|
|
1265
|
+
topLeftRadius,
|
|
1266
|
+
topRightRadius,
|
|
1267
|
+
bottomRightRadius,
|
|
1268
|
+
bottomLeftRadius,
|
|
1269
|
+
nextCurveCode,
|
|
1270
|
+
contentTranslateX,
|
|
1271
|
+
contentTranslateY,
|
|
1272
|
+
contentScale
|
|
1273
|
+
);
|
|
1274
|
+
},
|
|
562
1275
|
animateTo(target, animation) {
|
|
563
1276
|
'worklet';
|
|
564
1277
|
if (isRNRuntime()) return uiOnly();
|
|
@@ -573,7 +1286,7 @@ export function useSmoothClipDriver(
|
|
|
573
1286
|
react: {
|
|
574
1287
|
beginInteraction() {
|
|
575
1288
|
const { requestId, promise } =
|
|
576
|
-
createReactRequest<
|
|
1289
|
+
createReactRequest<CanonicalSmoothClipPresentation>(driverId);
|
|
577
1290
|
scheduleOnUI(() => {
|
|
578
1291
|
'worklet';
|
|
579
1292
|
const result = beginOnUI();
|
|
@@ -620,7 +1333,7 @@ export function useSmoothClipDriver(
|
|
|
620
1333
|
},
|
|
621
1334
|
cancel(animationId = 0, behavior = 'current') {
|
|
622
1335
|
const { requestId, promise } =
|
|
623
|
-
createReactRequest<
|
|
1336
|
+
createReactRequest<CanonicalSmoothClipPresentation>(driverId, true);
|
|
624
1337
|
scheduleOnUI(() => {
|
|
625
1338
|
'worklet';
|
|
626
1339
|
const result = cancelOnUI(animationId, behavior);
|
|
@@ -635,6 +1348,13 @@ export function useSmoothClipDriver(
|
|
|
635
1348
|
return promise;
|
|
636
1349
|
},
|
|
637
1350
|
},
|
|
1351
|
+
__smoothClipHandle: {
|
|
1352
|
+
driverId,
|
|
1353
|
+
presentation,
|
|
1354
|
+
ownership,
|
|
1355
|
+
activeAnimationId,
|
|
1356
|
+
disposed,
|
|
1357
|
+
},
|
|
638
1358
|
};
|
|
639
1359
|
|
|
640
1360
|
setDriverState(
|
|
@@ -686,7 +1406,9 @@ export function useSmoothClipDriver(
|
|
|
686
1406
|
active: SharedValue<number>,
|
|
687
1407
|
stale: SharedValue<number>,
|
|
688
1408
|
gone: SharedValue<number>,
|
|
689
|
-
setter: typeof setPresentationHostFunction
|
|
1409
|
+
setter: typeof setPresentationHostFunction,
|
|
1410
|
+
setterV2: typeof setPresentationV2HostFunction,
|
|
1411
|
+
protocolVersion: number
|
|
690
1412
|
) => {
|
|
691
1413
|
'worklet';
|
|
692
1414
|
// First, before the listener or the seed: this effect run owns the
|
|
@@ -695,10 +1417,8 @@ export function useSmoothClipDriver(
|
|
|
695
1417
|
gone.value = 0;
|
|
696
1418
|
let last: SmoothClipPresentation | null = null;
|
|
697
1419
|
const deliver = (next: SmoothClipPresentation) => {
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
clipPresentationEquals(last, next)
|
|
701
|
-
) {
|
|
1420
|
+
const canonical = canonicalizeClipPresentation(next);
|
|
1421
|
+
if (canonical === null || clipPresentationEquals(last, canonical)) {
|
|
702
1422
|
return;
|
|
703
1423
|
}
|
|
704
1424
|
// Record `last` only for values native has actually observed:
|
|
@@ -707,24 +1427,51 @@ export function useSmoothClipDriver(
|
|
|
707
1427
|
if (owner.value !== INTERACTIVE) return;
|
|
708
1428
|
if (suppressed.value > 0) {
|
|
709
1429
|
suppressed.value -= 1;
|
|
710
|
-
last =
|
|
1430
|
+
last = canonical;
|
|
1431
|
+
return;
|
|
1432
|
+
}
|
|
1433
|
+
if (protocolVersion !== 2 && !isV1Compatible(canonical)) {
|
|
711
1434
|
return;
|
|
712
1435
|
}
|
|
713
|
-
last =
|
|
1436
|
+
last = canonical;
|
|
714
1437
|
if (stale.value !== 0) stale.value = 0;
|
|
715
|
-
const { clip, contentTranslateX, contentTranslateY } =
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
1438
|
+
const { clip, contentTranslateX, contentTranslateY, contentScale } =
|
|
1439
|
+
canonical;
|
|
1440
|
+
// Declarative deliveries always record velocity samples — only the
|
|
1441
|
+
// setScalars hot path is gated by the velocityTracking option.
|
|
1442
|
+
if (protocolVersion === 2 && setterV2) {
|
|
1443
|
+
setterV2(
|
|
1444
|
+
nativeDriverId,
|
|
1445
|
+
clip.x,
|
|
1446
|
+
clip.y,
|
|
1447
|
+
clip.width,
|
|
1448
|
+
clip.height,
|
|
1449
|
+
clip.topLeftRadius,
|
|
1450
|
+
clip.topRightRadius,
|
|
1451
|
+
clip.bottomRightRadius,
|
|
1452
|
+
clip.bottomLeftRadius,
|
|
1453
|
+
curveCode(canonical),
|
|
1454
|
+
contentTranslateX,
|
|
1455
|
+
contentTranslateY,
|
|
1456
|
+
contentScale,
|
|
1457
|
+
false,
|
|
1458
|
+
false,
|
|
1459
|
+
true
|
|
1460
|
+
);
|
|
1461
|
+
} else {
|
|
1462
|
+
setter(
|
|
1463
|
+
nativeDriverId,
|
|
1464
|
+
clip.x,
|
|
1465
|
+
clip.y,
|
|
1466
|
+
clip.width,
|
|
1467
|
+
clip.height,
|
|
1468
|
+
clip.radius,
|
|
1469
|
+
contentTranslateX,
|
|
1470
|
+
contentTranslateY,
|
|
1471
|
+
false,
|
|
1472
|
+
false
|
|
1473
|
+
);
|
|
1474
|
+
}
|
|
728
1475
|
};
|
|
729
1476
|
source.addListener(listenerId, deliver);
|
|
730
1477
|
// Authoritative take-ownership seed. Creates the native entry before
|
|
@@ -733,24 +1480,45 @@ export function useSmoothClipDriver(
|
|
|
733
1480
|
// An animation worklet can win the scheduling race and create a latch
|
|
734
1481
|
// first; Native ownership then marks that intent as newer, so this
|
|
735
1482
|
// passive seed must not clear its active id or replay the target value.
|
|
736
|
-
const current = source.value;
|
|
737
|
-
if (owner.value === INTERACTIVE &&
|
|
1483
|
+
const current = canonicalizeClipPresentation(source.value);
|
|
1484
|
+
if (owner.value === INTERACTIVE && current !== null) {
|
|
738
1485
|
active.value = 0;
|
|
739
1486
|
owner.value = INTERACTIVE;
|
|
740
1487
|
stale.value = 0;
|
|
741
1488
|
last = current;
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
1489
|
+
if (protocolVersion === 2 && setterV2) {
|
|
1490
|
+
setterV2(
|
|
1491
|
+
nativeDriverId,
|
|
1492
|
+
current.clip.x,
|
|
1493
|
+
current.clip.y,
|
|
1494
|
+
current.clip.width,
|
|
1495
|
+
current.clip.height,
|
|
1496
|
+
current.clip.topLeftRadius,
|
|
1497
|
+
current.clip.topRightRadius,
|
|
1498
|
+
current.clip.bottomRightRadius,
|
|
1499
|
+
current.clip.bottomLeftRadius,
|
|
1500
|
+
curveCode(current),
|
|
1501
|
+
current.contentTranslateX,
|
|
1502
|
+
current.contentTranslateY,
|
|
1503
|
+
current.contentScale,
|
|
1504
|
+
true,
|
|
1505
|
+
false,
|
|
1506
|
+
true
|
|
1507
|
+
);
|
|
1508
|
+
} else if (isV1Compatible(current)) {
|
|
1509
|
+
setter(
|
|
1510
|
+
nativeDriverId,
|
|
1511
|
+
current.clip.x,
|
|
1512
|
+
current.clip.y,
|
|
1513
|
+
current.clip.width,
|
|
1514
|
+
current.clip.height,
|
|
1515
|
+
current.clip.radius,
|
|
1516
|
+
current.contentTranslateX,
|
|
1517
|
+
current.contentTranslateY,
|
|
1518
|
+
true,
|
|
1519
|
+
false
|
|
1520
|
+
);
|
|
1521
|
+
}
|
|
754
1522
|
}
|
|
755
1523
|
},
|
|
756
1524
|
presentation,
|
|
@@ -761,7 +1529,9 @@ export function useSmoothClipDriver(
|
|
|
761
1529
|
activeAnimationId,
|
|
762
1530
|
scalarsStale,
|
|
763
1531
|
disposed,
|
|
764
|
-
setPresentationHostFunction
|
|
1532
|
+
setPresentationHostFunction,
|
|
1533
|
+
setPresentationV2HostFunction,
|
|
1534
|
+
PRESENTATION_PROTOCOL_VERSION
|
|
765
1535
|
);
|
|
766
1536
|
|
|
767
1537
|
return () => {
|