react-native-smooth-clip-view 0.4.5 → 0.5.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 (65) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/README.md +52 -0
  3. package/android/src/main/cpp/SmoothClipAndroid.h +22 -0
  4. package/android/src/main/cpp/SmoothClipBindings.cpp +37 -3
  5. package/android/src/main/cpp/SmoothClipRegistry.cpp +85 -2
  6. package/android/src/main/java/com/smoothclipview/ClipGeometryNormalizer.kt +209 -10
  7. package/android/src/main/java/com/smoothclipview/SmoothClipBackdropView.kt +59 -0
  8. package/android/src/main/java/com/smoothclipview/SmoothClipBackdropViewManager.kt +63 -0
  9. package/android/src/main/java/com/smoothclipview/SmoothClipBindings.kt +15 -0
  10. package/android/src/main/java/com/smoothclipview/SmoothClipView.kt +316 -34
  11. package/android/src/main/java/com/smoothclipview/SmoothClipViewManager.kt +19 -0
  12. package/android/src/main/java/com/smoothclipview/SmoothClipViewPackage.kt +1 -1
  13. package/cpp/SmoothClipAnimationCurve.h +36 -11
  14. package/cpp/SmoothClipRegistry.h +11 -4
  15. package/ios/SmoothClipBackdropView.h +17 -0
  16. package/ios/SmoothClipBackdropView.mm +267 -0
  17. package/ios/SmoothClipGeometry.h +8 -7
  18. package/ios/SmoothClipRegistry.mm +192 -9
  19. package/ios/SmoothClipTurboModule.cpp +33 -11
  20. package/ios/SmoothClipView.mm +713 -22
  21. package/ios/SmoothClipViewRegistry.h +14 -0
  22. package/lib/module/SmoothClipBackdropView.js +40 -0
  23. package/lib/module/SmoothClipBackdropView.js.map +1 -0
  24. package/lib/module/SmoothClipBackdropViewNativeComponent.ts +15 -0
  25. package/lib/module/SmoothClipView.ios.js +17 -1
  26. package/lib/module/SmoothClipView.ios.js.map +1 -1
  27. package/lib/module/SmoothClipViewNativeComponent.ts +4 -0
  28. package/lib/module/geometry.js +44 -6
  29. package/lib/module/geometry.js.map +1 -1
  30. package/lib/module/groups.native.js +10 -3
  31. package/lib/module/groups.native.js.map +1 -1
  32. package/lib/module/index.js +1 -0
  33. package/lib/module/index.js.map +1 -1
  34. package/lib/module/presentationCodec.js +6 -2
  35. package/lib/module/presentationCodec.js.map +1 -1
  36. package/lib/typescript/src/SmoothClipBackdropView.d.ts +155 -0
  37. package/lib/typescript/src/SmoothClipBackdropView.d.ts.map +1 -0
  38. package/lib/typescript/src/SmoothClipBackdropViewNativeComponent.d.ts +8 -0
  39. package/lib/typescript/src/SmoothClipBackdropViewNativeComponent.d.ts.map +1 -0
  40. package/lib/typescript/src/SmoothClipView.android.d.ts +1 -1
  41. package/lib/typescript/src/SmoothClipView.android.d.ts.map +1 -1
  42. package/lib/typescript/src/SmoothClipView.d.ts +1 -1
  43. package/lib/typescript/src/SmoothClipView.d.ts.map +1 -1
  44. package/lib/typescript/src/SmoothClipView.ios.d.ts +15 -1
  45. package/lib/typescript/src/SmoothClipView.ios.d.ts.map +1 -1
  46. package/lib/typescript/src/SmoothClipViewNativeComponent.d.ts +4 -0
  47. package/lib/typescript/src/SmoothClipViewNativeComponent.d.ts.map +1 -1
  48. package/lib/typescript/src/geometry.d.ts +19 -0
  49. package/lib/typescript/src/geometry.d.ts.map +1 -1
  50. package/lib/typescript/src/groups.native.d.ts.map +1 -1
  51. package/lib/typescript/src/index.d.ts +4 -2
  52. package/lib/typescript/src/index.d.ts.map +1 -1
  53. package/lib/typescript/src/presentationCodec.d.ts +1 -1
  54. package/lib/typescript/src/presentationCodec.d.ts.map +1 -1
  55. package/package.json +4 -1
  56. package/src/SmoothClipBackdropView.tsx +53 -0
  57. package/src/SmoothClipBackdropViewNativeComponent.ts +15 -0
  58. package/src/SmoothClipView.android.tsx +4 -1
  59. package/src/SmoothClipView.ios.tsx +24 -1
  60. package/src/SmoothClipView.tsx +4 -1
  61. package/src/SmoothClipViewNativeComponent.ts +4 -0
  62. package/src/geometry.ts +59 -4
  63. package/src/groups.native.ts +9 -3
  64. package/src/index.ts +7 -1
  65. package/src/presentationCodec.ts +8 -2
