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.
Files changed (199) hide show
  1. package/README.md +132 -388
  2. package/android/build.gradle +3 -3
  3. package/android/proguard-rules.pro +2 -2
  4. package/android/src/main/cpp/SmoothClipAndroid.h +9 -12
  5. package/android/src/main/cpp/SmoothClipBindings.cpp +300 -704
  6. package/android/src/main/cpp/SmoothClipRegistry.cpp +348 -594
  7. package/android/src/main/java/com/smoothclipview/ClipGeometryNormalizer.kt +36 -227
  8. package/android/src/main/java/com/smoothclipview/SmoothClipBindings.kt +16 -19
  9. package/android/src/main/java/com/smoothclipview/SmoothClipModule.kt +36 -294
  10. package/android/src/main/java/com/smoothclipview/SmoothClipView.kt +344 -488
  11. package/android/src/main/java/com/smoothclipview/SmoothClipViewManager.kt +75 -77
  12. package/cpp/SmoothClipAnimationCurve.h +237 -346
  13. package/cpp/SmoothClipRegistry.h +24 -52
  14. package/cpp/SmoothClipSharedGeometry.h +64 -94
  15. package/cpp/SmoothClipVelocityTracker.h +5 -5
  16. package/ios/SmoothClipGeometry.h +23 -34
  17. package/ios/SmoothClipRegistry.mm +206 -711
  18. package/ios/SmoothClipTurboModule.cpp +217 -715
  19. package/ios/SmoothClipTurboModule.h +54 -297
  20. package/ios/SmoothClipView.mm +599 -505
  21. package/ios/SmoothClipViewRegistry.h +2 -7
  22. package/lib/module/NativeSmoothClipModule.js.map +1 -1
  23. package/lib/module/SmoothClipView.ios.js +84 -17
  24. package/lib/module/SmoothClipView.ios.js.map +1 -1
  25. package/lib/module/SmoothClipView.js +1 -1
  26. package/lib/module/SmoothClipView.js.map +1 -1
  27. package/lib/module/SmoothClipViewNativeComponent.ts +20 -26
  28. package/lib/module/capabilities.native.js +1 -19
  29. package/lib/module/capabilities.native.js.map +1 -1
  30. package/lib/module/capabilityTypes.js +0 -9
  31. package/lib/module/capabilityTypes.js.map +1 -1
  32. package/lib/module/controllerInternals.js +28 -0
  33. package/lib/module/controllerInternals.js.map +1 -0
  34. package/lib/module/controllerLifecycle.js +9 -0
  35. package/lib/module/controllerLifecycle.js.map +1 -0
  36. package/lib/module/controllerTypes.js +4 -0
  37. package/lib/module/controllerTypes.js.map +1 -0
  38. package/lib/module/controllers.android.js +4 -0
  39. package/lib/module/controllers.android.js.map +1 -0
  40. package/lib/module/controllers.ios.js +4 -0
  41. package/lib/module/controllers.ios.js.map +1 -0
  42. package/lib/module/controllers.js +4 -0
  43. package/lib/module/controllers.js.map +1 -0
  44. package/lib/module/controllers.native.js +89 -0
  45. package/lib/module/controllers.native.js.map +1 -0
  46. package/lib/module/geometry.js +71 -51
  47. package/lib/module/geometry.js.map +1 -1
  48. package/lib/module/groupTypes.js +4 -0
  49. package/lib/module/{groupDriverTypes.js.map → groupTypes.js.map} +1 -1
  50. package/lib/module/groups.android.js +4 -0
  51. package/lib/module/groups.android.js.map +1 -0
  52. package/lib/module/groups.ios.js +4 -0
  53. package/lib/module/groups.ios.js.map +1 -0
  54. package/lib/module/groups.js +4 -0
  55. package/lib/module/groups.js.map +1 -0
  56. package/lib/module/groups.native.js +399 -0
  57. package/lib/module/groups.native.js.map +1 -0
  58. package/lib/module/ids.js +8 -0
  59. package/lib/module/ids.js.map +1 -0
  60. package/lib/module/index.js +3 -3
  61. package/lib/module/index.js.map +1 -1
  62. package/lib/module/nativeCompletion.js +37 -16
  63. package/lib/module/nativeCompletion.js.map +1 -1
  64. package/lib/module/presentationCodec.js +81 -0
  65. package/lib/module/presentationCodec.js.map +1 -0
  66. package/lib/typescript/src/NativeSmoothClipModule.d.ts +14 -23
  67. package/lib/typescript/src/NativeSmoothClipModule.d.ts.map +1 -1
  68. package/lib/typescript/src/SmoothClipView.d.ts +1 -1
  69. package/lib/typescript/src/SmoothClipView.d.ts.map +1 -1
  70. package/lib/typescript/src/SmoothClipView.ios.d.ts +18 -14
  71. package/lib/typescript/src/SmoothClipView.ios.d.ts.map +1 -1
  72. package/lib/typescript/src/SmoothClipViewNativeComponent.d.ts +10 -5
  73. package/lib/typescript/src/SmoothClipViewNativeComponent.d.ts.map +1 -1
  74. package/lib/typescript/src/capabilities.native.d.ts +1 -1
  75. package/lib/typescript/src/capabilities.native.d.ts.map +1 -1
  76. package/lib/typescript/src/capabilityTypes.d.ts +0 -6
  77. package/lib/typescript/src/capabilityTypes.d.ts.map +1 -1
  78. package/lib/typescript/src/controllerInternals.d.ts +14 -0
  79. package/lib/typescript/src/controllerInternals.d.ts.map +1 -0
  80. package/lib/typescript/src/controllerLifecycle.d.ts +3 -0
  81. package/lib/typescript/src/controllerLifecycle.d.ts.map +1 -0
  82. package/lib/typescript/src/controllerTypes.d.ts +62 -0
  83. package/lib/typescript/src/controllerTypes.d.ts.map +1 -0
  84. package/lib/typescript/src/controllers.android.d.ts +2 -0
  85. package/lib/typescript/src/controllers.android.d.ts.map +1 -0
  86. package/lib/typescript/src/controllers.d.ts +2 -0
  87. package/lib/typescript/src/controllers.d.ts.map +1 -0
  88. package/lib/typescript/src/controllers.ios.d.ts +2 -0
  89. package/lib/typescript/src/controllers.ios.d.ts.map +1 -0
  90. package/lib/typescript/src/controllers.native.d.ts +3 -0
  91. package/lib/typescript/src/controllers.native.d.ts.map +1 -0
  92. package/lib/typescript/src/geometry.d.ts +22 -12
  93. package/lib/typescript/src/geometry.d.ts.map +1 -1
  94. package/lib/typescript/src/groupTypes.d.ts +31 -0
  95. package/lib/typescript/src/groupTypes.d.ts.map +1 -0
  96. package/lib/typescript/src/groups.android.d.ts +2 -0
  97. package/lib/typescript/src/groups.android.d.ts.map +1 -0
  98. package/lib/typescript/src/groups.d.ts +2 -0
  99. package/lib/typescript/src/groups.d.ts.map +1 -0
  100. package/lib/typescript/src/groups.ios.d.ts +2 -0
  101. package/lib/typescript/src/groups.ios.d.ts.map +1 -0
  102. package/lib/typescript/src/groups.native.d.ts +3 -0
  103. package/lib/typescript/src/groups.native.d.ts.map +1 -0
  104. package/lib/typescript/src/ids.d.ts +2 -0
  105. package/lib/typescript/src/ids.d.ts.map +1 -0
  106. package/lib/typescript/src/index.d.ts +6 -6
  107. package/lib/typescript/src/index.d.ts.map +1 -1
  108. package/lib/typescript/src/nativeCompletion.d.ts +16 -6
  109. package/lib/typescript/src/nativeCompletion.d.ts.map +1 -1
  110. package/lib/typescript/src/presentationCodec.d.ts +10 -0
  111. package/lib/typescript/src/presentationCodec.d.ts.map +1 -0
  112. package/package.json +8 -3
  113. package/src/NativeSmoothClipModule.ts +41 -292
  114. package/src/SmoothClipView.ios.tsx +130 -23
  115. package/src/SmoothClipView.tsx +5 -1
  116. package/src/SmoothClipViewNativeComponent.ts +20 -26
  117. package/src/capabilities.native.ts +2 -52
  118. package/src/capabilityTypes.ts +0 -14
  119. package/src/controllerInternals.ts +52 -0
  120. package/src/controllerLifecycle.ts +6 -0
  121. package/src/controllerTypes.ts +87 -0
  122. package/src/controllers.android.ts +1 -0
  123. package/src/controllers.ios.ts +1 -0
  124. package/src/controllers.native.ts +104 -0
  125. package/src/controllers.ts +1 -0
  126. package/src/geometry.ts +118 -74
  127. package/src/groupTypes.ts +53 -0
  128. package/src/groups.android.ts +1 -0
  129. package/src/groups.ios.ts +1 -0
  130. package/src/groups.native.ts +604 -0
  131. package/src/groups.ts +1 -0
  132. package/src/ids.ts +6 -0
  133. package/src/index.ts +20 -28
  134. package/src/nativeCompletion.ts +67 -32
  135. package/src/presentationCodec.ts +134 -0
  136. package/lib/module/driverState.js +0 -82
  137. package/lib/module/driverState.js.map +0 -1
  138. package/lib/module/driverTypes.js +0 -4
  139. package/lib/module/driverTypes.js.map +0 -1
  140. package/lib/module/drivers.android.js +0 -6
  141. package/lib/module/drivers.android.js.map +0 -1
  142. package/lib/module/drivers.ios.js +0 -6
  143. package/lib/module/drivers.ios.js.map +0 -1
  144. package/lib/module/drivers.js +0 -4
  145. package/lib/module/drivers.js.map +0 -1
  146. package/lib/module/drivers.native.js +0 -756
  147. package/lib/module/drivers.native.js.map +0 -1
  148. package/lib/module/groupDriverTypes.js +0 -4
  149. package/lib/module/groupDrivers.android.js +0 -4
  150. package/lib/module/groupDrivers.android.js.map +0 -1
  151. package/lib/module/groupDrivers.ios.js +0 -4
  152. package/lib/module/groupDrivers.ios.js.map +0 -1
  153. package/lib/module/groupDrivers.js +0 -4
  154. package/lib/module/groupDrivers.js.map +0 -1
  155. package/lib/module/groupDrivers.native.js +0 -799
  156. package/lib/module/groupDrivers.native.js.map +0 -1
  157. package/lib/module/presentationProtocol.js +0 -15
  158. package/lib/module/presentationProtocol.js.map +0 -1
  159. package/lib/module/reactRequests.js +0 -56
  160. package/lib/module/reactRequests.js.map +0 -1
  161. package/lib/typescript/src/driverState.d.ts +0 -26
  162. package/lib/typescript/src/driverState.d.ts.map +0 -1
  163. package/lib/typescript/src/driverTypes.d.ts +0 -121
  164. package/lib/typescript/src/driverTypes.d.ts.map +0 -1
  165. package/lib/typescript/src/drivers.android.d.ts +0 -3
  166. package/lib/typescript/src/drivers.android.d.ts.map +0 -1
  167. package/lib/typescript/src/drivers.d.ts +0 -3
  168. package/lib/typescript/src/drivers.d.ts.map +0 -1
  169. package/lib/typescript/src/drivers.ios.d.ts +0 -3
  170. package/lib/typescript/src/drivers.ios.d.ts.map +0 -1
  171. package/lib/typescript/src/drivers.native.d.ts +0 -5
  172. package/lib/typescript/src/drivers.native.d.ts.map +0 -1
  173. package/lib/typescript/src/groupDriverTypes.d.ts +0 -66
  174. package/lib/typescript/src/groupDriverTypes.d.ts.map +0 -1
  175. package/lib/typescript/src/groupDrivers.android.d.ts +0 -2
  176. package/lib/typescript/src/groupDrivers.android.d.ts.map +0 -1
  177. package/lib/typescript/src/groupDrivers.d.ts +0 -2
  178. package/lib/typescript/src/groupDrivers.d.ts.map +0 -1
  179. package/lib/typescript/src/groupDrivers.ios.d.ts +0 -2
  180. package/lib/typescript/src/groupDrivers.ios.d.ts.map +0 -1
  181. package/lib/typescript/src/groupDrivers.native.d.ts +0 -4
  182. package/lib/typescript/src/groupDrivers.native.d.ts.map +0 -1
  183. package/lib/typescript/src/presentationProtocol.d.ts +0 -4
  184. package/lib/typescript/src/presentationProtocol.d.ts.map +0 -1
  185. package/lib/typescript/src/reactRequests.d.ts +0 -9
  186. package/lib/typescript/src/reactRequests.d.ts.map +0 -1
  187. package/src/driverState.ts +0 -118
  188. package/src/driverTypes.ts +0 -171
  189. package/src/drivers.android.ts +0 -8
  190. package/src/drivers.ios.ts +0 -8
  191. package/src/drivers.native.ts +0 -1599
  192. package/src/drivers.ts +0 -6
  193. package/src/groupDriverTypes.ts +0 -117
  194. package/src/groupDrivers.android.ts +0 -1
  195. package/src/groupDrivers.ios.ts +0 -1
  196. package/src/groupDrivers.native.ts +0 -1189
  197. package/src/groupDrivers.ts +0 -1
  198. package/src/presentationProtocol.ts +0 -28
  199. 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';
package/src/ids.ts ADDED
@@ -0,0 +1,6 @@
1
+ let nextSmoothClipId = Date.now() * 1024;
2
+
3
+ export function allocateSmoothClipId(): number {
4
+ nextSmoothClipId += 1;
5
+ return nextSmoothClipId;
6
+ }