react-native-smooth-clip-view 0.2.4 → 0.2.6

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 (40) hide show
  1. package/README.md +62 -25
  2. package/android/CMakeLists.txt +0 -1
  3. package/android/src/main/cpp/SmoothClipAndroid.h +5 -0
  4. package/android/src/main/cpp/SmoothClipBindings.cpp +28 -8
  5. package/android/src/main/cpp/SmoothClipRegistry.cpp +198 -189
  6. package/android/src/main/java/com/smoothclipview/ClipGeometryNormalizer.kt +61 -11
  7. package/android/src/main/java/com/smoothclipview/SmoothClipBindings.kt +20 -0
  8. package/android/src/main/java/com/smoothclipview/SmoothClipModule.kt +1 -0
  9. package/android/src/main/java/com/smoothclipview/SmoothClipView.kt +105 -34
  10. package/cpp/SmoothClipAnimationCurve.h +468 -0
  11. package/cpp/SmoothClipAnimationId.h +32 -0
  12. package/cpp/SmoothClipRegistry.h +11 -1
  13. package/cpp/SmoothClipVelocityTracker.h +32 -15
  14. package/ios/SmoothClipRegistry.mm +246 -97
  15. package/ios/SmoothClipTurboModule.cpp +4 -2
  16. package/ios/SmoothClipTurboModule.h +2 -1
  17. package/ios/SmoothClipView.mm +73 -16
  18. package/lib/module/NativeSmoothClipModule.js.map +1 -1
  19. package/lib/module/drivers.android.js +3 -1
  20. package/lib/module/drivers.android.js.map +1 -1
  21. package/lib/module/drivers.ios.js +3 -391
  22. package/lib/module/drivers.ios.js.map +1 -1
  23. package/lib/module/drivers.native.js +470 -0
  24. package/lib/module/drivers.native.js.map +1 -0
  25. package/lib/typescript/src/NativeSmoothClipModule.d.ts +1 -1
  26. package/lib/typescript/src/NativeSmoothClipModule.d.ts.map +1 -1
  27. package/lib/typescript/src/driverTypes.d.ts +6 -3
  28. package/lib/typescript/src/driverTypes.d.ts.map +1 -1
  29. package/lib/typescript/src/drivers.android.d.ts +2 -2
  30. package/lib/typescript/src/drivers.android.d.ts.map +1 -1
  31. package/lib/typescript/src/drivers.ios.d.ts +2 -4
  32. package/lib/typescript/src/drivers.ios.d.ts.map +1 -1
  33. package/lib/typescript/src/drivers.native.d.ts +5 -0
  34. package/lib/typescript/src/drivers.native.d.ts.map +1 -0
  35. package/package.json +1 -1
  36. package/src/NativeSmoothClipModule.ts +2 -1
  37. package/src/driverTypes.ts +6 -3
  38. package/src/drivers.android.ts +4 -2
  39. package/src/drivers.ios.ts +4 -687
  40. package/src/drivers.native.ts +805 -0
@@ -3,6 +3,7 @@
3
3
  #import "SmoothClipGeometry.h"
4
4
  #import "SmoothClipViewRegistry.h"
5
5
 
6
+ #include "SmoothClipAnimationCurve.h"
6
7
  #include "SmoothClipVelocityTracker.h"
7
8
 
8
9
  #import <QuartzCore/QuartzCore.h>
@@ -332,11 +333,18 @@ static std::array<double, 7> SmoothClipVelocityChannels(
332
333
  }
333
334
 
