react-native-smooth-clip-view 0.1.0 → 0.2.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 (113) hide show
  1. package/README.md +164 -43
  2. package/SmoothClipView.podspec +8 -2
  3. package/android/CMakeLists.txt +39 -0
  4. package/android/build.gradle +69 -2
  5. package/android/proguard-rules.pro +6 -0
  6. package/android/src/main/cpp/SmoothClipAndroid.h +59 -0
  7. package/android/src/main/cpp/SmoothClipBindings.cpp +428 -0
  8. package/android/src/main/cpp/SmoothClipRegistry.cpp +913 -0
  9. package/android/src/main/java/com/smoothclipview/ClipGeometryNormalizer.kt +25 -0
  10. package/android/src/main/java/com/smoothclipview/SmoothClipBindings.kt +55 -0
  11. package/android/src/main/java/com/smoothclipview/SmoothClipModule.kt +128 -0
  12. package/android/src/main/java/com/smoothclipview/SmoothClipView.kt +164 -30
  13. package/android/src/main/java/com/smoothclipview/SmoothClipViewManager.kt +107 -10
  14. package/android/src/main/java/com/smoothclipview/SmoothClipViewPackage.kt +18 -2
  15. package/cpp/SmoothClipRegistry.h +93 -0
  16. package/cpp/SmoothClipSharedGeometry.h +50 -0
  17. package/ios/SmoothClipModuleProvider.h +9 -0
  18. package/ios/SmoothClipModuleProvider.mm +16 -0
  19. package/ios/SmoothClipRegistry.mm +717 -0
  20. package/ios/SmoothClipTurboModule.cpp +290 -0
  21. package/ios/SmoothClipTurboModule.h +132 -0
  22. package/ios/SmoothClipView.mm +976 -85
  23. package/ios/SmoothClipViewRegistry.h +27 -0
  24. package/lib/module/NativeSmoothClipModule.js +5 -0
  25. package/lib/module/NativeSmoothClipModule.js.map +1 -0
  26. package/lib/module/SmoothClipView.android.js +4 -0
  27. package/lib/module/SmoothClipView.android.js.map +1 -0
  28. package/lib/module/SmoothClipView.ios.js +34 -0
  29. package/lib/module/SmoothClipView.ios.js.map +1 -0
  30. package/lib/module/SmoothClipView.js +26 -12
  31. package/lib/module/SmoothClipView.js.map +1 -1
  32. package/lib/module/SmoothClipView.web.js +28 -12
  33. package/lib/module/SmoothClipView.web.js.map +1 -1
  34. package/lib/module/SmoothClipViewNativeComponent.ts +14 -1
  35. package/lib/module/driverState.js +102 -0
  36. package/lib/module/driverState.js.map +1 -0
  37. package/lib/module/driverTypes.js +4 -0
  38. package/lib/module/driverTypes.js.map +1 -0
  39. package/lib/module/drivers.android.js +4 -0
  40. package/lib/module/drivers.android.js.map +1 -0
  41. package/lib/module/drivers.ios.js +380 -0
  42. package/lib/module/drivers.ios.js.map +1 -0
  43. package/lib/module/drivers.js +276 -0
  44. package/lib/module/drivers.js.map +1 -0
  45. package/lib/module/geometry.js +19 -0
  46. package/lib/module/geometry.js.map +1 -1
  47. package/lib/module/index.js +2 -0
  48. package/lib/module/index.js.map +1 -1
  49. package/lib/module/index.web.js +2 -0
  50. package/lib/module/index.web.js.map +1 -1
  51. package/lib/module/nativeCompletion.js +21 -0
  52. package/lib/module/nativeCompletion.js.map +1 -0
  53. package/lib/module/reactRequests.js +50 -0
  54. package/lib/module/reactRequests.js.map +1 -0
  55. package/lib/module/smoothClipNative.android.js +15 -0
  56. package/lib/module/smoothClipNative.android.js.map +1 -0
  57. package/lib/module/smoothClipNative.js +8 -0
  58. package/lib/module/smoothClipNative.js.map +1 -0
  59. package/lib/typescript/src/NativeSmoothClipModule.d.ts +19 -0
  60. package/lib/typescript/src/NativeSmoothClipModule.d.ts.map +1 -0
  61. package/lib/typescript/src/SmoothClipView.android.d.ts +3 -0
  62. package/lib/typescript/src/SmoothClipView.android.d.ts.map +1 -0
  63. package/lib/typescript/src/SmoothClipView.d.ts +5 -6
  64. package/lib/typescript/src/SmoothClipView.d.ts.map +1 -1
  65. package/lib/typescript/src/SmoothClipView.ios.d.ts +9 -0
  66. package/lib/typescript/src/SmoothClipView.ios.d.ts.map +1 -0
  67. package/lib/typescript/src/SmoothClipView.web.d.ts +5 -7
  68. package/lib/typescript/src/SmoothClipView.web.d.ts.map +1 -1
  69. package/lib/typescript/src/SmoothClipViewNativeComponent.d.ts +4 -0
  70. package/lib/typescript/src/SmoothClipViewNativeComponent.d.ts.map +1 -1
  71. package/lib/typescript/src/driverState.d.ts +31 -0
  72. package/lib/typescript/src/driverState.d.ts.map +1 -0
  73. package/lib/typescript/src/driverTypes.d.ts +61 -0
  74. package/lib/typescript/src/driverTypes.d.ts.map +1 -0
  75. package/lib/typescript/src/drivers.android.d.ts +3 -0
  76. package/lib/typescript/src/drivers.android.d.ts.map +1 -0
  77. package/lib/typescript/src/drivers.d.ts +5 -0
  78. package/lib/typescript/src/drivers.d.ts.map +1 -0
  79. package/lib/typescript/src/drivers.ios.d.ts +5 -0
  80. package/lib/typescript/src/drivers.ios.d.ts.map +1 -0
  81. package/lib/typescript/src/geometry.d.ts +8 -0
  82. package/lib/typescript/src/geometry.d.ts.map +1 -1
  83. package/lib/typescript/src/index.d.ts +4 -1
  84. package/lib/typescript/src/index.d.ts.map +1 -1
  85. package/lib/typescript/src/index.web.d.ts +4 -1
  86. package/lib/typescript/src/index.web.d.ts.map +1 -1
  87. package/lib/typescript/src/nativeCompletion.d.ts +7 -0
  88. package/lib/typescript/src/nativeCompletion.d.ts.map +1 -0
  89. package/lib/typescript/src/reactRequests.d.ts +7 -0
  90. package/lib/typescript/src/reactRequests.d.ts.map +1 -0
  91. package/lib/typescript/src/smoothClipNative.android.d.ts +4 -0
  92. package/lib/typescript/src/smoothClipNative.android.d.ts.map +1 -0
  93. package/lib/typescript/src/smoothClipNative.d.ts +3 -0
  94. package/lib/typescript/src/smoothClipNative.d.ts.map +1 -0
  95. package/package.json +12 -7
  96. package/src/NativeSmoothClipModule.ts +104 -0
  97. package/src/SmoothClipView.android.tsx +2 -0
  98. package/src/SmoothClipView.ios.tsx +36 -0
  99. package/src/SmoothClipView.tsx +31 -27
  100. package/src/SmoothClipView.web.tsx +26 -22
  101. package/src/SmoothClipViewNativeComponent.ts +14 -1
  102. package/src/driverState.ts +150 -0
  103. package/src/driverTypes.ts +91 -0
  104. package/src/drivers.android.ts +6 -0
  105. package/src/drivers.ios.ts +665 -0
  106. package/src/drivers.ts +416 -0
  107. package/src/geometry.ts +39 -0
  108. package/src/index.ts +19 -1
  109. package/src/index.web.ts +19 -1
  110. package/src/nativeCompletion.ts +37 -0
  111. package/src/reactRequests.ts +64 -0
  112. package/src/smoothClipNative.android.ts +20 -0
  113. package/src/smoothClipNative.ts +5 -0
