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
@@ -138,10 +138,11 @@ inline double cubicBezier(
138
138
 
139
139
  // --- Presentation channels ------------------------------------------------
140
140
 
141
- // The first thirteen channels are geometry/content/appearance; the final eight are shadow.
142
- // Curve and shadow presence are categorical and stay outside the scalar array.
143
- constexpr std::size_t kBaseChannelCount = 13;
144
- constexpr std::size_t kChannelCount = 21;
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
- std::max(0.0, channels[19]),
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], clamp01(channels[12])};
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(
@@ -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, platforms that ignore it): the integrator then falls back to its
77
- // own clock plus the min() frame-clock anchor. iOS constructs this struct
78
- // without the member and inherits the NaN default — CoreAnimation anchors
79
- // its own animations, so the hint is Android-only by design.
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
@@ -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. The portable continuous
153
- // fallback uses kappa=1 so each cubic joins its straight edges with zero
154
- // curvature, matching the portable Android path. Hit testing derives
155
- // from this rendered path rather than maintaining a second curve model.
156
- // Uniform
157
- // continuous shapes use CALayer.cornerCurve for Apple's platform-native
158
- // compositor shape and are intentionally not pixel-identical cross-platform.
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