react-native-smooth-clip-view 0.1.0 → 0.2.1

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