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.
- package/CHANGELOG.md +66 -0
- package/README.md +52 -0
- package/android/src/main/cpp/SmoothClipAndroid.h +22 -0
- package/android/src/main/cpp/SmoothClipBindings.cpp +37 -3
- package/android/src/main/cpp/SmoothClipRegistry.cpp +85 -2
- package/android/src/main/java/com/smoothclipview/ClipGeometryNormalizer.kt +209 -10
- package/android/src/main/java/com/smoothclipview/SmoothClipBackdropView.kt +59 -0
- package/android/src/main/java/com/smoothclipview/SmoothClipBackdropViewManager.kt +63 -0
- package/android/src/main/java/com/smoothclipview/SmoothClipBindings.kt +15 -0
- package/android/src/main/java/com/smoothclipview/SmoothClipView.kt +316 -34
- package/android/src/main/java/com/smoothclipview/SmoothClipViewManager.kt +19 -0
- package/android/src/main/java/com/smoothclipview/SmoothClipViewPackage.kt +1 -1
- package/cpp/SmoothClipAnimationCurve.h +36 -11
- package/cpp/SmoothClipRegistry.h +11 -4
- package/ios/SmoothClipBackdropView.h +17 -0
- package/ios/SmoothClipBackdropView.mm +267 -0
- package/ios/SmoothClipGeometry.h +8 -7
- package/ios/SmoothClipRegistry.mm +192 -9
- package/ios/SmoothClipTurboModule.cpp +33 -11
- package/ios/SmoothClipView.mm +713 -22
- package/ios/SmoothClipViewRegistry.h +14 -0
- package/lib/module/SmoothClipBackdropView.js +40 -0
- package/lib/module/SmoothClipBackdropView.js.map +1 -0
- package/lib/module/SmoothClipBackdropViewNativeComponent.ts +15 -0
- package/lib/module/SmoothClipView.ios.js +17 -1
- package/lib/module/SmoothClipView.ios.js.map +1 -1
- package/lib/module/SmoothClipViewNativeComponent.ts +4 -0
- package/lib/module/geometry.js +44 -6
- package/lib/module/geometry.js.map +1 -1
- package/lib/module/groups.native.js +10 -3
- package/lib/module/groups.native.js.map +1 -1
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/presentationCodec.js +6 -2
- package/lib/module/presentationCodec.js.map +1 -1
- package/lib/typescript/src/SmoothClipBackdropView.d.ts +155 -0
- package/lib/typescript/src/SmoothClipBackdropView.d.ts.map +1 -0
- package/lib/typescript/src/SmoothClipBackdropViewNativeComponent.d.ts +8 -0
- package/lib/typescript/src/SmoothClipBackdropViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/SmoothClipView.android.d.ts +1 -1
- package/lib/typescript/src/SmoothClipView.android.d.ts.map +1 -1
- package/lib/typescript/src/SmoothClipView.d.ts +1 -1
- package/lib/typescript/src/SmoothClipView.d.ts.map +1 -1
- package/lib/typescript/src/SmoothClipView.ios.d.ts +15 -1
- package/lib/typescript/src/SmoothClipView.ios.d.ts.map +1 -1
- package/lib/typescript/src/SmoothClipViewNativeComponent.d.ts +4 -0
- package/lib/typescript/src/SmoothClipViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/geometry.d.ts +19 -0
- package/lib/typescript/src/geometry.d.ts.map +1 -1
- package/lib/typescript/src/groups.native.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +4 -2
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/presentationCodec.d.ts +1 -1
- package/lib/typescript/src/presentationCodec.d.ts.map +1 -1
- package/package.json +4 -1
- package/src/SmoothClipBackdropView.tsx +53 -0
- package/src/SmoothClipBackdropViewNativeComponent.ts +15 -0
- package/src/SmoothClipView.android.tsx +4 -1
- package/src/SmoothClipView.ios.tsx +24 -1
- package/src/SmoothClipView.tsx +4 -1
- package/src/SmoothClipViewNativeComponent.ts +4 -0
- package/src/geometry.ts +59 -4
- package/src/groups.native.ts +9 -3
- package/src/index.ts +7 -1
- package/src/presentationCodec.ts +8 -2
|
@@ -138,10 +138,11 @@ inline double cubicBezier(
|
|
|
138
138
|
|
|
139
139
|
// --- Presentation channels ------------------------------------------------
|
|
140
140
|
|
|
141
|
-
// The first
|
|
142
|
-
// Curve and shadow presence are categorical and stay
|
|
143
|
-
|
|
144
|
-
constexpr std::size_t
|
|
141
|
+
// The first fifteen channels are geometry/content/appearance/backdrop; the
|
|
142
|
+
// final eight are shadow. Curve and shadow presence are categorical and stay
|
|
143
|
+
// outside the scalar array.
|
|
144
|
+
constexpr std::size_t kBaseChannelCount = 15;
|
|
145
|
+
constexpr std::size_t kChannelCount = 23;
|
|
145
146
|
using Channels = std::array<double, kChannelCount>;
|
|
146
147
|
|
|
147
148
|
inline double resolvedRadius(double overrideValue, double shorthand) {
|
|
@@ -179,6 +180,8 @@ inline Channels toChannels(const Presentation &presentation) {
|
|
|
179
180
|
presentation.contentScale,
|
|
180
181
|
presentation.rotation,
|
|
181
182
|
presentation.opacity,
|
|
183
|
+
presentation.backdropTranslateX,
|
|
184
|
+
presentation.backdropTranslateY,
|
|
182
185
|
presentation.shadow.red,
|
|
183
186
|
presentation.shadow.green,
|
|
184
187
|
presentation.shadow.blue,
|
|
@@ -365,6 +368,27 @@ inline double relativeSpringEnergy(
|
|
|
365
368
|
return currentEnergy / initialEnergy;
|
|
366
369
|
}
|
|
367
370
|
|
|
371
|
+
/**
|
|
372
|
+
* Seconds until the normalized spring trajectory started with `velocity`
|
|
373
|
+
* settles under the animation's relative-energy threshold, sampled at 120 Hz
|
|
374
|
+
* and capped at ten seconds. Every layer of a run (clip, content, shadow,
|
|
375
|
+
* backdrop) derives its Core Animation duration from this one rule, so they
|
|
376
|
+
* are removed in the same frame.
|
|
377
|
+
*/
|
|
378
|
+
inline double springSettleDuration(
|
|
379
|
+
const SpringAnimation &animation,
|
|
380
|
+
double velocity) {
|
|
381
|
+
ScalarSpringState state{0, velocity};
|
|
382
|
+
constexpr double step = 1.0 / 120.0;
|
|
383
|
+
double elapsed = 0;
|
|
384
|
+
while (elapsed < 10.0 &&
|
|
385
|
+
relativeSpringEnergy(state, animation) > animation.energyThreshold) {
|
|
386
|
+
state = advanceScalarSpring(state, animation, step);
|
|
387
|
+
elapsed += step;
|
|
388
|
+
}
|
|
389
|
+
return elapsed;
|
|
390
|
+
}
|
|
391
|
+
|
|
368
392
|
inline bool springScaleStaysPositive(
|
|
369
393
|
const Presentation &start,
|
|
370
394
|
const Presentation &target,
|
|
@@ -460,16 +484,17 @@ inline Presentation fromChannels(
|
|
|
460
484
|
geometry.curve = curve;
|
|
461
485
|
Shadow shadow{
|
|
462
486
|
shadowEnabled,
|
|
463
|
-
std::clamp(channels[13], 0.0, 1.0),
|
|
464
|
-
std::clamp(channels[14], 0.0, 1.0),
|
|
465
487
|
std::clamp(channels[15], 0.0, 1.0),
|
|
466
488
|
std::clamp(channels[16], 0.0, 1.0),
|
|
467
|
-
channels[17],
|
|
468
|
-
channels[18],
|
|
469
|
-
|
|
470
|
-
channels[20]
|
|
489
|
+
std::clamp(channels[17], 0.0, 1.0),
|
|
490
|
+
std::clamp(channels[18], 0.0, 1.0),
|
|
491
|
+
channels[19],
|
|
492
|
+
channels[20],
|
|
493
|
+
std::max(0.0, channels[21]),
|
|
494
|
+
channels[22]};
|
|
471
495
|
return Presentation{
|
|
472
|
-
geometry, channels[8], channels[9], channels[10], shadow, channels[11],
|
|
496
|
+
geometry, channels[8], channels[9], channels[10], shadow, channels[11],
|
|
497
|
+
clamp01(channels[12]), channels[13], channels[14]};
|
|
473
498
|
}
|
|
474
499
|
|
|
475
500
|
inline Presentation interpolate(
|
package/cpp/SmoothClipRegistry.h
CHANGED
|
@@ -46,6 +46,12 @@ struct Presentation {
|
|
|
46
46
|
Shadow shadow{};
|
|
47
47
|
double rotation = 0; // Unwrapped radians, clockwise in host coordinates.
|
|
48
48
|
double opacity = 1;
|
|
49
|
+
// Translation of every SmoothClipBackdropView bound to the driver, in
|
|
50
|
+
// host points. A channel of the same run as the aperture, so content that
|
|
51
|
+
// must stay locked to the window (a canvas centred in it) is sampled by
|
|
52
|
+
// the same native clock as the clip and never lands a frame late.
|
|
53
|
+
double backdropTranslateX = 0;
|
|
54
|
+
double backdropTranslateY = 0;
|
|
49
55
|
};
|
|
50
56
|
|
|
51
57
|
struct TimingAnimation {
|
|
@@ -73,10 +79,11 @@ struct AnimationStart {
|
|
|
73
79
|
// Reanimated-rule start stamp, captured on the UI runtime at animateTo time
|
|
74
80
|
// as `__frameTimestamp || _getAnimationTimestamp()` and converted to
|
|
75
81
|
// CLOCK_MONOTONIC seconds. NaN means no stamp was captured (older callers,
|
|
76
|
-
// tests
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
//
|
|
82
|
+
// tests): the integrator then falls back to its own clock plus the min()
|
|
83
|
+
// frame-clock anchor. iOS does not use this member: it carries the same
|
|
84
|
+
// stamp on the animation group (`GroupState::beginTimeHint` in
|
|
85
|
+
// SmoothClipRegistry.mm) and applies it as the shared Core Animation
|
|
86
|
+
// `beginTime`, bounded by the same sanity window.
|
|
80
87
|
double startedAtHintS = std::numeric_limits<double>::quiet_NaN();
|
|
81
88
|
};
|
|
82
89
|
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#import <React/RCTViewComponentView.h>
|
|
2
|
+
#import <UIKit/UIKit.h>
|
|
3
|
+
|
|
4
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A view translated by its controller's backdrop channel. The registry writes
|
|
8
|
+
* the channel to an inner content layer on every setFrame and adds a
|
|
9
|
+
* translation animation to every native run on the clip's shared Core
|
|
10
|
+
* Animation epoch, so the content is sampled by the same clock as the
|
|
11
|
+
* aperture and never lands a frame late. The view's own layer keeps React
|
|
12
|
+
* Native's transform.
|
|
13
|
+
*/
|
|
14
|
+
@interface SmoothClipBackdropView : RCTViewComponentView
|
|
15
|
+
@end
|
|
16
|
+
|
|
17
|
+
NS_ASSUME_NONNULL_END
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
#import "SmoothClipBackdropView.h"
|
|
2
|
+
|
|
3
|
+
#import "SmoothClipViewRegistry.h"
|
|
4
|
+
|
|
5
|
+
#include "SmoothClipAnimationCurve.h"
|
|
6
|
+
|
|
7
|
+
#import <QuartzCore/QuartzCore.h>
|
|
8
|
+
#import <React/RCTFabricComponentsPlugins.h>
|
|
9
|
+
#import <react/renderer/components/SmoothClipViewSpec/ComponentDescriptors.h>
|
|
10
|
+
#import <react/renderer/components/SmoothClipViewSpec/Props.h>
|
|
11
|
+
#import <react/renderer/components/SmoothClipViewSpec/RCTComponentViewHelpers.h>
|
|
12
|
+
|
|
13
|
+
#include <algorithm>
|
|
14
|
+
#include <cmath>
|
|
15
|
+
|
|
16
|
+
using namespace facebook::react;
|
|
17
|
+
|
|
18
|
+
static NSString *const kSmoothClipBackdropAnimationKey = @"smoothClip.backdrop";
|
|
19
|
+
|
|
20
|
+
@interface SmoothClipBackdropContentView : UIView
|
|
21
|
+
@end
|
|
22
|
+
|
|
23
|
+
@implementation SmoothClipBackdropContentView
|
|
24
|
+
|
|
25
|
+
- (id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event {
|
|
26
|
+
// The registry drives the translation explicitly, by a model write or by
|
|
27
|
+
// the animations installed below; no implicit action per frame.
|
|
28
|
+
return (id<CAAction>)[NSNull null];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@end
|
|
32
|
+
|
|
33
|
+
@implementation SmoothClipBackdropView {
|
|
34
|
+
SmoothClipBackdropContentView *_content;
|
|
35
|
+
uint64_t _driverId;
|
|
36
|
+
CGPoint _translation;
|
|
37
|
+
BOOL _animating;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
+ (ComponentDescriptorProvider)componentDescriptorProvider {
|
|
41
|
+
return concreteComponentDescriptorProvider<
|
|
42
|
+
SmoothClipBackdropViewComponentDescriptor>();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
+ (BOOL)shouldBeRecycled {
|
|
46
|
+
return NO;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
- (instancetype)initWithFrame:(CGRect)frame {
|
|
50
|
+
if (self = [super initWithFrame:frame]) {
|
|
51
|
+
static const auto defaultProps =
|
|
52
|
+
std::make_shared<const SmoothClipBackdropViewProps>();
|
|
53
|
+
_props = defaultProps;
|
|
54
|
+
_content = [[SmoothClipBackdropContentView alloc] initWithFrame:self.bounds];
|
|
55
|
+
_content.autoresizesSubviews = NO;
|
|
56
|
+
[self addSubview:_content];
|
|
57
|
+
_driverId = 0;
|
|
58
|
+
_translation = CGPointZero;
|
|
59
|
+
_animating = NO;
|
|
60
|
+
}
|
|
61
|
+
return self;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
- (void)dealloc {
|
|
65
|
+
if (_driverId != 0) {
|
|
66
|
+
smoothclip::unregisterBackdropView(_driverId, self);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
- (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView
|
|
71
|
+
index:(NSInteger)index {
|
|
72
|
+
NSAssert(childComponentView.superview == nil,
|
|
73
|
+
@"SmoothClipBackdropView attempted to mount an already-mounted child");
|
|
74
|
+
[_content insertSubview:childComponentView atIndex:index];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
- (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView
|
|
78
|
+
index:(NSInteger)index {
|
|
79
|
+
NSAssert(childComponentView.superview == _content,
|
|
80
|
+
@"SmoothClipBackdropView attempted to unmount a child from another parent");
|
|
81
|
+
[childComponentView removeFromSuperview];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
- (void)updateLayoutMetrics:(const LayoutMetrics &)layoutMetrics
|
|
85
|
+
oldLayoutMetrics:(const LayoutMetrics &)oldLayoutMetrics {
|
|
86
|
+
[super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:oldLayoutMetrics];
|
|
87
|
+
// Bounds and centre, never frame: the content layer carries the channel as
|
|
88
|
+
// a translation transform, and a frame write under a transform re-derives
|
|
89
|
+
// the position through the transform's inverse, folding the current
|
|
90
|
+
// translation into the position for good. Fabric sets props (and so the
|
|
91
|
+
// channel) before it lays the view out.
|
|
92
|
+
const CGRect bounds = self.bounds;
|
|
93
|
+
_content.bounds = bounds;
|
|
94
|
+
_content.center = CGPointMake(CGRectGetMidX(bounds), CGRectGetMidY(bounds));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
- (void)updateProps:(const Props::Shared &)props
|
|
98
|
+
oldProps:(const Props::Shared &)oldProps {
|
|
99
|
+
const auto &newProps =
|
|
100
|
+
*std::static_pointer_cast<const SmoothClipBackdropViewProps>(props);
|
|
101
|
+
[super updateProps:props oldProps:oldProps];
|
|
102
|
+
const uint64_t nextDriverId =
|
|
103
|
+
isfinite(newProps.driverId) && newProps.driverId > 0
|
|
104
|
+
? static_cast<uint64_t>(newProps.driverId)
|
|
105
|
+
: 0;
|
|
106
|
+
if (nextDriverId == _driverId) return;
|
|
107
|
+
if (_driverId != 0) {
|
|
108
|
+
smoothclip::unregisterBackdropView(_driverId, self);
|
|
109
|
+
}
|
|
110
|
+
_driverId = nextDriverId;
|
|
111
|
+
if (_driverId != 0) {
|
|
112
|
+
smoothclip::registerBackdropView(_driverId, self);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
- (void)prepareForRecycle {
|
|
117
|
+
if (_driverId != 0) {
|
|
118
|
+
smoothclip::unregisterBackdropView(_driverId, self);
|
|
119
|
+
_driverId = 0;
|
|
120
|
+
}
|
|
121
|
+
[self stopBackdropAnimation];
|
|
122
|
+
[self writeTranslation:CGPointZero];
|
|
123
|
+
[super prepareForRecycle];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// MARK: - Registry hooks
|
|
127
|
+
|
|
128
|
+
- (void)writeTranslation:(CGPoint)translation {
|
|
129
|
+
_translation = translation;
|
|
130
|
+
_content.layer.transform =
|
|
131
|
+
CATransform3DMakeTranslation(translation.x, translation.y, 0);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
- (void)stopBackdropAnimation {
|
|
135
|
+
if (!_animating) return;
|
|
136
|
+
[_content.layer removeAnimationForKey:kSmoothClipBackdropAnimationKey];
|
|
137
|
+
_animating = NO;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
- (CGPoint)smoothClipBackdropCurrentTranslation {
|
|
141
|
+
CALayer *layer = _animating ? _content.layer.presentationLayer : nil;
|
|
142
|
+
if (layer == nil) return _translation;
|
|
143
|
+
const CATransform3D transform = layer.transform;
|
|
144
|
+
return CGPointMake(transform.m41, transform.m42);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
- (void)smoothClipApplyBackdrop:(CGPoint)translation {
|
|
148
|
+
[self stopBackdropAnimation];
|
|
149
|
+
[self writeTranslation:translation];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
- (void)installBackdropGroup:(CAAnimationGroup *)group
|
|
153
|
+
sharedBeginTime:(CFTimeInterval)sharedBeginTime {
|
|
154
|
+
if (sharedBeginTime <= 0) sharedBeginTime = CACurrentMediaTime();
|
|
155
|
+
// The clip's epoch, translated into this layer's clock: a stamped epoch
|
|
156
|
+
// may sit up to one frame ahead of the commit, and until it arrives the
|
|
157
|
+
// run presents its first frame rather than the target the model holds.
|
|
158
|
+
group.beginTime = [_content.layer convertTime:sharedBeginTime fromLayer:nil];
|
|
159
|
+
group.fillMode = kCAFillModeBackwards;
|
|
160
|
+
[_content.layer addAnimation:group forKey:kSmoothClipBackdropAnimationKey];
|
|
161
|
+
_animating = YES;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
- (BOOL)smoothClipAnimateBackdropTo:(CGPoint)target
|
|
165
|
+
timing:(smoothclip::TimingAnimation)timing
|
|
166
|
+
sharedBeginTime:(CFTimeInterval)sharedBeginTime {
|
|
167
|
+
const CGPoint from = [self smoothClipBackdropCurrentTranslation];
|
|
168
|
+
[self stopBackdropAnimation];
|
|
169
|
+
[self writeTranslation:target];
|
|
170
|
+
if (self.window == nil) return NO;
|
|
171
|
+
CAMediaTimingFunction *function = [CAMediaTimingFunction
|
|
172
|
+
functionWithControlPoints:timing.controlPoint1X
|
|
173
|
+
:timing.controlPoint1Y
|
|
174
|
+
:timing.controlPoint2X
|
|
175
|
+
:timing.controlPoint2Y];
|
|
176
|
+
CABasicAnimation *x =
|
|
177
|
+
[CABasicAnimation animationWithKeyPath:@"transform.translation.x"];
|
|
178
|
+
x.fromValue = @(from.x);
|
|
179
|
+
x.toValue = @(target.x);
|
|
180
|
+
x.timingFunction = function;
|
|
181
|
+
CABasicAnimation *y =
|
|
182
|
+
[CABasicAnimation animationWithKeyPath:@"transform.translation.y"];
|
|
183
|
+
y.fromValue = @(from.y);
|
|
184
|
+
y.toValue = @(target.y);
|
|
185
|
+
y.timingFunction = function;
|
|
186
|
+
CAAnimationGroup *group = [CAAnimationGroup animation];
|
|
187
|
+
group.animations = @[ x, y ];
|
|
188
|
+
group.duration = std::max(0.0, timing.durationMs) / 1000.0;
|
|
189
|
+
[self installBackdropGroup:group sharedBeginTime:sharedBeginTime];
|
|
190
|
+
return YES;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
- (BOOL)smoothClipAnimateBackdropTo:(CGPoint)target
|
|
194
|
+
spring:(smoothclip::SpringAnimation)spring
|
|
195
|
+
sharedBeginTime:(CFTimeInterval)sharedBeginTime {
|
|
196
|
+
const CGPoint from = [self smoothClipBackdropCurrentTranslation];
|
|
197
|
+
[self stopBackdropAnimation];
|
|
198
|
+
[self writeTranslation:target];
|
|
199
|
+
if (self.window == nil) return NO;
|
|
200
|
+
// The same normalized trajectory and settling rule as the clip host, so
|
|
201
|
+
// both layers stop in the same frame.
|
|
202
|
+
const double duration =
|
|
203
|
+
smoothclip::springSettleDuration(spring, spring.initialVelocity);
|
|
204
|
+
const auto animation = [&](NSString *keyPath, double fromValue, double toValue) {
|
|
205
|
+
CASpringAnimation *result = [CASpringAnimation animationWithKeyPath:keyPath];
|
|
206
|
+
result.fromValue = @(fromValue);
|
|
207
|
+
result.toValue = @(toValue);
|
|
208
|
+
result.mass = spring.mass;
|
|
209
|
+
result.stiffness = spring.stiffness;
|
|
210
|
+
result.damping = spring.damping;
|
|
211
|
+
result.initialVelocity = spring.initialVelocity;
|
|
212
|
+
result.duration = duration;
|
|
213
|
+
return result;
|
|
214
|
+
};
|
|
215
|
+
CAAnimationGroup *group = [CAAnimationGroup animation];
|
|
216
|
+
group.animations = @[
|
|
217
|
+
animation(@"transform.translation.x", from.x, target.x),
|
|
218
|
+
animation(@"transform.translation.y", from.y, target.y),
|
|
219
|
+
];
|
|
220
|
+
group.duration = duration;
|
|
221
|
+
[self installBackdropGroup:group sharedBeginTime:sharedBeginTime];
|
|
222
|
+
return YES;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
- (void)smoothClipCancelBackdropUsingTarget:(BOOL)useTarget {
|
|
226
|
+
if (!_animating) return;
|
|
227
|
+
const CGPoint visible = [self smoothClipBackdropCurrentTranslation];
|
|
228
|
+
[self stopBackdropAnimation];
|
|
229
|
+
if (!useTarget) [self writeTranslation:visible];
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
- (BOOL)smoothClipPauseBackdropAtMediaTime:(CFTimeInterval)mediaTime {
|
|
233
|
+
CAAnimation *installed =
|
|
234
|
+
[_content.layer animationForKey:kSmoothClipBackdropAnimationKey];
|
|
235
|
+
if (!_animating || installed == nil) return NO;
|
|
236
|
+
CAAnimation *paused = [installed copy];
|
|
237
|
+
const CFTimeInterval localNow =
|
|
238
|
+
[_content.layer convertTime:mediaTime fromLayer:nil];
|
|
239
|
+
paused.speed = 0;
|
|
240
|
+
paused.timeOffset = std::clamp(
|
|
241
|
+
localNow - installed.beginTime, (CFTimeInterval)0, installed.duration);
|
|
242
|
+
paused.beginTime = 0;
|
|
243
|
+
[_content.layer removeAnimationForKey:kSmoothClipBackdropAnimationKey];
|
|
244
|
+
[_content.layer addAnimation:paused forKey:kSmoothClipBackdropAnimationKey];
|
|
245
|
+
return YES;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
- (BOOL)smoothClipResumeBackdropAtMediaTime:(CFTimeInterval)mediaTime
|
|
249
|
+
catchUpBy:(CFTimeInterval)catchUp {
|
|
250
|
+
CAAnimation *installed =
|
|
251
|
+
[_content.layer animationForKey:kSmoothClipBackdropAnimationKey];
|
|
252
|
+
if (!_animating || installed == nil) return NO;
|
|
253
|
+
CAAnimation *resumed = [installed copy];
|
|
254
|
+
const CFTimeInterval localNow =
|
|
255
|
+
[_content.layer convertTime:mediaTime fromLayer:nil];
|
|
256
|
+
const CFTimeInterval resumedTime = std::min(
|
|
257
|
+
installed.duration,
|
|
258
|
+
std::max((CFTimeInterval)0, installed.timeOffset + catchUp));
|
|
259
|
+
resumed.speed = 1;
|
|
260
|
+
resumed.timeOffset = 0;
|
|
261
|
+
resumed.beginTime = localNow - resumedTime;
|
|
262
|
+
[_content.layer removeAnimationForKey:kSmoothClipBackdropAnimationKey];
|
|
263
|
+
[_content.layer addAnimation:resumed forKey:kSmoothClipBackdropAnimationKey];
|
|
264
|
+
return YES;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
@end
|
package/ios/SmoothClipGeometry.h
CHANGED
|
@@ -149,13 +149,14 @@ NS_INLINE CGPathRef SmoothClipCreateRoundedRectPath(
|
|
|
149
149
|
const CGFloat minY = CGRectGetMinY(rect);
|
|
150
150
|
const CGFloat maxX = CGRectGetMaxX(rect);
|
|
151
151
|
const CGFloat maxY = CGRectGetMaxY(rect);
|
|
152
|
-
// Circular is the exact cubic-circle coefficient.
|
|
153
|
-
//
|
|
154
|
-
//
|
|
155
|
-
//
|
|
156
|
-
//
|
|
157
|
-
// continuous
|
|
158
|
-
//
|
|
152
|
+
// Circular is the exact cubic-circle coefficient. This continuous fallback
|
|
153
|
+
// uses kappa=1 so each cubic joins its straight edges with zero curvature
|
|
154
|
+
// in one segment, which keeps the topology Core Animation interpolates.
|
|
155
|
+
// It only feeds the unequal-radii mask and the shadow outline: uniform
|
|
156
|
+
// continuous shapes use CALayer.cornerCurve, Apple's platform-native
|
|
157
|
+
// compositor shape. Android draws continuous as a Figma smoothed corner
|
|
158
|
+
// instead, so this path does not mirror it. Hit testing derives from this
|
|
159
|
+
// rendered path rather than maintaining a second curve model.
|
|
159
160
|
const CGFloat controlFactor =
|
|
160
161
|
curve == SmoothClipCornerCurveContinuous
|
|
161
162
|
? 1.0
|