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
|
@@ -1,20 +1,37 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
// The shared
|
|
3
|
+
// The shared native driver: iOS and Android both execute this file, via the
|
|
4
4
|
// one-line re-exports in drivers.ios.ts / drivers.android.ts that Metro's
|
|
5
5
|
// platform resolution picks up from index.ts's `./drivers` import. Only the
|
|
6
|
-
// `./smoothClipNative` import below is platform-split.
|
|
7
|
-
// suffix) is the web/Reanimated fallback, not this driver's sibling.
|
|
6
|
+
// `./smoothClipNative` import below is platform-split.
|
|
8
7
|
import { useEffect, useRef } from 'react';
|
|
8
|
+
import { Platform } from 'react-native';
|
|
9
9
|
import { useSharedValue } from 'react-native-reanimated';
|
|
10
10
|
import { isRNRuntime, scheduleOnRN, scheduleOnUI } from 'react-native-worklets';
|
|
11
11
|
import NativeSmoothClipModule from './smoothClipNative';
|
|
12
|
+
import { getSmoothClipCapabilities } from './capabilities';
|
|
12
13
|
import { allocateDriverId, attachDriverState, createDriverState, detachDriverState, getDriverState, setDriverState } from "./driverState.js";
|
|
13
14
|
import { completeNativeAnimation, synchronizeNativeCompletion } from "./nativeCompletion.js";
|
|
14
|
-
import { clipPresentationEquals, createClipPresentation, isFiniteClipPresentation } from "./geometry.js";
|
|
15
|
+
import { canonicalizeClipPresentation, clipPresentationEquals, createClipPresentation, isFiniteClipPresentation } from "./geometry.js";
|
|
15
16
|
import { createReactRequest, rejectDriverRequests, resolveReactRequest } from "./reactRequests.js";
|
|
16
17
|
const INTERACTIVE = 0;
|
|
17
18
|
const NATIVE = 1;
|
|
19
|
+
|
|
20
|
+
// Only Android consumes the Reanimated start stamp (its frame-clock anchor —
|
|
21
|
+
// docs/android-frame-clock-anchor.md). iOS's TurboModule reads its declared
|
|
22
|
+
// parameters positionally and drops the trailing argument, so capturing the
|
|
23
|
+
// stamp there is a wasted native _getAnimationTimestamp() call per animateTo.
|
|
24
|
+
// Resolved once at module scope; the worklet captures the primitive.
|
|
25
|
+
const NEEDS_START_STAMP = Platform.OS === 'android';
|
|
26
|
+
const PRESENTATION_PROTOCOL_VERSION = getSmoothClipCapabilities().presentationProtocolVersion;
|
|
27
|
+
const NativeSmoothClipModuleV2 = NativeSmoothClipModule;
|
|
28
|
+
|
|
29
|
+
// Widened like the animate* functions below: one trailing argument beyond the
|
|
30
|
+
// TurboModule spec — whether this write should record an `'inherit'` velocity
|
|
31
|
+
// sample. Android's JSI bindings read it (absent means record, the pre-flag
|
|
32
|
+
// behavior); the iOS TurboModule reads its declared parameters positionally
|
|
33
|
+
// and drops it, so iOS always records.
|
|
34
|
+
|
|
18
35
|
const setPresentationHostFunction = NativeSmoothClipModule.setClipPresentation;
|
|
19
36
|
const beginInteractionHostFunction = NativeSmoothClipModule.beginInteraction;
|
|
20
37
|
|
|
@@ -31,15 +48,27 @@ const animateKeyframesHostFunction = NativeSmoothClipModule.animateKeyframes;
|
|
|
31
48
|
const rejectAnimationHostFunction = NativeSmoothClipModule.rejectAnimation;
|
|
32
49
|
const cancelAnimationHostFunction = NativeSmoothClipModule.cancelAnimation;
|
|
33
50
|
const destroyDriverHostFunction = NativeSmoothClipModule.destroyDriver;
|
|
51
|
+
const setPresentationV2HostFunction = NativeSmoothClipModuleV2.setClipPresentationV2;
|
|
52
|
+
const beginInteractionV2HostFunction = NativeSmoothClipModuleV2.beginInteractionV2;
|
|
53
|
+
const snapshotCurrentV2HostFunction = NativeSmoothClipModuleV2.snapshotCurrentV2;
|
|
54
|
+
const animateTimingV2HostFunction = NativeSmoothClipModuleV2.animateTimingV2;
|
|
55
|
+
const animateSpringV2HostFunction = NativeSmoothClipModuleV2.animateSpringV2;
|
|
56
|
+
const animateKeyframesV2HostFunction = NativeSmoothClipModuleV2.animateKeyframesV2;
|
|
57
|
+
const animateTimingFromV2HostFunction = NativeSmoothClipModuleV2.animateTimingFromV2;
|
|
58
|
+
const animateSpringFromV2HostFunction = NativeSmoothClipModuleV2.animateSpringFromV2;
|
|
59
|
+
const animateKeyframesFromV2HostFunction = NativeSmoothClipModuleV2.animateKeyframesFromV2;
|
|
60
|
+
const cancelAnimationV2HostFunction = NativeSmoothClipModuleV2.cancelAnimationV2;
|
|
34
61
|
|
|
35
|
-
// Workaround for react-native-worklets 0.10
|
|
62
|
+
// Workaround for react-native-worklets 0.10.0 (fixed upstream in >=0.10.1,
|
|
63
|
+
// which drops the registry/proxy design; kept while the peer range still
|
|
64
|
+
// allows 0.10.0 consumers): the RN-side serialization cache
|
|
36
65
|
// hands out one remote-function registry id per function forever, but the
|
|
37
66
|
// registry entry is deleted as soon as any transient UI-side proxy of that
|
|
38
67
|
// function is garbage collected. A later scheduleOnRN for the same function
|
|
39
68
|
// then reads `undefined` from the registry and aborts in Value::getObject.
|
|
40
69
|
// Pinning one UI-side reference for the app lifetime keeps the proxy — and
|
|
41
|
-
// with it the registry entry — alive. Remove once
|
|
42
|
-
//
|
|
70
|
+
// with it the registry entry — alive. Remove once the peer range requires
|
|
71
|
+
// worklets >=0.10.1.
|
|
43
72
|
scheduleOnUI(resolver => {
|
|
44
73
|
'worklet';
|
|
45
74
|
|
|
@@ -55,7 +84,7 @@ function reduceMotionCode(value) {
|
|
|
55
84
|
return 0;
|
|
56
85
|
}
|
|
57
86
|
}
|
|
58
|
-
function animationIsFinite(
|
|
87
|
+
function animationIsFinite(animation, strictV2) {
|
|
59
88
|
'worklet';
|
|
60
89
|
|
|
61
90
|
if (animation.from !== undefined && !isFiniteClipPresentation(animation.from)) {
|
|
@@ -65,34 +94,53 @@ function animationIsFinite(target, animation) {
|
|
|
65
94
|
}
|
|
66
95
|
if (animation.type === 'timing') {
|
|
67
96
|
const [x1,, x2] = animation.controlPoints;
|
|
68
|
-
return Number.isFinite(animation.duration) && animation.controlPoints.every(Number.isFinite) &&
|
|
97
|
+
return Number.isFinite(animation.duration) && (!strictV2 || animation.duration >= 0) && animation.controlPoints.every(Number.isFinite) && (
|
|
69
98
|
// A cubic-bezier easing is only defined for x within [0,1] (the CSS /
|
|
70
99
|
// CoreAnimation / Reanimated Easing.bezier contract). Out-of-range x
|
|
71
100
|
// makes the Android parameter solve meaningless while CoreAnimation
|
|
72
101
|
// silently clamps — reject like any other invalid input instead of
|
|
73
102
|
// diverging per platform.
|
|
74
|
-
x1 >= 0 && x1 <= 1 && x2 >= 0 && x2 <= 1;
|
|
103
|
+
!strictV2 || x1 >= 0 && x1 <= 1 && x2 >= 0 && x2 <= 1);
|
|
75
104
|
}
|
|
76
105
|
if (animation.type === 'spring') {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
let previousOffset = -1;
|
|
83
|
-
for (const frame of animation.frames) {
|
|
84
|
-
if (!Number.isFinite(frame.offset) || frame.offset < 0 || frame.offset > 1 || frame.offset <= previousOffset || !isFiniteClipPresentation(frame.presentation)) {
|
|
85
|
-
return false;
|
|
86
|
-
}
|
|
87
|
-
previousOffset = frame.offset;
|
|
106
|
+
const mass = animation.mass ?? 1;
|
|
107
|
+
const stiffness = animation.stiffness ?? 100;
|
|
108
|
+
const damping = animation.damping ?? 10;
|
|
109
|
+
const initialVelocity = animation.initialVelocity === 'inherit' ? 0 : animation.initialVelocity ?? 0;
|
|
110
|
+
return [mass, stiffness, damping, initialVelocity].every(Number.isFinite) && (!strictV2 || mass > 0 && stiffness > 0 && damping >= 0);
|
|
88
111
|
}
|
|
89
|
-
|
|
112
|
+
// Keyframe frames validate inside flattenFiniteKeyframes — one traversal
|
|
113
|
+
// both checks and flattens, instead of walking every frame twice. The type
|
|
114
|
+
// check is statically always-true for the TS union but load-bearing for JS
|
|
115
|
+
// callers: an unrecognized type must reject here, or it would skip the
|
|
116
|
+
// keyframe gate entirely and hand native a null frames array.
|
|
117
|
+
return animation.type === 'keyframes' && (!strictV2 || animation.duration >= 0);
|
|
90
118
|
}
|
|
91
119
|
function presentationFromNative(values, fallback, offset = 0) {
|
|
92
120
|
'worklet';
|
|
93
121
|
|
|
122
|
+
if (values.length >= offset + 12) {
|
|
123
|
+
const presentation = canonicalizeClipPresentation({
|
|
124
|
+
clip: {
|
|
125
|
+
x: values[offset],
|
|
126
|
+
y: values[offset + 1],
|
|
127
|
+
width: values[offset + 2],
|
|
128
|
+
height: values[offset + 3],
|
|
129
|
+
radius: 0,
|
|
130
|
+
topLeftRadius: values[offset + 4],
|
|
131
|
+
topRightRadius: values[offset + 5],
|
|
132
|
+
bottomRightRadius: values[offset + 6],
|
|
133
|
+
bottomLeftRadius: values[offset + 7],
|
|
134
|
+
curve: values[offset + 8] === 1 ? 'continuous' : 'circular'
|
|
135
|
+
},
|
|
136
|
+
contentTranslateX: values[offset + 9],
|
|
137
|
+
contentTranslateY: values[offset + 10],
|
|
138
|
+
contentScale: values[offset + 11]
|
|
139
|
+
});
|
|
140
|
+
return presentation ?? fallback;
|
|
141
|
+
}
|
|
94
142
|
if (values.length < offset + 7) return fallback;
|
|
95
|
-
const presentation = {
|
|
143
|
+
const presentation = canonicalizeClipPresentation({
|
|
96
144
|
clip: {
|
|
97
145
|
x: values[offset],
|
|
98
146
|
y: values[offset + 1],
|
|
@@ -102,20 +150,83 @@ function presentationFromNative(values, fallback, offset = 0) {
|
|
|
102
150
|
},
|
|
103
151
|
contentTranslateX: values[offset + 5],
|
|
104
152
|
contentTranslateY: values[offset + 6]
|
|
105
|
-
};
|
|
106
|
-
return
|
|
153
|
+
});
|
|
154
|
+
return presentation ?? fallback;
|
|
155
|
+
}
|
|
156
|
+
function curveCode(presentation) {
|
|
157
|
+
'worklet';
|
|
158
|
+
|
|
159
|
+
return presentation.clip.curve === 'continuous' ? 1 : 0;
|
|
160
|
+
}
|
|
161
|
+
function isV1Compatible(presentation) {
|
|
162
|
+
'worklet';
|
|
163
|
+
|
|
164
|
+
const clip = presentation.clip;
|
|
165
|
+
return clip.curve === 'circular' && clip.topLeftRadius === clip.radius && clip.topRightRadius === clip.radius && clip.bottomRightRadius === clip.radius && clip.bottomLeftRadius === clip.radius && presentation.contentScale === 1;
|
|
107
166
|
}
|
|
108
|
-
function
|
|
167
|
+
function springScaleIsProvablyPositive(start, target, animation) {
|
|
109
168
|
'worklet';
|
|
110
169
|
|
|
170
|
+
if (start.contentScale === target.contentScale) return true;
|
|
171
|
+
|
|
172
|
+
// A zero-velocity critically/over-damped step is monotonic, so its sampled
|
|
173
|
+
// scale remains between two already-positive endpoints. Inherited or
|
|
174
|
+
// under-damped configurations can overshoot; callers must compile those
|
|
175
|
+
// plans to exact keyframes instead of risking a non-positive transform.
|
|
176
|
+
const mass = animation.mass ?? 1;
|
|
177
|
+
const stiffness = animation.stiffness ?? 100;
|
|
178
|
+
const damping = animation.damping ?? 10;
|
|
179
|
+
return animation.initialVelocity === 0 && damping * damping >= 4 * mass * stiffness;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Validates and flattens the keyframe list in one traversal (the flat number
|
|
183
|
+
// array is the wire format the native side reads back positionally). Returns
|
|
184
|
+
// null when any frame is invalid — same rejection contract the two-pass
|
|
185
|
+
// validate-then-flatten had.
|
|
186
|
+
function flattenFiniteKeyframes(target, animation) {
|
|
187
|
+
'worklet';
|
|
188
|
+
|
|
189
|
+
if (!Number.isFinite(animation.duration) || animation.frames.length < 2) {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
111
192
|
const values = [];
|
|
193
|
+
const canonicalFrames = [];
|
|
194
|
+
let previousOffset = -1;
|
|
112
195
|
for (const frame of animation.frames) {
|
|
196
|
+
const {
|
|
197
|
+
offset
|
|
198
|
+
} = frame;
|
|
199
|
+
const presentation = canonicalizeClipPresentation(frame.presentation);
|
|
200
|
+
if (!Number.isFinite(offset) || offset < 0 || offset > 1 || offset <= previousOffset || presentation === null || presentation.clip.curve !== target.clip.curve) {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
previousOffset = offset;
|
|
204
|
+
canonicalFrames.push(presentation);
|
|
113
205
|
const {
|
|
114
206
|
clip,
|
|
115
207
|
contentTranslateX,
|
|
116
|
-
contentTranslateY
|
|
117
|
-
|
|
118
|
-
|
|
208
|
+
contentTranslateY,
|
|
209
|
+
contentScale
|
|
210
|
+
} = presentation;
|
|
211
|
+
values.push(offset, clip.x, clip.y, clip.width, clip.height, clip.topLeftRadius, clip.topRightRadius, clip.bottomRightRadius, clip.bottomLeftRadius, curveCode(presentation), contentTranslateX, contentTranslateY, contentScale);
|
|
212
|
+
}
|
|
213
|
+
if (animation.frames[0]?.offset !== 0 || previousOffset !== 1 || !clipPresentationEquals(canonicalFrames[canonicalFrames.length - 1] ?? null, target)) {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
return {
|
|
217
|
+
frames: values,
|
|
218
|
+
canonicalFrames
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
function flattenV1Keyframes(frames, offsets) {
|
|
222
|
+
'worklet';
|
|
223
|
+
|
|
224
|
+
const values = [];
|
|
225
|
+
for (let index = 0; index < frames.length; index += 1) {
|
|
226
|
+
const presentation = frames[index];
|
|
227
|
+
const offset = offsets[index];
|
|
228
|
+
if (presentation === undefined || offset === undefined) continue;
|
|
229
|
+
values.push(offset, presentation.clip.x, presentation.clip.y, presentation.clip.width, presentation.clip.height, presentation.clip.radius, presentation.contentTranslateX, presentation.contentTranslateY);
|
|
119
230
|
}
|
|
120
231
|
return values;
|
|
121
232
|
}
|
|
@@ -125,7 +236,11 @@ function uiOnly() {
|
|
|
125
236
|
throw new Error('[SmoothClipView] driver.ui methods must run on the UI runtime. Use driver.react from React code.');
|
|
126
237
|
}
|
|
127
238
|
export function useSmoothClipDriver(initialValue, options = {}) {
|
|
128
|
-
const
|
|
239
|
+
const requestedInitialPresentation = 'clip' in initialValue ? initialValue : createClipPresentation(initialValue);
|
|
240
|
+
const initialPresentation = canonicalizeClipPresentation(requestedInitialPresentation);
|
|
241
|
+
if (initialPresentation === null) {
|
|
242
|
+
throw new Error('[SmoothClipView] Initial presentation must be finite.');
|
|
243
|
+
}
|
|
129
244
|
const presentation = useSharedValue(initialPresentation);
|
|
130
245
|
const ownership = useSharedValue(INTERACTIVE);
|
|
131
246
|
const suppressDeliveries = useSharedValue(0);
|
|
@@ -148,6 +263,11 @@ export function useSmoothClipDriver(initialValue, options = {}) {
|
|
|
148
263
|
if (driverRef.current === null) {
|
|
149
264
|
const driverId = allocateDriverId();
|
|
150
265
|
const reduceMotion = reduceMotionCode(options.reduceMotion ?? 'system');
|
|
266
|
+
// Captured once, like reduceMotion. Only the per-frame setScalars stream
|
|
267
|
+
// consults it; set(), the fused animateTo `from` seed, and the effect's
|
|
268
|
+
// declarative deliveries always record, so every once-per-call channel
|
|
269
|
+
// keeps its pre-flag 'inherit' behavior.
|
|
270
|
+
const velocityTracking = options.velocityTracking === true;
|
|
151
271
|
const seedPresentation = next => {
|
|
152
272
|
'worklet';
|
|
153
273
|
|
|
@@ -165,8 +285,9 @@ export function useSmoothClipDriver(initialValue, options = {}) {
|
|
|
165
285
|
const beginOnUI = () => {
|
|
166
286
|
'worklet';
|
|
167
287
|
|
|
168
|
-
|
|
169
|
-
|
|
288
|
+
const fallback = canonicalizeClipPresentation(presentation.value) ?? initialPresentation;
|
|
289
|
+
if (disposed.value !== 0) return fallback;
|
|
290
|
+
const current = presentationFromNative(PRESENTATION_PROTOCOL_VERSION === 2 && beginInteractionV2HostFunction ? beginInteractionV2HostFunction(driverId) : beginInteractionHostFunction(driverId), fallback);
|
|
170
291
|
activeAnimationId.value = 0;
|
|
171
292
|
ownership.value = INTERACTIVE;
|
|
172
293
|
scalarsStale.value = 0;
|
|
@@ -176,28 +297,72 @@ export function useSmoothClipDriver(initialValue, options = {}) {
|
|
|
176
297
|
const setOnUI = next => {
|
|
177
298
|
'worklet';
|
|
178
299
|
|
|
179
|
-
|
|
300
|
+
const canonical = canonicalizeClipPresentation(next);
|
|
301
|
+
if (disposed.value !== 0 || canonical === null) return;
|
|
180
302
|
activeAnimationId.value = 0;
|
|
181
303
|
ownership.value = INTERACTIVE;
|
|
182
304
|
scalarsStale.value = 0;
|
|
183
305
|
const {
|
|
184
306
|
clip,
|
|
185
307
|
contentTranslateX,
|
|
186
|
-
contentTranslateY
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
308
|
+
contentTranslateY,
|
|
309
|
+
contentScale
|
|
310
|
+
} = canonical;
|
|
311
|
+
// Always records: only the per-frame setScalars hot path is gated by
|
|
312
|
+
// velocityTracking. set() already pays full validation and a SharedValue
|
|
313
|
+
// seed per call, so the sample recording is noise here.
|
|
314
|
+
if (PRESENTATION_PROTOCOL_VERSION === 2 && setPresentationV2HostFunction) {
|
|
315
|
+
setPresentationV2HostFunction(driverId, clip.x, clip.y, clip.width, clip.height, clip.topLeftRadius, clip.topRightRadius, clip.bottomRightRadius, clip.bottomLeftRadius, curveCode(canonical), contentTranslateX, contentTranslateY, contentScale, true, false, true);
|
|
316
|
+
} else if (isV1Compatible(canonical)) {
|
|
317
|
+
setPresentationHostFunction(driverId, clip.x, clip.y, clip.width, clip.height, clip.radius, contentTranslateX, contentTranslateY, true, false);
|
|
318
|
+
} else {
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
seedPresentation(canonical);
|
|
190
322
|
};
|
|
191
|
-
const setScalarsOnUI = (x, y, width, height, radius, contentTranslateX, contentTranslateY, overridePendingAnimation = false) => {
|
|
323
|
+
const setScalarsOnUI = (x, y, width, height, radius, contentTranslateX, contentTranslateY, overridePendingAnimation = false, forceRecordVelocity = false) => {
|
|
192
324
|
'worklet';
|
|
193
325
|
|
|
326
|
+
// Resolved in the body, not as a default-parameter initializer: the
|
|
327
|
+
// worklets Babel plugin only captures closure variables referenced
|
|
328
|
+
// inside the function body, so `= velocityTracking` up in the parameter
|
|
329
|
+
// list would throw "Property 'velocityTracking' doesn't exist" on the
|
|
330
|
+
// UI runtime.
|
|
331
|
+
const recordVelocity = forceRecordVelocity || velocityTracking;
|
|
332
|
+
// Number.isFinite (not a fused arithmetic gate): it also rejects
|
|
333
|
+
// non-number types, which arithmetic would coerce past the gate and
|
|
334
|
+
// into a UI-runtime throw from the binding's asNumber. The gate must
|
|
335
|
+
// stay on this side of the bridge: the native binding drops non-finite
|
|
336
|
+
// writes silently, and flipping the ownership SharedValues below for a
|
|
337
|
+
// write native never applied would let the deliver listener record
|
|
338
|
+
// values native has not observed — a later identical declarative write
|
|
339
|
+
// then dedupes against them and is swallowed for good.
|
|
194
340
|
if (disposed.value !== 0 || !Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(width) || !Number.isFinite(height) || !Number.isFinite(radius) || !Number.isFinite(contentTranslateX) || !Number.isFinite(contentTranslateY)) {
|
|
195
341
|
return;
|
|
196
342
|
}
|
|
197
343
|
if (activeAnimationId.value !== 0) activeAnimationId.value = 0;
|
|
198
344
|
if (ownership.value !== INTERACTIVE) ownership.value = INTERACTIVE;
|
|
199
345
|
if (scalarsStale.value === 0) scalarsStale.value = 1;
|
|
200
|
-
setPresentationHostFunction(driverId, x, y, width, height, radius, contentTranslateX, contentTranslateY, true, overridePendingAnimation);
|
|
346
|
+
setPresentationHostFunction(driverId, x, y, width, height, radius, contentTranslateX, contentTranslateY, true, overridePendingAnimation, recordVelocity);
|
|
347
|
+
};
|
|
348
|
+
const setPresentationScalarsOnUI = (x, y, width, height, topLeftRadius, topRightRadius, bottomRightRadius, bottomLeftRadius, nextCurveCode, contentTranslateX, contentTranslateY, contentScale, overridePendingAnimation = false, forceRecordVelocity = false) => {
|
|
349
|
+
'worklet';
|
|
350
|
+
|
|
351
|
+
const values = [x, y, width, height, topLeftRadius, topRightRadius, bottomRightRadius, bottomLeftRadius, nextCurveCode, contentTranslateX, contentTranslateY, contentScale];
|
|
352
|
+
if (disposed.value !== 0 || !values.every(Number.isFinite) || nextCurveCode !== 0 && nextCurveCode !== 1 || contentScale <= 0) {
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
const recordVelocity = forceRecordVelocity || velocityTracking;
|
|
356
|
+
if (PRESENTATION_PROTOCOL_VERSION !== 2 || !setPresentationV2HostFunction) {
|
|
357
|
+
if (nextCurveCode === 0 && contentScale === 1 && topLeftRadius === topRightRadius && topLeftRadius === bottomRightRadius && topLeftRadius === bottomLeftRadius) {
|
|
358
|
+
setScalarsOnUI(x, y, width, height, topLeftRadius, contentTranslateX, contentTranslateY, overridePendingAnimation, forceRecordVelocity);
|
|
359
|
+
}
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
if (activeAnimationId.value !== 0) activeAnimationId.value = 0;
|
|
363
|
+
if (ownership.value !== INTERACTIVE) ownership.value = INTERACTIVE;
|
|
364
|
+
if (scalarsStale.value === 0) scalarsStale.value = 1;
|
|
365
|
+
setPresentationV2HostFunction(driverId, x, y, width, height, topLeftRadius, topRightRadius, bottomRightRadius, bottomLeftRadius, nextCurveCode, contentTranslateX, contentTranslateY, contentScale, true, overridePendingAnimation, recordVelocity);
|
|
201
366
|
};
|
|
202
367
|
const animateOnUI = (target, animation) => {
|
|
203
368
|
'worklet';
|
|
@@ -206,7 +371,43 @@ export function useSmoothClipDriver(initialValue, options = {}) {
|
|
|
206
371
|
// invalid stale call cannot mint a native rejection id/completion against
|
|
207
372
|
// a driver whose listener and registry entry are already gone.
|
|
208
373
|
if (disposed.value !== 0) return 0;
|
|
209
|
-
|
|
374
|
+
const canonicalTarget = canonicalizeClipPresentation(target);
|
|
375
|
+
const canonicalFrom = animation.from === undefined ? undefined : canonicalizeClipPresentation(animation.from);
|
|
376
|
+
const keyframeResult = animation.type === 'keyframes' ? canonicalTarget === null ? null : flattenFiniteKeyframes(canonicalTarget, animation) : null;
|
|
377
|
+
if (canonicalTarget === null || animation.from !== undefined && canonicalFrom === null || animation.type === 'keyframes' && keyframeResult === null) {
|
|
378
|
+
return rejectAnimationHostFunction(driverId);
|
|
379
|
+
}
|
|
380
|
+
const resolvedFrom = canonicalFrom ?? undefined;
|
|
381
|
+
const v1Compatible = isV1Compatible(canonicalTarget) && (resolvedFrom === undefined || isV1Compatible(resolvedFrom)) && (animation.type !== 'keyframes' || (keyframeResult?.canonicalFrames.every(isV1Compatible) ?? false));
|
|
382
|
+
// V1-representable plans deliberately stay on the original seven-scalar
|
|
383
|
+
// bridge even when V2 is installed. Besides preserving bridge shape,
|
|
384
|
+
// this retains V1's finite-value/clamping and host-normalization rules.
|
|
385
|
+
const useV2 = !v1Compatible;
|
|
386
|
+
if (!animationIsFinite(animation, useV2)) {
|
|
387
|
+
return rejectAnimationHostFunction(driverId);
|
|
388
|
+
}
|
|
389
|
+
const hasV2Protocol = PRESENTATION_PROTOCOL_VERSION === 2 && snapshotCurrentV2HostFunction !== undefined && animateTimingV2HostFunction !== undefined && animateSpringV2HostFunction !== undefined && animateKeyframesV2HostFunction !== undefined;
|
|
390
|
+
const hasAtomicFromMethod = resolvedFrom === undefined || (animation.type === 'timing' ? animateTimingFromV2HostFunction !== undefined : animation.type === 'spring' ? animateSpringFromV2HostFunction !== undefined : animateKeyframesFromV2HostFunction !== undefined);
|
|
391
|
+
// This is intentionally before sampling or any ownership/group/
|
|
392
|
+
// presentation mutation. New JS against old native must fail closed,
|
|
393
|
+
// and an explicit V2 start must never degrade to set()+animate().
|
|
394
|
+
if (useV2 && (!hasV2Protocol || !hasAtomicFromMethod)) {
|
|
395
|
+
return rejectAnimationHostFunction(driverId);
|
|
396
|
+
}
|
|
397
|
+
if (useV2 && resolvedFrom !== undefined && animation.type === 'keyframes' && !clipPresentationEquals(keyframeResult?.canonicalFrames[0] ?? null, resolvedFrom)) {
|
|
398
|
+
return rejectAnimationHostFunction(driverId);
|
|
399
|
+
}
|
|
400
|
+
const currentPresentation = canonicalizeClipPresentation(presentation.value) ?? initialPresentation;
|
|
401
|
+
const sampledStart = resolvedFrom ?? (useV2 ? presentationFromNative(snapshotCurrentV2HostFunction(driverId), currentPresentation) : currentPresentation);
|
|
402
|
+
// setPresentationScalars intentionally leaves the public SharedValue
|
|
403
|
+
// stale to keep the hot path allocation-free. Validate an implicit
|
|
404
|
+
// retarget against the sampled native-visible start, not that stale JS
|
|
405
|
+
// value; otherwise a streamed curve transition cannot settle natively on
|
|
406
|
+
// its new (now stable) curve.
|
|
407
|
+
if (canonicalTarget.clip.curve !== sampledStart.clip.curve) {
|
|
408
|
+
return rejectAnimationHostFunction(driverId);
|
|
409
|
+
}
|
|
410
|
+
if (useV2 && animation.type === 'spring' && !springScaleIsProvablyPositive(sampledStart, canonicalTarget, animation)) {
|
|
210
411
|
return rejectAnimationHostFunction(driverId);
|
|
211
412
|
}
|
|
212
413
|
// Past the hook's cleanup the native entry is gone for good, and an
|
|
@@ -215,14 +416,19 @@ export function useSmoothClipDriver(initialValue, options = {}) {
|
|
|
215
416
|
// rejectAnimation, which would mint an id and a completion for a driver
|
|
216
417
|
// whose JS state is already detached. This is the "unsupported dispatch"
|
|
217
418
|
// arm of the documented 0 contract.
|
|
218
|
-
const from =
|
|
219
|
-
if (from !== undefined) {
|
|
419
|
+
const from = resolvedFrom;
|
|
420
|
+
if (from !== undefined && !useV2) {
|
|
220
421
|
// Fused hot write: exactly setScalars(from…) issued immediately
|
|
221
422
|
// before the handoff, so native's latest value — the animation
|
|
222
423
|
// start below — is the caller's explicit presentation. Also
|
|
223
424
|
// re-grabs from a running animation, which the implicit
|
|
224
|
-
// interactive-start path would silently skip.
|
|
225
|
-
|
|
425
|
+
// interactive-start path would silently skip. Always records a
|
|
426
|
+
// velocity sample regardless of velocityTracking: this is one write
|
|
427
|
+
// per animateTo, not the per-frame stream the flag exists to keep
|
|
428
|
+
// cheap, and an unrecorded seed would invalidate the history a
|
|
429
|
+
// set()-driven drag just recorded (the tracker's coalesce and
|
|
430
|
+
// identical-re-record rules were designed for exactly this write).
|
|
431
|
+
setScalarsOnUI(from.clip.x, from.clip.y, from.clip.width, from.clip.height, from.clip.radius, from.contentTranslateX, from.contentTranslateY, true, true);
|
|
226
432
|
}
|
|
227
433
|
|
|
228
434
|
// After a setScalars hot write the SharedValue no longer matches what
|
|
@@ -231,12 +437,17 @@ export function useSmoothClipDriver(initialValue, options = {}) {
|
|
|
231
437
|
// latest value instead.
|
|
232
438
|
const scalarsWereStale = scalarsStale.value !== 0;
|
|
233
439
|
const hasInteractiveStart = ownership.value === INTERACTIVE && !scalarsWereStale;
|
|
234
|
-
const start = presentation.value;
|
|
235
|
-
ownership.
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
440
|
+
const start = canonicalizeClipPresentation(presentation.value) ?? initialPresentation;
|
|
441
|
+
// V1 preserves the old optimistic SharedValue ownership transition.
|
|
442
|
+
// V2 commits it only after native has atomically accepted the plan.
|
|
443
|
+
if (!useV2) {
|
|
444
|
+
ownership.value = NATIVE;
|
|
445
|
+
presentation.value = canonicalTarget;
|
|
446
|
+
scalarsStale.value = 0;
|
|
447
|
+
}
|
|
448
|
+
const dispatchStart = useV2 && from !== undefined ? from : start;
|
|
449
|
+
const startClip = dispatchStart.clip;
|
|
450
|
+
const targetClip = canonicalTarget.clip;
|
|
240
451
|
// Reanimated stamps a parallel animation's t0 on this same runtime as
|
|
241
452
|
// `__frameTimestamp || _getAnimationTimestamp()` (valueSetter.ts).
|
|
242
453
|
// Capturing the identical value here and handing it to the integrator
|
|
@@ -247,18 +458,36 @@ export function useSmoothClipDriver(initialValue, options = {}) {
|
|
|
247
458
|
// its rAF flush where __frameTimestamp is cleared, keeps the wall
|
|
248
459
|
// clock — a lead lasting the whole animation. NaN when the globals are
|
|
249
460
|
// absent (tests, non-worklets runtimes): native then falls back to its
|
|
250
|
-
// own clock plus the min() anchor, the pre-stamp behavior.
|
|
461
|
+
// own clock plus the min() anchor, the pre-stamp behavior. On iOS the
|
|
462
|
+
// stamp is always NaN: Core Animation anchors at commit time and the
|
|
463
|
+
// TurboModule discards the argument, so the capture would only cost.
|
|
251
464
|
const workletGlobal = globalThis;
|
|
252
|
-
const startedAtMs = workletGlobal.__frameTimestamp || (typeof workletGlobal._getAnimationTimestamp === 'function' ? workletGlobal._getAnimationTimestamp() : Number.NaN);
|
|
465
|
+
const startedAtMs = !NEEDS_START_STAMP ? Number.NaN : workletGlobal.__frameTimestamp || (typeof workletGlobal._getAnimationTimestamp === 'function' ? workletGlobal._getAnimationTimestamp() : Number.NaN);
|
|
253
466
|
let animationId;
|
|
254
|
-
if (animation.type === 'timing') {
|
|
467
|
+
if (animation.type === 'timing' && useV2) {
|
|
468
|
+
const [x1, y1, x2, y2] = animation.controlPoints;
|
|
469
|
+
animationId = from !== undefined ? animateTimingFromV2HostFunction(driverId, startClip.x, startClip.y, startClip.width, startClip.height, startClip.topLeftRadius, startClip.topRightRadius, startClip.bottomRightRadius, startClip.bottomLeftRadius, curveCode(dispatchStart), dispatchStart.contentTranslateX, dispatchStart.contentTranslateY, dispatchStart.contentScale, targetClip.x, targetClip.y, targetClip.width, targetClip.height, targetClip.topLeftRadius, targetClip.topRightRadius, targetClip.bottomRightRadius, targetClip.bottomLeftRadius, curveCode(canonicalTarget), canonicalTarget.contentTranslateX, canonicalTarget.contentTranslateY, canonicalTarget.contentScale, animation.duration, x1, y1, x2, y2, reduceMotion, startedAtMs) : animateTimingV2HostFunction(driverId, hasInteractiveStart, startClip.x, startClip.y, startClip.width, startClip.height, startClip.topLeftRadius, startClip.topRightRadius, startClip.bottomRightRadius, startClip.bottomLeftRadius, curveCode(dispatchStart), dispatchStart.contentTranslateX, dispatchStart.contentTranslateY, dispatchStart.contentScale, targetClip.x, targetClip.y, targetClip.width, targetClip.height, targetClip.topLeftRadius, targetClip.topRightRadius, targetClip.bottomRightRadius, targetClip.bottomLeftRadius, curveCode(canonicalTarget), canonicalTarget.contentTranslateX, canonicalTarget.contentTranslateY, canonicalTarget.contentScale, animation.duration, x1, y1, x2, y2, reduceMotion, startedAtMs);
|
|
470
|
+
} else if (animation.type === 'timing') {
|
|
255
471
|
const [x1, y1, x2, y2] = animation.controlPoints;
|
|
256
|
-
animationId = animateTimingHostFunction(driverId, hasInteractiveStart, startClip.x, startClip.y, startClip.width, startClip.height, startClip.radius, start.contentTranslateX, start.contentTranslateY, targetClip.x, targetClip.y, targetClip.width, targetClip.height, targetClip.radius,
|
|
472
|
+
animationId = animateTimingHostFunction(driverId, hasInteractiveStart, startClip.x, startClip.y, startClip.width, startClip.height, startClip.radius, start.contentTranslateX, start.contentTranslateY, targetClip.x, targetClip.y, targetClip.width, targetClip.height, targetClip.radius, canonicalTarget.contentTranslateX, canonicalTarget.contentTranslateY, Math.max(0, animation.duration), x1, y1, x2, y2, reduceMotion, startedAtMs);
|
|
473
|
+
} else if (animation.type === 'spring' && useV2) {
|
|
474
|
+
const inheritVelocity = animation.initialVelocity === undefined || animation.initialVelocity === 'inherit';
|
|
475
|
+
const mass = animation.mass ?? 1;
|
|
476
|
+
const stiffness = animation.stiffness ?? 100;
|
|
477
|
+
const damping = animation.damping ?? 10;
|
|
478
|
+
const initialVelocity = inheritVelocity ? 0 : animation.initialVelocity;
|
|
479
|
+
animationId = from !== undefined ? animateSpringFromV2HostFunction(driverId, startClip.x, startClip.y, startClip.width, startClip.height, startClip.topLeftRadius, startClip.topRightRadius, startClip.bottomRightRadius, startClip.bottomLeftRadius, curveCode(dispatchStart), dispatchStart.contentTranslateX, dispatchStart.contentTranslateY, dispatchStart.contentScale, targetClip.x, targetClip.y, targetClip.width, targetClip.height, targetClip.topLeftRadius, targetClip.topRightRadius, targetClip.bottomRightRadius, targetClip.bottomLeftRadius, curveCode(canonicalTarget), canonicalTarget.contentTranslateX, canonicalTarget.contentTranslateY, canonicalTarget.contentScale, mass, stiffness, damping, initialVelocity, inheritVelocity, reduceMotion, startedAtMs) : animateSpringV2HostFunction(driverId, hasInteractiveStart, startClip.x, startClip.y, startClip.width, startClip.height, startClip.topLeftRadius, startClip.topRightRadius, startClip.bottomRightRadius, startClip.bottomLeftRadius, curveCode(dispatchStart), dispatchStart.contentTranslateX, dispatchStart.contentTranslateY, dispatchStart.contentScale, targetClip.x, targetClip.y, targetClip.width, targetClip.height, targetClip.topLeftRadius, targetClip.topRightRadius, targetClip.bottomRightRadius, targetClip.bottomLeftRadius, curveCode(canonicalTarget), canonicalTarget.contentTranslateX, canonicalTarget.contentTranslateY, canonicalTarget.contentScale, mass, stiffness, damping, initialVelocity, inheritVelocity, reduceMotion, startedAtMs);
|
|
257
480
|
} else if (animation.type === 'spring') {
|
|
258
481
|
const inheritVelocity = animation.initialVelocity === undefined || animation.initialVelocity === 'inherit';
|
|
259
|
-
animationId = animateSpringHostFunction(driverId, hasInteractiveStart, startClip.x, startClip.y, startClip.width, startClip.height, startClip.radius, start.contentTranslateX, start.contentTranslateY, targetClip.x, targetClip.y, targetClip.width, targetClip.height, targetClip.radius,
|
|
482
|
+
animationId = animateSpringHostFunction(driverId, hasInteractiveStart, startClip.x, startClip.y, startClip.width, startClip.height, startClip.radius, start.contentTranslateX, start.contentTranslateY, targetClip.x, targetClip.y, targetClip.width, targetClip.height, targetClip.radius, canonicalTarget.contentTranslateX, canonicalTarget.contentTranslateY, Math.max(0.0001, animation.mass ?? 1), Math.max(0.0001, animation.stiffness ?? 100), Math.max(0, animation.damping ?? 10), inheritVelocity ? 0 : animation.initialVelocity, inheritVelocity, reduceMotion, startedAtMs);
|
|
483
|
+
} else if (useV2) {
|
|
484
|
+
const frames = keyframeResult.frames;
|
|
485
|
+
animationId = from !== undefined ? animateKeyframesFromV2HostFunction(driverId, startClip.x, startClip.y, startClip.width, startClip.height, startClip.topLeftRadius, startClip.topRightRadius, startClip.bottomRightRadius, startClip.bottomLeftRadius, curveCode(dispatchStart), dispatchStart.contentTranslateX, dispatchStart.contentTranslateY, dispatchStart.contentScale, targetClip.x, targetClip.y, targetClip.width, targetClip.height, targetClip.topLeftRadius, targetClip.topRightRadius, targetClip.bottomRightRadius, targetClip.bottomLeftRadius, curveCode(canonicalTarget), canonicalTarget.contentTranslateX, canonicalTarget.contentTranslateY, canonicalTarget.contentScale, animation.duration, frames, reduceMotion, startedAtMs) : animateKeyframesV2HostFunction(driverId, hasInteractiveStart, startClip.x, startClip.y, startClip.width, startClip.height, startClip.topLeftRadius, startClip.topRightRadius, startClip.bottomRightRadius, startClip.bottomLeftRadius, curveCode(dispatchStart), dispatchStart.contentTranslateX, dispatchStart.contentTranslateY, dispatchStart.contentScale, targetClip.x, targetClip.y, targetClip.width, targetClip.height, targetClip.topLeftRadius, targetClip.topRightRadius, targetClip.bottomRightRadius, targetClip.bottomLeftRadius, curveCode(canonicalTarget), canonicalTarget.contentTranslateX, canonicalTarget.contentTranslateY, canonicalTarget.contentScale, animation.duration, frames, reduceMotion, startedAtMs);
|
|
260
486
|
} else {
|
|
261
|
-
animationId = animateKeyframesHostFunction(driverId, hasInteractiveStart, startClip.x, startClip.y, startClip.width, startClip.height, startClip.radius, start.contentTranslateX, start.contentTranslateY, targetClip.x, targetClip.y, targetClip.width, targetClip.height, targetClip.radius,
|
|
487
|
+
animationId = animateKeyframesHostFunction(driverId, hasInteractiveStart, startClip.x, startClip.y, startClip.width, startClip.height, startClip.radius, start.contentTranslateX, start.contentTranslateY, targetClip.x, targetClip.y, targetClip.width, targetClip.height, targetClip.radius, canonicalTarget.contentTranslateX, canonicalTarget.contentTranslateY, Math.max(0, animation.duration),
|
|
488
|
+
// Non-null here: the validation gate above rejected null before any
|
|
489
|
+
// side effect.
|
|
490
|
+
flattenV1Keyframes(keyframeResult.canonicalFrames, animation.frames.map(frame => frame.offset)), reduceMotion, startedAtMs);
|
|
262
491
|
}
|
|
263
492
|
if (animationId === 0) {
|
|
264
493
|
// The 0 sentinel: off-main, invalid-id, or otherwise unsupported
|
|
@@ -269,12 +498,19 @@ export function useSmoothClipDriver(initialValue, options = {}) {
|
|
|
269
498
|
// effect, with a fresh id and one finished:false completion once the
|
|
270
499
|
// native entry exists — in the pre-seed window rejectAnimation has no
|
|
271
500
|
// entry and returns the bare 0 sentinel with no completion instead.
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
501
|
+
if (!useV2) {
|
|
502
|
+
ownership.value = INTERACTIVE;
|
|
503
|
+
presentation.value = start;
|
|
504
|
+
if (scalarsWereStale) scalarsStale.value = 1;
|
|
505
|
+
activeAnimationId.value = 0;
|
|
506
|
+
}
|
|
276
507
|
return rejectAnimationHostFunction(driverId);
|
|
277
508
|
}
|
|
509
|
+
if (useV2) {
|
|
510
|
+
ownership.value = NATIVE;
|
|
511
|
+
presentation.value = canonicalTarget;
|
|
512
|
+
scalarsStale.value = 0;
|
|
513
|
+
}
|
|
278
514
|
activeAnimationId.value = animationId;
|
|
279
515
|
return animationId;
|
|
280
516
|
};
|
|
@@ -284,10 +520,11 @@ export function useSmoothClipDriver(initialValue, options = {}) {
|
|
|
284
520
|
// Past the hook's cleanup the native entry is gone; the call would fail
|
|
285
521
|
// defined (handled=false) but it is the last driver.ui path that still
|
|
286
522
|
// crosses into native after the tombstone. Guard it like the rest.
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
523
|
+
const fallback = canonicalizeClipPresentation(presentation.value) ?? initialPresentation;
|
|
524
|
+
if (disposed.value !== 0) return fallback;
|
|
525
|
+
const values = PRESENTATION_PROTOCOL_VERSION === 2 && cancelAnimationV2HostFunction ? cancelAnimationV2HostFunction(driverId, animationId, behavior === 'target' ? 1 : 0) : cancelAnimationHostFunction(driverId, animationId, behavior === 'target' ? 1 : 0);
|
|
526
|
+
if (values.length < 8 || values[0] !== 1) return fallback;
|
|
527
|
+
const current = presentationFromNative(values, fallback, 1);
|
|
291
528
|
activeAnimationId.value = 0;
|
|
292
529
|
ownership.value = INTERACTIVE;
|
|
293
530
|
scalarsStale.value = 0;
|
|
@@ -316,6 +553,12 @@ export function useSmoothClipDriver(initialValue, options = {}) {
|
|
|
316
553
|
if (isRNRuntime()) return uiOnly();
|
|
317
554
|
setScalarsOnUI(x, y, width, height, radius, contentTranslateX, contentTranslateY);
|
|
318
555
|
},
|
|
556
|
+
setPresentationScalars(x, y, width, height, topLeftRadius, topRightRadius, bottomRightRadius, bottomLeftRadius, nextCurveCode, contentTranslateX, contentTranslateY, contentScale) {
|
|
557
|
+
'worklet';
|
|
558
|
+
|
|
559
|
+
if (isRNRuntime()) return uiOnly();
|
|
560
|
+
setPresentationScalarsOnUI(x, y, width, height, topLeftRadius, topRightRadius, bottomRightRadius, bottomLeftRadius, nextCurveCode, contentTranslateX, contentTranslateY, contentScale);
|
|
561
|
+
},
|
|
319
562
|
animateTo(target, animation) {
|
|
320
563
|
'worklet';
|
|
321
564
|
|
|
@@ -391,6 +634,13 @@ export function useSmoothClipDriver(initialValue, options = {}) {
|
|
|
391
634
|
});
|
|
392
635
|
return promise;
|
|
393
636
|
}
|
|
637
|
+
},
|
|
638
|
+
__smoothClipHandle: {
|
|
639
|
+
driverId,
|
|
640
|
+
presentation,
|
|
641
|
+
ownership,
|
|
642
|
+
activeAnimationId,
|
|
643
|
+
disposed
|
|
394
644
|
}
|
|
395
645
|
};
|
|
396
646
|
setDriverState(driver, createDriverState(driverId, initialPresentation, presentation, callbackRef, activeAnimationId, ownership));
|
|
@@ -413,7 +663,7 @@ export function useSmoothClipDriver(initialValue, options = {}) {
|
|
|
413
663
|
synchronizeNativeCompletion(activeAnimationId, ownership, result.animationId, result.finished);
|
|
414
664
|
completeNativeAnimation(driverId, result.animationId, result.finished);
|
|
415
665
|
});
|
|
416
|
-
scheduleOnUI((source, listenerId, nativeDriverId, owner, suppressed, active, stale, gone, setter) => {
|
|
666
|
+
scheduleOnUI((source, listenerId, nativeDriverId, owner, suppressed, active, stale, gone, setter, setterV2, protocolVersion) => {
|
|
417
667
|
'worklet';
|
|
418
668
|
|
|
419
669
|
// First, before the listener or the seed: this effect run owns the
|
|
@@ -422,7 +672,8 @@ export function useSmoothClipDriver(initialValue, options = {}) {
|
|
|
422
672
|
gone.value = 0;
|
|
423
673
|
let last = null;
|
|
424
674
|
const deliver = next => {
|
|
425
|
-
|
|
675
|
+
const canonical = canonicalizeClipPresentation(next);
|
|
676
|
+
if (canonical === null || clipPresentationEquals(last, canonical)) {
|
|
426
677
|
return;
|
|
427
678
|
}
|
|
428
679
|
// Record `last` only for values native has actually observed:
|
|
@@ -431,17 +682,27 @@ export function useSmoothClipDriver(initialValue, options = {}) {
|
|
|
431
682
|
if (owner.value !== INTERACTIVE) return;
|
|
432
683
|
if (suppressed.value > 0) {
|
|
433
684
|
suppressed.value -= 1;
|
|
434
|
-
last =
|
|
685
|
+
last = canonical;
|
|
435
686
|
return;
|
|
436
687
|
}
|
|
437
|
-
|
|
688
|
+
if (protocolVersion !== 2 && !isV1Compatible(canonical)) {
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
last = canonical;
|
|
438
692
|
if (stale.value !== 0) stale.value = 0;
|
|
439
693
|
const {
|
|
440
694
|
clip,
|
|
441
695
|
contentTranslateX,
|
|
442
|
-
contentTranslateY
|
|
443
|
-
|
|
444
|
-
|
|
696
|
+
contentTranslateY,
|
|
697
|
+
contentScale
|
|
698
|
+
} = canonical;
|
|
699
|
+
// Declarative deliveries always record velocity samples — only the
|
|
700
|
+
// setScalars hot path is gated by the velocityTracking option.
|
|
701
|
+
if (protocolVersion === 2 && setterV2) {
|
|
702
|
+
setterV2(nativeDriverId, clip.x, clip.y, clip.width, clip.height, clip.topLeftRadius, clip.topRightRadius, clip.bottomRightRadius, clip.bottomLeftRadius, curveCode(canonical), contentTranslateX, contentTranslateY, contentScale, false, false, true);
|
|
703
|
+
} else {
|
|
704
|
+
setter(nativeDriverId, clip.x, clip.y, clip.width, clip.height, clip.radius, contentTranslateX, contentTranslateY, false, false);
|
|
705
|
+
}
|
|
445
706
|
};
|
|
446
707
|
source.addListener(listenerId, deliver);
|
|
447
708
|
// Authoritative take-ownership seed. Creates the native entry before
|
|
@@ -450,15 +711,19 @@ export function useSmoothClipDriver(initialValue, options = {}) {
|
|
|
450
711
|
// An animation worklet can win the scheduling race and create a latch
|
|
451
712
|
// first; Native ownership then marks that intent as newer, so this
|
|
452
713
|
// passive seed must not clear its active id or replay the target value.
|
|
453
|
-
const current = source.value;
|
|
454
|
-
if (owner.value === INTERACTIVE &&
|
|
714
|
+
const current = canonicalizeClipPresentation(source.value);
|
|
715
|
+
if (owner.value === INTERACTIVE && current !== null) {
|
|
455
716
|
active.value = 0;
|
|
456
717
|
owner.value = INTERACTIVE;
|
|
457
718
|
stale.value = 0;
|
|
458
719
|
last = current;
|
|
459
|
-
|
|
720
|
+
if (protocolVersion === 2 && setterV2) {
|
|
721
|
+
setterV2(nativeDriverId, current.clip.x, current.clip.y, current.clip.width, current.clip.height, current.clip.topLeftRadius, current.clip.topRightRadius, current.clip.bottomRightRadius, current.clip.bottomLeftRadius, curveCode(current), current.contentTranslateX, current.contentTranslateY, current.contentScale, true, false, true);
|
|
722
|
+
} else if (isV1Compatible(current)) {
|
|
723
|
+
setter(nativeDriverId, current.clip.x, current.clip.y, current.clip.width, current.clip.height, current.clip.radius, current.contentTranslateX, current.contentTranslateY, true, false);
|
|
724
|
+
}
|
|
460
725
|
}
|
|
461
|
-
}, presentation, driverId, driverId, ownership, suppressDeliveries, activeAnimationId, scalarsStale, disposed, setPresentationHostFunction);
|
|
726
|
+
}, presentation, driverId, driverId, ownership, suppressDeliveries, activeAnimationId, scalarsStale, disposed, setPresentationHostFunction, setPresentationV2HostFunction, PRESENTATION_PROTOCOL_VERSION);
|
|
462
727
|
return () => {
|
|
463
728
|
subscription.remove();
|
|
464
729
|
rejectDriverRequests(driverId);
|