@@ -1,5 +1,6 @@
1
1
  #import "SmoothClipViewRegistry.h"
2
2
 
3
+ #import "SmoothClipBackdropView.h"
3
4
  #import "SmoothClipView.h"
4
5
 
5
6
  #import <React/RCTUtils.h>
@@ -57,6 +58,21 @@
57
58
  - (void)smoothClipClearVelocitySamples;
58
59
  @end
59
60
 
61
+ @interface SmoothClipBackdropView (Registry)
62
+ - (CGPoint)smoothClipBackdropCurrentTranslation;
63
+ - (void)smoothClipApplyBackdrop:(CGPoint)translation;
64
+ - (BOOL)smoothClipAnimateBackdropTo:(CGPoint)target
65
+ timing:(smoothclip::TimingAnimation)timing
66
+ sharedBeginTime:(CFTimeInterval)sharedBeginTime;
67
+ - (BOOL)smoothClipAnimateBackdropTo:(CGPoint)target
68
+ spring:(smoothclip::SpringAnimation)spring
69
+ sharedBeginTime:(CFTimeInterval)sharedBeginTime;
70
+ - (void)smoothClipCancelBackdropUsingTarget:(BOOL)useTarget;
71
+ - (BOOL)smoothClipPauseBackdropAtMediaTime:(CFTimeInterval)mediaTime;
72
+ - (BOOL)smoothClipResumeBackdropAtMediaTime:(CFTimeInterval)mediaTime
73
+ catchUpBy:(CFTimeInterval)catchUp;
74
+ @end
75
+
60
76
  namespace smoothclip {
61
77
  namespace {
62
78
 
@@ -91,6 +107,9 @@ struct DriverState {
91
107
  bool hasLatest = false;
92
108
  Ownership ownership = Ownership::Interactive;
93
109
  std::vector<ViewKey> views;
110
+ // Backdrop views bound to the driver: translated by the presentation's
111
+ // backdrop channel on every write and animated on every run's epoch.
112
+ std::vector<ViewKey> backdrops;
94
113
  std::optional<ActiveAnimation> animation;
95
114
  // Set by destroyDriver while views are still registered (StrictMode effect
96
115
  // replay, hosts mounted in another subtree). The entry is erased when the
@@ -116,8 +135,19 @@ struct GroupState {
116
135
  std::unordered_set<uint64_t> remainingDriverIds;
117
136
  bool finished = true;
118
137
  bool mutating = false;
138
+ // Media time the caller's UI frame was stamped with; 0 when none was given.
139
+ CFTimeInterval beginTimeHint = 0;
119
140
  };
120
141
 
142
+ // A frame stamp sits at most one frame ahead of the commit that carries it.
143
+ // Anything further ahead is not a frame stamp and must not hold the run.
144
+ // Behind the commit it is trusted only within the same window the Android
145
+ // frame loop uses (kStartStampSanityWindowS): a group held pending longer than
146
+ // that (a host that could not display, the app inactive) or a stamp from a
147
+ // rescaled clock (Slow Animations) would otherwise begin fully elapsed and
148
+ // snap to its target.
149
+ constexpr CFTimeInterval kMaxBeginTimeLeadS = 0.25;
150
+
121
151
  std::unordered_map<uint64_t, DriverState> &registry() {
122
152
  static std::unordered_map<uint64_t, DriverState> value;
123
153
  return value;
@@ -199,6 +229,7 @@ void tryStartGroup(int32_t groupId);
199
229
  void finishGroupForHostLoss(int32_t groupId);
200
230
  DriverSnapshot snapshotForDriver(uint64_t driverId);
201
231
  SmoothClipView *viewForKey(ViewKey key);
232
+ SmoothClipBackdropView *backdropForKey(ViewKey key);
202
233
 
203
234
  bool animationNeedsForegroundReconciliation(const DriverState &state) {
204
235
  if (!state.animation.has_value()) return false;
@@ -248,6 +279,9 @@ bool pauseSpringForInactivity(
248
279
  smoothClipPauseSpringAnimation:animation.animationId
249
280
  atMediaTime:mediaTime] && paused;
250
281
  }
282
+ for (const ViewKey key : state.backdrops) {
283
+ [backdropForKey(key) smoothClipPauseBackdropAtMediaTime:mediaTime];
284
+ }
251
285
  if (!paused) animation.needsForegroundReconciliation = true;
252
286
  return paused;
253
287
  }
@@ -268,6 +302,10 @@ bool resumeSpringAfterInactivity(
268
302
  atMediaTime:mediaTime
269
303
  catchUpBy:catchUp] && resumed;
270
304
  }
305
+ for (const ViewKey key : state.backdrops) {
306
+ [backdropForKey(key) smoothClipResumeBackdropAtMediaTime:mediaTime
307
+ catchUpBy:catchUp];
308
+ }
271
309
  if (!resumed) animation.needsForegroundReconciliation = true;
272
310
  return resumed;
273
311
  }
@@ -328,6 +366,83 @@ SmoothClipView *viewForKey(ViewKey key) {
328
366
  return (__bridge SmoothClipView *)(reinterpret_cast<void *>(key));
329
367
  }
330
368
 
369
+ ViewKey keyForBackdrop(SmoothClipBackdropView *view) {
370
+ return reinterpret_cast<ViewKey>((__bridge void *)view);
371
+ }
372
+
373
+ SmoothClipBackdropView *backdropForKey(ViewKey key) {
374
+ return (__bridge SmoothClipBackdropView *)(reinterpret_cast<void *>(key));
375
+ }
376
+
377
+ CGPoint backdropTranslation(const Presentation &presentation) {
378
+ return CGPointMake(
379
+ presentation.backdropTranslateX, presentation.backdropTranslateY);
380
+ }
381
+
382
+ // The clip host knows nothing of the backdrop channel, so a presentation
383
+ // read back from it carries none. A bound backdrop's visible translation is
384
+ // the truth; without one, the driver's model (a pending run's start, else the
385
+ // latest target).
386
+ void overlayBackdrop(const DriverState &state, Presentation &presentation) {
387
+ if (!state.backdrops.empty()) {
388
+ const CGPoint translation = [backdropForKey(state.backdrops.front())
389
+ smoothClipBackdropCurrentTranslation];
390
+ presentation.backdropTranslateX = translation.x;
391
+ presentation.backdropTranslateY = translation.y;
392
+ return;
393
+ }
394
+ const Presentation &model =
395
+ state.animation.has_value() && !state.animation->started
396
+ ? state.animation->start
397
+ : state.latest;
398
+ presentation.backdropTranslateX = model.backdropTranslateX;
399
+ presentation.backdropTranslateY = model.backdropTranslateY;
400
+ }
401
+
402
+ void applyBackdrops(const DriverState &state, const Presentation &presentation) {
403
+ const CGPoint translation = backdropTranslation(presentation);
404
+ for (const ViewKey key : state.backdrops) {
405
+ [backdropForKey(key) smoothClipApplyBackdrop:translation];
406
+ }
407
+ }
408
+
409
+ void cancelBackdrops(const DriverState &state, bool useTarget) {
410
+ for (const ViewKey key : state.backdrops) {
411
+ [backdropForKey(key) smoothClipCancelBackdropUsingTarget:useTarget];
412
+ }
413
+ }
414
+
415
+ // Freezes every bound backdrop where it is and reads the channel back into
416
+ // the frozen presentation.
417
+ void freezeBackdropsInto(const DriverState &state, Presentation &presentation) {
418
+ cancelBackdrops(state, false);
419
+ overlayBackdrop(state, presentation);
420
+ }
421
+
422
+ // Runs the pending animation's backdrop channel on every bound backdrop, on
423
+ // the same epoch as the clip's group. An inherited spring velocity was
424
+ // resolved once by the registry before dispatch (resolvedSpringVelocity), so
425
+ // `active.spring` carries the same trajectory the clip host runs.
426
+ void installBackdropAnimations(
427
+ const DriverState &state,
428
+ CFTimeInterval sharedBeginTime) {
429
+ if (!state.animation.has_value() || state.backdrops.empty()) return;
430
+ const ActiveAnimation &active = *state.animation;
431
+ const CGPoint target = backdropTranslation(state.latest);
432
+ for (const ViewKey key : state.backdrops) {
433
+ SmoothClipBackdropView *backdrop = backdropForKey(key);
434
+ if (active.kind == AnimationKind::Timing) {
435
+ [backdrop smoothClipAnimateBackdropTo:target
436
+ timing:active.timing
437
+ sharedBeginTime:sharedBeginTime];
438
+ } else {
439
+ [backdrop smoothClipAnimateBackdropTo:target
440
+ spring:active.spring
441
+ sharedBeginTime:sharedBeginTime];
442
+ }
443
+ }
444
+ }
445
+
331
446
  bool shouldReduceMotion(int32_t setting) {
332
447
  return setting == 1 ||
333
448
  (setting == 0 && UIAccessibilityIsReduceMotionEnabled());
@@ -468,6 +583,7 @@ Presentation canonicalFrozenPresentation(
468
583
  if (!hasCanonical && hasParticipantFallback) {
469
584
  canonical = participantFallback;
470
585
  }
586
+ freezeBackdropsInto(state, canonical);
471
587
  return canonical;
472
588
  }
473
589
 
@@ -478,7 +594,9 @@ Presentation canonicalVisiblePresentation(const DriverState &state) {
478
594
  for (const ViewKey key : state.views) {
479
595
  SmoothClipView *candidate = viewForKey(key);
480
596
  if ([candidate smoothClipIsJoinable]) {
481
- return [candidate smoothClipCurrentPresentation];
597
+ Presentation visible = [candidate smoothClipCurrentPresentation];
598
+ overlayBackdrop(state, visible);
599
+ return visible;
482
600
  }
483
601
  }
484
602
  return state.latest;
@@ -554,11 +672,13 @@ std::vector<DriverSnapshot> cancelGroupInternal(
554
672
  for (const ViewKey key : state.views) {
555
673
  [viewForKey(key) smoothClipCancelAnimationUsingTarget:YES];
556
674
  }
675
+ cancelBackdrops(state, true);
557
676
  if (wasPending) {
558
677
  for (const ViewKey key : state.views) {
559
678
  [viewForKey(key) smoothClipApplyPresentation:resolved
560
679
  recordVelocitySample:NO];
561
680
  }
681
+ applyBackdrops(state, resolved);
562
682
  }
563
683
  } else {
564
684
  resolved = canonicalFrozenPresentation(state);
@@ -614,6 +734,7 @@ void cancelActive(
614
734
  for (const ViewKey key : state.views) {
615
735
  [viewForKey(key) smoothClipCancelAnimationUsingTarget:useTarget];
616
736
  }
737
+ cancelBackdrops(state, useTarget);
617
738
  emitCompletion(driverId, state, animationId, false);
618
739
  }
619
740
 
@@ -628,6 +749,7 @@ void finishStandaloneAtTarget(uint64_t driverId, DriverState &state) {
628
749
  for (const ViewKey key : state.views) {
629
750
  [viewForKey(key) smoothClipCancelAnimationUsingTarget:YES];
630
751
  }
752
+ cancelBackdrops(state, true);
631
753
  state.ownership = Ownership::Interactive;
632
754
  emitCompletion(driverId, state, animationId, true);
633
755
  }
@@ -646,6 +768,7 @@ void finishIfNoInstalledParticipants(uint64_t driverId, DriverState &state) {
646
768
  for (const ViewKey key : state.views) {
647
769
  [viewForKey(key) smoothClipCancelAnimationUsingTarget:YES];
648
770
  }
771
+ cancelBackdrops(state, true);
649
772
  state.ownership = Ownership::Interactive;
650
773
  emitCompletion(driverId, state, animationId, finished);
651
774
  }
@@ -662,6 +785,7 @@ void applyPresentation(
662
785
  [viewForKey(key) smoothClipApplyPresentation:presentation
663
786
  recordVelocitySample:recordVelocitySample];
664
787
  }
788
+ applyBackdrops(state, presentation);
665
789
  }
666
790
 
667
791
  void prepareAnimation(
@@ -753,6 +877,7 @@ void startPendingAnimation(DriverState &state) {
753
877
  [participant smoothClipApplyPresentation:start recordVelocitySample:NO];
754
878
  installPendingAnimation(state, participant, key, start);
755
879
  }
880
+ installBackdropAnimations(state, 0);
756
881
  }
757
882
 
758
883
  void tryStartGroup(int32_t groupId) {
@@ -773,7 +898,18 @@ void tryStartGroup(int32_t groupId) {
773
898
  }
774
899
  }
775
900
 
776
- const CFTimeInterval sharedBeginTime = CACurrentMediaTime();
901
+ // Anchor the run to the UI frame that started it when JS stamped one
902
+ // (Reanimated's target vsync): a `withTiming` begun in that same frame and
903
+ // this Core Animation run then trace one epoch, instead of the run leading
904
+ // by the rest of the frame. A recent stale stamp (a run held pending until
905
+ // its host could display) starts partway through, as that JS clock did; one
906
+ // outside the sanity window starts now.
907
+ const CFTimeInterval now = CACurrentMediaTime();
908
+ const CFTimeInterval hint = groupIterator->second.beginTimeHint;
909
+ const CFTimeInterval sharedBeginTime =
910
+ hint > 0 && hint <= now + kMaxBeginTimeLeadS &&
911
+ hint >= now - kStartStampSanityWindowS
912
+ ? hint : now;
777
913
  [CATransaction begin];
778
914
  [CATransaction setDisableActions:YES];
779
915
  for (const uint64_t driverId : driverIds) {
@@ -788,6 +924,7 @@ void tryStartGroup(int32_t groupId) {
788
924
  installPendingAnimation(
789
925
  state, participant, key, start, sharedBeginTime);
790
926
  }
927
+ installBackdropAnimations(state, sharedBeginTime);
791
928
  }
792
929
  [CATransaction commit];
793
930
  }
@@ -947,6 +1084,7 @@ void registerView(
947
1084
  state.views.push_back(key);
948
1085
  }
949
1086
  [view smoothClipApplyPresentation:visible recordVelocitySample:NO];
1087
+ applyBackdrops(state, visible);
950
1088
  if (startsPendingRun) {
951
1089
  startPendingAnimation(state);
952
1090
  }
@@ -1002,7 +1140,45 @@ void unregisterView(uint64_t driverId, SmoothClipView *view) {
1002
1140
  state.views.erase(
1003
1141
  std::remove(state.views.begin(), state.views.end(), key),
1004
1142
  state.views.end());
1005
- if (state.destroyed && state.views.empty()) registry().erase(iterator);
1143
+ if (state.destroyed && state.views.empty() && state.backdrops.empty()) {
1144
+ registry().erase(iterator);
1145
+ }
1146
+ }
1147
+
1148
+ void registerBackdropView(uint64_t driverId, SmoothClipBackdropView *view) {
1149
+ NSCAssert(NSThread.isMainThread, @"SmoothClip registry is main-thread only");
1150
+ auto &state = registry()[driverId];
1151
+ const ViewKey key = keyForBackdrop(view);
1152
+ if (std::find(state.backdrops.begin(), state.backdrops.end(), key) !=
1153
+ state.backdrops.end()) {
1154
+ return;
1155
+ }
1156
+ // Adopt the driver's current value: a pending run's start, an already
1157
+ // bound backdrop's visible value mid-run, else the model. A backdrop bound
1158
+ // while a run is in flight follows from the next run.
1159
+ Presentation visible =
1160
+ state.animation.has_value() && !state.animation->started
1161
+ ? state.animation->start
1162
+ : state.latest;
1163
+ if (!state.backdrops.empty()) overlayBackdrop(state, visible);
1164
+ state.backdrops.push_back(key);
1165
+ if (state.hasLatest) {
1166
+ [view smoothClipApplyBackdrop:backdropTranslation(visible)];
1167
+ }
1168
+ }
1169
+
1170
+ void unregisterBackdropView(uint64_t driverId, SmoothClipBackdropView *view) {
1171
+ NSCAssert(NSThread.isMainThread, @"SmoothClip registry is main-thread only");
1172
+ auto iterator = registry().find(driverId);
1173
+ if (iterator == registry().end()) return;
1174
+ DriverState &state = iterator->second;
1175
+ const ViewKey key = keyForBackdrop(view);
1176
+ state.backdrops.erase(
1177
+ std::remove(state.backdrops.begin(), state.backdrops.end(), key),
1178
+ state.backdrops.end());
1179
+ if (state.destroyed && state.views.empty() && state.backdrops.empty()) {
1180
+ registry().erase(iterator);
1181
+ }
1006
1182
  }
1007
1183
 
1008
1184
  void setPresentation(
@@ -1325,7 +1501,8 @@ int32_t createGroupAnimation(
1325
1501
  SpringAnimation spring,
1326
1502
  double durationMs,
1327
1503
  int32_t reduceMotion,
1328
- int32_t completionTag) {
1504
+ int32_t completionTag,
1505
+ double startedAtHintS) {
1329
1506
  std::vector<Presentation> resolvedStarts;
1330
1507
  bool reduced = false;
1331
1508
  if (!NSThread.isMainThread || controllerId == 0 ||
@@ -1377,6 +1554,9 @@ int32_t createGroupAnimation(
1377
1554
  driverIds,
1378
1555
  std::unordered_set<uint64_t>(driverIds.begin(), driverIds.end()),
1379
1556
  };
1557
+ if (std::isfinite(startedAtHintS) && startedAtHintS > 0) {
1558
+ group.beginTimeHint = startedAtHintS;
1559
+ }
1380
1560
  groupRegistry().emplace(groupId, std::move(group));
1381
1561
 
1382
1562
  for (std::size_t entryIndex = 0; entryIndex < entries.size(); entryIndex += 1) {
@@ -1420,7 +1600,6 @@ int32_t animateTimingGroup(
1420
1600
  TimingAnimation animation,
1421
1601
  int32_t completionTag,
1422
1602
  double startedAtHintS) {
1423
- (void)startedAtHintS;
1424
1603
  return createGroupAnimation(
1425
1604
  controllerId,
1426
1605
  std::move(entries),
@@ -1429,7 +1608,8 @@ int32_t animateTimingGroup(
1429
1608
  {},
1430
1609
  animation.durationMs,
1431
1610
  animation.reduceMotion,
1432
- completionTag);
1611
+ completionTag,
1612
+ startedAtHintS);
1433
1613
  }
1434
1614
 
1435
1615
  int32_t animateSpringGroup(
@@ -1438,7 +1618,6 @@ int32_t animateSpringGroup(
1438
1618
  SpringAnimation animation,
1439
1619
  int32_t completionTag,
1440
1620
  double startedAtHintS) {
1441
- (void)startedAtHintS;
1442
1621
  return createGroupAnimation(
1443
1622
  controllerId,
1444
1623
  std::move(entries),
@@ -1447,7 +1626,8 @@ int32_t animateSpringGroup(
1447
1626
  animation,
1448
1627
  0,
1449
1628
  animation.reduceMotion,
1450
- completionTag);
1629
+ completionTag,
1630
+ startedAtHintS);
1451
1631
  }
1452
1632
 
1453
1633
  std::vector<DriverSnapshot> cancelAnimationGroup(
@@ -1544,6 +1724,7 @@ int32_t animateTiming(
1544
1724
  state.animation->participants.insert(key);
1545
1725
  }
1546
1726
  }
1727
+ installBackdropAnimations(state, 0);
1547
1728
  return animationId;
1548
1729
  }
1549
1730
 
@@ -1633,6 +1814,7 @@ int32_t animateSpring(
1633
1814
  state.animation->participants.insert(key);
1634
1815
  }
1635
1816
  }
1817
+ installBackdropAnimations(state, 0);
1636
1818
  return animationId;
1637
1819
  }
1638
1820
 
@@ -1709,6 +1891,7 @@ CancelResult cancelAnimation(
1709
1891
  [viewForKey(key) smoothClipApplyPresentation:target
1710
1892
  recordVelocitySample:NO];
1711
1893
  }
1894
+ applyBackdrops(state, target);
1712
1895
  }
1713
1896
  state.ownership = Ownership::Interactive;
1714
1897
  return {true, target};
@@ -1733,7 +1916,7 @@ void destroyDriver(uint64_t driverId) {
1733
1916
  for (const ViewKey key : state.views) {
1734
1917
  [viewForKey(key) smoothClipClearVelocitySamples];
1735
1918
  }
1736
- if (state.views.empty()) {
1919
+ if (state.views.empty() && state.backdrops.empty()) {
1737
1920
  registry().erase(iterator);
1738
1921
  } else {
1739
1922
  // Views can outlive the hook briefly (StrictMode effect replay, hosts in
@@ -5,12 +5,13 @@
5
5
  #include "SmoothClipRegistrySnapshot.h"
6
6
 
7
7
  #include <cmath>
8
+ #include <limits>
8
9
  #include <utility>
9
10
 
10
11
  namespace facebook::react {
11
12
  namespace {
12
13
 
13
- constexpr size_t kPresentationStride = 23;
14
+ constexpr size_t kPresentationStride = 25;
14
15
  constexpr size_t kSnapshotStride = kPresentationStride + 1;
15
16
  constexpr size_t kMotionEntryStride = kPresentationStride * 2 + 2;
16
17
 
@@ -67,7 +68,9 @@ smoothclip::Presentation makePresentation(
67
68
  double shadowBlurRadius = 0,
68
69
  double shadowSpreadDistance = 0,
69
70
  double rotation = 0,
70
- double opacity = 1) {
71
+ double opacity = 1,
72
+ double backdropTranslateX = 0,
73
+ double backdropTranslateY = 0) {
71
74
  const bool uniform = topLeftRadius == topRightRadius &&
72
75
  topLeftRadius == bottomRightRadius &&
73
76
  topLeftRadius == bottomLeftRadius;
@@ -85,7 +88,7 @@ smoothclip::Presentation makePresentation(
85
88
  contentScale,
86
89
  {shadowEnabled, shadowRed, shadowGreen, shadowBlue, shadowAlpha,
87
90
  shadowOffsetX, shadowOffsetY, shadowBlurRadius, shadowSpreadDistance},
88
- rotation, opacity};
91
+ rotation, opacity, backdropTranslateX, backdropTranslateY};
89
92
  }
90
93
 
91
94
  bool finitePresentation(const smoothclip::Presentation &presentation) {
@@ -101,6 +104,8 @@ bool finitePresentation(const smoothclip::Presentation &presentation) {
101
104
  std::isfinite(presentation.contentScale) &&
102
105
  presentation.contentScale > 0 &&
103
106
  std::isfinite(presentation.rotation) && std::isfinite(presentation.opacity) &&
107
+ std::isfinite(presentation.backdropTranslateX) &&
108
+ std::isfinite(presentation.backdropTranslateY) &&
104
109
  std::isfinite(presentation.shadow.red) &&
105
110
  std::isfinite(presentation.shadow.green) &&
106
111
  std::isfinite(presentation.shadow.blue) &&
@@ -122,10 +127,14 @@ bool presentationAt(
122
127
  size_t offset,
123
128
  smoothclip::Presentation &result) {
124
129
  double values[kPresentationStride];
130
+ // One length read for the whole packet: `size` is a JSI call of its own,
131
+ // and this runs per clip per drag frame.
132
+ if (offset + kPresentationStride > array.size(runtime)) return false;
125
133
  for (size_t index = 0; index < kPresentationStride; index += 1) {
126
- if (!numberAt(runtime, array, offset + index, values[index])) {
127
- return false;
128
- }
134
+ const jsi::Value value = array.getValueAtIndex(runtime, offset + index);
135
+ if (!value.isNumber()) return false;
136
+ values[index] = value.asNumber();
137
+ if (!std::isfinite(values[index])) return false;
129
138
  }
130
139
  const int32_t curveCode = static_cast<int32_t>(values[8]);
131
140
  if (values[8] != curveCode || !validCurveCode(curveCode) ||
@@ -145,7 +154,8 @@ bool presentationAt(
145
154
  values[11],
146
155
  values[12] == 1,
147
156
  values[13], values[14], values[15], values[16],
148
- values[17], values[18], values[19], values[20], values[21], values[22]);
157
+ values[17], values[18], values[19], values[20], values[21], values[22],
158
+ values[23], values[24]);
149
159
  return finitePresentation(result);
150
160
  }
151
161
 
@@ -187,6 +197,8 @@ void writePresentation(
187
197
  result.setValueAtIndex(runtime, offset + 20, presentation.shadow.spreadDistance);
188
198
  result.setValueAtIndex(runtime, offset + 21, presentation.rotation);
189
199
  result.setValueAtIndex(runtime, offset + 22, presentation.opacity);
200
+ result.setValueAtIndex(runtime, offset + 23, presentation.backdropTranslateX);
201
+ result.setValueAtIndex(runtime, offset + 24, presentation.backdropTranslateY);
190
202
  }
191
203
 
192
204
  jsi::Array snapshotArray(
@@ -387,6 +399,16 @@ bool SmoothClipTurboModule::setClipPresentationBatch(
387
399
  return smoothclip::setPresentationBatch(parsed);
388
400
  }
389
401
 
402
+ // JS stamps a run with Reanimated's frame timestamp in milliseconds on the
403
+ // CACurrentMediaTime base; the registry takes seconds. Anything else means
404
+ // "start now".
405
+ static double startedAtHintSeconds(double startTimestampMs) {
406
+ if (!std::isfinite(startTimestampMs) || startTimestampMs <= 0) {
407
+ return std::numeric_limits<double>::quiet_NaN();
408
+ }
409
+ return startTimestampMs / 1000.0;
410
+ }
411
+
390
412
  int32_t SmoothClipTurboModule::animateTimingGroup(
391
413
  jsi::Runtime &runtime,
392
414
  double controllerId,
@@ -399,7 +421,6 @@ int32_t SmoothClipTurboModule::animateTimingGroup(
399
421
  int32_t reduceMotion,
400
422
  int32_t completionTag,
401
423
  double startTimestamp) {
402
- (void)startTimestamp;
403
424
  std::vector<smoothclip::GroupMotionEntry> parsed;
404
425
  if (!validDriverId(controllerId) ||
405
426
  !fixedGroupEntriesAt(runtime, entries, parsed) ||
@@ -417,7 +438,8 @@ int32_t SmoothClipTurboModule::animateTimingGroup(
417
438
  controlPoint2X,
418
439
  controlPoint2Y,
419
440
  reduceMotion},
420
- completionTag);
441
+ completionTag,
442
+ startedAtHintSeconds(startTimestamp));
421
443
  }
422
444
 
423
445
  int32_t SmoothClipTurboModule::animateSpringGroup(
@@ -432,7 +454,6 @@ int32_t SmoothClipTurboModule::animateSpringGroup(
432
454
  int32_t reduceMotion,
433
455
  int32_t completionTag,
434
456
  double startTimestamp) {
435
- (void)startTimestamp;
436
457
  std::vector<smoothclip::GroupMotionEntry> parsed;
437
458
  if (!validDriverId(controllerId) ||
438
459
  !fixedGroupEntriesAt(runtime, entries, parsed) ||
@@ -452,7 +473,8 @@ int32_t SmoothClipTurboModule::animateSpringGroup(
452
473
  false,
453
474
  reduceMotion,
454
475
  energyThreshold},
455
- completionTag);
476
+ completionTag,
477
+ startedAtHintSeconds(startTimestamp));
456
478
  }
457
479
 
458
480
  jsi::Array SmoothClipTurboModule::cancelAnimationGroup(