react-native-smooth-clip-view 0.3.0 → 0.4.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 +132 -388
- package/android/build.gradle +3 -3
- package/android/proguard-rules.pro +2 -2
- package/android/src/main/cpp/SmoothClipAndroid.h +9 -12
- package/android/src/main/cpp/SmoothClipBindings.cpp +300 -704
- package/android/src/main/cpp/SmoothClipRegistry.cpp +348 -594
- package/android/src/main/java/com/smoothclipview/ClipGeometryNormalizer.kt +36 -227
- package/android/src/main/java/com/smoothclipview/SmoothClipBindings.kt +16 -19
- package/android/src/main/java/com/smoothclipview/SmoothClipModule.kt +36 -294
- package/android/src/main/java/com/smoothclipview/SmoothClipView.kt +344 -488
- package/android/src/main/java/com/smoothclipview/SmoothClipViewManager.kt +75 -77
- package/cpp/SmoothClipAnimationCurve.h +237 -346
- package/cpp/SmoothClipRegistry.h +24 -52
- package/cpp/SmoothClipSharedGeometry.h +64 -94
- package/cpp/SmoothClipVelocityTracker.h +5 -5
- package/ios/SmoothClipGeometry.h +23 -34
- package/ios/SmoothClipRegistry.mm +206 -711
- package/ios/SmoothClipTurboModule.cpp +217 -715
- package/ios/SmoothClipTurboModule.h +54 -297
- package/ios/SmoothClipView.mm +599 -505
- package/ios/SmoothClipViewRegistry.h +2 -7
- package/lib/module/NativeSmoothClipModule.js.map +1 -1
- package/lib/module/SmoothClipView.ios.js +84 -17
- package/lib/module/SmoothClipView.ios.js.map +1 -1
- package/lib/module/SmoothClipView.js +1 -1
- package/lib/module/SmoothClipView.js.map +1 -1
- package/lib/module/SmoothClipViewNativeComponent.ts +20 -26
- package/lib/module/capabilities.native.js +1 -19
- package/lib/module/capabilities.native.js.map +1 -1
- package/lib/module/capabilityTypes.js +0 -9
- package/lib/module/capabilityTypes.js.map +1 -1
- package/lib/module/controllerInternals.js +28 -0
- package/lib/module/controllerInternals.js.map +1 -0
- package/lib/module/controllerLifecycle.js +9 -0
- package/lib/module/controllerLifecycle.js.map +1 -0
- package/lib/module/controllerTypes.js +4 -0
- package/lib/module/controllerTypes.js.map +1 -0
- package/lib/module/controllers.android.js +4 -0
- package/lib/module/controllers.android.js.map +1 -0
- package/lib/module/controllers.ios.js +4 -0
- package/lib/module/controllers.ios.js.map +1 -0
- package/lib/module/controllers.js +4 -0
- package/lib/module/controllers.js.map +1 -0
- package/lib/module/controllers.native.js +89 -0
- package/lib/module/controllers.native.js.map +1 -0
- package/lib/module/geometry.js +71 -51
- package/lib/module/geometry.js.map +1 -1
- package/lib/module/groupTypes.js +4 -0
- package/lib/module/{groupDriverTypes.js.map → groupTypes.js.map} +1 -1
- package/lib/module/groups.android.js +4 -0
- package/lib/module/groups.android.js.map +1 -0
- package/lib/module/groups.ios.js +4 -0
- package/lib/module/groups.ios.js.map +1 -0
- package/lib/module/groups.js +4 -0
- package/lib/module/groups.js.map +1 -0
- package/lib/module/groups.native.js +399 -0
- package/lib/module/groups.native.js.map +1 -0
- package/lib/module/ids.js +8 -0
- package/lib/module/ids.js.map +1 -0
- package/lib/module/index.js +3 -3
- package/lib/module/index.js.map +1 -1
- package/lib/module/nativeCompletion.js +37 -16
- package/lib/module/nativeCompletion.js.map +1 -1
- package/lib/module/presentationCodec.js +81 -0
- package/lib/module/presentationCodec.js.map +1 -0
- package/lib/typescript/src/NativeSmoothClipModule.d.ts +14 -23
- package/lib/typescript/src/NativeSmoothClipModule.d.ts.map +1 -1
- package/lib/typescript/src/SmoothClipView.d.ts +1 -1
- package/lib/typescript/src/SmoothClipView.d.ts.map +1 -1
- package/lib/typescript/src/SmoothClipView.ios.d.ts +18 -14
- package/lib/typescript/src/SmoothClipView.ios.d.ts.map +1 -1
- package/lib/typescript/src/SmoothClipViewNativeComponent.d.ts +10 -5
- package/lib/typescript/src/SmoothClipViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/capabilities.native.d.ts +1 -1
- package/lib/typescript/src/capabilities.native.d.ts.map +1 -1
- package/lib/typescript/src/capabilityTypes.d.ts +0 -6
- package/lib/typescript/src/capabilityTypes.d.ts.map +1 -1
- package/lib/typescript/src/controllerInternals.d.ts +14 -0
- package/lib/typescript/src/controllerInternals.d.ts.map +1 -0
- package/lib/typescript/src/controllerLifecycle.d.ts +3 -0
- package/lib/typescript/src/controllerLifecycle.d.ts.map +1 -0
- package/lib/typescript/src/controllerTypes.d.ts +62 -0
- package/lib/typescript/src/controllerTypes.d.ts.map +1 -0
- package/lib/typescript/src/controllers.android.d.ts +2 -0
- package/lib/typescript/src/controllers.android.d.ts.map +1 -0
- package/lib/typescript/src/controllers.d.ts +2 -0
- package/lib/typescript/src/controllers.d.ts.map +1 -0
- package/lib/typescript/src/controllers.ios.d.ts +2 -0
- package/lib/typescript/src/controllers.ios.d.ts.map +1 -0
- package/lib/typescript/src/controllers.native.d.ts +3 -0
- package/lib/typescript/src/controllers.native.d.ts.map +1 -0
- package/lib/typescript/src/geometry.d.ts +22 -12
- package/lib/typescript/src/geometry.d.ts.map +1 -1
- package/lib/typescript/src/groupTypes.d.ts +31 -0
- package/lib/typescript/src/groupTypes.d.ts.map +1 -0
- package/lib/typescript/src/groups.android.d.ts +2 -0
- package/lib/typescript/src/groups.android.d.ts.map +1 -0
- package/lib/typescript/src/groups.d.ts +2 -0
- package/lib/typescript/src/groups.d.ts.map +1 -0
- package/lib/typescript/src/groups.ios.d.ts +2 -0
- package/lib/typescript/src/groups.ios.d.ts.map +1 -0
- package/lib/typescript/src/groups.native.d.ts +3 -0
- package/lib/typescript/src/groups.native.d.ts.map +1 -0
- package/lib/typescript/src/ids.d.ts +2 -0
- package/lib/typescript/src/ids.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +6 -6
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/nativeCompletion.d.ts +16 -6
- package/lib/typescript/src/nativeCompletion.d.ts.map +1 -1
- package/lib/typescript/src/presentationCodec.d.ts +10 -0
- package/lib/typescript/src/presentationCodec.d.ts.map +1 -0
- package/package.json +8 -3
- package/src/NativeSmoothClipModule.ts +41 -292
- package/src/SmoothClipView.ios.tsx +130 -23
- package/src/SmoothClipView.tsx +5 -1
- package/src/SmoothClipViewNativeComponent.ts +20 -26
- package/src/capabilities.native.ts +2 -52
- package/src/capabilityTypes.ts +0 -14
- package/src/controllerInternals.ts +52 -0
- package/src/controllerLifecycle.ts +6 -0
- package/src/controllerTypes.ts +87 -0
- package/src/controllers.android.ts +1 -0
- package/src/controllers.ios.ts +1 -0
- package/src/controllers.native.ts +104 -0
- package/src/controllers.ts +1 -0
- package/src/geometry.ts +118 -74
- package/src/groupTypes.ts +53 -0
- package/src/groups.android.ts +1 -0
- package/src/groups.ios.ts +1 -0
- package/src/groups.native.ts +604 -0
- package/src/groups.ts +1 -0
- package/src/ids.ts +6 -0
- package/src/index.ts +20 -28
- package/src/nativeCompletion.ts +67 -32
- package/src/presentationCodec.ts +134 -0
- package/lib/module/driverState.js +0 -82
- package/lib/module/driverState.js.map +0 -1
- package/lib/module/driverTypes.js +0 -4
- package/lib/module/driverTypes.js.map +0 -1
- package/lib/module/drivers.android.js +0 -6
- package/lib/module/drivers.android.js.map +0 -1
- package/lib/module/drivers.ios.js +0 -6
- package/lib/module/drivers.ios.js.map +0 -1
- package/lib/module/drivers.js +0 -4
- package/lib/module/drivers.js.map +0 -1
- package/lib/module/drivers.native.js +0 -756
- package/lib/module/drivers.native.js.map +0 -1
- package/lib/module/groupDriverTypes.js +0 -4
- package/lib/module/groupDrivers.android.js +0 -4
- package/lib/module/groupDrivers.android.js.map +0 -1
- package/lib/module/groupDrivers.ios.js +0 -4
- package/lib/module/groupDrivers.ios.js.map +0 -1
- package/lib/module/groupDrivers.js +0 -4
- package/lib/module/groupDrivers.js.map +0 -1
- package/lib/module/groupDrivers.native.js +0 -799
- package/lib/module/groupDrivers.native.js.map +0 -1
- package/lib/module/presentationProtocol.js +0 -15
- package/lib/module/presentationProtocol.js.map +0 -1
- package/lib/module/reactRequests.js +0 -56
- package/lib/module/reactRequests.js.map +0 -1
- package/lib/typescript/src/driverState.d.ts +0 -26
- package/lib/typescript/src/driverState.d.ts.map +0 -1
- package/lib/typescript/src/driverTypes.d.ts +0 -121
- package/lib/typescript/src/driverTypes.d.ts.map +0 -1
- package/lib/typescript/src/drivers.android.d.ts +0 -3
- package/lib/typescript/src/drivers.android.d.ts.map +0 -1
- package/lib/typescript/src/drivers.d.ts +0 -3
- package/lib/typescript/src/drivers.d.ts.map +0 -1
- package/lib/typescript/src/drivers.ios.d.ts +0 -3
- package/lib/typescript/src/drivers.ios.d.ts.map +0 -1
- package/lib/typescript/src/drivers.native.d.ts +0 -5
- package/lib/typescript/src/drivers.native.d.ts.map +0 -1
- package/lib/typescript/src/groupDriverTypes.d.ts +0 -66
- package/lib/typescript/src/groupDriverTypes.d.ts.map +0 -1
- package/lib/typescript/src/groupDrivers.android.d.ts +0 -2
- package/lib/typescript/src/groupDrivers.android.d.ts.map +0 -1
- package/lib/typescript/src/groupDrivers.d.ts +0 -2
- package/lib/typescript/src/groupDrivers.d.ts.map +0 -1
- package/lib/typescript/src/groupDrivers.ios.d.ts +0 -2
- package/lib/typescript/src/groupDrivers.ios.d.ts.map +0 -1
- package/lib/typescript/src/groupDrivers.native.d.ts +0 -4
- package/lib/typescript/src/groupDrivers.native.d.ts.map +0 -1
- package/lib/typescript/src/presentationProtocol.d.ts +0 -4
- package/lib/typescript/src/presentationProtocol.d.ts.map +0 -1
- package/lib/typescript/src/reactRequests.d.ts +0 -9
- package/lib/typescript/src/reactRequests.d.ts.map +0 -1
- package/src/driverState.ts +0 -118
- package/src/driverTypes.ts +0 -171
- package/src/drivers.android.ts +0 -8
- package/src/drivers.ios.ts +0 -8
- package/src/drivers.native.ts +0 -1599
- package/src/drivers.ts +0 -6
- package/src/groupDriverTypes.ts +0 -117
- package/src/groupDrivers.android.ts +0 -1
- package/src/groupDrivers.ios.ts +0 -1
- package/src/groupDrivers.native.ts +0 -1189
- package/src/groupDrivers.ts +0 -1
- package/src/presentationProtocol.ts +0 -28
- package/src/reactRequests.ts +0 -76
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
SmoothClipAnimation,
|
|
3
|
+
SmoothClipCompletion,
|
|
4
|
+
SmoothClipReactRun,
|
|
5
|
+
SmoothClipRef,
|
|
6
|
+
SmoothClipRunHandle,
|
|
7
|
+
} from './controllerTypes';
|
|
8
|
+
import type {
|
|
9
|
+
CanonicalSmoothClipPresentation,
|
|
10
|
+
SmoothClipPresentation,
|
|
11
|
+
} from './geometry';
|
|
12
|
+
|
|
13
|
+
export type SmoothClipGroupFrame = Readonly<{
|
|
14
|
+
clip: SmoothClipRef;
|
|
15
|
+
frame: SmoothClipPresentation;
|
|
16
|
+
}>;
|
|
17
|
+
|
|
18
|
+
export type SmoothClipGroupTarget = Readonly<{
|
|
19
|
+
clip: SmoothClipRef;
|
|
20
|
+
target: SmoothClipPresentation;
|
|
21
|
+
}>;
|
|
22
|
+
|
|
23
|
+
export type SmoothClipGroupSnapshot = Readonly<{
|
|
24
|
+
clip: SmoothClipRef;
|
|
25
|
+
frame: CanonicalSmoothClipPresentation;
|
|
26
|
+
ready: boolean;
|
|
27
|
+
}>;
|
|
28
|
+
|
|
29
|
+
export type SmoothClipGroupOptions = Readonly<{
|
|
30
|
+
/** Stable React-runtime callback for UI- and React-runtime transactions. */
|
|
31
|
+
onAnimationComplete?: (result: SmoothClipCompletion) => void;
|
|
32
|
+
}>;
|
|
33
|
+
|
|
34
|
+
export type SmoothClipGroup = Readonly<{
|
|
35
|
+
ui: Readonly<{
|
|
36
|
+
setFrames(entries: readonly SmoothClipGroupFrame[]): void;
|
|
37
|
+
beginInteraction(
|
|
38
|
+
clips: readonly SmoothClipRef[]
|
|
39
|
+
): readonly SmoothClipGroupSnapshot[];
|
|
40
|
+
animateTo(
|
|
41
|
+
entries: readonly SmoothClipGroupTarget[],
|
|
42
|
+
animation: SmoothClipAnimation,
|
|
43
|
+
completionTag?: number
|
|
44
|
+
): SmoothClipRunHandle | null;
|
|
45
|
+
cancel(run: SmoothClipRunHandle): readonly SmoothClipGroupSnapshot[];
|
|
46
|
+
}>;
|
|
47
|
+
react: Readonly<{
|
|
48
|
+
animateTo(
|
|
49
|
+
entries: readonly SmoothClipGroupTarget[],
|
|
50
|
+
animation: SmoothClipAnimation
|
|
51
|
+
): SmoothClipReactRun;
|
|
52
|
+
}>;
|
|
53
|
+
}>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useSmoothClipGroup } from './groups.native';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useSmoothClipGroup } from './groups.native';
|
|
@@ -0,0 +1,604 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
import { Platform } from 'react-native';
|
|
3
|
+
import { scheduleOnRN, scheduleOnUI } from 'react-native-worklets';
|
|
4
|
+
import type {
|
|
5
|
+
ClipReduceMotion,
|
|
6
|
+
SmoothClipAnimation,
|
|
7
|
+
SmoothClipCompletion,
|
|
8
|
+
SmoothClipReactRun,
|
|
9
|
+
SmoothClipRef,
|
|
10
|
+
SmoothClipRunHandle,
|
|
11
|
+
} from './controllerTypes';
|
|
12
|
+
import {
|
|
13
|
+
unwrapSmoothClipRef,
|
|
14
|
+
type InternalSmoothClipRef,
|
|
15
|
+
} from './controllerInternals';
|
|
16
|
+
import { allocateSmoothClipId } from './ids';
|
|
17
|
+
import { canonicalizeClipPresentation } from './geometry';
|
|
18
|
+
import type {
|
|
19
|
+
SmoothClipGroup,
|
|
20
|
+
SmoothClipGroupFrame,
|
|
21
|
+
SmoothClipGroupOptions,
|
|
22
|
+
SmoothClipGroupSnapshot,
|
|
23
|
+
SmoothClipGroupTarget,
|
|
24
|
+
} from './groupTypes';
|
|
25
|
+
import { subscribeGroupCompletion } from './nativeCompletion';
|
|
26
|
+
import {
|
|
27
|
+
appendPresentationPacket,
|
|
28
|
+
PRESENTATION_STRIDE,
|
|
29
|
+
presentationFromPacket,
|
|
30
|
+
} from './presentationCodec';
|
|
31
|
+
import NativeSmoothClipModule from './smoothClipNative';
|
|
32
|
+
|
|
33
|
+
const beginGroupInteractionHostFunction =
|
|
34
|
+
NativeSmoothClipModule.beginGroupInteraction;
|
|
35
|
+
const snapshotGroupHostFunction = NativeSmoothClipModule.snapshotGroup;
|
|
36
|
+
const setClipPresentationBatchHostFunction =
|
|
37
|
+
NativeSmoothClipModule.setClipPresentationBatch;
|
|
38
|
+
const animateTimingGroupHostFunction =
|
|
39
|
+
NativeSmoothClipModule.animateTimingGroup;
|
|
40
|
+
const animateSpringGroupHostFunction =
|
|
41
|
+
NativeSmoothClipModule.animateSpringGroup;
|
|
42
|
+
const cancelAnimationGroupHostFunction =
|
|
43
|
+
NativeSmoothClipModule.cancelAnimationGroup;
|
|
44
|
+
|
|
45
|
+
const SNAPSHOT_STRIDE = PRESENTATION_STRIDE + 1;
|
|
46
|
+
const MOTION_ENTRY_STRIDE = PRESENTATION_STRIDE * 2 + 2;
|
|
47
|
+
const NEEDS_START_STAMP = Platform.OS === 'android';
|
|
48
|
+
const DEFAULT_MASS = 4;
|
|
49
|
+
const DEFAULT_STIFFNESS = 900;
|
|
50
|
+
const DEFAULT_DAMPING = 120;
|
|
51
|
+
const DEFAULT_ENERGY_THRESHOLD = 6e-9;
|
|
52
|
+
|
|
53
|
+
type InternalRunHandle = SmoothClipRunHandle &
|
|
54
|
+
Readonly<{
|
|
55
|
+
controllerId: number;
|
|
56
|
+
id: number;
|
|
57
|
+
refs: readonly SmoothClipRef[];
|
|
58
|
+
}>;
|
|
59
|
+
|
|
60
|
+
type PendingRun = {
|
|
61
|
+
controllerId: number;
|
|
62
|
+
settled: boolean;
|
|
63
|
+
cancelRequested: boolean;
|
|
64
|
+
refs: readonly SmoothClipRef[];
|
|
65
|
+
resolve(finished: boolean): void;
|
|
66
|
+
resolveNativeId(id: number): void;
|
|
67
|
+
nativeId: Promise<number>;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
type GroupState = {
|
|
71
|
+
callbackRef: {
|
|
72
|
+
current: ((result: SmoothClipCompletion) => void) | undefined;
|
|
73
|
+
};
|
|
74
|
+
runs: Map<number, PendingRun>;
|
|
75
|
+
deferredCompletions: Map<
|
|
76
|
+
number,
|
|
77
|
+
Readonly<{
|
|
78
|
+
completionTag: number;
|
|
79
|
+
finished: boolean;
|
|
80
|
+
snapshots: readonly number[];
|
|
81
|
+
}>
|
|
82
|
+
>;
|
|
83
|
+
deliveredCompletions: Set<number>;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const groupStates = new Map<number, GroupState>();
|
|
87
|
+
const clientRuns = new Map<number, PendingRun>();
|
|
88
|
+
let nextClientRunId = 0;
|
|
89
|
+
|
|
90
|
+
function internalRef(ref: SmoothClipRef): InternalSmoothClipRef {
|
|
91
|
+
'worklet';
|
|
92
|
+
const internal = unwrapSmoothClipRef(ref);
|
|
93
|
+
if (!internal) {
|
|
94
|
+
throw new Error('[SmoothClipView] A group received an invalid clip ref.');
|
|
95
|
+
}
|
|
96
|
+
return internal;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function validatedRefs(
|
|
100
|
+
refs: readonly SmoothClipRef[]
|
|
101
|
+
): readonly InternalSmoothClipRef[] {
|
|
102
|
+
'worklet';
|
|
103
|
+
if (refs.length === 0) {
|
|
104
|
+
throw new Error('[SmoothClipView] A group must contain a clip.');
|
|
105
|
+
}
|
|
106
|
+
const seen: Record<string, true> = {};
|
|
107
|
+
return refs.map((ref) => {
|
|
108
|
+
const internal = internalRef(ref);
|
|
109
|
+
const key = String(internal.id);
|
|
110
|
+
if (seen[key]) {
|
|
111
|
+
throw new Error(
|
|
112
|
+
'[SmoothClipView] A group cannot contain duplicate clips.'
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
seen[key] = true;
|
|
116
|
+
return internal;
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function snapshotsFromPacket(
|
|
121
|
+
refs: readonly SmoothClipRef[],
|
|
122
|
+
values: readonly number[]
|
|
123
|
+
): readonly SmoothClipGroupSnapshot[] {
|
|
124
|
+
'worklet';
|
|
125
|
+
if (values.length !== refs.length * SNAPSHOT_STRIDE) {
|
|
126
|
+
throw new Error(
|
|
127
|
+
'[SmoothClipView] Native returned an invalid group snapshot.'
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
return refs.map((clip, index) => {
|
|
131
|
+
const offset = index * SNAPSHOT_STRIDE;
|
|
132
|
+
const ready = values[offset];
|
|
133
|
+
const frame = presentationFromPacket(values, offset + 1);
|
|
134
|
+
if ((ready !== 0 && ready !== 1) || frame === null) {
|
|
135
|
+
throw new Error(
|
|
136
|
+
'[SmoothClipView] Native returned an invalid group snapshot.'
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
return { clip, frame, ready: ready === 1 };
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function beginGroupOnUI(
|
|
144
|
+
refs: readonly SmoothClipRef[]
|
|
145
|
+
): readonly SmoothClipGroupSnapshot[] {
|
|
146
|
+
'worklet';
|
|
147
|
+
const internals = validatedRefs(refs);
|
|
148
|
+
return snapshotsFromPacket(
|
|
149
|
+
refs,
|
|
150
|
+
beginGroupInteractionHostFunction(internals.map((ref) => ref.id))
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function setFramesOnUI(entries: readonly SmoothClipGroupFrame[]): void {
|
|
155
|
+
'worklet';
|
|
156
|
+
const internals = validatedRefs(entries.map((entry) => entry.clip));
|
|
157
|
+
const packet: number[] = [];
|
|
158
|
+
entries.forEach((entry, index) => {
|
|
159
|
+
const frame = canonicalizeClipPresentation(entry.frame);
|
|
160
|
+
if (frame === null) {
|
|
161
|
+
throw new Error('[SmoothClipView] A group frame is invalid.');
|
|
162
|
+
}
|
|
163
|
+
packet.push(internals[index]?.id ?? 0);
|
|
164
|
+
appendPresentationPacket(packet, frame);
|
|
165
|
+
});
|
|
166
|
+
// A UI frame can race the Fabric host's first native registration (or its
|
|
167
|
+
// teardown). That is an availability result, not malformed input. The next
|
|
168
|
+
// streamed frame retries, while beginInteraction remains the explicit
|
|
169
|
+
// readiness gate for an atomic handoff.
|
|
170
|
+
setClipPresentationBatchHostFunction(packet);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function reduceMotionCode(value: ClipReduceMotion | undefined): number {
|
|
174
|
+
'worklet';
|
|
175
|
+
if (value === 'always') return 1;
|
|
176
|
+
if (value === 'never') return 2;
|
|
177
|
+
return 0;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function startTimestamp(): number {
|
|
181
|
+
'worklet';
|
|
182
|
+
if (!NEEDS_START_STAMP) return Number.NaN;
|
|
183
|
+
const runtime = globalThis as {
|
|
184
|
+
__frameTimestamp?: number;
|
|
185
|
+
_getAnimationTimestamp?: () => number;
|
|
186
|
+
};
|
|
187
|
+
return (
|
|
188
|
+
runtime.__frameTimestamp ||
|
|
189
|
+
(typeof runtime._getAnimationTimestamp === 'function'
|
|
190
|
+
? runtime._getAnimationTimestamp()
|
|
191
|
+
: Number.NaN)
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function animationIsValid(animation: SmoothClipAnimation): boolean {
|
|
196
|
+
'worklet';
|
|
197
|
+
if (animation.type === 'timing') {
|
|
198
|
+
const [x1, , x2] = animation.controlPoints;
|
|
199
|
+
return (
|
|
200
|
+
Number.isFinite(animation.duration) &&
|
|
201
|
+
animation.duration >= 0 &&
|
|
202
|
+
animation.controlPoints.every(Number.isFinite) &&
|
|
203
|
+
x1 >= 0 &&
|
|
204
|
+
x1 <= 1 &&
|
|
205
|
+
x2 >= 0 &&
|
|
206
|
+
x2 <= 1
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
const mass = animation.mass ?? DEFAULT_MASS;
|
|
210
|
+
const stiffness = animation.stiffness ?? DEFAULT_STIFFNESS;
|
|
211
|
+
const damping = animation.damping ?? DEFAULT_DAMPING;
|
|
212
|
+
const velocity = animation.velocity ?? 0;
|
|
213
|
+
const energyThreshold = animation.energyThreshold ?? DEFAULT_ENERGY_THRESHOLD;
|
|
214
|
+
return (
|
|
215
|
+
[mass, stiffness, damping, velocity, energyThreshold].every(
|
|
216
|
+
Number.isFinite
|
|
217
|
+
) &&
|
|
218
|
+
mass > 0 &&
|
|
219
|
+
stiffness > 0 &&
|
|
220
|
+
damping >= 0 &&
|
|
221
|
+
energyThreshold > 0
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function validCompletionTag(completionTag: number): boolean {
|
|
226
|
+
'worklet';
|
|
227
|
+
return (
|
|
228
|
+
Number.isInteger(completionTag) &&
|
|
229
|
+
completionTag >= 0 &&
|
|
230
|
+
completionTag <= 0x7fffffff
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function validInternalCompletionTag(completionTag: number): boolean {
|
|
235
|
+
'worklet';
|
|
236
|
+
return (
|
|
237
|
+
Number.isInteger(completionTag) &&
|
|
238
|
+
completionTag >= -0x7fffffff &&
|
|
239
|
+
completionTag <= 0x7fffffff
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function animateGroupOnUI(
|
|
244
|
+
controllerId: number,
|
|
245
|
+
entries: readonly SmoothClipGroupTarget[],
|
|
246
|
+
animation: SmoothClipAnimation,
|
|
247
|
+
completionTag: number,
|
|
248
|
+
allowInternalCompletionTag = false
|
|
249
|
+
): SmoothClipRunHandle | null {
|
|
250
|
+
'worklet';
|
|
251
|
+
const completionTagIsValid = allowInternalCompletionTag
|
|
252
|
+
? validInternalCompletionTag(completionTag)
|
|
253
|
+
: validCompletionTag(completionTag);
|
|
254
|
+
if (!animationIsValid(animation) || !completionTagIsValid) {
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
let internals: readonly InternalSmoothClipRef[];
|
|
258
|
+
try {
|
|
259
|
+
internals = validatedRefs(entries.map((entry) => entry.clip));
|
|
260
|
+
} catch {
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
const packet: number[] = [];
|
|
264
|
+
for (let index = 0; index < entries.length; index += 1) {
|
|
265
|
+
const entry = entries[index];
|
|
266
|
+
const target =
|
|
267
|
+
entry === undefined ? null : canonicalizeClipPresentation(entry.target);
|
|
268
|
+
if (entry === undefined || target === null) return null;
|
|
269
|
+
packet.push(internals[index]?.id ?? 0, 0);
|
|
270
|
+
// Fixed native packets retain a start slot. With hasFrom=0 its contents
|
|
271
|
+
// are ignored; duplicating the target avoids a second sentinel format.
|
|
272
|
+
appendPresentationPacket(packet, target);
|
|
273
|
+
appendPresentationPacket(packet, target);
|
|
274
|
+
}
|
|
275
|
+
if (packet.length !== entries.length * MOTION_ENTRY_STRIDE) return null;
|
|
276
|
+
|
|
277
|
+
const timestamp = startTimestamp();
|
|
278
|
+
let id: number;
|
|
279
|
+
if (animation.type === 'timing') {
|
|
280
|
+
const [x1, y1, x2, y2] = animation.controlPoints;
|
|
281
|
+
id = animateTimingGroupHostFunction(
|
|
282
|
+
controllerId,
|
|
283
|
+
packet,
|
|
284
|
+
animation.duration,
|
|
285
|
+
x1,
|
|
286
|
+
y1,
|
|
287
|
+
x2,
|
|
288
|
+
y2,
|
|
289
|
+
reduceMotionCode(animation.reduceMotion),
|
|
290
|
+
completionTag,
|
|
291
|
+
timestamp
|
|
292
|
+
);
|
|
293
|
+
} else {
|
|
294
|
+
id = animateSpringGroupHostFunction(
|
|
295
|
+
controllerId,
|
|
296
|
+
packet,
|
|
297
|
+
animation.mass ?? DEFAULT_MASS,
|
|
298
|
+
animation.stiffness ?? DEFAULT_STIFFNESS,
|
|
299
|
+
animation.damping ?? DEFAULT_DAMPING,
|
|
300
|
+
animation.velocity ?? 0,
|
|
301
|
+
animation.energyThreshold ?? DEFAULT_ENERGY_THRESHOLD,
|
|
302
|
+
reduceMotionCode(animation.reduceMotion),
|
|
303
|
+
completionTag,
|
|
304
|
+
timestamp
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
if (id <= 0) return null;
|
|
308
|
+
return {
|
|
309
|
+
controllerId,
|
|
310
|
+
id,
|
|
311
|
+
refs: entries.map((entry) => entry.clip),
|
|
312
|
+
} as unknown as InternalRunHandle;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function cancelGroupOnUI(
|
|
316
|
+
controllerId: number,
|
|
317
|
+
handle: SmoothClipRunHandle
|
|
318
|
+
): readonly SmoothClipGroupSnapshot[] {
|
|
319
|
+
'worklet';
|
|
320
|
+
const internal = handle as InternalRunHandle;
|
|
321
|
+
if (
|
|
322
|
+
internal.controllerId !== controllerId ||
|
|
323
|
+
!Number.isInteger(internal.id) ||
|
|
324
|
+
internal.id <= 0 ||
|
|
325
|
+
!Array.isArray(internal.refs)
|
|
326
|
+
) {
|
|
327
|
+
return [];
|
|
328
|
+
}
|
|
329
|
+
let internals: readonly InternalSmoothClipRef[];
|
|
330
|
+
try {
|
|
331
|
+
internals = validatedRefs(internal.refs);
|
|
332
|
+
} catch {
|
|
333
|
+
return [];
|
|
334
|
+
}
|
|
335
|
+
const values = cancelAnimationGroupHostFunction(internal.id, 0);
|
|
336
|
+
if (values.length > 0) {
|
|
337
|
+
return snapshotsFromPacket(internal.refs, values);
|
|
338
|
+
}
|
|
339
|
+
const snapshots = snapshotGroupHostFunction(
|
|
340
|
+
internals.map((internalRefValue) => internalRefValue.id)
|
|
341
|
+
);
|
|
342
|
+
return snapshots.length === 0
|
|
343
|
+
? []
|
|
344
|
+
: snapshotsFromPacket(internal.refs, snapshots);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function validReactEntries(entries: readonly SmoothClipGroupTarget[]): boolean {
|
|
348
|
+
if (entries.length === 0) return false;
|
|
349
|
+
const seen = new Set<number>();
|
|
350
|
+
for (const entry of entries) {
|
|
351
|
+
const ref = unwrapSmoothClipRef(entry.clip);
|
|
352
|
+
if (
|
|
353
|
+
ref === undefined ||
|
|
354
|
+
seen.has(ref.id) ||
|
|
355
|
+
canonicalizeClipPresentation(entry.target) === null
|
|
356
|
+
) {
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
seen.add(ref.id);
|
|
360
|
+
}
|
|
361
|
+
return true;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function settleRun(run: PendingRun, finished: boolean): void {
|
|
365
|
+
if (run.settled) return;
|
|
366
|
+
run.settled = true;
|
|
367
|
+
run.resolve(finished);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function registerNativeGroupRun(
|
|
371
|
+
controllerId: number,
|
|
372
|
+
clientRunId: number,
|
|
373
|
+
groupId: number
|
|
374
|
+
): void {
|
|
375
|
+
const run = clientRuns.get(clientRunId);
|
|
376
|
+
clientRuns.delete(clientRunId);
|
|
377
|
+
if (!run) return;
|
|
378
|
+
run.resolveNativeId(groupId);
|
|
379
|
+
const state = groupStates.get(controllerId);
|
|
380
|
+
if (!state || groupId <= 0) {
|
|
381
|
+
settleRun(run, false);
|
|
382
|
+
state?.callbackRef.current?.({ finished: false });
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
state.runs.set(groupId, run);
|
|
386
|
+
const deferred = state.deferredCompletions.get(groupId);
|
|
387
|
+
if (deferred !== undefined) {
|
|
388
|
+
state.deferredCompletions.delete(groupId);
|
|
389
|
+
handleNativeGroupCompletion(
|
|
390
|
+
controllerId,
|
|
391
|
+
groupId,
|
|
392
|
+
deferred.completionTag,
|
|
393
|
+
deferred.finished,
|
|
394
|
+
deferred.snapshots
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function handleNativeGroupCompletion(
|
|
400
|
+
controllerId: number,
|
|
401
|
+
groupId: number,
|
|
402
|
+
completionTag: number,
|
|
403
|
+
finished: boolean,
|
|
404
|
+
values: readonly number[]
|
|
405
|
+
): void {
|
|
406
|
+
const state = groupStates.get(controllerId);
|
|
407
|
+
if (!state || state.deliveredCompletions.has(groupId)) return;
|
|
408
|
+
const run = state.runs.get(groupId);
|
|
409
|
+
if (!run) {
|
|
410
|
+
// Non-negative tags belong to UI-runtime runs, which deliberately have no
|
|
411
|
+
// Promise registry on React. Negative tags are private correlation keys
|
|
412
|
+
// for React runs and may arrive before scheduleOnRN registers the run.
|
|
413
|
+
if (completionTag >= 0) {
|
|
414
|
+
state.deliveredCompletions.add(groupId);
|
|
415
|
+
state.callbackRef.current?.({
|
|
416
|
+
finished,
|
|
417
|
+
...(completionTag > 0 ? { completionTag } : {}),
|
|
418
|
+
});
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
state.deferredCompletions.set(groupId, {
|
|
422
|
+
completionTag,
|
|
423
|
+
finished,
|
|
424
|
+
snapshots: values,
|
|
425
|
+
});
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
state.deliveredCompletions.add(groupId);
|
|
429
|
+
state.runs.delete(groupId);
|
|
430
|
+
settleRun(run, finished);
|
|
431
|
+
state.callbackRef.current?.({
|
|
432
|
+
finished,
|
|
433
|
+
...(completionTag > 0 ? { completionTag } : {}),
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function cancelRegisteredRun(
|
|
438
|
+
controllerId: number,
|
|
439
|
+
groupId: number,
|
|
440
|
+
refs: readonly SmoothClipRef[]
|
|
441
|
+
): void {
|
|
442
|
+
scheduleOnUI(
|
|
443
|
+
(ownerId: number, id: number, clips: readonly SmoothClipRef[]) => {
|
|
444
|
+
'worklet';
|
|
445
|
+
cancelGroupOnUI(ownerId, {
|
|
446
|
+
controllerId: ownerId,
|
|
447
|
+
id,
|
|
448
|
+
refs: clips,
|
|
449
|
+
} as unknown as InternalRunHandle);
|
|
450
|
+
},
|
|
451
|
+
controllerId,
|
|
452
|
+
groupId,
|
|
453
|
+
refs
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
export function useSmoothClipGroup(
|
|
458
|
+
options: SmoothClipGroupOptions = {}
|
|
459
|
+
): SmoothClipGroup {
|
|
460
|
+
const controllerIdRef = useRef(0);
|
|
461
|
+
const groupRef = useRef<SmoothClipGroup | null>(null);
|
|
462
|
+
const callbackRef = useRef(options.onAnimationComplete);
|
|
463
|
+
callbackRef.current = options.onAnimationComplete;
|
|
464
|
+
if (controllerIdRef.current === 0) {
|
|
465
|
+
controllerIdRef.current = allocateSmoothClipId();
|
|
466
|
+
}
|
|
467
|
+
const controllerId = controllerIdRef.current;
|
|
468
|
+
|
|
469
|
+
if (groupRef.current === null) {
|
|
470
|
+
groupRef.current = {
|
|
471
|
+
ui: {
|
|
472
|
+
setFrames(entries) {
|
|
473
|
+
'worklet';
|
|
474
|
+
setFramesOnUI(entries);
|
|
475
|
+
},
|
|
476
|
+
beginInteraction(refs) {
|
|
477
|
+
'worklet';
|
|
478
|
+
return beginGroupOnUI(refs);
|
|
479
|
+
},
|
|
480
|
+
animateTo(entries, animation, completionTag = 0) {
|
|
481
|
+
'worklet';
|
|
482
|
+
return animateGroupOnUI(
|
|
483
|
+
controllerId,
|
|
484
|
+
entries,
|
|
485
|
+
animation,
|
|
486
|
+
completionTag
|
|
487
|
+
);
|
|
488
|
+
},
|
|
489
|
+
cancel(run) {
|
|
490
|
+
'worklet';
|
|
491
|
+
return cancelGroupOnUI(controllerId, run);
|
|
492
|
+
},
|
|
493
|
+
},
|
|
494
|
+
react: {
|
|
495
|
+
animateTo(entries, animation): SmoothClipReactRun {
|
|
496
|
+
if (!validReactEntries(entries) || !animationIsValid(animation)) {
|
|
497
|
+
callbackRef.current?.({ finished: false });
|
|
498
|
+
return {
|
|
499
|
+
finished: Promise.resolve(false),
|
|
500
|
+
cancel() {},
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
let resolve!: (finished: boolean) => void;
|
|
504
|
+
let resolveNativeId!: (id: number) => void;
|
|
505
|
+
const finished = new Promise<boolean>((accept) => {
|
|
506
|
+
resolve = accept;
|
|
507
|
+
});
|
|
508
|
+
const nativeId = new Promise<number>((accept) => {
|
|
509
|
+
resolveNativeId = accept;
|
|
510
|
+
});
|
|
511
|
+
const run: PendingRun = {
|
|
512
|
+
controllerId,
|
|
513
|
+
settled: false,
|
|
514
|
+
cancelRequested: false,
|
|
515
|
+
refs: entries.map((entry) => entry.clip),
|
|
516
|
+
resolve,
|
|
517
|
+
resolveNativeId,
|
|
518
|
+
nativeId,
|
|
519
|
+
};
|
|
520
|
+
nextClientRunId = (nextClientRunId % 0x7ffffffe) + 1;
|
|
521
|
+
const clientRunId = nextClientRunId;
|
|
522
|
+
clientRuns.set(clientRunId, run);
|
|
523
|
+
scheduleOnUI(
|
|
524
|
+
(
|
|
525
|
+
id: number,
|
|
526
|
+
motionEntries: readonly SmoothClipGroupTarget[],
|
|
527
|
+
spec: SmoothClipAnimation,
|
|
528
|
+
pendingRunId: number
|
|
529
|
+
) => {
|
|
530
|
+
'worklet';
|
|
531
|
+
const handle = animateGroupOnUI(
|
|
532
|
+
id,
|
|
533
|
+
motionEntries,
|
|
534
|
+
spec,
|
|
535
|
+
-pendingRunId,
|
|
536
|
+
true
|
|
537
|
+
);
|
|
538
|
+
scheduleOnRN(
|
|
539
|
+
registerNativeGroupRun,
|
|
540
|
+
id,
|
|
541
|
+
pendingRunId,
|
|
542
|
+
(handle as InternalRunHandle | null)?.id ?? 0
|
|
543
|
+
);
|
|
544
|
+
},
|
|
545
|
+
controllerId,
|
|
546
|
+
entries,
|
|
547
|
+
animation,
|
|
548
|
+
clientRunId
|
|
549
|
+
);
|
|
550
|
+
return {
|
|
551
|
+
finished,
|
|
552
|
+
cancel() {
|
|
553
|
+
if (run.settled || run.cancelRequested) return;
|
|
554
|
+
run.cancelRequested = true;
|
|
555
|
+
nativeId.then((groupId) => {
|
|
556
|
+
if (groupId > 0 && !run.settled) {
|
|
557
|
+
cancelRegisteredRun(run.controllerId, groupId, run.refs);
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
},
|
|
561
|
+
};
|
|
562
|
+
},
|
|
563
|
+
},
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
useEffect(() => {
|
|
568
|
+
const state: GroupState = {
|
|
569
|
+
callbackRef,
|
|
570
|
+
runs: new Map(),
|
|
571
|
+
deferredCompletions: new Map(),
|
|
572
|
+
deliveredCompletions: new Set(),
|
|
573
|
+
};
|
|
574
|
+
groupStates.set(controllerId, state);
|
|
575
|
+
const unsubscribe = subscribeGroupCompletion(
|
|
576
|
+
controllerId,
|
|
577
|
+
({ groupId, completionTag, finished, snapshots }) => {
|
|
578
|
+
handleNativeGroupCompletion(
|
|
579
|
+
controllerId,
|
|
580
|
+
groupId,
|
|
581
|
+
completionTag,
|
|
582
|
+
finished,
|
|
583
|
+
snapshots
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
);
|
|
587
|
+
return () => {
|
|
588
|
+
unsubscribe();
|
|
589
|
+
if (groupStates.get(controllerId) === state) {
|
|
590
|
+
groupStates.delete(controllerId);
|
|
591
|
+
}
|
|
592
|
+
for (const run of state.runs.values()) settleRun(run, false);
|
|
593
|
+
state.runs.clear();
|
|
594
|
+
for (const [clientRunId, run] of clientRuns) {
|
|
595
|
+
if (run.controllerId !== controllerId) continue;
|
|
596
|
+
clientRuns.delete(clientRunId);
|
|
597
|
+
run.resolveNativeId(0);
|
|
598
|
+
settleRun(run, false);
|
|
599
|
+
}
|
|
600
|
+
};
|
|
601
|
+
}, [controllerId]);
|
|
602
|
+
|
|
603
|
+
return groupRef.current;
|
|
604
|
+
}
|
package/src/groups.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useSmoothClipGroup } from './groups.native';
|