@@ -0,0 +1,717 @@
1
+ #import "SmoothClipViewRegistry.h"
2
+
3
+ #import "SmoothClipView.h"
4
+
5
+ #import <React/RCTUtils.h>
6
+ #import <QuartzCore/QuartzCore.h>
7
+ #import <UIKit/UIKit.h>
8
+ #if defined(SMOOTH_CLIP_ENABLE_SIGNPOSTS) && SMOOTH_CLIP_ENABLE_SIGNPOSTS
9
+ #import <os/signpost.h>
10
+ #endif
11
+
12
+ #include <algorithm>
13
+ #include <cmath>
14
+ #include <limits>
15
+ #include <mutex>
16
+ #include <optional>
17
+ #include <unordered_map>
18
+ #include <unordered_set>
19
+ #include <utility>
20
+ #include <vector>
21
+
22
+ @interface SmoothClipView (Registry)
23
+ - (void)smoothClipApplyPresentation:(smoothclip::Presentation)presentation;
24
+ - (smoothclip::Presentation)smoothClipFreezePresentation;
25
+ - (smoothclip::Presentation)smoothClipCurrentPresentation;
26
+ - (void)smoothClipAnimateTiming:(smoothclip::Presentation)presentation
27
+ animation:(smoothclip::TimingAnimation)animation
28
+ animationId:(int32_t)animationId;
29
+ - (void)smoothClipAnimateSpring:(smoothclip::Presentation)presentation
30
+ animation:(smoothclip::SpringAnimation)animation
31
+ animationId:(int32_t)animationId;
32
+ - (void)smoothClipAnimateKeyframes:(smoothclip::Presentation)presentation
33
+ keyframes:(const std::vector<smoothclip::Keyframe> &)keyframes
34
+ durationMs:(double)durationMs
35
+ animationId:(int32_t)animationId;
36
+ - (void)smoothClipCancelAnimationUsingTarget:(BOOL)useTarget;
37
+ - (BOOL)smoothClipIsJoinable;
38
+ @end
39
+
40
+ namespace smoothclip {
41
+ namespace {
42
+
43
+ using ViewKey = uintptr_t;
44
+
45
+ enum class Ownership { Interactive, Native };
46
+ enum class AnimationKind { Timing, Spring, Keyframes };
47
+
48
+ struct ActiveAnimation {
49
+ int32_t animationId;
50
+ std::unordered_set<ViewKey> participants;
51
+ bool finished = true;
52
+ AnimationKind kind = AnimationKind::Timing;
53
+ // Geometry the transition started from. Used when a view must join the
54
+ // animation but there is no laid-out peer to sample presentation from.
55
+ Presentation start{{0, 0, 0, 0, 0}, 0, 0};
56
+ TimingAnimation timing{};
57
+ SpringAnimation spring{};
58
+ std::vector<Keyframe> keyframes;
59
+ double durationMs = 0;
60
+ CFTimeInterval startedAt = 0;
61
+ };
62
+
63
+ struct DriverState {
64
+ Presentation latest{{0, 0, 0, 0, 0}, 0, 0};
65
+ bool hasLatest = false;
66
+ Ownership ownership = Ownership::Interactive;
67
+ std::vector<ViewKey> views;
68
+ std::optional<ActiveAnimation> animation;
69
+ int32_t nextAnimationId = 0;
70
+ // Set by destroyDriver while views are still registered (StrictMode effect
71
+ // replay, hosts mounted in another subtree). The entry is erased when the
72
+ // last view leaves and revived by a take-ownership setPresentation.
73
+ bool destroyed = false;
74
+ };
75
+
76
+ struct CompletionSink {
77
+ const void *owner = nullptr;
78
+ CompletionCallback callback;
79
+ };
80
+
81
+ std::unordered_map<uint64_t, DriverState> &registry() {
82
+ static std::unordered_map<uint64_t, DriverState> value;
83
+ return value;
84
+ }
85
+
86
+ CompletionSink &completionSink() {
87
+ static CompletionSink sink;
88
+ return sink;
89
+ }
90
+
91
+ // The sink is written from the TurboModule ctor/dtor on the JS thread and
92
+ // invoked on main. Invoking under the lock also keeps the owning module
93
+ // alive for the duration of the callback.
94
+ std::mutex &completionSinkMutex() {
95
+ static std::mutex value;
96
+ return value;
97
+ }
98
+
99
+ Presentation unavailablePresentation() {
100
+ // Non-finite geometry makes the JS side fall back to its current value
101
+ // instead of applying zeros.
102
+ return {{NAN, NAN, NAN, NAN, NAN}, NAN, NAN};
103
+ }
104
+
105
+ #if defined(SMOOTH_CLIP_ENABLE_SIGNPOSTS) && SMOOTH_CLIP_ENABLE_SIGNPOSTS
106
+ os_log_t signpostLog() {
107
+ static os_log_t log = os_log_create(
108
+ "com.smoothclipview", "clip-driver");
109
+ return log;
110
+ }
111
+ #endif
112
+
113
+ ViewKey keyForView(SmoothClipView *view) {
114
+ return reinterpret_cast<ViewKey>((__bridge void *)view);
115
+ }
116
+
117
+ SmoothClipView *viewForKey(ViewKey key) {
118
+ return (__bridge SmoothClipView *)(reinterpret_cast<void *>(key));
119
+ }
120
+
121
+ bool shouldReduceMotion(int32_t setting) {
122
+ return setting == 1 ||
123
+ (setting == 0 && UIAccessibilityIsReduceMotionEnabled());
124
+ }
125
+
126
+ void emitCompletion(
127
+ uint64_t driverId,
128
+ DriverState &state,
129
+ int32_t animationId,
130
+ bool finished) {
131
+ state.animation.reset();
132
+ {
133
+ std::lock_guard<std::mutex> lock(completionSinkMutex());
134
+ if (completionSink().callback) {
135
+ completionSink().callback(driverId, animationId, finished);
136
+ }
137
+ }
138
+ #if defined(SMOOTH_CLIP_ENABLE_SIGNPOSTS) && SMOOTH_CLIP_ENABLE_SIGNPOSTS
139
+ os_signpost_event_emit(
140
+ signpostLog(), OS_SIGNPOST_ID_EXCLUSIVE, "completion",
141
+ "driver=%llu animation=%d finished=%{public}d",
142
+ driverId, animationId, finished);
143
+ #endif
144
+ }
145
+
146
+ void emitStandaloneCompletion(
147
+ uint64_t driverId,
148
+ int32_t animationId,
149
+ bool finished) {
150
+ {
151
+ std::lock_guard<std::mutex> lock(completionSinkMutex());
152
+ if (completionSink().callback) {
153
+ completionSink().callback(driverId, animationId, finished);
154
+ }
155
+ }
156
+ #if defined(SMOOTH_CLIP_ENABLE_SIGNPOSTS) && SMOOTH_CLIP_ENABLE_SIGNPOSTS
157
+ os_signpost_event_emit(
158
+ signpostLog(), OS_SIGNPOST_ID_EXCLUSIVE, "completion",
159
+ "driver=%llu animation=%d finished=%{public}d",
160
+ driverId, animationId, finished);
161
+ #endif
162
+ }
163
+
164
+ int32_t allocateAnimationId(DriverState &state) {
165
+ state.nextAnimationId =
166
+ state.nextAnimationId == std::numeric_limits<int32_t>::max()
167
+ ? 1
168
+ : state.nextAnimationId + 1;
169
+ return state.nextAnimationId;
170
+ }
171
+
172
+ Presentation canonicalFrozenPresentation(DriverState &state) {
173
+ Presentation canonical = state.latest;
174
+ bool hasCanonical = false;
175
+ for (const ViewKey key : state.views) {
176
+ const Presentation frozen = [viewForKey(key) smoothClipFreezePresentation];
177
+ if (!hasCanonical) {
178
+ canonical = frozen;
179
+ hasCanonical = true;
180
+ }
181
+ }
182
+ return canonical;
183
+ }
184
+
185
+ void cancelActive(
186
+ uint64_t driverId,
187
+ DriverState &state,
188
+ bool useTarget) {
189
+ if (!state.animation.has_value()) return;
190
+ const int32_t animationId = state.animation->animationId;
191
+ for (const ViewKey key : state.animation->participants) {
192
+ [viewForKey(key) smoothClipCancelAnimationUsingTarget:useTarget];
193
+ }
194
+ emitCompletion(driverId, state, animationId, false);
195
+ }
196
+
197
+ void applyPresentation(DriverState &state, Presentation presentation) {
198
+ state.latest = presentation;
199
+ state.hasLatest = true;
200
+ state.ownership = Ownership::Interactive;
201
+ for (const ViewKey key : state.views) {
202
+ [viewForKey(key) smoothClipApplyPresentation:presentation];
203
+ }
204
+ }
205
+
206
+ void prepareAnimation(
207
+ uint64_t driverId,
208
+ DriverState &state,
209
+ AnimationStart start,
210
+ Presentation target) {
211
+ const bool acceptsInteractiveStart =
212
+ start.hasInteractiveStart && state.ownership == Ownership::Interactive;
213
+ cancelActive(driverId, state, false);
214
+ if (acceptsInteractiveStart) {
215
+ applyPresentation(state, start.interactiveStart);
216
+ }
217
+ state.latest = target;
218
+ state.hasLatest = true;
219
+ state.ownership = Ownership::Native;
220
+ }
221
+
222
+ // Mirrors prepareAnimation's start resolution. Must be evaluated before
223
+ // prepareAnimation overwrites state.latest with the target.
224
+ Presentation resolvedAnimationStart(
225
+ const DriverState &state,
226
+ const AnimationStart &start) {
227
+ return start.hasInteractiveStart && state.ownership == Ownership::Interactive
228
+ ? start.interactiveStart
229
+ : state.latest;
230
+ }
231
+
232
+ // Installs the remaining part of the active animation on a view that joins
233
+ // mid-flight, starting from `visible`. Shared by registerView (mount during
234
+ // animation) and joinActiveAnimation (first layout after such a mount).
235
+ void dispatchActiveAnimationJoin(
236
+ DriverState &state,
237
+ SmoothClipView *view,
238
+ ViewKey key,
239
+ const Presentation &visible) {
240
+ auto &active = *state.animation;
241
+ active.participants.insert(key);
242
+ const double elapsedMs = (CACurrentMediaTime() - active.startedAt) * 1000.0;
243
+ const double remainingMs = MAX(0, active.durationMs - elapsedMs);
244
+ if (active.kind == AnimationKind::Timing) {
245
+ TimingAnimation timing = active.timing;
246
+ timing.durationMs = remainingMs;
247
+ [view smoothClipAnimateTiming:state.latest
248
+ animation:timing
249
+ animationId:active.animationId];
250
+ } else if (active.kind == AnimationKind::Spring) {
251
+ [view smoothClipAnimateSpring:state.latest
252
+ animation:active.spring
253
+ animationId:active.animationId];
254
+ } else {
255
+ const double progress = active.durationMs <= 0
256
+ ? 1
257
+ : MIN(1, MAX(0, elapsedMs / active.durationMs));
258
+ std::vector<Keyframe> remaining;
259
+ remaining.push_back({0, visible});
260
+ for (const Keyframe &frame : active.keyframes) {
261
+ if (frame.offset <= progress) continue;
262
+ const double offset = (frame.offset - progress) / (1 - progress);
263
+ remaining.push_back({offset, frame.presentation});
264
+ }
265
+ if (remaining.size() == 1) remaining.push_back({1, state.latest});
266
+ [view smoothClipAnimateKeyframes:state.latest
267
+ keyframes:remaining
268
+ durationMs:remainingMs
269
+ animationId:active.animationId];
270
+ }
271
+ }
272
+
273
+ } // namespace
274
+
275
+ void setCompletionCallback(
276
+ const void *owner,
277
+ CompletionCallback callback) {
278
+ std::lock_guard<std::mutex> lock(completionSinkMutex());
279
+ completionSink().owner = owner;
280
+ completionSink().callback = std::move(callback);
281
+ }
282
+
283
+ void clearCompletionCallback(const void *owner) {
284
+ std::lock_guard<std::mutex> lock(completionSinkMutex());
285
+ if (completionSink().owner != owner) return;
286
+ completionSink().owner = nullptr;
287
+ completionSink().callback = nullptr;
288
+ }
289
+
290
+ void registerView(
291
+ uint64_t driverId,
292
+ SmoothClipView *view,
293
+ Presentation initialPresentation) {
294
+ NSCAssert(NSThread.isMainThread, @"SmoothClip registry is main-thread only");
295
+ auto &state = registry()[driverId];
296
+ state.destroyed = false;
297
+ // Seed canonical geometry from the mounting view's initial props BEFORE
298
+ // sampling `visible`; otherwise the first registration captures the default
299
+ // empty {0,0,0,0} rect, seeding the view (and the presentation layer the
300
+ // opening timing animation samples as its "from") to the top-left instead of
301
+ // the real origin rectangle.
302
+ if (!state.hasLatest) {
303
+ state.latest = initialPresentation;
304
+ state.hasLatest = true;
305
+ }
306
+ const bool shouldReplay =
307
+ state.animation.has_value() && !state.views.empty();
308
+ Presentation visible = state.latest;
309
+ if (shouldReplay) {
310
+ // A registered peer that never laid out reports zero geometry; joining
311
+ // from it would visibly collapse the clip. Prefer the first laid-out
312
+ // peer and fall back to the animation's start.
313
+ SmoothClipView *reference = nil;
314
+ for (const ViewKey candidate : state.views) {
315
+ SmoothClipView *candidateView = viewForKey(candidate);
316
+ if ([candidateView smoothClipIsJoinable]) {
317
+ reference = candidateView;
318
+ break;
319
+ }
320
+ }
321
+ visible = reference != nil
322
+ ? [reference smoothClipCurrentPresentation]
323
+ : state.animation->start;
324
+ }
325
+ const ViewKey key = keyForView(view);
326
+ if (std::find(state.views.begin(), state.views.end(), key) ==
327
+ state.views.end()) {
328
+ state.views.push_back(key);
329
+ }
330
+ [view smoothClipApplyPresentation:visible];
331
+ if (!shouldReplay || !state.animation.has_value()) return;
332
+
333
+ dispatchActiveAnimationJoin(state, view, key, visible);
334
+ }
335
+
336
+ bool joinActiveAnimation(uint64_t driverId, SmoothClipView *view) {
337
+ NSCAssert(NSThread.isMainThread, @"SmoothClip registry is main-thread only");
338
+ auto iterator = registry().find(driverId);
339
+ if (iterator == registry().end()) return false;
340
+ auto &state = iterator->second;
341
+ if (!state.animation.has_value() || state.views.empty()) return false;
342
+ const ViewKey key = keyForView(view);
343
+ if (std::find(state.views.begin(), state.views.end(), key) ==
344
+ state.views.end()) {
345
+ return false;
346
+ }
347
+ // Join from a laid-out peer's live presentation, exactly as a view that had
348
+ // registered at this moment would. When the joining view is the only
349
+ // registered host, its own layer never received geometry (the install was
350
+ // deferred for lack of layout), so fall back to the animation's start.
351
+ ViewKey canonicalKey = 0;
352
+ for (const ViewKey candidate : state.views) {
353
+ if (candidate != key && [viewForKey(candidate) smoothClipIsJoinable]) {
354
+ canonicalKey = candidate;
355
+ break;
356
+ }
357
+ }
358
+ const Presentation visible = canonicalKey != 0
359
+ ? [viewForKey(canonicalKey) smoothClipCurrentPresentation]
360
+ : state.animation->start;
361
+ [view smoothClipApplyPresentation:visible];
362
+ dispatchActiveAnimationJoin(state, view, key, visible);
363
+ return true;
364
+ }
365
+
366
+ void unregisterView(uint64_t driverId, SmoothClipView *view) {
367
+ NSCAssert(NSThread.isMainThread, @"SmoothClip registry is main-thread only");
368
+ auto iterator = registry().find(driverId);
369
+ if (iterator == registry().end()) return;
370
+
371
+ auto &state = iterator->second;
372
+ const ViewKey key = keyForView(view);
373
+ if (state.animation.has_value() &&
374
+ state.animation->participants.erase(key) > 0) {
375
+ state.animation->finished = false;
376
+ if (state.animation->participants.empty()) {
377
+ // The animation ends here; without releasing ownership a host that
378
+ // remounts on this driver later would drop every interactive update.
379
+ state.ownership = Ownership::Interactive;
380
+ emitCompletion(
381
+ driverId, state, state.animation->animationId, false);
382
+ }
383
+ }
384
+ state.views.erase(
385
+ std::remove(state.views.begin(), state.views.end(), key),
386
+ state.views.end());
387
+ if (state.destroyed && state.views.empty()) {
388
+ registry().erase(iterator);
389
+ }
390
+ }
391
+
392
+ void setPresentation(
393
+ uint64_t driverId,
394
+ Presentation presentation,
395
+ bool takeOwnership) {
396
+ // CALayer writes require the main thread (not main-queue drain context).
397
+ if (!NSThread.isMainThread) {
398
+ // Never block an off-main caller: a synchronous hop can deadlock against
399
+ // the worklets UI-runtime mutex (runOnUISync executes on the calling
400
+ // thread while holding it, and the main thread routinely waits for the
401
+ // same mutex when draining scheduled worklets).
402
+ RCTExecuteOnMainQueue(^{
403
+ setPresentation(driverId, presentation, takeOwnership);
404
+ });
405
+ return;
406
+ }
407
+ auto iterator = registry().find(driverId);
408
+ if (!takeOwnership) {
409
+ if (iterator == registry().end() || iterator->second.destroyed) {
410
+ // A stale interactive delivery must not resurrect a destroyed driver.
411
+ return;
412
+ }
413
+ } else if (iterator == registry().end()) {
414
+ // Same create-or-revive semantics operator[] had; a single hash lookup.
415
+ iterator = registry().try_emplace(driverId).first;
416
+ }
417
+ #if defined(SMOOTH_CLIP_ENABLE_SIGNPOSTS) && SMOOTH_CLIP_ENABLE_SIGNPOSTS
418
+ const bool signpostsEnabled = os_signpost_enabled(signpostLog());
419
+ os_signpost_id_t identifier = OS_SIGNPOST_ID_NULL;
420
+ if (signpostsEnabled) {
421
+ identifier = os_signpost_id_generate(signpostLog());
422
+ os_signpost_interval_begin(
423
+ signpostLog(), identifier, "lookup-fanout", "driver=%llu", driverId);
424
+ }
425
+ #endif
426
+ auto &state = iterator->second;
427
+ if (!takeOwnership && state.ownership != Ownership::Interactive) {
428
+ #if defined(SMOOTH_CLIP_ENABLE_SIGNPOSTS) && SMOOTH_CLIP_ENABLE_SIGNPOSTS
429
+ if (signpostsEnabled) {
430
+ os_signpost_interval_end(
431
+ signpostLog(), identifier, "lookup-fanout", "ignored=native-owner");
432
+ }
433
+ #endif
434
+ return;
435
+ }
436
+ if (takeOwnership) {
437
+ state.destroyed = false;
438
+ cancelActive(driverId, state, false);
439
+ }
440
+ applyPresentation(state, presentation);
441
+ #if defined(SMOOTH_CLIP_ENABLE_SIGNPOSTS) && SMOOTH_CLIP_ENABLE_SIGNPOSTS
442
+ if (signpostsEnabled) {
443
+ os_signpost_interval_end(
444
+ signpostLog(), identifier, "lookup-fanout", "views=%lu",
445
+ static_cast<unsigned long>(state.views.size()));
446
+ }
447
+ #endif
448
+ }
449
+
450
+ Presentation beginInteraction(uint64_t driverId) {
451
+ if (!NSThread.isMainThread) {
452
+ // See setPresentation: blocking off-main risks a deadlock, so the call
453
+ // fails defined and the JS side keeps its current value.
454
+ return unavailablePresentation();
455
+ } auto iterator = registry().find(driverId);
456
+ if (iterator == registry().end()) return unavailablePresentation();
457
+ auto &state = iterator->second;
458
+ state.destroyed = false;
459
+ if (!state.animation.has_value()) {
460
+ state.ownership = Ownership::Interactive;
461
+ return state.latest;
462
+ }
463
+
464
+ const int32_t animationId = state.animation->animationId;
465
+ const Presentation canonical = canonicalFrozenPresentation(state);
466
+ state.latest = canonical;
467
+ state.hasLatest = true;
468
+ state.ownership = Ownership::Interactive;
469
+ emitCompletion(driverId, state, animationId, false);
470
+ return canonical;
471
+ }
472
+
473
+ int32_t animateTiming(
474
+ uint64_t driverId,
475
+ AnimationStart start,
476
+ Presentation presentation,
477
+ TimingAnimation animation) {
478
+ if (!NSThread.isMainThread) {
479
+ // See setPresentation: blocking off-main risks a deadlock. 0 is the
480
+ // documented rejection sentinel.
481
+ return 0;
482
+ } auto iterator = registry().find(driverId);
483
+ if (iterator == registry().end()) return 0;
484
+ auto &state = iterator->second;
485
+ state.destroyed = false;
486
+ const int32_t animationId = allocateAnimationId(state);
487
+ const Presentation resolvedStart = resolvedAnimationStart(state, start);
488
+ prepareAnimation(driverId, state, start, presentation);
489
+
490
+ if (shouldReduceMotion(animation.reduceMotion) || animation.durationMs <= 0) {
491
+ applyPresentation(state, presentation);
492
+ emitCompletion(driverId, state, animationId, true);
493
+ return animationId;
494
+ }
495
+
496
+ ActiveAnimation active{animationId};
497
+ active.kind = AnimationKind::Timing;
498
+ active.start = resolvedStart;
499
+ active.timing = animation;
500
+ active.durationMs = animation.durationMs;
501
+ active.startedAt = CACurrentMediaTime();
502
+ active.participants.insert(state.views.begin(), state.views.end());
503
+ state.animation = std::move(active);
504
+ if (state.views.empty()) {
505
+ state.ownership = Ownership::Interactive;
506
+ emitCompletion(driverId, state, animationId, true);
507
+ return animationId;
508
+ }
509
+ #if defined(SMOOTH_CLIP_ENABLE_SIGNPOSTS) && SMOOTH_CLIP_ENABLE_SIGNPOSTS
510
+ os_signpost_event_emit(
511
+ signpostLog(), OS_SIGNPOST_ID_EXCLUSIVE, "transition-setup",
512
+ "driver=%llu animation=%d views=%lu type=timing",
513
+ driverId, animationId,
514
+ static_cast<unsigned long>(state.views.size()));
515
+ #endif
516
+ for (const ViewKey key : state.views) {
517
+ [viewForKey(key) smoothClipAnimateTiming:presentation
518
+ animation:animation
519
+ animationId:animationId];
520
+ }
521
+ return animationId;
522
+ }
523
+
524
+ int32_t animateSpring(
525
+ uint64_t driverId,
526
+ AnimationStart start,
527
+ Presentation presentation,
528
+ SpringAnimation animation) {
529
+ if (!NSThread.isMainThread) {
530
+ // See setPresentation: blocking off-main risks a deadlock.
531
+ return 0;
532
+ } auto iterator = registry().find(driverId);
533
+ if (iterator == registry().end()) return 0;
534
+ auto &state = iterator->second;
535
+ state.destroyed = false;
536
+ const int32_t animationId = allocateAnimationId(state);
537
+ const Presentation resolvedStart = resolvedAnimationStart(state, start);
538
+ prepareAnimation(driverId, state, start, presentation);
539
+
540
+ if (shouldReduceMotion(animation.reduceMotion)) {
541
+ applyPresentation(state, presentation);
542
+ emitCompletion(driverId, state, animationId, true);
543
+ return animationId;
544
+ }
545
+
546
+ ActiveAnimation active{animationId};
547
+ active.kind = AnimationKind::Spring;
548
+ active.start = resolvedStart;
549
+ active.spring = animation;
550
+ active.startedAt = CACurrentMediaTime();
551
+ active.participants.insert(state.views.begin(), state.views.end());
552
+ state.animation = std::move(active);
553
+ if (state.views.empty()) {
554
+ state.ownership = Ownership::Interactive;
555
+ emitCompletion(driverId, state, animationId, true);
556
+ return animationId;
557
+ }
558
+ #if defined(SMOOTH_CLIP_ENABLE_SIGNPOSTS) && SMOOTH_CLIP_ENABLE_SIGNPOSTS
559
+ os_signpost_event_emit(
560
+ signpostLog(), OS_SIGNPOST_ID_EXCLUSIVE, "transition-setup",
561
+ "driver=%llu animation=%d views=%lu type=spring",
562
+ driverId, animationId,
563
+ static_cast<unsigned long>(state.views.size()));
564
+ #endif
565
+ for (const ViewKey key : state.views) {
566
+ [viewForKey(key) smoothClipAnimateSpring:presentation
567
+ animation:animation
568
+ animationId:animationId];
569
+ }
570
+ return animationId;
571
+ }
572
+
573
+ int32_t animateKeyframes(
574
+ uint64_t driverId,
575
+ AnimationStart start,
576
+ Presentation presentation,
577
+ double durationMs,
578
+ std::vector<Keyframe> keyframes,
579
+ int32_t reduceMotion) {
580
+ if (!NSThread.isMainThread) {
581
+ // See setPresentation: blocking off-main risks a deadlock.
582
+ return 0;
583
+ } auto iterator = registry().find(driverId);
584
+ if (iterator == registry().end()) return 0;
585
+ auto &state = iterator->second;
586
+ state.destroyed = false;
587
+ const int32_t animationId = allocateAnimationId(state);
588
+ const Presentation resolvedStart = resolvedAnimationStart(state, start);
589
+ prepareAnimation(driverId, state, start, presentation);
590
+ if (shouldReduceMotion(reduceMotion) || durationMs <= 0) {
591
+ applyPresentation(state, presentation);
592
+ emitCompletion(driverId, state, animationId, true);
593
+ return animationId;
594
+ }
595
+
596
+ ActiveAnimation active{animationId};
597
+ active.kind = AnimationKind::Keyframes;
598
+ active.start = resolvedStart;
599
+ active.keyframes = keyframes;
600
+ active.durationMs = durationMs;
601
+ active.startedAt = CACurrentMediaTime();
602
+ active.participants.insert(state.views.begin(), state.views.end());
603
+ state.animation = std::move(active);
604
+ if (state.views.empty()) {
605
+ state.ownership = Ownership::Interactive;
606
+ emitCompletion(driverId, state, animationId, true);
607
+ return animationId;
608
+ }
609
+ for (const ViewKey key : state.views) {
610
+ [viewForKey(key) smoothClipAnimateKeyframes:presentation
611
+ keyframes:keyframes
612
+ durationMs:durationMs
613
+ animationId:animationId];
614
+ }
615
+ return animationId;
616
+ }
617
+
618
+ int32_t rejectAnimation(uint64_t driverId) {
619
+ if (!NSThread.isMainThread) {
620
+ // See setPresentation: blocking off-main risks a deadlock.
621
+ return 0;
622
+ } auto iterator = registry().find(driverId);
623
+ if (iterator == registry().end()) return 0;
624
+ const int32_t animationId = allocateAnimationId(iterator->second);
625
+ emitStandaloneCompletion(driverId, animationId, false);
626
+ return animationId;
627
+ }
628
+
629
+ CancelResult cancelAnimation(
630
+ uint64_t driverId,
631
+ int32_t animationId,
632
+ bool useTarget) {
633
+ if (!NSThread.isMainThread) {
634
+ // See setPresentation: blocking off-main risks a deadlock.
635
+ return {false, unavailablePresentation()};
636
+ } auto iterator = registry().find(driverId);
637
+ if (iterator == registry().end()) {
638
+ return {false, {{0, 0, 0, 0, 0}, 0, 0}};
639
+ }
640
+ auto &state = iterator->second;
641
+ if (!state.animation.has_value() ||
642
+ (animationId > 0 && animationId != state.animation->animationId)) {
643
+ return {false, state.latest};
644
+ }
645
+
646
+ #if defined(SMOOTH_CLIP_ENABLE_SIGNPOSTS) && SMOOTH_CLIP_ENABLE_SIGNPOSTS
647
+ os_signpost_event_emit(
648
+ signpostLog(), OS_SIGNPOST_ID_EXCLUSIVE, "cancellation",
649
+ "driver=%llu animation=%d target=%{public}d",
650
+ driverId, state.animation->animationId, useTarget);
651
+ #endif
652
+ if (useTarget) {
653
+ const Presentation target = state.latest;
654
+ cancelActive(driverId, state, true);
655
+ state.ownership = Ownership::Interactive;
656
+ return {true, target};
657
+ }
658
+ return {true, beginInteraction(driverId)};
659
+ }
660
+
661
+ void destroyDriver(uint64_t driverId) {
662
+ if (!NSThread.isMainThread) {
663
+ // See setPresentation: never block an off-main caller.
664
+ RCTExecuteOnMainQueue(^{
665
+ destroyDriver(driverId);
666
+ });
667
+ return;
668
+ } auto iterator = registry().find(driverId);
669
+ if (iterator == registry().end()) return;
670
+ auto &state = iterator->second;
671
+ cancelActive(driverId, state, false);
672
+ if (state.views.empty()) {
673
+ registry().erase(iterator);
674
+ } else {
675
+ // Views can outlive the hook briefly (StrictMode effect replay, hosts in
676
+ // another subtree). Keep a tombstone so their registration stays intact;
677
+ // it is erased when the last view leaves and revived by the hook's
678
+ // authoritative take-ownership re-seed.
679
+ state.destroyed = true;
680
+ state.ownership = Ownership::Interactive;
681
+ }
682
+ }
683
+
684
+ void viewAnimationDidStop(
685
+ uint64_t driverId,
686
+ int32_t animationId,
687
+ SmoothClipView *view,
688
+ bool finished) {
689
+ NSCAssert(NSThread.isMainThread, @"SmoothClip registry is main-thread only");
690
+ auto iterator = registry().find(driverId);
691
+ if (iterator == registry().end()) return;
692
+ auto &state = iterator->second;
693
+ if (!state.animation.has_value() ||
694
+ state.animation->animationId != animationId) return;
695
+
696
+ const ViewKey key = keyForView(view);
697
+ if (state.animation->participants.erase(key) == 0) return;
698
+ state.animation->finished = state.animation->finished && finished;
699
+ if (state.animation->participants.empty()) {
700
+ state.ownership = Ownership::Interactive;
701
+ emitCompletion(
702
+ driverId, state, animationId, state.animation->finished);
703
+ }
704
+ }
705
+
706
+ size_t registeredViewCount(uint64_t driverId) {
707
+ auto iterator = registry().find(driverId);
708
+ return iterator == registry().end() ? 0 : iterator->second.views.size();
709
+ }
710
+
711
+ bool hasActiveAnimation(uint64_t driverId) {
712
+ auto iterator = registry().find(driverId);
713
+ return iterator != registry().end() &&
714
+ iterator->second.animation.has_value();
715
+ }
716
+
717
+ } // namespace smoothclip