334
335
  - (void)smoothClipApplyPresentation:(smoothclip::Presentation)presentation {
336
+ [self smoothClipApplyPresentation:presentation recordVelocitySample:YES];
337
+ }
338
+
339
+ - (void)smoothClipApplyPresentation:(smoothclip::Presentation)presentation
340
+ recordVelocitySample:(BOOL)recordVelocitySample {
335
341
  _animationPaused = NO;
336
342
  [self stopLayerAnimationWithoutCallback];
337
343
  [self storeRequestedPresentation:presentation];
338
344
  [self applyRequestedClip];
339
- if (_hasLayout) {
345
+ // Skipped for writes that are not interactive motion (a latched
346
+ // cancel-to-target): they must not enter the 'inherit' history.
347
+ if (recordVelocitySample && _hasLayout) {
340
348
  [self recordInteractiveRect:_normalizedClip
341
349
  radius:_normalizedRadius
342
350
  contentTranslation:_normalizedContentTranslation];
@@ -380,6 +388,15 @@ static std::array<double, 7> SmoothClipVelocityChannels(
380
388
  return _pendingAnimationInstall;
381
389
  }
382
390
 
391
+ - (double)smoothClipSpringContinuationVelocity {
392
+ if (_activeAnimationId == 0 || _activeAnimationKind != 2 ||
393
+ _pendingAnimationInstall) {
394
+ return 0;
395
+ }
396
+ const double elapsed = MAX(0, CACurrentMediaTime() - _animationStartedAt);
397
+ return smoothclip::springContinuationVelocity(_springAnimation, elapsed);
398
+ }
399
+
383
400
  - (smoothclip::Presentation)smoothClipFreezePresentation {
384
401
  const smoothclip::Presentation visible =
385
402
  [self smoothClipCurrentPresentation];
@@ -623,13 +640,13 @@ static std::array<double, 7> SmoothClipVelocityChannels(
623
640
  forKey:@"smoothClip.content"];
624
641
  }
625
642
 
626
- - (void)startAnimationToRequestedGeometryWithDuration:(CFTimeInterval)duration {
643
+ - (BOOL)startAnimationToRequestedGeometryWithDuration:(CFTimeInterval)duration {
627
644
  if (![self smoothClipCanDisplay]) {
628
645
  _pendingAnimationInstall = YES;
629
- return;
646
+ return NO;
630
647
  }
631
648
  SmoothNormalizedClipGeometry target;
632
- if (![self normalizedRequestedGeometry:&target]) return;
649
+ if (![self normalizedRequestedGeometry:&target]) return NO;
633
650
  const smoothclip::Presentation current = [self smoothClipCurrentPresentation];
634
651
  [self installAnimationFromRect:CGRectMake(
635
652
  current.clip.x,
@@ -644,20 +661,21 @@ static std::array<double, 7> SmoothClipVelocityChannels(
644
661
  toRadius:target.radius
645
662
  toContentTranslation:_requestedContentTranslation
646
663
  duration:duration];
664
+ return YES;
647
665
  }
648
666
 
649
- - (void)smoothClipAnimateTiming:(smoothclip::Presentation)presentation
667
+ - (BOOL)smoothClipAnimateTiming:(smoothclip::Presentation)presentation
650
668
  animation:(smoothclip::TimingAnimation)animation
651
669
  animationId:(int32_t)animationId {
652
670
  [self storeRequestedPresentation:presentation];
653
671
  _timingAnimation = animation;
654
672
  _activeAnimationKind = 1;
655
673
  _activeAnimationId = animationId;
656
- [self startAnimationToRequestedGeometryWithDuration:
657
- MAX(0, animation.durationMs) / 1000.0];
674
+ return [self startAnimationToRequestedGeometryWithDuration:
675
+ MAX(0, animation.durationMs) / 1000.0];
658
676
  }
659
677
 
660
- - (void)smoothClipAnimateSpring:(smoothclip::Presentation)presentation
678
+ - (BOOL)smoothClipAnimateSpring:(smoothclip::Presentation)presentation
661
679
  animation:(smoothclip::SpringAnimation)animation
662
680
  animationId:(int32_t)animationId {
663
681
  [self storeRequestedPresentation:presentation];
@@ -675,7 +693,7 @@ static std::array<double, 7> SmoothClipVelocityChannels(
675
693
  }
676
694
  _activeAnimationKind = 2;
677
695
  _activeAnimationId = animationId;
678
- [self startAnimationToRequestedGeometryWithDuration:0];
696
+ return [self startAnimationToRequestedGeometryWithDuration:0];
679
697
  }
680
698
 
681
699
  - (CAKeyframeAnimation *)keyframeAnimationForKeyPath:(NSString *)keyPath
@@ -689,17 +707,17 @@ static std::array<double, 7> SmoothClipVelocityChannels(
689
707
  return animation;
690
708
  }
691
709
 
692
- - (void)smoothClipAnimateKeyframes:(smoothclip::Presentation)presentation
710
+ - (BOOL)smoothClipAnimateKeyframes:(smoothclip::Presentation)presentation
693
711
  keyframes:(const std::vector<smoothclip::Keyframe> &)keyframes
694
712
  durationMs:(double)durationMs
695
713
  animationId:(int32_t)animationId {
696
- if (keyframes.size() < 2) return;
714
+ if (keyframes.size() < 2) return NO;
697
715
  [self storeRequestedPresentation:presentation];
698
716
  _activeAnimationKind = 3;
699
717
  _activeAnimationId = animationId;
700
718
  if (![self smoothClipCanDisplay]) {
701
719
  _pendingAnimationInstall = YES;
702
- return;
720
+ return NO;
703
721
  }
704
722
  [self stopLayerAnimationWithoutCallback];
705
723
  _activeAnimationKind = 3;
@@ -726,7 +744,7 @@ static std::array<double, 7> SmoothClipVelocityChannels(
726
744
  frame.presentation.clip.height,
727
745
  frame.presentation.clip.radius,
728
746
  hostSize,
729
- &normalized)) return;
747
+ &normalized)) return NO;
730
748
  [times addObject:@(frame.offset)];
731
749
  [boundsValues addObject:[NSValue valueWithCGRect:normalized.rect]];
732
750
  [positionValues addObject:[NSValue valueWithCGPoint:CGPointMake(
@@ -737,7 +755,7 @@ static std::array<double, 7> SmoothClipVelocityChannels(
737
755
  }
738
756
 
739
757
  SmoothNormalizedClipGeometry target;
740
- if (![self normalizedRequestedGeometry:&target]) return;
758
+ if (![self normalizedRequestedGeometry:&target]) return NO;
741
759
  [self writeLayerRect:target.rect radius:target.radius];
742
760
  [self writeContentTranslation:_requestedContentTranslation];
743
761
  [self setClipContainerHidden:NO];
@@ -776,6 +794,7 @@ static std::array<double, 7> SmoothClipVelocityChannels(
776
794
  [_clipContainer.layer addAnimation:group forKey:@"smoothClip.geometry"];
777
795
  [_contentContainer.layer addAnimation:contentGroup
778
796
  forKey:@"smoothClip.content"];
797
+ return YES;
779
798
  }
780
799
 
781
800
  - (void)smoothClipCancelAnimationUsingTarget:(BOOL)useTarget {
@@ -809,6 +828,10 @@ static std::array<double, 7> SmoothClipVelocityChannels(
809
828
 
810
829
  - (void)applicationWillResignActive {
811
830
  if (_activeAnimationId == 0 || _animationPaused) return;
831
+ const double springContinuationVelocity =
832
+ _activeAnimationKind == 2
833
+ ? [self smoothClipSpringContinuationVelocity]
834
+ : 0;
812
835
  const smoothclip::Presentation visible =
813
836
  [self smoothClipCurrentPresentation];
814
837
  _pausedRect = CGRectMake(
@@ -822,6 +845,16 @@ static std::array<double, 7> SmoothClipVelocityChannels(
822
845
  visible.contentTranslateY);
823
846
  _pausedRemaining = MAX(
824
847
  0, _animationDuration - (CACurrentMediaTime() - _animationStartedAt));
848
+ if (_activeAnimationKind == 1 && _animationDuration > 0) {
849
+ const double progress = smoothclip::clamp01(
850
+ 1 - _pausedRemaining / _animationDuration);
851
+ _timingAnimation =
852
+ smoothclip::timingRemainder(_timingAnimation, progress).animation;
853
+ _pausedRemaining = _timingAnimation.durationMs / 1000.0;
854
+ } else if (_activeAnimationKind == 2) {
855
+ _springAnimation.initialVelocity = springContinuationVelocity;
856
+ _springAnimation.inheritVelocity = false;
857
+ }
825
858
  const int32_t animationId = _activeAnimationId;
826
859
  const NSInteger animationKind = _activeAnimationKind;
827
860
  [self stopLayerAnimationWithoutCallback];
@@ -944,10 +977,14 @@ static std::array<double, 7> SmoothClipVelocityChannels(
944
977
  const CGPoint visibleContentTranslation = CGPointMake(
945
978
  visible.contentTranslateX,
946
979
  visible.contentTranslateY);
947
- const CFTimeInterval remaining = wasAnimating
980
+ CFTimeInterval remaining = wasAnimating
948
981
  ? MAX(0, _animationDuration -
949
982
  (CACurrentMediaTime() - _animationStartedAt))
950
983
  : 0;
984
+ const double springContinuationVelocity =
985
+ wasAnimating && !_animationPaused && _activeAnimationKind == 2
986
+ ? [self smoothClipSpringContinuationVelocity]
987
+ : 0;
951
988
 
952
989
  [super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:oldLayoutMetrics];
953
990
  _hasLayout = YES;
@@ -980,7 +1017,16 @@ static std::array<double, 7> SmoothClipVelocityChannels(
980
1017
  [self stopLayerAnimationWithoutCallback];
981
1018
  _activeAnimationId = animationId;
982
1019
  _activeAnimationKind = animationKind;
983
- if (_activeAnimationKind == 3) {
1020
+ if (_activeAnimationKind == 1 && _animationDuration > 0) {
1021
+ const double progress = smoothclip::clamp01(
1022
+ 1 - remaining / _animationDuration);
1023
+ _timingAnimation =
1024
+ smoothclip::timingRemainder(_timingAnimation, progress).animation;
1025
+ remaining = _timingAnimation.durationMs / 1000.0;
1026
+ } else if (_activeAnimationKind == 2) {
1027
+ _springAnimation.initialVelocity = springContinuationVelocity;
1028
+ _springAnimation.inheritVelocity = false;
1029
+ } else if (_activeAnimationKind == 3) {
984
1030
  _activeAnimationKind = 1;
985
1031
  _timingAnimation = {remaining * 1000.0, 0, 0, 1, 1, 2};
986
1032
  }
@@ -1006,6 +1052,17 @@ static std::array<double, 7> SmoothClipVelocityChannels(
1006
1052
  } else {
1007
1053
  [self applyRequestedClip];
1008
1054
  }
1055
+ // First layout is the other moment this view can become displayable: when
1056
+ // the window attach ran first, didMoveToWindow saw _hasLayout == NO and did
1057
+ // nothing, and nothing else tells the registry. Android notifies from both
1058
+ // triggers (setViewHostGeometryAndroid and viewBecameDisplayableAndroid);
1059
+ // without this a held latch — including one re-latched by unregisterView
1060
+ // when the last displayable host left — would never start on iOS. Costs one
1061
+ // registry lookup per layout; viewBecameDisplayable returns immediately when
1062
+ // there is no animation to resume.
1063
+ if (_driverId != 0 && [self smoothClipCanDisplay]) {
1064
+ smoothclip::viewBecameDisplayable(_driverId, self);
1065
+ }
1009
1066
  }
1010
1067
 
1011
1068
  - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
@@ -1 +1 @@
1
- {"version":3,"names":["TurboModuleRegistry","getEnforcing"],"sourceRoot":"../../src","sources":["NativeSmoothClipModule.ts"],"mappings":";;AACA,SAASA,mBAAmB,QAAQ,cAAc;AAsGlD,eAAeA,mBAAmB,CAACC,YAAY,CAAO,wBAAwB,CAAC","ignoreList":[]}
1
+ {"version":3,"names":["TurboModuleRegistry","getEnforcing"],"sourceRoot":"../../src","sources":["NativeSmoothClipModule.ts"],"mappings":";;AACA,SAASA,mBAAmB,QAAQ,cAAc;AAuGlD,eAAeA,mBAAmB,CAACC,YAAY,CAAO,wBAAwB,CAAC","ignoreList":[]}
@@ -1,4 +1,6 @@
1
1
  "use strict";
2
2
 
3
- export { useSmoothClipDriver } from "./drivers.ios.js";
3
+ // Platform re-export only — the implementation is shared with iOS.
4
+ // Edit drivers.native.ts, not this file.
5
+ export { useSmoothClipDriver } from "./drivers.native.js";
4
6
  //# sourceMappingURL=drivers.android.js.map
@@ -1 +1 @@
1
- {"version":3,"names":["useSmoothClipDriver"],"sourceRoot":"../../src","sources":["drivers.android.ts"],"mappings":";;AAAA,SAASA,mBAAmB,QAAQ,kBAAe","ignoreList":[]}
1
+ {"version":3,"names":["useSmoothClipDriver"],"sourceRoot":"../../src","sources":["drivers.android.ts"],"mappings":";;AAAA;AACA;AACA,SAASA,mBAAmB,QAAQ,qBAAkB","ignoreList":[]}
@@ -1,394 +1,6 @@
1
1
  "use strict";
2
2
 
3
- import { useEffect, useRef } from 'react';
4
- import { useSharedValue } from 'react-native-reanimated';
5
- import { isRNRuntime, scheduleOnRN, scheduleOnUI } from 'react-native-worklets';
6
- import NativeSmoothClipModule from './smoothClipNative';
7
- import { allocateDriverId, attachDriverState, createDriverState, detachDriverState, getDriverState, setDriverState } from "./driverState.js";
8
- import { completeNativeAnimation, synchronizeNativeCompletion } from "./nativeCompletion.js";
9
- import { clipPresentationEquals, createClipPresentation, isFiniteClipPresentation } from "./geometry.js";
10
- import { createReactRequest, rejectDriverRequests, resolveReactRequest } from "./reactRequests.js";
11
- const INTERACTIVE = 0;
12
- const NATIVE = 1;
13
- const setPresentationHostFunction = NativeSmoothClipModule.setClipPresentation;
14
- const beginInteractionHostFunction = NativeSmoothClipModule.beginInteraction;
15
- const animateTimingHostFunction = NativeSmoothClipModule.animateTiming;
16
- const animateSpringHostFunction = NativeSmoothClipModule.animateSpring;
17
- const animateKeyframesHostFunction = NativeSmoothClipModule.animateKeyframes;
18
- const rejectAnimationHostFunction = NativeSmoothClipModule.rejectAnimation;
19
- const cancelAnimationHostFunction = NativeSmoothClipModule.cancelAnimation;
20
- const destroyDriverHostFunction = NativeSmoothClipModule.destroyDriver;
21
-
22
- // Workaround for react-native-worklets 0.10: the RN-side serialization cache
23
- // hands out one remote-function registry id per function forever, but the
24
- // registry entry is deleted as soon as any transient UI-side proxy of that
25
- // function is garbage collected. A later scheduleOnRN for the same function
26
- // then reads `undefined` from the registry and aborts in Value::getObject.
27
- // Pinning one UI-side reference for the app lifetime keeps the proxy — and
28
- // with it the registry entry — alive. Remove once worklets decouples registry
29
- // cleanup from proxy lifetime.
30
- scheduleOnUI(resolver => {
31
- 'worklet';
32
-
33
- globalThis.__smoothClipPinnedResolver = resolver;
34
- }, resolveReactRequest);
35
- function reduceMotionCode(value) {
36
- switch (value) {
37
- case 'always':
38
- return 1;
39
- case 'never':
40
- return 2;
41
- default:
42
- return 0;
43
- }
44
- }
45
- function animationIsFinite(target, animation) {
46
- 'worklet';
47
-
48
- if (animation.from !== undefined && !isFiniteClipPresentation(animation.from)) {
49
- // Silently dropping an invalid explicit start would re-open the exact
50
- // stale-start handoff `from` exists to close — reject the whole call.
51
- return false;
52
- }
53
- if (animation.type === 'timing') {
54
- return Number.isFinite(animation.duration) && animation.controlPoints.every(Number.isFinite);
55
- }
56
- if (animation.type === 'spring') {
57
- return [animation.mass ?? 1, animation.stiffness ?? 100, animation.damping ?? 10, animation.initialVelocity === 'inherit' ? 0 : animation.initialVelocity ?? 0].every(Number.isFinite);
58
- }
59
- if (!Number.isFinite(animation.duration) || animation.frames.length < 2) {
60
- return false;
61
- }
62
- let previousOffset = -1;
63
- for (const frame of animation.frames) {
64
- if (!Number.isFinite(frame.offset) || frame.offset < 0 || frame.offset > 1 || frame.offset <= previousOffset || !isFiniteClipPresentation(frame.presentation)) {
65
- return false;
66
- }
67
- previousOffset = frame.offset;
68
- }
69
- return animation.frames[0]?.offset === 0 && previousOffset === 1 && clipPresentationEquals(animation.frames[animation.frames.length - 1]?.presentation ?? null, target);
70
- }
71
- function presentationFromNative(values, fallback, offset = 0) {
72
- 'worklet';
73
-
74
- if (values.length < offset + 7) return fallback;
75
- const presentation = {
76
- clip: {
77
- x: values[offset],
78
- y: values[offset + 1],
79
- width: values[offset + 2],
80
- height: values[offset + 3],
81
- radius: values[offset + 4]
82
- },
83
- contentTranslateX: values[offset + 5],
84
- contentTranslateY: values[offset + 6]
85
- };
86
- return isFiniteClipPresentation(presentation) ? presentation : fallback;
87
- }
88
- function flattenedKeyframes(animation) {
89
- 'worklet';
90
-
91
- const values = [];
92
- for (const frame of animation.frames) {
93
- const {
94
- clip,
95
- contentTranslateX,
96
- contentTranslateY
97
- } = frame.presentation;
98
- values.push(frame.offset, clip.x, clip.y, clip.width, clip.height, clip.radius, contentTranslateX, contentTranslateY);
99
- }
100
- return values;
101
- }
102
- function uiOnly() {
103
- 'worklet';
104
-
105
- throw new Error('[SmoothClipView] driver.ui methods must run on the UI runtime. Use driver.react from React code.');
106
- }
107
- export function useSmoothClipDriver(initialValue, options = {}) {
108
- const initialPresentation = 'clip' in initialValue ? initialValue : createClipPresentation(initialValue);
109
- const presentation = useSharedValue(initialPresentation);
110
- const ownership = useSharedValue(INTERACTIVE);
111
- const suppressDeliveries = useSharedValue(0);
112
- const activeAnimationId = useSharedValue(0);
113
- // Non-zero after a setScalars hot write: driver.presentation is then stale,
114
- // so animateTo must start from the native registry's latest value instead
115
- // of snapping back to the stale SharedValue.
116
- const scalarsStale = useSharedValue(0);
117
- const callbackRef = useRef(options.onAnimationComplete);
118
- const driverRef = useRef(null);
119
- callbackRef.current = options.onAnimationComplete;
120
- if (driverRef.current === null) {
121
- const driverId = allocateDriverId();
122
- const reduceMotion = reduceMotionCode(options.reduceMotion ?? 'system');
123
- const seedPresentation = next => {
124
- 'worklet';
125
-
126
- if (!clipPresentationEquals(presentation.value, next)) {
127
- suppressDeliveries.value += 1;
128
- presentation.value = next;
129
- }
130
- };
131
- const beginOnUI = () => {
132
- 'worklet';
133
-
134
- const current = presentationFromNative(beginInteractionHostFunction(driverId), presentation.value);
135
- activeAnimationId.value = 0;
136
- ownership.value = INTERACTIVE;
137
- scalarsStale.value = 0;
138
- seedPresentation(current);
139
- return current;
140
- };
141
- const setOnUI = next => {
142
- 'worklet';
143
-
144
- if (!isFiniteClipPresentation(next)) return;
145
- activeAnimationId.value = 0;
146
- ownership.value = INTERACTIVE;
147
- scalarsStale.value = 0;
148
- const {
149
- clip,
150
- contentTranslateX,
151
- contentTranslateY
152
- } = next;
153
- setPresentationHostFunction(driverId, clip.x, clip.y, clip.width, clip.height, clip.radius, contentTranslateX, contentTranslateY, true);
154
- seedPresentation(next);
155
- };
156
- const setScalarsOnUI = (x, y, width, height, radius, contentTranslateX, contentTranslateY) => {
157
- 'worklet';
158
-
159
- if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(width) || !Number.isFinite(height) || !Number.isFinite(radius) || !Number.isFinite(contentTranslateX) || !Number.isFinite(contentTranslateY)) {
160
- return;
161
- }
162
- if (activeAnimationId.value !== 0) activeAnimationId.value = 0;
163
- if (ownership.value !== INTERACTIVE) ownership.value = INTERACTIVE;
164
- if (scalarsStale.value === 0) scalarsStale.value = 1;
165
- setPresentationHostFunction(driverId, x, y, width, height, radius, contentTranslateX, contentTranslateY, true);
166
- };
167
- const animateOnUI = (target, animation) => {
168
- 'worklet';
169
-
170
- if (!isFiniteClipPresentation(target) || !animationIsFinite(target, animation)) {
171
- return rejectAnimationHostFunction(driverId);
172
- }
173
- const from = animation.from;
174
- if (from !== undefined) {
175
- // Fused hot write: exactly setScalars(from…) issued immediately
176
- // before the handoff, so native's latest value — the animation
177
- // start below — is the caller's explicit presentation. Also
178
- // re-grabs from a running animation, which the implicit
179
- // interactive-start path would silently skip.
180
- setScalarsOnUI(from.clip.x, from.clip.y, from.clip.width, from.clip.height, from.clip.radius, from.contentTranslateX, from.contentTranslateY);
181
- }
182
-
183
- // After a setScalars hot write the SharedValue no longer matches what
184
- // is on screen; passing it as the start would snap the animation back.
185
- // Without an interactive start, native resolves the start from its own
186
- // latest value instead.
187
- const scalarsWereStale = scalarsStale.value !== 0;
188
- const hasInteractiveStart = ownership.value === INTERACTIVE && !scalarsWereStale;
189
- const start = presentation.value;
190
- ownership.value = NATIVE;
191
- presentation.value = target;
192
- scalarsStale.value = 0;
193
- const startClip = start.clip;
194
- const targetClip = target.clip;
195
- let animationId;
196
- if (animation.type === 'timing') {
197
- const [x1, y1, x2, y2] = animation.controlPoints;
198
- animationId = animateTimingHostFunction(driverId, hasInteractiveStart, startClip.x, startClip.y, startClip.width, startClip.height, startClip.radius, start.contentTranslateX, start.contentTranslateY, targetClip.x, targetClip.y, targetClip.width, targetClip.height, targetClip.radius, target.contentTranslateX, target.contentTranslateY, Math.max(0, animation.duration), x1, y1, x2, y2, reduceMotion);
199
- } else if (animation.type === 'spring') {
200
- const inheritVelocity = animation.initialVelocity === undefined || animation.initialVelocity === 'inherit';
201
- animationId = animateSpringHostFunction(driverId, hasInteractiveStart, startClip.x, startClip.y, startClip.width, startClip.height, startClip.radius, start.contentTranslateX, start.contentTranslateY, targetClip.x, targetClip.y, targetClip.width, targetClip.height, targetClip.radius, target.contentTranslateX, target.contentTranslateY, Math.max(0.0001, animation.mass ?? 1), Math.max(0.0001, animation.stiffness ?? 100), Math.max(0, animation.damping ?? 10), inheritVelocity ? 0 : animation.initialVelocity, inheritVelocity, reduceMotion);
202
- } else {
203
- animationId = animateKeyframesHostFunction(driverId, hasInteractiveStart, startClip.x, startClip.y, startClip.width, startClip.height, startClip.radius, start.contentTranslateX, start.contentTranslateY, targetClip.x, targetClip.y, targetClip.width, targetClip.height, targetClip.radius, target.contentTranslateX, target.contentTranslateY, Math.max(0, animation.duration), flattenedKeyframes(animation), reduceMotion);
204
- }
205
- if (animationId === 0) {
206
- // Native rejected the transition after ownership was transferred.
207
- // Restore interactive ownership and the pre-animation value, then
208
- // emit a standalone finished:false completion so completion-driven
209
- // state machines observe exactly one result per animateTo call.
210
- ownership.value = INTERACTIVE;
211
- presentation.value = start;
212
- if (scalarsWereStale) scalarsStale.value = 1;
213
- activeAnimationId.value = 0;
214
- return rejectAnimationHostFunction(driverId);
215
- }
216
- activeAnimationId.value = animationId;
217
- return animationId;
218
- };
219
- const cancelOnUI = (animationId = 0, behavior = 'current') => {
220
- 'worklet';
221
-
222
- const values = cancelAnimationHostFunction(driverId, animationId, behavior === 'target' ? 1 : 0);
223
- if (values.length < 8 || values[0] !== 1) return presentation.value;
224
- const current = presentationFromNative(values, presentation.value, 1);
225
- activeAnimationId.value = 0;
226
- ownership.value = INTERACTIVE;
227
- scalarsStale.value = 0;
228
- seedPresentation(current);
229
- return current;
230
- };
231
- const driver = {
232
- kind: 'hybrid',
233
- presentation,
234
- ui: {
235
- beginInteraction() {
236
- 'worklet';
237
-
238
- if (isRNRuntime()) return uiOnly();
239
- return beginOnUI();
240
- },
241
- set(next) {
242
- 'worklet';
243
-
244
- if (isRNRuntime()) return uiOnly();
245
- setOnUI(next);
246
- },
247
- setScalars(x, y, width, height, radius, contentTranslateX, contentTranslateY) {
248
- 'worklet';
249
-
250
- if (isRNRuntime()) return uiOnly();
251
- setScalarsOnUI(x, y, width, height, radius, contentTranslateX, contentTranslateY);
252
- },
253
- animateTo(target, animation) {
254
- 'worklet';
255
-
256
- if (isRNRuntime()) return uiOnly();
257
- return animateOnUI(target, animation);
258
- },
259
- cancel(animationId = 0, behavior = 'current') {
260
- 'worklet';
261
-
262
- if (isRNRuntime()) return uiOnly();
263
- return cancelOnUI(animationId, behavior);
264
- }
265
- },
266
- react: {
267
- beginInteraction() {
268
- const {
269
- requestId,
270
- promise
271
- } = createReactRequest(driverId);
272
- scheduleOnUI(() => {
273
- 'worklet';
274
-
275
- const result = beginOnUI();
276
- scheduleOnRN(resolveReactRequest, driverId, requestId, result);
277
- });
278
- return promise;
279
- },
280
- set(next) {
281
- const {
282
- requestId,
283
- promise
284
- } = createReactRequest(driverId);
285
- scheduleOnUI(() => {
286
- 'worklet';
287
-
288
- setOnUI(next);
289
- scheduleOnRN(resolveReactRequest, driverId, requestId, undefined);
290
- });
291
- return promise;
292
- },
293
- animateTo(target, animation) {
294
- const {
295
- requestId,
296
- promise
297
- } = createReactRequest(driverId, true);
298
- scheduleOnUI(() => {
299
- 'worklet';
300
-
301
- const result = animateOnUI(target, animation);
302
- scheduleOnRN(resolveReactRequest, driverId, requestId, result, true);
303
- });
304
- return promise;
305
- },
306
- cancel(animationId = 0, behavior = 'current') {
307
- const {
308
- requestId,
309
- promise
310
- } = createReactRequest(driverId, true);
311
- scheduleOnUI(() => {
312
- 'worklet';
313
-
314
- const result = cancelOnUI(animationId, behavior);
315
- scheduleOnRN(resolveReactRequest, driverId, requestId, result, true);
316
- });
317
- return promise;
318
- }
319
- }
320
- };
321
- setDriverState(driver, createDriverState(driverId, initialPresentation, presentation, callbackRef, activeAnimationId, ownership));
322
- driverRef.current = driver;
323
- }
324
- const driver = driverRef.current;
325
- useEffect(() => {
326
- const currentDriver = driverRef.current;
327
- if (!currentDriver) return undefined;
328
- const state = getDriverState(currentDriver);
329
- const {
330
- driverId
331
- } = state;
332
- // Re-attach on every effect run: StrictMode/<Activity> replay the effect
333
- // after its cleanup detached this state and destroyed the native driver.
334
- // The authoritative seed below recreates the native entry in that case.
335
- attachDriverState(state);
336
- const subscription = NativeSmoothClipModule.onClipAnimationComplete(result => {
337
- if (result.driverId !== driverId) return;
338
- synchronizeNativeCompletion(activeAnimationId, ownership, result.animationId, result.finished);
339
- completeNativeAnimation(driverId, result.animationId, result.finished);
340
- });
341
- scheduleOnUI((source, listenerId, nativeDriverId, owner, suppressed, active, stale, setter) => {
342
- 'worklet';
343
-
344
- let last = null;
345
- const deliver = next => {
346
- if (!isFiniteClipPresentation(next) || clipPresentationEquals(last, next)) {
347
- return;
348
- }
349
- // Record `last` only for values native has actually observed:
350
- // deliveries dropped during native ownership must stay eligible
351
- // for re-delivery once interactive ownership returns.
352
- if (owner.value !== INTERACTIVE) return;
353
- if (suppressed.value > 0) {
354
- suppressed.value -= 1;
355
- last = next;
356
- return;
357
- }
358
- last = next;
359
- if (stale.value !== 0) stale.value = 0;
360
- const {
361
- clip,
362
- contentTranslateX,
363
- contentTranslateY
364
- } = next;
365
- setter(nativeDriverId, clip.x, clip.y, clip.width, clip.height, clip.radius, contentTranslateX, contentTranslateY, false);
366
- };
367
- source.addListener(listenerId, deliver);
368
- // Authoritative take-ownership seed. Creates the native entry before
369
- // any view mounts on the first run, and revives a driver destroyed by
370
- // an effect-replay cleanup (StrictMode/<Activity>) on later runs.
371
- const current = source.value;
372
- if (isFiniteClipPresentation(current)) {
373
- active.value = 0;
374
- owner.value = INTERACTIVE;
375
- stale.value = 0;
376
- last = current;
377
- setter(nativeDriverId, current.clip.x, current.clip.y, current.clip.width, current.clip.height, current.clip.radius, current.contentTranslateX, current.contentTranslateY, true);
378
- }
379
- }, presentation, driverId, driverId, ownership, suppressDeliveries, activeAnimationId, scalarsStale, setPresentationHostFunction);
380
- return () => {
381
- subscription.remove();
382
- rejectDriverRequests(driverId);
383
- detachDriverState(state);
384
- scheduleOnUI((source, listenerId, nativeDriverId, destroy) => {
385
- 'worklet';
386
-
387
- source.removeListener(listenerId);
388
- destroy(nativeDriverId);
389
- }, presentation, driverId, driverId, destroyDriverHostFunction);
390
- };
391
- }, [activeAnimationId, ownership, presentation, scalarsStale, suppressDeliveries]);
392
- return driver;
393
- }
3
+ // Platform re-export only — the implementation is shared with Android.
4
+ // Edit drivers.native.ts, not this file.
5
+ export { useSmoothClipDriver } from "./drivers.native.js";
394
6
  //# sourceMappingURL=drivers.ios.js.map