react-native-smooth-clip-view 0.2.9 → 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 +60 -21
- package/android/build.gradle +6 -2
- package/android/proguard-rules.pro +3 -3
- package/android/src/main/cpp/SmoothClipAndroid.h +10 -0
- package/android/src/main/cpp/SmoothClipBindings.cpp +890 -4
- package/android/src/main/cpp/SmoothClipRegistry.cpp +1056 -44
- 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 +371 -18
- package/android/src/main/java/com/smoothclipview/SmoothClipViewManager.kt +138 -24
- package/cpp/SmoothClipAnimationCurve.h +151 -122
- package/cpp/SmoothClipRegistry.h +95 -3
- package/cpp/SmoothClipSharedGeometry.h +146 -7
- package/cpp/SmoothClipVelocityTracker.h +13 -9
- package/ios/SmoothClipGeometry.h +184 -6
- package/ios/SmoothClipRegistry.mm +1028 -22
- 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 +263 -63
- 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 +25 -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 +42 -11
- package/src/drivers.native.ts +824 -124
- 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
|
@@ -0,0 +1,1189 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
import { Platform } from 'react-native';
|
|
3
|
+
import { useSharedValue } from 'react-native-reanimated';
|
|
4
|
+
import { isRNRuntime, scheduleOnRN, scheduleOnUI } from 'react-native-worklets';
|
|
5
|
+
import NativeSmoothClipModule from './smoothClipNative';
|
|
6
|
+
import { getSmoothClipCapabilities } from './capabilities';
|
|
7
|
+
import { allocateDriverId } from './driverState';
|
|
8
|
+
import type { SmoothClipDriver, SmoothClipDriverHandle } from './driverTypes';
|
|
9
|
+
import type {
|
|
10
|
+
SmoothClipBatchEntry,
|
|
11
|
+
SmoothClipGroupAnimationResult,
|
|
12
|
+
SmoothClipGroupCancelBehavior,
|
|
13
|
+
SmoothClipGroupDriver,
|
|
14
|
+
SmoothClipGroupDriverOptions,
|
|
15
|
+
SmoothClipGroupKeyframeAnimation,
|
|
16
|
+
SmoothClipGroupKeyframeEntry,
|
|
17
|
+
SmoothClipGroupMotionAnimation,
|
|
18
|
+
SmoothClipGroupMotionEntry,
|
|
19
|
+
SmoothClipGroupSnapshot,
|
|
20
|
+
} from './groupDriverTypes';
|
|
21
|
+
import {
|
|
22
|
+
canonicalizeClipPresentation,
|
|
23
|
+
clipPresentationEquals,
|
|
24
|
+
type CanonicalSmoothClipPresentation,
|
|
25
|
+
} from './geometry';
|
|
26
|
+
|
|
27
|
+
const INTERACTIVE = 0;
|
|
28
|
+
const NATIVE = 1;
|
|
29
|
+
const PRESENTATION_STRIDE = 12;
|
|
30
|
+
const SNAPSHOT_STRIDE = 13;
|
|
31
|
+
const MOTION_ENTRY_STRIDE = 26;
|
|
32
|
+
const NEEDS_START_STAMP = Platform.OS === 'android';
|
|
33
|
+
const GROUPS_SUPPORTED = getSmoothClipCapabilities().groups;
|
|
34
|
+
|
|
35
|
+
type NativeGroupHostFunctions = Readonly<{
|
|
36
|
+
beginGroupInteractionV2(driverIds: readonly number[]): readonly number[];
|
|
37
|
+
snapshotGroupV2(driverIds: readonly number[]): readonly number[];
|
|
38
|
+
setClipPresentationBatchV2(entries: readonly number[]): boolean;
|
|
39
|
+
animateTimingGroupV2(...args: readonly unknown[]): number;
|
|
40
|
+
animateSpringGroupV2(...args: readonly unknown[]): number;
|
|
41
|
+
animateKeyframesGroupV2(...args: readonly unknown[]): number;
|
|
42
|
+
cancelAnimationGroupV2(groupId: number, behavior: number): readonly number[];
|
|
43
|
+
onClipGroupAnimationComplete?: typeof NativeSmoothClipModule.onClipGroupAnimationComplete;
|
|
44
|
+
}>;
|
|
45
|
+
type RequiredNativeGroupCommands = Required<
|
|
46
|
+
Omit<NativeGroupHostFunctions, 'onClipGroupAnimationComplete'>
|
|
47
|
+
>;
|
|
48
|
+
|
|
49
|
+
const nativeGroups =
|
|
50
|
+
NativeSmoothClipModule as unknown as Partial<NativeGroupHostFunctions>;
|
|
51
|
+
const beginGroupInteractionV2HostFunction =
|
|
52
|
+
nativeGroups.beginGroupInteractionV2;
|
|
53
|
+
const snapshotGroupV2HostFunction = nativeGroups.snapshotGroupV2;
|
|
54
|
+
const setClipPresentationBatchV2HostFunction =
|
|
55
|
+
nativeGroups.setClipPresentationBatchV2;
|
|
56
|
+
const animateTimingGroupV2HostFunction = nativeGroups.animateTimingGroupV2;
|
|
57
|
+
const animateSpringGroupV2HostFunction = nativeGroups.animateSpringGroupV2;
|
|
58
|
+
const animateKeyframesGroupV2HostFunction =
|
|
59
|
+
nativeGroups.animateKeyframesGroupV2;
|
|
60
|
+
const cancelAnimationGroupV2HostFunction = nativeGroups.cancelAnimationGroupV2;
|
|
61
|
+
|
|
62
|
+
type RuntimeGroupRecord = Readonly<{
|
|
63
|
+
controllerId: number;
|
|
64
|
+
drivers: readonly SmoothClipDriver[];
|
|
65
|
+
}>;
|
|
66
|
+
|
|
67
|
+
type NativeGroupDriverState = {
|
|
68
|
+
callbackRef: {
|
|
69
|
+
current: SmoothClipGroupDriverOptions['onAnimationComplete'];
|
|
70
|
+
};
|
|
71
|
+
completionDeferrals: number;
|
|
72
|
+
deferredCompletions: SmoothClipGroupAnimationResult[];
|
|
73
|
+
effectGeneration: number;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const nativeGroupDriverStates = new Map<number, NativeGroupDriverState>();
|
|
77
|
+
|
|
78
|
+
function deliverNativeGroupCompletion(
|
|
79
|
+
controllerId: number,
|
|
80
|
+
groupId: number,
|
|
81
|
+
finished: boolean
|
|
82
|
+
): void {
|
|
83
|
+
const state = nativeGroupDriverStates.get(controllerId);
|
|
84
|
+
if (!state) return;
|
|
85
|
+
const result = { groupId, finished };
|
|
86
|
+
if (state.completionDeferrals > 0) {
|
|
87
|
+
state.deferredCompletions.push(result);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
state.callbackRef.current?.(result);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function deferNativeGroupCompletions(controllerId: number): void {
|
|
94
|
+
const state = nativeGroupDriverStates.get(controllerId);
|
|
95
|
+
if (state) state.completionDeferrals += 1;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function releaseNativeGroupCompletions(controllerId: number): void {
|
|
99
|
+
const state = nativeGroupDriverStates.get(controllerId);
|
|
100
|
+
if (!state || state.completionDeferrals === 0) return;
|
|
101
|
+
state.completionDeferrals -= 1;
|
|
102
|
+
if (state.completionDeferrals !== 0) return;
|
|
103
|
+
const deferred = state.deferredCompletions.splice(0);
|
|
104
|
+
for (const result of deferred) state.callbackRef.current?.(result);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
type GroupRuntimeGlobal = typeof globalThis & {
|
|
108
|
+
__smoothClipGroupRecords?: Record<string, RuntimeGroupRecord>;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
function runtimeGroupRecords(): Record<string, RuntimeGroupRecord> {
|
|
112
|
+
'worklet';
|
|
113
|
+
const runtime = globalThis as GroupRuntimeGlobal;
|
|
114
|
+
runtime.__smoothClipGroupRecords ??= {};
|
|
115
|
+
return runtime.__smoothClipGroupRecords;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function fail(message: string): never {
|
|
119
|
+
'worklet';
|
|
120
|
+
throw new Error(`[SmoothClipView] ${message}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function uiOnly(): never {
|
|
124
|
+
'worklet';
|
|
125
|
+
return fail(
|
|
126
|
+
'group.ui methods must run on the UI runtime. Use group.react from React code.'
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function curveCode(presentation: CanonicalSmoothClipPresentation): number {
|
|
131
|
+
'worklet';
|
|
132
|
+
return presentation.clip.curve === 'continuous' ? 1 : 0;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function appendPresentation(
|
|
136
|
+
values: number[],
|
|
137
|
+
presentation: CanonicalSmoothClipPresentation
|
|
138
|
+
): void {
|
|
139
|
+
'worklet';
|
|
140
|
+
const { clip, contentTranslateX, contentTranslateY, contentScale } =
|
|
141
|
+
presentation;
|
|
142
|
+
values.push(
|
|
143
|
+
clip.x,
|
|
144
|
+
clip.y,
|
|
145
|
+
clip.width,
|
|
146
|
+
clip.height,
|
|
147
|
+
clip.topLeftRadius,
|
|
148
|
+
clip.topRightRadius,
|
|
149
|
+
clip.bottomRightRadius,
|
|
150
|
+
clip.bottomLeftRadius,
|
|
151
|
+
curveCode(presentation),
|
|
152
|
+
contentTranslateX,
|
|
153
|
+
contentTranslateY,
|
|
154
|
+
contentScale
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function presentationFromValues(
|
|
159
|
+
values: readonly number[],
|
|
160
|
+
offset: number
|
|
161
|
+
): CanonicalSmoothClipPresentation | null {
|
|
162
|
+
'worklet';
|
|
163
|
+
if (values.length < offset + PRESENTATION_STRIDE) return null;
|
|
164
|
+
return canonicalizeClipPresentation({
|
|
165
|
+
clip: {
|
|
166
|
+
x: values[offset] as number,
|
|
167
|
+
y: values[offset + 1] as number,
|
|
168
|
+
width: values[offset + 2] as number,
|
|
169
|
+
height: values[offset + 3] as number,
|
|
170
|
+
radius: 0,
|
|
171
|
+
topLeftRadius: values[offset + 4] as number,
|
|
172
|
+
topRightRadius: values[offset + 5] as number,
|
|
173
|
+
bottomRightRadius: values[offset + 6] as number,
|
|
174
|
+
bottomLeftRadius: values[offset + 7] as number,
|
|
175
|
+
curve: values[offset + 8] === 1 ? 'continuous' : 'circular',
|
|
176
|
+
},
|
|
177
|
+
contentTranslateX: values[offset + 9] as number,
|
|
178
|
+
contentTranslateY: values[offset + 10] as number,
|
|
179
|
+
contentScale: values[offset + 11] as number,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
type ValidDriver = Readonly<{
|
|
184
|
+
driver: SmoothClipDriver;
|
|
185
|
+
handle: SmoothClipDriverHandle;
|
|
186
|
+
}>;
|
|
187
|
+
|
|
188
|
+
function validateDrivers(
|
|
189
|
+
drivers: readonly SmoothClipDriver[]
|
|
190
|
+
): readonly ValidDriver[] {
|
|
191
|
+
'worklet';
|
|
192
|
+
if (drivers.length === 0) return fail('A group must contain a driver.');
|
|
193
|
+
const seen: Record<string, true> = {};
|
|
194
|
+
const result: ValidDriver[] = [];
|
|
195
|
+
for (const driver of drivers) {
|
|
196
|
+
const handle = driver.__smoothClipHandle;
|
|
197
|
+
if (
|
|
198
|
+
driver.kind !== 'hybrid' ||
|
|
199
|
+
handle === undefined ||
|
|
200
|
+
!Number.isSafeInteger(handle.driverId) ||
|
|
201
|
+
handle.driverId <= 0 ||
|
|
202
|
+
handle.disposed.value !== 0
|
|
203
|
+
) {
|
|
204
|
+
return fail('Every group participant must be a live SmoothClip driver.');
|
|
205
|
+
}
|
|
206
|
+
const key = String(handle.driverId);
|
|
207
|
+
if (seen[key]) return fail('A group cannot contain duplicate drivers.');
|
|
208
|
+
seen[key] = true;
|
|
209
|
+
result.push({ driver, handle });
|
|
210
|
+
}
|
|
211
|
+
return result;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function requireNativeGroups(): RequiredNativeGroupCommands {
|
|
215
|
+
'worklet';
|
|
216
|
+
if (
|
|
217
|
+
!GROUPS_SUPPORTED ||
|
|
218
|
+
beginGroupInteractionV2HostFunction === undefined ||
|
|
219
|
+
snapshotGroupV2HostFunction === undefined ||
|
|
220
|
+
setClipPresentationBatchV2HostFunction === undefined ||
|
|
221
|
+
animateTimingGroupV2HostFunction === undefined ||
|
|
222
|
+
animateSpringGroupV2HostFunction === undefined ||
|
|
223
|
+
animateKeyframesGroupV2HostFunction === undefined ||
|
|
224
|
+
cancelAnimationGroupV2HostFunction === undefined
|
|
225
|
+
) {
|
|
226
|
+
return fail('Native grouped presentation protocol V2 is unavailable.');
|
|
227
|
+
}
|
|
228
|
+
return {
|
|
229
|
+
beginGroupInteractionV2: beginGroupInteractionV2HostFunction,
|
|
230
|
+
snapshotGroupV2: snapshotGroupV2HostFunction,
|
|
231
|
+
setClipPresentationBatchV2: setClipPresentationBatchV2HostFunction,
|
|
232
|
+
animateTimingGroupV2: animateTimingGroupV2HostFunction,
|
|
233
|
+
animateSpringGroupV2: animateSpringGroupV2HostFunction,
|
|
234
|
+
animateKeyframesGroupV2: animateKeyframesGroupV2HostFunction,
|
|
235
|
+
cancelAnimationGroupV2: cancelAnimationGroupV2HostFunction,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
type ParsedGroupSnapshots = Readonly<{
|
|
240
|
+
snapshots: readonly SmoothClipGroupSnapshot[];
|
|
241
|
+
unavailable: boolean;
|
|
242
|
+
}>;
|
|
243
|
+
|
|
244
|
+
function parseSnapshots(
|
|
245
|
+
drivers: readonly SmoothClipDriver[],
|
|
246
|
+
values: readonly number[]
|
|
247
|
+
): ParsedGroupSnapshots {
|
|
248
|
+
'worklet';
|
|
249
|
+
if (values.length !== drivers.length * SNAPSHOT_STRIDE) {
|
|
250
|
+
return fail('Native returned an invalid group snapshot.');
|
|
251
|
+
}
|
|
252
|
+
const snapshots: SmoothClipGroupSnapshot[] = [];
|
|
253
|
+
let unavailable = false;
|
|
254
|
+
for (let index = 0; index < drivers.length; index += 1) {
|
|
255
|
+
const offset = index * SNAPSHOT_STRIDE;
|
|
256
|
+
const readyCode = values[offset];
|
|
257
|
+
let presentation = presentationFromValues(values, offset + 1);
|
|
258
|
+
if (readyCode !== 0 && readyCode !== 1) {
|
|
259
|
+
return fail('Native returned an invalid group snapshot.');
|
|
260
|
+
}
|
|
261
|
+
if (presentation === null && readyCode === 0) {
|
|
262
|
+
presentation = canonicalizeClipPresentation(
|
|
263
|
+
drivers[index]?.__smoothClipHandle?.presentation.value as never
|
|
264
|
+
);
|
|
265
|
+
unavailable = true;
|
|
266
|
+
}
|
|
267
|
+
if (presentation === null) {
|
|
268
|
+
return fail('Native returned an invalid group snapshot.');
|
|
269
|
+
}
|
|
270
|
+
snapshots.push({
|
|
271
|
+
driver: drivers[index] as SmoothClipDriver,
|
|
272
|
+
presentation,
|
|
273
|
+
ready: readyCode === 1,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
return { snapshots, unavailable };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function applyInteractiveSnapshots(
|
|
280
|
+
snapshots: readonly SmoothClipGroupSnapshot[]
|
|
281
|
+
): void {
|
|
282
|
+
'worklet';
|
|
283
|
+
// Keep individual driver listeners from re-emitting a transaction that the
|
|
284
|
+
// group engine has already committed atomically.
|
|
285
|
+
for (const snapshot of snapshots) {
|
|
286
|
+
const handle = snapshot.driver.__smoothClipHandle;
|
|
287
|
+
if (handle) handle.ownership.value = NATIVE;
|
|
288
|
+
}
|
|
289
|
+
for (const snapshot of snapshots) {
|
|
290
|
+
const handle = snapshot.driver.__smoothClipHandle;
|
|
291
|
+
if (!handle) continue;
|
|
292
|
+
handle.activeAnimationId.value = 0;
|
|
293
|
+
handle.presentation.value = snapshot.presentation;
|
|
294
|
+
handle.ownership.value = INTERACTIVE;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function suspensionPolicyCode(value: 'pause' | 'finish' | undefined): number {
|
|
299
|
+
'worklet';
|
|
300
|
+
return value === 'finish' ? 1 : 0;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function reduceMotionCode(value: 'system' | 'always' | 'never'): number {
|
|
304
|
+
'worklet';
|
|
305
|
+
if (value === 'always') return 1;
|
|
306
|
+
if (value === 'never') return 2;
|
|
307
|
+
return 0;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function animationStartTimestamp(): number {
|
|
311
|
+
'worklet';
|
|
312
|
+
if (!NEEDS_START_STAMP) return Number.NaN;
|
|
313
|
+
const runtime = globalThis as {
|
|
314
|
+
__frameTimestamp?: number;
|
|
315
|
+
_getAnimationTimestamp?: () => number;
|
|
316
|
+
};
|
|
317
|
+
return (
|
|
318
|
+
runtime.__frameTimestamp ||
|
|
319
|
+
(typeof runtime._getAnimationTimestamp === 'function'
|
|
320
|
+
? runtime._getAnimationTimestamp()
|
|
321
|
+
: Number.NaN)
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function animationIsFinite(
|
|
326
|
+
animation: SmoothClipGroupMotionAnimation | SmoothClipGroupKeyframeAnimation
|
|
327
|
+
): boolean {
|
|
328
|
+
'worklet';
|
|
329
|
+
if (
|
|
330
|
+
animation.suspensionPolicy !== undefined &&
|
|
331
|
+
animation.suspensionPolicy !== 'pause' &&
|
|
332
|
+
animation.suspensionPolicy !== 'finish'
|
|
333
|
+
) {
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
if (animation.type === 'timing') {
|
|
337
|
+
if (
|
|
338
|
+
!Array.isArray(animation.controlPoints) ||
|
|
339
|
+
animation.controlPoints.length !== 4
|
|
340
|
+
) {
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
const [x1, y1, x2, y2] = animation.controlPoints;
|
|
344
|
+
return (
|
|
345
|
+
Number.isFinite(animation.duration) &&
|
|
346
|
+
animation.duration >= 0 &&
|
|
347
|
+
[x1, y1, x2, y2].every(Number.isFinite) &&
|
|
348
|
+
x1 >= 0 &&
|
|
349
|
+
x1 <= 1 &&
|
|
350
|
+
x2 >= 0 &&
|
|
351
|
+
x2 <= 1
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
if (animation.type === 'spring') {
|
|
355
|
+
const initialVelocity =
|
|
356
|
+
animation.initialVelocity === 'inherit' ||
|
|
357
|
+
animation.initialVelocity === undefined
|
|
358
|
+
? 0
|
|
359
|
+
: animation.initialVelocity;
|
|
360
|
+
return (
|
|
361
|
+
[
|
|
362
|
+
animation.mass ?? 1,
|
|
363
|
+
animation.stiffness ?? 100,
|
|
364
|
+
animation.damping ?? 10,
|
|
365
|
+
initialVelocity,
|
|
366
|
+
].every(Number.isFinite) &&
|
|
367
|
+
(animation.mass ?? 1) > 0 &&
|
|
368
|
+
(animation.stiffness ?? 100) > 0 &&
|
|
369
|
+
(animation.damping ?? 10) >= 0
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
return (
|
|
373
|
+
animation.type === 'keyframes' &&
|
|
374
|
+
Number.isFinite(animation.duration) &&
|
|
375
|
+
animation.duration >= 0
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function springScaleIsProvablyPositive(
|
|
380
|
+
starts: readonly CanonicalSmoothClipPresentation[],
|
|
381
|
+
targets: readonly CanonicalSmoothClipPresentation[],
|
|
382
|
+
animation: Extract<SmoothClipGroupMotionAnimation, { type: 'spring' }>
|
|
383
|
+
): boolean {
|
|
384
|
+
'worklet';
|
|
385
|
+
let changesScale = false;
|
|
386
|
+
for (let index = 0; index < starts.length; index += 1) {
|
|
387
|
+
if (starts[index]?.contentScale !== targets[index]?.contentScale) {
|
|
388
|
+
changesScale = true;
|
|
389
|
+
break;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
if (!changesScale) return true;
|
|
393
|
+
|
|
394
|
+
// A zero-velocity critically/over-damped unit-step response is monotonic,
|
|
395
|
+
// so interpolation stays between two already-positive endpoints. Other
|
|
396
|
+
// configurations may overshoot; callers can compile those to keyframes.
|
|
397
|
+
const mass = animation.mass ?? 1;
|
|
398
|
+
const stiffness = animation.stiffness ?? 100;
|
|
399
|
+
const damping = animation.damping ?? 10;
|
|
400
|
+
return (
|
|
401
|
+
animation.initialVelocity === 0 && damping * damping >= 4 * mass * stiffness
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function snapshotsForMotionStarts(
|
|
406
|
+
validDrivers: readonly ValidDriver[],
|
|
407
|
+
entries: readonly SmoothClipGroupMotionEntry[]
|
|
408
|
+
): readonly CanonicalSmoothClipPresentation[] {
|
|
409
|
+
'worklet';
|
|
410
|
+
const missingFrom = entries.some((entry) => entry.from === undefined);
|
|
411
|
+
const sampled = missingFrom
|
|
412
|
+
? parseSnapshots(
|
|
413
|
+
validDrivers.map((entry) => entry.driver),
|
|
414
|
+
requireNativeGroups().snapshotGroupV2(
|
|
415
|
+
validDrivers.map((entry) => entry.handle.driverId)
|
|
416
|
+
)
|
|
417
|
+
).snapshots
|
|
418
|
+
: [];
|
|
419
|
+
const starts: CanonicalSmoothClipPresentation[] = [];
|
|
420
|
+
for (let index = 0; index < entries.length; index += 1) {
|
|
421
|
+
const explicit = entries[index]?.from;
|
|
422
|
+
const presentation =
|
|
423
|
+
explicit === undefined
|
|
424
|
+
? (sampled[index]?.presentation ?? null)
|
|
425
|
+
: canonicalizeClipPresentation(explicit);
|
|
426
|
+
if (presentation === null) return fail('A group start is invalid.');
|
|
427
|
+
starts.push(presentation);
|
|
428
|
+
}
|
|
429
|
+
return starts;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function registerNativeGroup(
|
|
433
|
+
controllerId: number,
|
|
434
|
+
groupId: number,
|
|
435
|
+
drivers: readonly SmoothClipDriver[],
|
|
436
|
+
targets: readonly CanonicalSmoothClipPresentation[]
|
|
437
|
+
): void {
|
|
438
|
+
'worklet';
|
|
439
|
+
const records = runtimeGroupRecords();
|
|
440
|
+
records[String(groupId)] = { controllerId, drivers };
|
|
441
|
+
for (let index = 0; index < drivers.length; index += 1) {
|
|
442
|
+
const handle = drivers[index]?.__smoothClipHandle;
|
|
443
|
+
const target = targets[index];
|
|
444
|
+
if (!handle || !target) continue;
|
|
445
|
+
handle.ownership.value = NATIVE;
|
|
446
|
+
handle.activeAnimationId.value = groupId;
|
|
447
|
+
handle.presentation.value = target;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function synchronizeGroupCompletionOnUI(
|
|
452
|
+
controllerId: number,
|
|
453
|
+
groupId: number,
|
|
454
|
+
finished: boolean,
|
|
455
|
+
driverIds: readonly number[]
|
|
456
|
+
): void {
|
|
457
|
+
'worklet';
|
|
458
|
+
const records = runtimeGroupRecords();
|
|
459
|
+
const record = records[String(groupId)];
|
|
460
|
+
if (
|
|
461
|
+
!record ||
|
|
462
|
+
record.controllerId !== controllerId ||
|
|
463
|
+
record.drivers.length !== driverIds.length
|
|
464
|
+
) {
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
for (let index = 0; index < record.drivers.length; index += 1) {
|
|
468
|
+
if (
|
|
469
|
+
record.drivers[index]?.__smoothClipHandle?.driverId !== driverIds[index]
|
|
470
|
+
) {
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
let snapshots: readonly SmoothClipGroupSnapshot[] = [];
|
|
475
|
+
if (!finished) {
|
|
476
|
+
try {
|
|
477
|
+
snapshots = parseSnapshots(
|
|
478
|
+
record.drivers,
|
|
479
|
+
requireNativeGroups().snapshotGroupV2(
|
|
480
|
+
record.drivers.map(
|
|
481
|
+
(driver) => driver.__smoothClipHandle?.driverId ?? 0
|
|
482
|
+
)
|
|
483
|
+
)
|
|
484
|
+
).snapshots;
|
|
485
|
+
} catch {
|
|
486
|
+
snapshots = [];
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
for (let index = 0; index < record.drivers.length; index += 1) {
|
|
490
|
+
const driver = record.drivers[index];
|
|
491
|
+
if (driver === undefined) continue;
|
|
492
|
+
const handle = driver.__smoothClipHandle;
|
|
493
|
+
if (handle === undefined || handle.activeAnimationId.value !== groupId) {
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
const snapshot = snapshots[index];
|
|
497
|
+
if (snapshot) handle.presentation.value = snapshot.presentation;
|
|
498
|
+
handle.activeAnimationId.value = 0;
|
|
499
|
+
handle.ownership.value = INTERACTIVE;
|
|
500
|
+
}
|
|
501
|
+
delete records[String(groupId)];
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
type PendingGroupRequest = Readonly<{
|
|
505
|
+
resolve(value: unknown): void;
|
|
506
|
+
reject(error: Error): void;
|
|
507
|
+
teardownValue?: unknown;
|
|
508
|
+
releasesCompletions?: boolean;
|
|
509
|
+
}>;
|
|
510
|
+
|
|
511
|
+
const pendingGroupRequests = new Map<number, PendingGroupRequest>();
|
|
512
|
+
const requestIdsByController = new Map<number, Set<number>>();
|
|
513
|
+
let nextGroupRequestId = 0;
|
|
514
|
+
|
|
515
|
+
function createGroupRequest<T>(
|
|
516
|
+
controllerId: number,
|
|
517
|
+
teardownResolution?: Readonly<{ value: unknown }>,
|
|
518
|
+
deferCompletions = false
|
|
519
|
+
): Readonly<{ requestId: number; promise: Promise<T> }> {
|
|
520
|
+
nextGroupRequestId = (nextGroupRequestId % 0x7ffffffe) + 1;
|
|
521
|
+
const requestId = nextGroupRequestId;
|
|
522
|
+
let resolveRequest!: (value: T) => void;
|
|
523
|
+
let rejectRequest!: (error: Error) => void;
|
|
524
|
+
const promise = new Promise<T>((resolve, reject) => {
|
|
525
|
+
resolveRequest = resolve;
|
|
526
|
+
rejectRequest = reject;
|
|
527
|
+
});
|
|
528
|
+
pendingGroupRequests.set(requestId, {
|
|
529
|
+
resolve: resolveRequest as (value: unknown) => void,
|
|
530
|
+
reject: rejectRequest,
|
|
531
|
+
...(teardownResolution === undefined
|
|
532
|
+
? {}
|
|
533
|
+
: { teardownValue: teardownResolution.value }),
|
|
534
|
+
...(deferCompletions ? { releasesCompletions: true } : {}),
|
|
535
|
+
});
|
|
536
|
+
const ids = requestIdsByController.get(controllerId) ?? new Set<number>();
|
|
537
|
+
ids.add(requestId);
|
|
538
|
+
requestIdsByController.set(controllerId, ids);
|
|
539
|
+
if (deferCompletions) deferNativeGroupCompletions(controllerId);
|
|
540
|
+
return { requestId, promise };
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function settleGroupRequest(
|
|
544
|
+
controllerId: number,
|
|
545
|
+
requestId: number,
|
|
546
|
+
succeeded: boolean,
|
|
547
|
+
value: unknown,
|
|
548
|
+
message: string
|
|
549
|
+
): void {
|
|
550
|
+
const request = pendingGroupRequests.get(requestId);
|
|
551
|
+
pendingGroupRequests.delete(requestId);
|
|
552
|
+
const ids = requestIdsByController.get(controllerId);
|
|
553
|
+
ids?.delete(requestId);
|
|
554
|
+
if (ids?.size === 0) requestIdsByController.delete(controllerId);
|
|
555
|
+
if (!request) return;
|
|
556
|
+
if (succeeded) request.resolve(value);
|
|
557
|
+
else request.reject(new Error(message));
|
|
558
|
+
if (request.releasesCompletions) {
|
|
559
|
+
// One turn resolves the Promise; the second lets reactions already
|
|
560
|
+
// attached to that Promise observe settlement before callbacks run.
|
|
561
|
+
queueMicrotask(() => {
|
|
562
|
+
queueMicrotask(() => releaseNativeGroupCompletions(controllerId));
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function rejectGroupRequests(controllerId: number): void {
|
|
568
|
+
const ids = requestIdsByController.get(controllerId);
|
|
569
|
+
if (ids) {
|
|
570
|
+
for (const requestId of ids) {
|
|
571
|
+
const request = pendingGroupRequests.get(requestId);
|
|
572
|
+
pendingGroupRequests.delete(requestId);
|
|
573
|
+
if (!request) continue;
|
|
574
|
+
if ('teardownValue' in request) request.resolve(request.teardownValue);
|
|
575
|
+
else
|
|
576
|
+
request.reject(
|
|
577
|
+
new Error('[SmoothClipView] Group driver was destroyed.')
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
requestIdsByController.delete(controllerId);
|
|
581
|
+
}
|
|
582
|
+
const state = nativeGroupDriverStates.get(controllerId);
|
|
583
|
+
if (state) state.completionDeferrals = 0;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function detachNativeGroupDriverState(
|
|
587
|
+
controllerId: number,
|
|
588
|
+
state: NativeGroupDriverState
|
|
589
|
+
): void {
|
|
590
|
+
state.completionDeferrals = 0;
|
|
591
|
+
const deferred = state.deferredCompletions.splice(0);
|
|
592
|
+
if (nativeGroupDriverStates.get(controllerId) === state) {
|
|
593
|
+
nativeGroupDriverStates.delete(controllerId);
|
|
594
|
+
}
|
|
595
|
+
if (deferred.length === 0) return;
|
|
596
|
+
queueMicrotask(() => {
|
|
597
|
+
queueMicrotask(() => {
|
|
598
|
+
for (const result of deferred) state.callbackRef.current?.(result);
|
|
599
|
+
});
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function queueNativeTeardownCompletion(
|
|
604
|
+
controllerId: number,
|
|
605
|
+
groupId: number
|
|
606
|
+
): void {
|
|
607
|
+
const state = nativeGroupDriverStates.get(controllerId);
|
|
608
|
+
if (!state) return;
|
|
609
|
+
queueMicrotask(() => {
|
|
610
|
+
queueMicrotask(() => {
|
|
611
|
+
state.callbackRef.current?.({ groupId, finished: false });
|
|
612
|
+
});
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function finalizeNativeGroupDriverTeardown(
|
|
617
|
+
controllerId: number,
|
|
618
|
+
effectGeneration: number
|
|
619
|
+
): void {
|
|
620
|
+
const state = nativeGroupDriverStates.get(controllerId);
|
|
621
|
+
if (!state || state.effectGeneration !== effectGeneration) return;
|
|
622
|
+
detachNativeGroupDriverState(controllerId, state);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function errorMessage(error: unknown): string {
|
|
626
|
+
'worklet';
|
|
627
|
+
return error instanceof Error
|
|
628
|
+
? error.message
|
|
629
|
+
: '[SmoothClipView] Group operation failed.';
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
export function useSmoothClipGroupDriver(
|
|
633
|
+
options: SmoothClipGroupDriverOptions = {}
|
|
634
|
+
): SmoothClipGroupDriver {
|
|
635
|
+
const callbackRef = useRef(options.onAnimationComplete);
|
|
636
|
+
const stateRef = useRef<NativeGroupDriverState | null>(null);
|
|
637
|
+
const groupRef = useRef<SmoothClipGroupDriver | null>(null);
|
|
638
|
+
const disposed = useSharedValue(0);
|
|
639
|
+
callbackRef.current = options.onAnimationComplete;
|
|
640
|
+
if (stateRef.current === null) {
|
|
641
|
+
stateRef.current = {
|
|
642
|
+
callbackRef,
|
|
643
|
+
completionDeferrals: 0,
|
|
644
|
+
deferredCompletions: [],
|
|
645
|
+
effectGeneration: 0,
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
if (groupRef.current === null) {
|
|
650
|
+
const controllerId = allocateDriverId();
|
|
651
|
+
const reduceMotion = reduceMotionCode(options.reduceMotion ?? 'system');
|
|
652
|
+
|
|
653
|
+
const beginOnUI = (
|
|
654
|
+
drivers: readonly SmoothClipDriver[]
|
|
655
|
+
): readonly SmoothClipGroupSnapshot[] => {
|
|
656
|
+
'worklet';
|
|
657
|
+
if (disposed.value !== 0) return fail('Group driver was destroyed.');
|
|
658
|
+
const valid = validateDrivers(drivers);
|
|
659
|
+
const values = requireNativeGroups().beginGroupInteractionV2(
|
|
660
|
+
valid.map((entry) => entry.handle.driverId)
|
|
661
|
+
);
|
|
662
|
+
const parsed = parseSnapshots(drivers, values);
|
|
663
|
+
// An unavailable wire presentation means native rejected the atomic
|
|
664
|
+
// interaction preflight. Return readiness without changing JS ownership.
|
|
665
|
+
if (!parsed.unavailable) applyInteractiveSnapshots(parsed.snapshots);
|
|
666
|
+
return parsed.snapshots;
|
|
667
|
+
};
|
|
668
|
+
|
|
669
|
+
const snapshotOnUI = (
|
|
670
|
+
drivers: readonly SmoothClipDriver[]
|
|
671
|
+
): readonly SmoothClipGroupSnapshot[] => {
|
|
672
|
+
'worklet';
|
|
673
|
+
if (disposed.value !== 0) return fail('Group driver was destroyed.');
|
|
674
|
+
const valid = validateDrivers(drivers);
|
|
675
|
+
return parseSnapshots(
|
|
676
|
+
drivers,
|
|
677
|
+
requireNativeGroups().snapshotGroupV2(
|
|
678
|
+
valid.map((entry) => entry.handle.driverId)
|
|
679
|
+
)
|
|
680
|
+
).snapshots;
|
|
681
|
+
};
|
|
682
|
+
|
|
683
|
+
const setBatchOnUI = (entries: readonly SmoothClipBatchEntry[]): void => {
|
|
684
|
+
'worklet';
|
|
685
|
+
if (disposed.value !== 0) return fail('Group driver was destroyed.');
|
|
686
|
+
const valid = validateDrivers(entries.map((entry) => entry.driver));
|
|
687
|
+
const canonical: CanonicalSmoothClipPresentation[] = [];
|
|
688
|
+
const values: number[] = [];
|
|
689
|
+
for (let index = 0; index < entries.length; index += 1) {
|
|
690
|
+
const presentation = canonicalizeClipPresentation(
|
|
691
|
+
entries[index]?.presentation as never
|
|
692
|
+
);
|
|
693
|
+
if (presentation === null)
|
|
694
|
+
return fail('A batch presentation is invalid.');
|
|
695
|
+
canonical.push(presentation);
|
|
696
|
+
values.push(valid[index]?.handle.driverId as number);
|
|
697
|
+
appendPresentation(values, presentation);
|
|
698
|
+
}
|
|
699
|
+
if (!requireNativeGroups().setClipPresentationBatchV2(values)) {
|
|
700
|
+
return fail('Native rejected the complete presentation batch.');
|
|
701
|
+
}
|
|
702
|
+
for (const entry of valid) entry.handle.ownership.value = NATIVE;
|
|
703
|
+
for (let index = 0; index < valid.length; index += 1) {
|
|
704
|
+
const handle = valid[index]?.handle;
|
|
705
|
+
const presentation = canonical[index];
|
|
706
|
+
if (!handle || !presentation) continue;
|
|
707
|
+
handle.activeAnimationId.value = 0;
|
|
708
|
+
handle.presentation.value = presentation;
|
|
709
|
+
handle.ownership.value = INTERACTIVE;
|
|
710
|
+
}
|
|
711
|
+
};
|
|
712
|
+
|
|
713
|
+
const animateOnUI = (
|
|
714
|
+
entries:
|
|
715
|
+
| readonly SmoothClipGroupMotionEntry[]
|
|
716
|
+
| readonly SmoothClipGroupKeyframeEntry[],
|
|
717
|
+
animation:
|
|
718
|
+
SmoothClipGroupMotionAnimation | SmoothClipGroupKeyframeAnimation
|
|
719
|
+
): number => {
|
|
720
|
+
'worklet';
|
|
721
|
+
if (disposed.value !== 0) return fail('Group driver was destroyed.');
|
|
722
|
+
if (!animationIsFinite(animation))
|
|
723
|
+
return fail('Group animation is invalid.');
|
|
724
|
+
const drivers = entries.map((entry) => entry.driver);
|
|
725
|
+
const valid = validateDrivers(drivers);
|
|
726
|
+
const targets: CanonicalSmoothClipPresentation[] = [];
|
|
727
|
+
for (const entry of entries) {
|
|
728
|
+
const target = canonicalizeClipPresentation(entry.target);
|
|
729
|
+
if (target === null) return fail('A group target is invalid.');
|
|
730
|
+
targets.push(target);
|
|
731
|
+
}
|
|
732
|
+
const starts = snapshotsForMotionStarts(
|
|
733
|
+
valid,
|
|
734
|
+
entries as readonly SmoothClipGroupMotionEntry[]
|
|
735
|
+
);
|
|
736
|
+
for (let index = 0; index < starts.length; index += 1) {
|
|
737
|
+
if (starts[index]?.clip.curve !== targets[index]?.clip.curve) {
|
|
738
|
+
return fail('Curve-changing animations must use streamed rendering.');
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
const native = requireNativeGroups();
|
|
743
|
+
const values: number[] = [];
|
|
744
|
+
let groupId = 0;
|
|
745
|
+
if (animation.type === 'keyframes') {
|
|
746
|
+
for (let index = 0; index < entries.length; index += 1) {
|
|
747
|
+
const entry = entries[index] as SmoothClipGroupKeyframeEntry;
|
|
748
|
+
const target = targets[index] as CanonicalSmoothClipPresentation;
|
|
749
|
+
const start = starts[index] as CanonicalSmoothClipPresentation;
|
|
750
|
+
const frames: Array<
|
|
751
|
+
Readonly<{
|
|
752
|
+
offset: number;
|
|
753
|
+
presentation: CanonicalSmoothClipPresentation;
|
|
754
|
+
}>
|
|
755
|
+
> = [];
|
|
756
|
+
let previousOffset = -1;
|
|
757
|
+
for (const frame of entry.frames) {
|
|
758
|
+
const canonical = canonicalizeClipPresentation(frame.presentation);
|
|
759
|
+
const presentation =
|
|
760
|
+
frames.length === 0 && entry.from === undefined
|
|
761
|
+
? start
|
|
762
|
+
: canonical;
|
|
763
|
+
if (
|
|
764
|
+
canonical === null ||
|
|
765
|
+
presentation === null ||
|
|
766
|
+
!Number.isFinite(frame.offset) ||
|
|
767
|
+
frame.offset <= previousOffset ||
|
|
768
|
+
frame.offset < 0 ||
|
|
769
|
+
frame.offset > 1 ||
|
|
770
|
+
presentation.clip.curve !== target.clip.curve
|
|
771
|
+
) {
|
|
772
|
+
return fail('Group keyframes are inconsistent.');
|
|
773
|
+
}
|
|
774
|
+
previousOffset = frame.offset;
|
|
775
|
+
frames.push({ offset: frame.offset, presentation });
|
|
776
|
+
}
|
|
777
|
+
if (
|
|
778
|
+
frames.length < 2 ||
|
|
779
|
+
frames[0]?.offset !== 0 ||
|
|
780
|
+
frames[frames.length - 1]?.offset !== 1 ||
|
|
781
|
+
!clipPresentationEquals(
|
|
782
|
+
frames[frames.length - 1]?.presentation ?? null,
|
|
783
|
+
target
|
|
784
|
+
) ||
|
|
785
|
+
(entry.from !== undefined &&
|
|
786
|
+
!clipPresentationEquals(frames[0]?.presentation ?? null, start))
|
|
787
|
+
) {
|
|
788
|
+
return fail('Group keyframes are inconsistent.');
|
|
789
|
+
}
|
|
790
|
+
values.push(valid[index]?.handle.driverId as number);
|
|
791
|
+
values.push(entry.from === undefined ? 0 : 1);
|
|
792
|
+
appendPresentation(values, start);
|
|
793
|
+
appendPresentation(values, target);
|
|
794
|
+
values.push(frames.length);
|
|
795
|
+
for (const frame of frames) {
|
|
796
|
+
values.push(frame.offset);
|
|
797
|
+
appendPresentation(values, frame.presentation);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
groupId = native.animateKeyframesGroupV2(
|
|
801
|
+
controllerId,
|
|
802
|
+
values,
|
|
803
|
+
animation.duration,
|
|
804
|
+
reduceMotion,
|
|
805
|
+
suspensionPolicyCode(animation.suspensionPolicy),
|
|
806
|
+
animationStartTimestamp()
|
|
807
|
+
);
|
|
808
|
+
} else {
|
|
809
|
+
for (let index = 0; index < entries.length; index += 1) {
|
|
810
|
+
const entry = entries[index] as SmoothClipGroupMotionEntry;
|
|
811
|
+
values.push(valid[index]?.handle.driverId as number);
|
|
812
|
+
values.push(entry.from === undefined ? 0 : 1);
|
|
813
|
+
appendPresentation(
|
|
814
|
+
values,
|
|
815
|
+
starts[index] as CanonicalSmoothClipPresentation
|
|
816
|
+
);
|
|
817
|
+
appendPresentation(
|
|
818
|
+
values,
|
|
819
|
+
targets[index] as CanonicalSmoothClipPresentation
|
|
820
|
+
);
|
|
821
|
+
}
|
|
822
|
+
if (values.length !== entries.length * MOTION_ENTRY_STRIDE) {
|
|
823
|
+
return fail('Internal group wire encoding failed.');
|
|
824
|
+
}
|
|
825
|
+
if (animation.type === 'timing') {
|
|
826
|
+
const [x1, y1, x2, y2] = animation.controlPoints;
|
|
827
|
+
groupId = native.animateTimingGroupV2(
|
|
828
|
+
controllerId,
|
|
829
|
+
values,
|
|
830
|
+
animation.duration,
|
|
831
|
+
x1,
|
|
832
|
+
y1,
|
|
833
|
+
x2,
|
|
834
|
+
y2,
|
|
835
|
+
reduceMotion,
|
|
836
|
+
suspensionPolicyCode(animation.suspensionPolicy),
|
|
837
|
+
animationStartTimestamp()
|
|
838
|
+
);
|
|
839
|
+
} else {
|
|
840
|
+
if (!springScaleIsProvablyPositive(starts, targets, animation)) {
|
|
841
|
+
return fail(
|
|
842
|
+
'This scale-changing spring is not provably positive; compile it to keyframes.'
|
|
843
|
+
);
|
|
844
|
+
}
|
|
845
|
+
const inheritVelocity =
|
|
846
|
+
animation.initialVelocity === undefined ||
|
|
847
|
+
animation.initialVelocity === 'inherit';
|
|
848
|
+
groupId = native.animateSpringGroupV2(
|
|
849
|
+
controllerId,
|
|
850
|
+
values,
|
|
851
|
+
animation.mass ?? 1,
|
|
852
|
+
animation.stiffness ?? 100,
|
|
853
|
+
animation.damping ?? 10,
|
|
854
|
+
inheritVelocity ? 0 : animation.initialVelocity,
|
|
855
|
+
inheritVelocity,
|
|
856
|
+
reduceMotion,
|
|
857
|
+
suspensionPolicyCode(animation.suspensionPolicy),
|
|
858
|
+
animationStartTimestamp()
|
|
859
|
+
);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
if (!Number.isInteger(groupId) || groupId <= 0) {
|
|
863
|
+
return fail('Native rejected the complete group animation.');
|
|
864
|
+
}
|
|
865
|
+
registerNativeGroup(controllerId, groupId, drivers, targets);
|
|
866
|
+
return groupId;
|
|
867
|
+
};
|
|
868
|
+
|
|
869
|
+
const cancelOnUI = (
|
|
870
|
+
groupId: number,
|
|
871
|
+
behavior: SmoothClipGroupCancelBehavior = 'freeze'
|
|
872
|
+
): readonly SmoothClipGroupSnapshot[] => {
|
|
873
|
+
'worklet';
|
|
874
|
+
if (disposed.value !== 0) return fail('Group driver was destroyed.');
|
|
875
|
+
if (!Number.isSafeInteger(groupId) || groupId <= 0) {
|
|
876
|
+
return fail('A group ID must be a positive integer.');
|
|
877
|
+
}
|
|
878
|
+
if (behavior !== 'freeze' && behavior !== 'finish') {
|
|
879
|
+
return fail('Group cancel behavior is invalid.');
|
|
880
|
+
}
|
|
881
|
+
const records = runtimeGroupRecords();
|
|
882
|
+
const record = records[String(groupId)];
|
|
883
|
+
if (!record || record.controllerId !== controllerId) return [];
|
|
884
|
+
const values = requireNativeGroups().cancelAnimationGroupV2(
|
|
885
|
+
groupId,
|
|
886
|
+
behavior === 'finish' ? 1 : 0
|
|
887
|
+
);
|
|
888
|
+
if (values.length === 0) {
|
|
889
|
+
for (const driver of record.drivers) {
|
|
890
|
+
const handle = driver.__smoothClipHandle;
|
|
891
|
+
if (!handle || handle.activeAnimationId.value !== groupId) continue;
|
|
892
|
+
handle.activeAnimationId.value = 0;
|
|
893
|
+
handle.ownership.value = INTERACTIVE;
|
|
894
|
+
}
|
|
895
|
+
delete records[String(groupId)];
|
|
896
|
+
return [];
|
|
897
|
+
}
|
|
898
|
+
const snapshots = parseSnapshots(record.drivers, values).snapshots;
|
|
899
|
+
applyInteractiveSnapshots(snapshots);
|
|
900
|
+
delete records[String(groupId)];
|
|
901
|
+
return snapshots;
|
|
902
|
+
};
|
|
903
|
+
|
|
904
|
+
const group: SmoothClipGroupDriver = {
|
|
905
|
+
kind: 'group',
|
|
906
|
+
ui: {
|
|
907
|
+
beginInteraction(drivers) {
|
|
908
|
+
'worklet';
|
|
909
|
+
if (isRNRuntime()) return uiOnly();
|
|
910
|
+
return beginOnUI(drivers);
|
|
911
|
+
},
|
|
912
|
+
snapshotCurrent(drivers) {
|
|
913
|
+
'worklet';
|
|
914
|
+
if (isRNRuntime()) return uiOnly();
|
|
915
|
+
return snapshotOnUI(drivers);
|
|
916
|
+
},
|
|
917
|
+
setBatch(entries) {
|
|
918
|
+
'worklet';
|
|
919
|
+
if (isRNRuntime()) return uiOnly();
|
|
920
|
+
setBatchOnUI(entries);
|
|
921
|
+
},
|
|
922
|
+
animateTo(entries, animation) {
|
|
923
|
+
'worklet';
|
|
924
|
+
if (isRNRuntime()) return uiOnly();
|
|
925
|
+
return animateOnUI(entries, animation);
|
|
926
|
+
},
|
|
927
|
+
cancel(groupId, behavior = 'freeze') {
|
|
928
|
+
'worklet';
|
|
929
|
+
if (isRNRuntime()) return uiOnly();
|
|
930
|
+
return cancelOnUI(groupId, behavior);
|
|
931
|
+
},
|
|
932
|
+
},
|
|
933
|
+
react: {
|
|
934
|
+
beginInteraction(drivers) {
|
|
935
|
+
const { requestId, promise } = createGroupRequest<
|
|
936
|
+
readonly SmoothClipGroupSnapshot[]
|
|
937
|
+
>(controllerId, undefined, true);
|
|
938
|
+
scheduleOnUI(() => {
|
|
939
|
+
'worklet';
|
|
940
|
+
try {
|
|
941
|
+
const result = beginOnUI(drivers);
|
|
942
|
+
scheduleOnRN(
|
|
943
|
+
settleGroupRequest,
|
|
944
|
+
controllerId,
|
|
945
|
+
requestId,
|
|
946
|
+
true,
|
|
947
|
+
result,
|
|
948
|
+
''
|
|
949
|
+
);
|
|
950
|
+
} catch (error) {
|
|
951
|
+
scheduleOnRN(
|
|
952
|
+
settleGroupRequest,
|
|
953
|
+
controllerId,
|
|
954
|
+
requestId,
|
|
955
|
+
false,
|
|
956
|
+
undefined,
|
|
957
|
+
errorMessage(error)
|
|
958
|
+
);
|
|
959
|
+
}
|
|
960
|
+
});
|
|
961
|
+
return promise;
|
|
962
|
+
},
|
|
963
|
+
snapshotCurrent(drivers) {
|
|
964
|
+
const { requestId, promise } =
|
|
965
|
+
createGroupRequest<readonly SmoothClipGroupSnapshot[]>(
|
|
966
|
+
controllerId
|
|
967
|
+
);
|
|
968
|
+
scheduleOnUI(() => {
|
|
969
|
+
'worklet';
|
|
970
|
+
try {
|
|
971
|
+
const result = snapshotOnUI(drivers);
|
|
972
|
+
scheduleOnRN(
|
|
973
|
+
settleGroupRequest,
|
|
974
|
+
controllerId,
|
|
975
|
+
requestId,
|
|
976
|
+
true,
|
|
977
|
+
result,
|
|
978
|
+
''
|
|
979
|
+
);
|
|
980
|
+
} catch (error) {
|
|
981
|
+
scheduleOnRN(
|
|
982
|
+
settleGroupRequest,
|
|
983
|
+
controllerId,
|
|
984
|
+
requestId,
|
|
985
|
+
false,
|
|
986
|
+
undefined,
|
|
987
|
+
errorMessage(error)
|
|
988
|
+
);
|
|
989
|
+
}
|
|
990
|
+
});
|
|
991
|
+
return promise;
|
|
992
|
+
},
|
|
993
|
+
setBatch(entries) {
|
|
994
|
+
const { requestId, promise } = createGroupRequest<void>(
|
|
995
|
+
controllerId,
|
|
996
|
+
{ value: undefined },
|
|
997
|
+
true
|
|
998
|
+
);
|
|
999
|
+
scheduleOnUI(() => {
|
|
1000
|
+
'worklet';
|
|
1001
|
+
try {
|
|
1002
|
+
setBatchOnUI(entries);
|
|
1003
|
+
scheduleOnRN(
|
|
1004
|
+
settleGroupRequest,
|
|
1005
|
+
controllerId,
|
|
1006
|
+
requestId,
|
|
1007
|
+
true,
|
|
1008
|
+
undefined,
|
|
1009
|
+
''
|
|
1010
|
+
);
|
|
1011
|
+
} catch (error) {
|
|
1012
|
+
scheduleOnRN(
|
|
1013
|
+
settleGroupRequest,
|
|
1014
|
+
controllerId,
|
|
1015
|
+
requestId,
|
|
1016
|
+
false,
|
|
1017
|
+
undefined,
|
|
1018
|
+
errorMessage(error)
|
|
1019
|
+
);
|
|
1020
|
+
}
|
|
1021
|
+
});
|
|
1022
|
+
return promise;
|
|
1023
|
+
},
|
|
1024
|
+
animateTo(entries, animation) {
|
|
1025
|
+
const { requestId, promise } = createGroupRequest<number>(
|
|
1026
|
+
controllerId,
|
|
1027
|
+
{ value: 0 },
|
|
1028
|
+
true
|
|
1029
|
+
);
|
|
1030
|
+
scheduleOnUI(() => {
|
|
1031
|
+
'worklet';
|
|
1032
|
+
try {
|
|
1033
|
+
const result = animateOnUI(entries, animation);
|
|
1034
|
+
scheduleOnRN(
|
|
1035
|
+
settleGroupRequest,
|
|
1036
|
+
controllerId,
|
|
1037
|
+
requestId,
|
|
1038
|
+
true,
|
|
1039
|
+
result,
|
|
1040
|
+
''
|
|
1041
|
+
);
|
|
1042
|
+
} catch (error) {
|
|
1043
|
+
scheduleOnRN(
|
|
1044
|
+
settleGroupRequest,
|
|
1045
|
+
controllerId,
|
|
1046
|
+
requestId,
|
|
1047
|
+
false,
|
|
1048
|
+
undefined,
|
|
1049
|
+
errorMessage(error)
|
|
1050
|
+
);
|
|
1051
|
+
}
|
|
1052
|
+
});
|
|
1053
|
+
return promise;
|
|
1054
|
+
},
|
|
1055
|
+
cancel(groupId, behavior = 'freeze') {
|
|
1056
|
+
const { requestId, promise } = createGroupRequest<
|
|
1057
|
+
readonly SmoothClipGroupSnapshot[]
|
|
1058
|
+
>(controllerId, undefined, true);
|
|
1059
|
+
scheduleOnUI(() => {
|
|
1060
|
+
'worklet';
|
|
1061
|
+
try {
|
|
1062
|
+
const result = cancelOnUI(groupId, behavior);
|
|
1063
|
+
scheduleOnRN(
|
|
1064
|
+
settleGroupRequest,
|
|
1065
|
+
controllerId,
|
|
1066
|
+
requestId,
|
|
1067
|
+
true,
|
|
1068
|
+
result,
|
|
1069
|
+
''
|
|
1070
|
+
);
|
|
1071
|
+
} catch (error) {
|
|
1072
|
+
scheduleOnRN(
|
|
1073
|
+
settleGroupRequest,
|
|
1074
|
+
controllerId,
|
|
1075
|
+
requestId,
|
|
1076
|
+
false,
|
|
1077
|
+
undefined,
|
|
1078
|
+
errorMessage(error)
|
|
1079
|
+
);
|
|
1080
|
+
}
|
|
1081
|
+
});
|
|
1082
|
+
return promise;
|
|
1083
|
+
},
|
|
1084
|
+
},
|
|
1085
|
+
};
|
|
1086
|
+
controllerIds.set(group, controllerId);
|
|
1087
|
+
groupRef.current = group;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
useEffect(() => {
|
|
1091
|
+
disposed.value = 0;
|
|
1092
|
+
const controllerId = controllerIdFor(groupRef.current);
|
|
1093
|
+
const state = stateRef.current;
|
|
1094
|
+
if (state === null) return undefined;
|
|
1095
|
+
state.effectGeneration += 1;
|
|
1096
|
+
const effectGeneration = state.effectGeneration;
|
|
1097
|
+
nativeGroupDriverStates.set(controllerId, state);
|
|
1098
|
+
let subscription: { remove(): void } | undefined;
|
|
1099
|
+
if (GROUPS_SUPPORTED) {
|
|
1100
|
+
try {
|
|
1101
|
+
subscription = nativeGroups.onClipGroupAnimationComplete?.((result) => {
|
|
1102
|
+
if (result.controllerId !== controllerId) return;
|
|
1103
|
+
scheduleOnUI(
|
|
1104
|
+
synchronizeGroupCompletionOnUI,
|
|
1105
|
+
result.controllerId,
|
|
1106
|
+
result.groupId,
|
|
1107
|
+
result.finished,
|
|
1108
|
+
result.driverIds
|
|
1109
|
+
);
|
|
1110
|
+
deliverNativeGroupCompletion(
|
|
1111
|
+
controllerId,
|
|
1112
|
+
result.groupId,
|
|
1113
|
+
result.finished
|
|
1114
|
+
);
|
|
1115
|
+
});
|
|
1116
|
+
} catch {
|
|
1117
|
+
// A V1 native module may expose neither the event hook nor a compatible
|
|
1118
|
+
// emitter facade. Unsupported setup is a no-op; UI methods still report
|
|
1119
|
+
// the normal capability error when invoked.
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
return () => {
|
|
1123
|
+
try {
|
|
1124
|
+
subscription?.remove();
|
|
1125
|
+
} catch {
|
|
1126
|
+
// Older native event subscriptions are not guaranteed to support V2
|
|
1127
|
+
// teardown. Cleanup must remain safe across a new-JS/V1-native pair.
|
|
1128
|
+
}
|
|
1129
|
+
rejectGroupRequests(controllerId);
|
|
1130
|
+
scheduleOnUI(
|
|
1131
|
+
(ownerId: number, generation: number, gone: { value: number }) => {
|
|
1132
|
+
'worklet';
|
|
1133
|
+
gone.value = 1;
|
|
1134
|
+
const records = runtimeGroupRecords();
|
|
1135
|
+
const native = GROUPS_SUPPORTED ? requireNativeGroups() : null;
|
|
1136
|
+
for (const key of Object.keys(records)) {
|
|
1137
|
+
const record = records[key];
|
|
1138
|
+
if (record?.controllerId !== ownerId) continue;
|
|
1139
|
+
const groupId = Number(key);
|
|
1140
|
+
const values = native?.cancelAnimationGroupV2(groupId, 0) ?? [];
|
|
1141
|
+
let snapshots: readonly SmoothClipGroupSnapshot[] = [];
|
|
1142
|
+
if (values.length !== 0) {
|
|
1143
|
+
try {
|
|
1144
|
+
snapshots = parseSnapshots(record.drivers, values).snapshots;
|
|
1145
|
+
} catch {
|
|
1146
|
+
snapshots = [];
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
for (let index = 0; index < record.drivers.length; index += 1) {
|
|
1150
|
+
const handle = record.drivers[index]?.__smoothClipHandle;
|
|
1151
|
+
if (!handle || handle.activeAnimationId.value !== groupId)
|
|
1152
|
+
continue;
|
|
1153
|
+
const snapshot = snapshots[index];
|
|
1154
|
+
if (snapshot) handle.presentation.value = snapshot.presentation;
|
|
1155
|
+
handle.activeAnimationId.value = 0;
|
|
1156
|
+
handle.ownership.value = INTERACTIVE;
|
|
1157
|
+
}
|
|
1158
|
+
delete records[key];
|
|
1159
|
+
if (values.length !== 0) {
|
|
1160
|
+
scheduleOnRN(queueNativeTeardownCompletion, ownerId, groupId);
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
scheduleOnRN(finalizeNativeGroupDriverTeardown, ownerId, generation);
|
|
1164
|
+
},
|
|
1165
|
+
controllerId,
|
|
1166
|
+
effectGeneration,
|
|
1167
|
+
disposed
|
|
1168
|
+
);
|
|
1169
|
+
};
|
|
1170
|
+
}, [disposed]);
|
|
1171
|
+
|
|
1172
|
+
return groupRef.current;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
// The public object intentionally has no internal fields. Keeping the
|
|
1176
|
+
// controller association in a WeakMap preserves that surface while allowing
|
|
1177
|
+
// StrictMode effect replays to reuse the same native callback owner.
|
|
1178
|
+
const controllerIds = new WeakMap<SmoothClipGroupDriver, number>();
|
|
1179
|
+
|
|
1180
|
+
function controllerIdFor(group: SmoothClipGroupDriver | null): number {
|
|
1181
|
+
if (group === null) return 0;
|
|
1182
|
+
const existing = controllerIds.get(group);
|
|
1183
|
+
if (existing !== undefined) return existing;
|
|
1184
|
+
const id = allocateDriverId();
|
|
1185
|
+
controllerIds.set(group, id);
|
|
1186
|
+
return id;
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
export type { SmoothClipGroupAnimationResult };
|