react-native-smooth-clip-view 0.2.9 → 0.4.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 (193) hide show
  1. package/README.md +132 -349
  2. package/android/build.gradle +8 -4
  3. package/android/proguard-rules.pro +3 -3
  4. package/android/src/main/cpp/SmoothClipAndroid.h +13 -6
  5. package/android/src/main/cpp/SmoothClipBindings.cpp +605 -123
  6. package/android/src/main/cpp/SmoothClipRegistry.cpp +975 -209
  7. package/android/src/main/java/com/smoothclipview/ClipGeometryNormalizer.kt +192 -118
  8. package/android/src/main/java/com/smoothclipview/SmoothClipBindings.kt +22 -4
  9. package/android/src/main/java/com/smoothclipview/SmoothClipModule.kt +81 -56
  10. package/android/src/main/java/com/smoothclipview/SmoothClipView.kt +449 -240
  11. package/android/src/main/java/com/smoothclipview/SmoothClipViewManager.kt +150 -38
  12. package/cpp/SmoothClipAnimationCurve.h +317 -397
  13. package/cpp/SmoothClipRegistry.h +84 -20
  14. package/cpp/SmoothClipSharedGeometry.h +131 -22
  15. package/cpp/SmoothClipVelocityTracker.h +16 -12
  16. package/ios/SmoothClipGeometry.h +188 -21
  17. package/ios/SmoothClipRegistry.mm +869 -368
  18. package/ios/SmoothClipRegistrySnapshot.h +13 -0
  19. package/ios/SmoothClipTurboModule.cpp +513 -156
  20. package/ios/SmoothClipTurboModule.h +123 -56
  21. package/ios/SmoothClipView.mm +1074 -449
  22. package/ios/SmoothClipViewRegistry.h +3 -7
  23. package/ios/reactnativesmoothclipview.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  24. package/ios/reactnativesmoothclipview.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  25. package/lib/module/NativeSmoothClipModule.js.map +1 -1
  26. package/lib/module/SmoothClipView.ios.js +94 -11
  27. package/lib/module/SmoothClipView.ios.js.map +1 -1
  28. package/lib/module/SmoothClipView.js +1 -46
  29. package/lib/module/SmoothClipView.js.map +1 -1
  30. package/lib/module/SmoothClipViewNativeComponent.ts +32 -12
  31. package/lib/module/capabilities.js +4 -0
  32. package/lib/module/capabilities.js.map +1 -0
  33. package/lib/module/capabilities.native.js +18 -0
  34. package/lib/module/capabilities.native.js.map +1 -0
  35. package/lib/module/capabilityTypes.js +2 -0
  36. package/lib/module/capabilityTypes.js.map +1 -0
  37. package/lib/module/controllerInternals.js +28 -0
  38. package/lib/module/controllerInternals.js.map +1 -0
  39. package/lib/module/controllerLifecycle.js +9 -0
  40. package/lib/module/controllerLifecycle.js.map +1 -0
  41. package/lib/module/controllerTypes.js +4 -0
  42. package/lib/module/controllerTypes.js.map +1 -0
  43. package/lib/module/controllers.android.js +4 -0
  44. package/lib/module/controllers.android.js.map +1 -0
  45. package/lib/module/controllers.ios.js +4 -0
  46. package/lib/module/controllers.ios.js.map +1 -0
  47. package/lib/module/controllers.js +4 -0
  48. package/lib/module/controllers.js.map +1 -0
  49. package/lib/module/controllers.native.js +89 -0
  50. package/lib/module/controllers.native.js.map +1 -0
  51. package/lib/module/geometry.js +180 -30
  52. package/lib/module/geometry.js.map +1 -1
  53. package/lib/module/groupTypes.js +4 -0
  54. package/lib/module/groupTypes.js.map +1 -0
  55. package/lib/module/groups.android.js +4 -0
  56. package/lib/module/groups.android.js.map +1 -0
  57. package/lib/module/groups.ios.js +4 -0
  58. package/lib/module/groups.ios.js.map +1 -0
  59. package/lib/module/groups.js +4 -0
  60. package/lib/module/groups.js.map +1 -0
  61. package/lib/module/groups.native.js +399 -0
  62. package/lib/module/groups.native.js.map +1 -0
  63. package/lib/module/ids.js +8 -0
  64. package/lib/module/ids.js.map +1 -0
  65. package/lib/module/index.js +4 -3
  66. package/lib/module/index.js.map +1 -1
  67. package/lib/module/nativeCompletion.js +37 -16
  68. package/lib/module/nativeCompletion.js.map +1 -1
  69. package/lib/module/presentationCodec.js +81 -0
  70. package/lib/module/presentationCodec.js.map +1 -0
  71. package/lib/typescript/src/NativeSmoothClipModule.d.ts +20 -4
  72. package/lib/typescript/src/NativeSmoothClipModule.d.ts.map +1 -1
  73. package/lib/typescript/src/SmoothClipView.d.ts +2 -10
  74. package/lib/typescript/src/SmoothClipView.d.ts.map +1 -1
  75. package/lib/typescript/src/SmoothClipView.ios.d.ts +139 -4
  76. package/lib/typescript/src/SmoothClipView.ios.d.ts.map +1 -1
  77. package/lib/typescript/src/SmoothClipViewNativeComponent.d.ts +16 -3
  78. package/lib/typescript/src/SmoothClipViewNativeComponent.d.ts.map +1 -1
  79. package/lib/typescript/src/capabilities.d.ts +3 -0
  80. package/lib/typescript/src/capabilities.d.ts.map +1 -0
  81. package/lib/typescript/src/capabilities.native.d.ts +3 -0
  82. package/lib/typescript/src/capabilities.native.d.ts.map +1 -0
  83. package/lib/typescript/src/capabilityTypes.d.ts +4 -0
  84. package/lib/typescript/src/capabilityTypes.d.ts.map +1 -0
  85. package/lib/typescript/src/controllerInternals.d.ts +14 -0
  86. package/lib/typescript/src/controllerInternals.d.ts.map +1 -0
  87. package/lib/typescript/src/controllerLifecycle.d.ts +3 -0
  88. package/lib/typescript/src/controllerLifecycle.d.ts.map +1 -0
  89. package/lib/typescript/src/controllerTypes.d.ts +62 -0
  90. package/lib/typescript/src/controllerTypes.d.ts.map +1 -0
  91. package/lib/typescript/src/controllers.android.d.ts +2 -0
  92. package/lib/typescript/src/controllers.android.d.ts.map +1 -0
  93. package/lib/typescript/src/controllers.d.ts +2 -0
  94. package/lib/typescript/src/controllers.d.ts.map +1 -0
  95. package/lib/typescript/src/controllers.ios.d.ts +2 -0
  96. package/lib/typescript/src/controllers.ios.d.ts.map +1 -0
  97. package/lib/typescript/src/controllers.native.d.ts +3 -0
  98. package/lib/typescript/src/controllers.native.d.ts.map +1 -0
  99. package/lib/typescript/src/geometry.d.ts +49 -5
  100. package/lib/typescript/src/geometry.d.ts.map +1 -1
  101. package/lib/typescript/src/groupTypes.d.ts +31 -0
  102. package/lib/typescript/src/groupTypes.d.ts.map +1 -0
  103. package/lib/typescript/src/groups.android.d.ts +2 -0
  104. package/lib/typescript/src/groups.android.d.ts.map +1 -0
  105. package/lib/typescript/src/groups.d.ts +2 -0
  106. package/lib/typescript/src/groups.d.ts.map +1 -0
  107. package/lib/typescript/src/groups.ios.d.ts +2 -0
  108. package/lib/typescript/src/groups.ios.d.ts.map +1 -0
  109. package/lib/typescript/src/groups.native.d.ts +3 -0
  110. package/lib/typescript/src/groups.native.d.ts.map +1 -0
  111. package/lib/typescript/src/ids.d.ts +2 -0
  112. package/lib/typescript/src/ids.d.ts.map +1 -0
  113. package/lib/typescript/src/index.d.ts +8 -5
  114. package/lib/typescript/src/index.d.ts.map +1 -1
  115. package/lib/typescript/src/nativeCompletion.d.ts +16 -6
  116. package/lib/typescript/src/nativeCompletion.d.ts.map +1 -1
  117. package/lib/typescript/src/presentationCodec.d.ts +10 -0
  118. package/lib/typescript/src/presentationCodec.d.ts.map +1 -0
  119. package/package.json +17 -13
  120. package/src/NativeSmoothClipModule.ts +80 -56
  121. package/src/SmoothClipView.ios.tsx +142 -16
  122. package/src/SmoothClipView.tsx +6 -76
  123. package/src/SmoothClipViewNativeComponent.ts +32 -12
  124. package/src/capabilities.native.ts +19 -0
  125. package/src/capabilities.ts +2 -0
  126. package/src/capabilityTypes.ts +3 -0
  127. package/src/controllerInternals.ts +52 -0
  128. package/src/controllerLifecycle.ts +6 -0
  129. package/src/controllerTypes.ts +87 -0
  130. package/src/controllers.android.ts +1 -0
  131. package/src/controllers.ios.ts +1 -0
  132. package/src/controllers.native.ts +104 -0
  133. package/src/controllers.ts +1 -0
  134. package/src/geometry.ts +293 -45
  135. package/src/groupTypes.ts +53 -0
  136. package/src/groups.android.ts +1 -0
  137. package/src/groups.ios.ts +1 -0
  138. package/src/groups.native.ts +604 -0
  139. package/src/groups.ts +1 -0
  140. package/src/ids.ts +6 -0
  141. package/src/index.ts +32 -12
  142. package/src/nativeCompletion.ts +67 -32
  143. package/src/presentationCodec.ts +134 -0
  144. package/lib/module/SmoothClipView.web.js +0 -54
  145. package/lib/module/SmoothClipView.web.js.map +0 -1
  146. package/lib/module/driverState.js +0 -110
  147. package/lib/module/driverState.js.map +0 -1
  148. package/lib/module/driverTypes.js +0 -4
  149. package/lib/module/driverTypes.js.map +0 -1
  150. package/lib/module/drivers.android.js +0 -6
  151. package/lib/module/drivers.android.js.map +0 -1
  152. package/lib/module/drivers.ios.js +0 -6
  153. package/lib/module/drivers.ios.js.map +0 -1
  154. package/lib/module/drivers.js +0 -284
  155. package/lib/module/drivers.js.map +0 -1
  156. package/lib/module/drivers.native.js +0 -556
  157. package/lib/module/drivers.native.js.map +0 -1
  158. package/lib/module/index.web.js +0 -8
  159. package/lib/module/index.web.js.map +0 -1
  160. package/lib/module/reactRequests.js +0 -56
  161. package/lib/module/reactRequests.js.map +0 -1
  162. package/lib/module/webClipPath.js +0 -25
  163. package/lib/module/webClipPath.js.map +0 -1
  164. package/lib/typescript/src/SmoothClipView.web.d.ts +0 -10
  165. package/lib/typescript/src/SmoothClipView.web.d.ts.map +0 -1
  166. package/lib/typescript/src/driverState.d.ts +0 -31
  167. package/lib/typescript/src/driverState.d.ts.map +0 -1
  168. package/lib/typescript/src/driverTypes.d.ts +0 -107
  169. package/lib/typescript/src/driverTypes.d.ts.map +0 -1
  170. package/lib/typescript/src/drivers.android.d.ts +0 -3
  171. package/lib/typescript/src/drivers.android.d.ts.map +0 -1
  172. package/lib/typescript/src/drivers.d.ts +0 -5
  173. package/lib/typescript/src/drivers.d.ts.map +0 -1
  174. package/lib/typescript/src/drivers.ios.d.ts +0 -3
  175. package/lib/typescript/src/drivers.ios.d.ts.map +0 -1
  176. package/lib/typescript/src/drivers.native.d.ts +0 -5
  177. package/lib/typescript/src/drivers.native.d.ts.map +0 -1
  178. package/lib/typescript/src/index.web.d.ts +0 -9
  179. package/lib/typescript/src/index.web.d.ts.map +0 -1
  180. package/lib/typescript/src/reactRequests.d.ts +0 -9
  181. package/lib/typescript/src/reactRequests.d.ts.map +0 -1
  182. package/lib/typescript/src/webClipPath.d.ts +0 -7
  183. package/lib/typescript/src/webClipPath.d.ts.map +0 -1
  184. package/src/SmoothClipView.web.tsx +0 -66
  185. package/src/driverState.ts +0 -158
  186. package/src/driverTypes.ts +0 -140
  187. package/src/drivers.android.ts +0 -8
  188. package/src/drivers.ios.ts +0 -8
  189. package/src/drivers.native.ts +0 -899
  190. package/src/drivers.ts +0 -427
  191. package/src/index.web.ts +0 -23
  192. package/src/reactRequests.ts +0 -76
  193. package/src/webClipPath.ts +0 -30
@@ -16,7 +16,9 @@
16
16
  #import <react/renderer/components/SmoothClipViewSpec/Props.h>
17
17
  #import <react/renderer/components/SmoothClipViewSpec/RCTComponentViewHelpers.h>
18
18
 
19
+ #include <algorithm>
19
20
  #include <cfloat>
21
+ #include <cmath>
20
22
 
21
23
  using namespace facebook::react;
22
24
 
@@ -51,26 +53,305 @@ using namespace facebook::react;
51
53
  finished:(BOOL)finished;
52
54
  @end
53
55
 
54
- static std::array<double, 7> SmoothClipVelocityChannels(
55
- CGRect rect, CGFloat radius, CGPoint contentTranslation) {
56
+ static SmoothClipCornerRadii SmoothClipPresentationRadii(
57
+ const smoothclip::Geometry &geometry) {
58
+ return {
59
+ (CGFloat)smoothclip::resolvedRadius(
60
+ geometry.topLeftRadius, geometry.radius),
61
+ (CGFloat)smoothclip::resolvedRadius(
62
+ geometry.topRightRadius, geometry.radius),
63
+ (CGFloat)smoothclip::resolvedRadius(
64
+ geometry.bottomRightRadius, geometry.radius),
65
+ (CGFloat)smoothclip::resolvedRadius(
66
+ geometry.bottomLeftRadius, geometry.radius),
67
+ };
68
+ }
69
+
70
+ static BOOL SmoothClipPresentationHasExplicitRadii(
71
+ const smoothclip::Geometry &geometry) {
72
+ return isfinite(geometry.topLeftRadius) ||
73
+ isfinite(geometry.topRightRadius) ||
74
+ isfinite(geometry.bottomRightRadius) ||
75
+ isfinite(geometry.bottomLeftRadius);
76
+ }
77
+
78
+ static SmoothClipCornerCurve SmoothClipPresentationCurve(
79
+ const smoothclip::Geometry &geometry) {
80
+ return geometry.curve == smoothclip::ClipCurve::Continuous
81
+ ? SmoothClipCornerCurveContinuous
82
+ : SmoothClipCornerCurveCircular;
83
+ }
84
+
85
+ static bool SmoothClipBuildPresentation(
86
+ double x,
87
+ double y,
88
+ double width,
89
+ double height,
90
+ double topLeftRadius,
91
+ double topRightRadius,
92
+ double bottomRightRadius,
93
+ double bottomLeftRadius,
94
+ NSInteger curveCode,
95
+ double contentTranslateX,
96
+ double contentTranslateY,
97
+ double contentScale,
98
+ BOOL shadowEnabled,
99
+ double shadowRed,
100
+ double shadowGreen,
101
+ double shadowBlue,
102
+ double shadowAlpha,
103
+ double shadowOffsetX,
104
+ double shadowOffsetY,
105
+ double shadowBlurRadius,
106
+ double shadowSpreadDistance,
107
+ smoothclip::Presentation *result) {
108
+ if (result == nullptr || !isfinite(x) || !isfinite(y) ||
109
+ !isfinite(width) || !isfinite(height) ||
110
+ !isfinite(topLeftRadius) || !isfinite(topRightRadius) ||
111
+ !isfinite(bottomRightRadius) || !isfinite(bottomLeftRadius) ||
112
+ !isfinite(contentTranslateX) || !isfinite(contentTranslateY) ||
113
+ !isfinite(contentScale) || !isfinite(shadowRed) ||
114
+ !isfinite(shadowGreen) || !isfinite(shadowBlue) ||
115
+ !isfinite(shadowAlpha) ||
116
+ !isfinite(shadowOffsetX) || !isfinite(shadowOffsetY) ||
117
+ !isfinite(shadowBlurRadius) || !isfinite(shadowSpreadDistance) ||
118
+ contentScale <= 0 || shadowRed < 0 || shadowRed > 1 ||
119
+ shadowGreen < 0 || shadowGreen > 1 ||
120
+ shadowBlue < 0 || shadowBlue > 1 ||
121
+ shadowAlpha < 0 || shadowAlpha > 1 || shadowBlurRadius < 0 ||
122
+ (curveCode != static_cast<NSInteger>(smoothclip::ClipCurve::Circular) &&
123
+ curveCode != static_cast<NSInteger>(smoothclip::ClipCurve::Continuous))) {
124
+ return false;
125
+ }
126
+
127
+ const bool uniform = topLeftRadius == topRightRadius &&
128
+ topLeftRadius == bottomRightRadius &&
129
+ topLeftRadius == bottomLeftRadius;
130
+ smoothclip::Geometry geometry{
131
+ x, y, width, height, uniform ? topLeftRadius : 0};
132
+ geometry.topLeftRadius = topLeftRadius;
133
+ geometry.topRightRadius = topRightRadius;
134
+ geometry.bottomRightRadius = bottomRightRadius;
135
+ geometry.bottomLeftRadius = bottomLeftRadius;
136
+ geometry.curve = static_cast<smoothclip::ClipCurve>(curveCode);
137
+ const smoothclip::Shadow shadow{
138
+ shadowEnabled,
139
+ shadowRed,
140
+ shadowGreen,
141
+ shadowBlue,
142
+ shadowAlpha,
143
+ shadowOffsetX,
144
+ shadowOffsetY,
145
+ shadowBlurRadius,
146
+ shadowSpreadDistance};
147
+ *result = {
148
+ geometry, contentTranslateX, contentTranslateY, contentScale, shadow};
149
+ return true;
150
+ }
151
+
152
+ static bool SmoothBoxShadowEqual(
153
+ const smoothclip::Shadow &first,
154
+ const smoothclip::Shadow &second) {
155
+ return first.enabled == second.enabled &&
156
+ first.red == second.red && first.green == second.green &&
157
+ first.blue == second.blue && first.alpha == second.alpha &&
158
+ first.offsetX == second.offsetX && first.offsetY == second.offsetY &&
159
+ first.blurRadius == second.blurRadius &&
160
+ first.spreadDistance == second.spreadDistance;
161
+ }
162
+
163
+ static bool SmoothBoxShadowVisible(
164
+ SmoothClipCanonicalGeometry geometry,
165
+ const smoothclip::Shadow &shadow) {
166
+ return shadow.enabled && shadow.alpha > 0 && !CGRectIsEmpty(geometry.rect);
167
+ }
168
+
169
+ static smoothclip::Shadow SmoothBoxShadowWithEffectiveAlpha(
170
+ SmoothClipCanonicalGeometry geometry,
171
+ smoothclip::Shadow shadow) {
172
+ if (!SmoothBoxShadowVisible(geometry, shadow)) {
173
+ shadow.alpha = 0;
174
+ }
175
+ return shadow;
176
+ }
177
+
178
+ static void SmoothBoxShadowNormalizeAnimationEndpoints(
179
+ SmoothClipCanonicalGeometry fromGeometry,
180
+ smoothclip::Shadow *fromShadow,
181
+ SmoothClipCanonicalGeometry toGeometry,
182
+ smoothclip::Shadow *toShadow) {
183
+ const bool fromVisible = SmoothBoxShadowVisible(fromGeometry, *fromShadow);
184
+ const bool toVisible = SmoothBoxShadowVisible(toGeometry, *toShadow);
185
+ if (fromVisible == toVisible) {
186
+ *fromShadow = SmoothBoxShadowWithEffectiveAlpha(
187
+ fromGeometry, *fromShadow);
188
+ *toShadow = SmoothBoxShadowWithEffectiveAlpha(toGeometry, *toShadow);
189
+ return;
190
+ }
191
+
192
+ smoothclip::Shadow *invisible = fromVisible ? toShadow : fromShadow;
193
+ const smoothclip::Shadow &visible = fromVisible ? *fromShadow : *toShadow;
194
+ invisible->enabled = true;
195
+ invisible->red = visible.red;
196
+ invisible->green = visible.green;
197
+ invisible->blue = visible.blue;
198
+ invisible->alpha = 0;
199
+ invisible->offsetX = visible.offsetX;
200
+ invisible->offsetY = visible.offsetY;
201
+ invisible->blurRadius = visible.blurRadius;
202
+ invisible->spreadDistance = visible.spreadDistance;
203
+ }
204
+
205
+ static bool SmoothBoxShadowColorEqual(
206
+ const smoothclip::Shadow &first,
207
+ const smoothclip::Shadow &second) {
208
+ return first.red == second.red && first.green == second.green &&
209
+ first.blue == second.blue && first.alpha == second.alpha;
210
+ }
211
+
212
+ static bool SmoothBoxShadowPathInputEqual(
213
+ SmoothClipCanonicalGeometry firstGeometry,
214
+ const smoothclip::Shadow &firstShadow,
215
+ SmoothClipCanonicalGeometry secondGeometry,
216
+ const smoothclip::Shadow &secondShadow) {
217
+ return CGRectEqualToRect(firstGeometry.rect, secondGeometry.rect) &&
218
+ SmoothClipCornerRadiiEqual(
219
+ firstGeometry.radii, secondGeometry.radii) &&
220
+ firstGeometry.curve == secondGeometry.curve &&
221
+ firstShadow.spreadDistance == secondShadow.spreadDistance;
222
+ }
223
+
224
+ static CGFloat SmoothClipAdjustedShadowRadius(
225
+ CGFloat radius,
226
+ CGFloat spread) {
227
+ const CGFloat magnitude = ABS(spread);
228
+ CGFloat multiplier = 1;
229
+ if (magnitude > 0 && radius < magnitude) {
230
+ multiplier = 1 + pow(radius / magnitude - 1, 3);
231
+ }
232
+ return MAX(0, radius + spread * multiplier);
233
+ }
234
+
235
+ static CGPathRef SmoothClipCreateShadowPath(
236
+ SmoothClipCanonicalGeometry geometry,
237
+ const smoothclip::Shadow &shadow) {
238
+ const CGFloat spread = shadow.spreadDistance;
239
+ CGRect rect = CGRectInset(geometry.rect, -spread, -spread);
240
+ if (CGRectGetWidth(rect) <= 0 || CGRectGetHeight(rect) <= 0) {
241
+ rect = CGRectZero;
242
+ }
243
+ const SmoothClipCornerRadii radii = {
244
+ SmoothClipAdjustedShadowRadius(geometry.radii.topLeft, spread),
245
+ SmoothClipAdjustedShadowRadius(geometry.radii.topRight, spread),
246
+ SmoothClipAdjustedShadowRadius(geometry.radii.bottomRight, spread),
247
+ SmoothClipAdjustedShadowRadius(geometry.radii.bottomLeft, spread),
248
+ };
249
+ return SmoothClipCreateRoundedRectPath(rect, radii, geometry.curve);
250
+ }
251
+
252
+ static NSString *SmoothClipCALayerCornerCurve(
253
+ SmoothClipCornerCurve curve) API_AVAILABLE(ios(13.0)) {
254
+ return curve == SmoothClipCornerCurveContinuous
255
+ ? kCACornerCurveContinuous
256
+ : kCACornerCurveCircular;
257
+ }
258
+
259
+ static CGAffineTransform SmoothClipContentTransform(
260
+ CGFloat scale,
261
+ CGPoint translation) {
262
+ // Construct the matrix directly. Concatenating a translation and a scale is
263
+ // order-sensitive and can multiply tx/ty; the presentation contract requires
264
+ // a centered uniform scale with translation remaining in unscaled points.
265
+ return CGAffineTransformMake(
266
+ scale, 0, 0, scale, translation.x, translation.y);
267
+ }
268
+
269
+ struct SmoothClipPathEndpoints {
270
+ CGPoint points[9];
271
+ size_t count;
272
+ };
273
+
274
+ static void SmoothClipCollectPathEndpoints(
275
+ void *context,
276
+ const CGPathElement *element) {
277
+ SmoothClipPathEndpoints *endpoints =
278
+ static_cast<SmoothClipPathEndpoints *>(context);
279
+ if (endpoints->count >= 9 ||
280
+ element->type == kCGPathElementCloseSubpath) {
281
+ return;
282
+ }
283
+ const size_t endpointIndex =
284
+ element->type == kCGPathElementAddCurveToPoint ? 2 :
285
+ element->type == kCGPathElementAddQuadCurveToPoint ? 1 : 0;
286
+ endpoints->points[endpoints->count++] = element->points[endpointIndex];
287
+ }
288
+
289
+ static SmoothClipCornerRadii SmoothClipRadiiFromFixedPath(
290
+ CGPathRef path,
291
+ CGRect rect,
292
+ SmoothClipCornerRadii fallback) {
293
+ if (path == nil) return fallback;
294
+ SmoothClipPathEndpoints endpoints{};
295
+ CGPathApply(path, &endpoints, SmoothClipCollectPathEndpoints);
296
+ if (endpoints.count != 9) return fallback;
297
+ return {
298
+ MAX(0, endpoints.points[0].x - CGRectGetMinX(rect)),
299
+ MAX(0, CGRectGetMaxX(rect) - endpoints.points[1].x),
300
+ MAX(0, CGRectGetMaxY(rect) - endpoints.points[3].y),
301
+ MAX(0, endpoints.points[5].x - CGRectGetMinX(rect)),
302
+ };
303
+ }
304
+
305
+ static std::array<double, 11> SmoothClipVelocityChannels(
306
+ CGRect rect,
307
+ SmoothClipCornerRadii radii,
308
+ CGPoint contentTranslation,
309
+ CGFloat contentScale) {
56
310
  return {CGRectGetMinX(rect),
57
311
  CGRectGetMinY(rect),
58
312
  CGRectGetWidth(rect),
59
313
  CGRectGetHeight(rect),
60
- radius,
314
+ radii.topLeft,
315
+ radii.topRight,
316
+ radii.bottomRight,
317
+ radii.bottomLeft,
61
318
  contentTranslation.x,
62
- contentTranslation.y};
319
+ contentTranslation.y,
320
+ contentScale};
321
+ }
322
+
323
+ static BOOL SmoothClipRectIntersectsHost(CGRect rect, CGRect host) {
324
+ const CGRect intersection = CGRectIntersection(rect, host);
325
+ return !CGRectIsNull(intersection) && !CGRectIsEmpty(intersection);
326
+ }
327
+
328
+ static smoothclip::Presentation SmoothClipCanonicalSnapshot(
329
+ smoothclip::Presentation presentation) {
330
+ smoothclip::canonicalizePresentation(presentation);
331
+ return presentation;
63
332
  }
64
333
 
65
334
  @implementation SmoothClipView {
335
+ CALayer *_shadowLayer;
66
336
  SmoothClipContainerView *_clipContainer;
67
337
  SmoothClipContainerView *_contentContainer;
338
+ CAShapeLayer *_unequalCornerMask;
68
339
  CGRect _requestedClip;
69
- CGRect _normalizedClip;
340
+ CGRect _canonicalClip;
70
341
  CGFloat _requestedRadius;
71
- CGFloat _normalizedRadius;
342
+ CGFloat _canonicalRadius;
343
+ SmoothClipCornerRadii _requestedRadii;
344
+ SmoothClipCornerRadii _canonicalRadii;
345
+ SmoothClipCornerCurve _requestedCurve;
346
+ SmoothClipCornerCurve _canonicalCurve;
347
+ BOOL _requestedHasExplicitRadii;
348
+ BOOL _canonicalHasExplicitRadii;
72
349
  CGPoint _requestedContentTranslation;
73
- CGPoint _normalizedContentTranslation;
350
+ CGPoint _canonicalContentTranslation;
351
+ CGFloat _requestedContentScale;
352
+ CGFloat _canonicalContentScale;
353
+ smoothclip::Shadow _requestedShadow;
354
+ smoothclip::Shadow _canonicalShadow;
74
355
  uint64_t _driverId;
75
356
  BOOL _hasLayout;
76
357
  BOOL _commandIsAuthoritative;
@@ -81,23 +362,11 @@ static std::array<double, 7> SmoothClipVelocityChannels(
81
362
 
82
363
  int32_t _activeAnimationId;
83
364
  NSInteger _activeAnimationKind;
84
- // Set when a native animation install arrived before the first layout.
85
- // The install is deferred and re-run through the registry at first layout
86
- // so it joins from live presentation geometry with the true remaining time.
87
- BOOL _pendingAnimationInstall;
88
- CFTimeInterval _animationStartedAt;
89
- CFTimeInterval _animationDuration;
90
- // Bumped on every CA install. updateLayoutMetrics captures it on entry so a
91
- // registry-driven install that happens inside its own lifecycle notification
92
- // (a latch resume during that very layout pass) is detectable: the local
93
- // rebuild must then be skipped, because its captured remaining time predates
94
- // the suspension and would shorten — or zero out — the fresh install.
95
- uint32_t _animationInstallGeneration;
96
365
  smoothclip::TimingAnimation _timingAnimation;
97
366
  smoothclip::SpringAnimation _springAnimation;
98
367
  SmoothClipAnimationDelegate *_animationDelegate;
99
368
 
100
- // 'inherit' velocity samples (normalized geometry, per view); recording/
369
+ // 'inherit' velocity samples (raw canonical geometry, per view); recording/
101
370
  // coalescing/projection live in the shared cpp/SmoothClipVelocityTracker.h
102
371
  // (behavior-paired with Android's per-driver history).
103
372
  smoothclip::VelocitySampleHistory _velocitySamples;
@@ -124,6 +393,9 @@ static std::array<double, 7> SmoothClipVelocityChannels(
124
393
  static const auto defaultProps =
125
394
  std::make_shared<const SmoothClipViewProps>();
126
395
  _props = defaultProps;
396
+ // SmoothClipView is the fixed viewport. Consumer overflow props are
397
+ // intentionally overridden again after every prop update below.
398
+ self.clipsToBounds = YES;
127
399
 
128
400
  _clipContainer = [[SmoothClipContainerView alloc] initWithFrame:CGRectZero];
129
401
  _clipContainer.layer.masksToBounds = YES;
@@ -139,20 +411,40 @@ static std::array<double, 7> SmoothClipVelocityChannels(
139
411
  [_clipContainer addSubview:_contentContainer];
140
412
  [self addSubview:_clipContainer];
141
413
 
414
+ _shadowLayer = nil;
415
+ // Created lazily so the common clipping-only path owns no shadow layer.
416
+
417
+ _unequalCornerMask = [CAShapeLayer layer];
418
+ _unequalCornerMask.actions = @{
419
+ @"path": [NSNull null],
420
+ @"bounds": [NSNull null],
421
+ @"position": [NSNull null],
422
+ };
423
+ _unequalCornerMask.fillColor = UIColor.blackColor.CGColor;
424
+ _unequalCornerMask.needsDisplayOnBoundsChange = NO;
425
+
142
426
  _requestedClip = CGRectZero;
143
- _normalizedClip = CGRectZero;
427
+ _canonicalClip = CGRectZero;
144
428
  _requestedRadius = 0;
145
- _normalizedRadius = 0;
429
+ _canonicalRadius = 0;
430
+ _requestedRadii = {0, 0, 0, 0};
431
+ _canonicalRadii = {0, 0, 0, 0};
432
+ _requestedCurve = SmoothClipCornerCurveCircular;
433
+ _canonicalCurve = SmoothClipCornerCurveCircular;
434
+ _requestedHasExplicitRadii = NO;
435
+ _canonicalHasExplicitRadii = NO;
146
436
  _requestedContentTranslation = CGPointZero;
147
- _normalizedContentTranslation = CGPointZero;
437
+ _canonicalContentTranslation = CGPointZero;
438
+ _requestedContentScale = 1;
439
+ _canonicalContentScale = 1;
440
+ _requestedShadow = {};
441
+ _canonicalShadow = {};
148
442
  _driverId = 0;
149
443
  _hasLayout = NO;
150
444
  _commandIsAuthoritative = NO;
151
445
  _ignoreAnimationCallback = NO;
152
446
  _activeAnimationId = 0;
153
447
  _activeAnimationKind = 0;
154
- _pendingAnimationInstall = NO;
155
- _animationInstallGeneration = 0;
156
448
  smoothclip::clearVelocitySamples(_velocitySamples);
157
449
  // App-state transitions are observed by the registry itself (see
158
450
  // installApplicationStateObservers): the active flag is process-global,
@@ -198,13 +490,72 @@ static std::array<double, 7> SmoothClipVelocityChannels(
198
490
  geometry.y,
199
491
  MAX(0, geometry.width),
200
492
  MAX(0, geometry.height));
201
- _requestedRadius = MAX(0, geometry.radius);
493
+ _requestedRadii = SmoothClipPresentationRadii(geometry);
494
+ _requestedRadius = SmoothClipCornerRadiiAreUniform(_requestedRadii)
495
+ ? MAX(0, _requestedRadii.topLeft)
496
+ : 0;
497
+ _requestedCurve = SmoothClipPresentationCurve(geometry);
498
+ _requestedHasExplicitRadii =
499
+ SmoothClipPresentationHasExplicitRadii(geometry);
202
500
  _requestedContentTranslation = CGPointMake(
203
501
  presentation.contentTranslateX,
204
502
  presentation.contentTranslateY);
503
+ _requestedContentScale = presentation.contentScale;
504
+ _requestedShadow = presentation.shadow;
505
+ }
506
+
507
+ - (CGColorRef)colorForShadow:(const smoothclip::Shadow &)shadow {
508
+ return [UIColor colorWithRed:smoothclip::clamp01(shadow.red)
509
+ green:smoothclip::clamp01(shadow.green)
510
+ blue:smoothclip::clamp01(shadow.blue)
511
+ alpha:smoothclip::clamp01(shadow.alpha)].CGColor;
512
+ }
513
+
514
+ - (CALayer *)ensureShadowLayer {
515
+ if (_shadowLayer != nil) return _shadowLayer;
516
+ _shadowLayer = [CALayer layer];
517
+ _shadowLayer.masksToBounds = NO;
518
+ _shadowLayer.actions = @{
519
+ @"shadowPath": [NSNull null],
520
+ @"shadowColor": [NSNull null],
521
+ @"shadowOpacity": [NSNull null],
522
+ @"shadowRadius": [NSNull null],
523
+ @"shadowOffset": [NSNull null],
524
+ @"bounds": [NSNull null],
525
+ @"position": [NSNull null],
526
+ };
527
+ [self.layer insertSublayer:_shadowLayer below:_clipContainer.layer];
528
+ return _shadowLayer;
205
529
  }
206
530
 
207
- - (BOOL)normalizedRequestedGeometry:(SmoothNormalizedClipGeometry *)geometry {
531
+ - (void)writeShadowModel:(const smoothclip::Shadow &)shadow
532
+ geometry:(SmoothClipCanonicalGeometry)geometry {
533
+ const smoothclip::Shadow effectiveShadow =
534
+ SmoothBoxShadowWithEffectiveAlpha(geometry, shadow);
535
+ const BOOL visible = SmoothBoxShadowVisible(geometry, effectiveShadow);
536
+ if (_shadowLayer == nil && !visible) return;
537
+
538
+ CALayer *shadowLayer = visible ? [self ensureShadowLayer] : _shadowLayer;
539
+ shadowLayer.bounds = self.bounds;
540
+ shadowLayer.position = CGPointMake(
541
+ CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds));
542
+ CGPathRef path = SmoothClipCreateShadowPath(geometry, effectiveShadow);
543
+ shadowLayer.shadowPath = path;
544
+ CGPathRelease(path);
545
+ shadowLayer.shadowColor = [self colorForShadow:effectiveShadow];
546
+ shadowLayer.shadowOpacity = visible ? 1 : 0;
547
+ shadowLayer.shadowRadius = MAX(0, effectiveShadow.blurRadius) / 2.0;
548
+ shadowLayer.shadowOffset = CGSizeMake(
549
+ effectiveShadow.offsetX, effectiveShadow.offsetY);
550
+ }
551
+
552
+ - (void)writeShadow:(const smoothclip::Shadow &)shadow
553
+ geometry:(SmoothClipCanonicalGeometry)geometry {
554
+ _canonicalShadow = shadow;
555
+ [self writeShadowModel:shadow geometry:geometry];
556
+ }
557
+
558
+ - (BOOL)canonicalRequestedGeometry:(SmoothClipCanonicalGeometry *)geometry {
208
559
  if (!_hasLayout) return NO;
209
560
  #if defined(SMOOTH_CLIP_ENABLE_SIGNPOSTS) && SMOOTH_CLIP_ENABLE_SIGNPOSTS
210
561
  const bool signpostsEnabled =
@@ -213,22 +564,24 @@ static std::array<double, 7> SmoothClipVelocityChannels(
213
564
  if (signpostsEnabled) {
214
565
  identifier = os_signpost_id_generate([SmoothClipView signpostLog]);
215
566
  os_signpost_interval_begin(
216
- [SmoothClipView signpostLog], identifier, "normalization");
567
+ [SmoothClipView signpostLog], identifier, "canonicalization");
217
568
  }
218
569
  #endif
219
- const CGSize hostSize = self.bounds.size;
220
- const BOOL valid = SmoothClipNormalizeGeometry(
570
+ const BOOL valid = SmoothClipCanonicalizeGeometry(
221
571
  CGRectGetMinX(_requestedClip),
222
572
  CGRectGetMinY(_requestedClip),
223
573
  CGRectGetWidth(_requestedClip),
224
574
  CGRectGetHeight(_requestedClip),
225
- _requestedRadius,
226
- CGSizeMake(MAX(0, hostSize.width), MAX(0, hostSize.height)),
575
+ _requestedRadii.topLeft,
576
+ _requestedRadii.topRight,
577
+ _requestedRadii.bottomRight,
578
+ _requestedRadii.bottomLeft,
579
+ _requestedCurve,
227
580
  geometry);
228
581
  #if defined(SMOOTH_CLIP_ENABLE_SIGNPOSTS) && SMOOTH_CLIP_ENABLE_SIGNPOSTS
229
582
  if (signpostsEnabled) {
230
583
  os_signpost_interval_end(
231
- [SmoothClipView signpostLog], identifier, "normalization");
584
+ [SmoothClipView signpostLog], identifier, "canonicalization");
232
585
  }
233
586
  #endif
234
587
  return valid;
@@ -243,14 +596,63 @@ static std::array<double, 7> SmoothClipVelocityChannels(
243
596
 
244
597
  - (void)syncVisibilityForRect:(CGRect)rect {
245
598
  [self setClipContainerHidden:CGRectIsEmpty(rect)];
599
+ _clipContainer.accessibilityElementsHidden =
600
+ !SmoothClipRectIntersectsHost(rect, self.bounds);
601
+ }
602
+
603
+ - (void)configureUnequalCornerMaskForGeometry:
604
+ (SmoothClipCanonicalGeometry)geometry {
605
+ _unequalCornerMask.bounds = geometry.rect;
606
+ _unequalCornerMask.position = CGPointMake(
607
+ CGRectGetMidX(geometry.rect), CGRectGetMidY(geometry.rect));
608
+ CGPathRef path = SmoothClipCreateRoundedRectPath(
609
+ geometry.rect, geometry.radii, geometry.curve);
610
+ _unequalCornerMask.path = path;
611
+ CGPathRelease(path);
612
+ }
613
+
614
+ - (void)applyStaticCornerRepresentation:
615
+ (SmoothClipCanonicalGeometry)geometry {
616
+ CALayer *layer = _clipContainer.layer;
617
+ if (SmoothClipCornerRadiiAreUniform(geometry.radii)) {
618
+ layer.mask = nil;
619
+ layer.cornerRadius = geometry.radii.topLeft;
620
+ layer.cornerCurve = SmoothClipCALayerCornerCurve(geometry.curve);
621
+ } else {
622
+ layer.cornerRadius = 0;
623
+ layer.cornerCurve = kCACornerCurveCircular;
624
+ [self configureUnequalCornerMaskForGeometry:geometry];
625
+ layer.mask = _unequalCornerMask;
626
+ }
246
627
  }
247
628
 
248
- - (void)writeLayerRect:(CGRect)rect radius:(CGFloat)radius {
249
- const BOOL rectChanged = !CGRectEqualToRect(_normalizedClip, rect);
250
- const BOOL radiusChanged = _normalizedRadius != radius;
251
- _normalizedClip = rect;
252
- _normalizedRadius = radius;
253
- if (!rectChanged && !radiusChanged) return;
629
+ - (SmoothClipCanonicalGeometry)canonicalGeometryValue {
630
+ return {
631
+ .rect = _canonicalClip,
632
+ .radius = _canonicalRadius,
633
+ .radii = _canonicalRadii,
634
+ .curve = _canonicalCurve,
635
+ };
636
+ }
637
+
638
+ - (void)writeLayerGeometry:(SmoothClipCanonicalGeometry)geometry
639
+ hasExplicitRadii:(BOOL)hasExplicitRadii {
640
+ const BOOL rectChanged =
641
+ !CGRectEqualToRect(_canonicalClip, geometry.rect);
642
+ const BOOL radiiChanged =
643
+ !SmoothClipCornerRadiiEqual(_canonicalRadii, geometry.radii);
644
+ const BOOL curveChanged = _canonicalCurve != geometry.curve;
645
+ const BOOL representationChanged =
646
+ _canonicalHasExplicitRadii != hasExplicitRadii;
647
+ _canonicalClip = geometry.rect;
648
+ _canonicalRadius = geometry.radius;
649
+ _canonicalRadii = geometry.radii;
650
+ _canonicalCurve = geometry.curve;
651
+ _canonicalHasExplicitRadii = hasExplicitRadii;
652
+ if (!rectChanged && !radiiChanged && !curveChanged &&
653
+ !representationChanged) {
654
+ return;
655
+ }
254
656
 
255
657
  #if defined(SMOOTH_CLIP_ENABLE_SIGNPOSTS) && SMOOTH_CLIP_ENABLE_SIGNPOSTS
256
658
  const bool signpostsEnabled =
@@ -264,12 +666,13 @@ static std::array<double, 7> SmoothClipVelocityChannels(
264
666
  #endif
265
667
  CALayer *layer = _clipContainer.layer;
266
668
  if (rectChanged) {
267
- layer.bounds = rect;
268
- layer.position = CGPointMake(CGRectGetMidX(rect), CGRectGetMidY(rect));
269
- }
270
- if (radiusChanged) {
271
- layer.cornerRadius = radius;
669
+ layer.bounds = geometry.rect;
670
+ layer.position = CGPointMake(
671
+ CGRectGetMidX(geometry.rect), CGRectGetMidY(geometry.rect));
272
672
  }
673
+ // The uniform common case stays on CALayer's compositor fast path and uses
674
+ // the platform's exact continuous-corner implementation.
675
+ [self applyStaticCornerRepresentation:geometry];
273
676
  #if defined(SMOOTH_CLIP_ENABLE_SIGNPOSTS) && SMOOTH_CLIP_ENABLE_SIGNPOSTS
274
677
  if (signpostsEnabled) {
275
678
  os_signpost_interval_end(
@@ -278,53 +681,101 @@ static std::array<double, 7> SmoothClipVelocityChannels(
278
681
  #endif
279
682
  }
280
683
 
281
- - (void)writeContentTranslation:(CGPoint)translation {
282
- if (CGPointEqualToPoint(_normalizedContentTranslation, translation)) return;
283
- _normalizedContentTranslation = translation;
684
+ - (void)writeContentTranslation:(CGPoint)translation
685
+ scale:(CGFloat)scale {
686
+ if (CGPointEqualToPoint(_canonicalContentTranslation, translation) &&
687
+ _canonicalContentScale == scale) {
688
+ return;
689
+ }
690
+ _canonicalContentTranslation = translation;
691
+ _canonicalContentScale = scale;
284
692
  _contentContainer.layer.affineTransform =
285
- CGAffineTransformMakeTranslation(translation.x, translation.y);
693
+ SmoothClipContentTransform(scale, translation);
286
694
  }
287
695
 
288
696
  - (void)applyRequestedClip {
289
- SmoothNormalizedClipGeometry geometry;
290
- if (![self normalizedRequestedGeometry:&geometry]) return;
291
-
292
- const BOOL visibilityChanged = _clipHidden != CGRectIsEmpty(geometry.rect);
293
- if (CGRectEqualToRect(_normalizedClip, geometry.rect) &&
294
- _normalizedRadius == geometry.radius &&
697
+ SmoothClipCanonicalGeometry geometry;
698
+ if (![self canonicalRequestedGeometry:&geometry]) return;
699
+
700
+ const BOOL accessibilityHidden =
701
+ !SmoothClipRectIntersectsHost(geometry.rect, self.bounds);
702
+ const BOOL visibilityChanged =
703
+ _clipHidden != CGRectIsEmpty(geometry.rect) ||
704
+ _clipContainer.accessibilityElementsHidden != accessibilityHidden;
705
+ if (CGRectEqualToRect(_canonicalClip, geometry.rect) &&
706
+ SmoothClipCornerRadiiEqual(_canonicalRadii, geometry.radii) &&
707
+ _canonicalCurve == geometry.curve &&
708
+ _canonicalHasExplicitRadii == _requestedHasExplicitRadii &&
295
709
  CGPointEqualToPoint(
296
- _normalizedContentTranslation, _requestedContentTranslation) &&
710
+ _canonicalContentTranslation, _requestedContentTranslation) &&
711
+ _canonicalContentScale == _requestedContentScale &&
712
+ SmoothBoxShadowEqual(_canonicalShadow, _requestedShadow) &&
297
713
  !visibilityChanged) {
298
714
  return;
299
715
  }
300
- [self writeLayerRect:geometry.rect radius:geometry.radius];
301
- [self writeContentTranslation:_requestedContentTranslation];
716
+ [self writeLayerGeometry:geometry
717
+ hasExplicitRadii:_requestedHasExplicitRadii];
718
+ [self writeContentTranslation:_requestedContentTranslation
719
+ scale:_requestedContentScale];
720
+ [self writeShadow:_requestedShadow geometry:geometry];
302
721
  [self syncVisibilityForRect:geometry.rect];
303
722
  }
304
723
 
305
724
  - (void)recordInteractiveRect:(CGRect)rect
306
- radius:(CGFloat)radius
307
- contentTranslation:(CGPoint)contentTranslation {
725
+ radii:(SmoothClipCornerRadii)radii
726
+ contentTranslation:(CGPoint)contentTranslation
727
+ contentScale:(CGFloat)contentScale {
308
728
  // Identical-value dedupe and same-frame coalescing live in the shared
309
729
  // tracker; coalescing keeps a distinct re-record issued sub-frame after
310
730
  // the last one (e.g. a fused animateTo `from` seed at gesture release)
311
731
  // from exploding the inherited velocity.
312
732
  smoothclip::recordVelocitySample(
313
733
  _velocitySamples,
314
- SmoothClipVelocityChannels(rect, radius, contentTranslation),
734
+ SmoothClipVelocityChannels(
735
+ rect, radii, contentTranslation, contentScale),
315
736
  CACurrentMediaTime());
316
737
  }
317
738
 
318
739
  - (CGFloat)inheritedVelocityToRect:(CGRect)target
319
- radius:(CGFloat)targetRadius
320
- contentTranslation:(CGPoint)targetContentTranslation {
740
+ radii:(SmoothClipCornerRadii)targetRadii
741
+ contentTranslation:(CGPoint)targetContentTranslation
742
+ contentScale:(CGFloat)targetContentScale {
321
743
  return smoothclip::inheritedVelocity(
322
744
  _velocitySamples,
323
745
  SmoothClipVelocityChannels(
324
- target, targetRadius, targetContentTranslation),
746
+ target,
747
+ targetRadii,
748
+ targetContentTranslation,
749
+ targetContentScale),
325
750
  CACurrentMediaTime());
326
751
  }
327
752
 
753
+ - (double)smoothClipInheritedVelocityToPresentation:
754
+ (smoothclip::Presentation)presentation {
755
+ SmoothClipCanonicalGeometry target;
756
+ const SmoothClipCornerRadii radii =
757
+ SmoothClipPresentationRadii(presentation.clip);
758
+ if (!SmoothClipCanonicalizeGeometry(
759
+ presentation.clip.x,
760
+ presentation.clip.y,
761
+ presentation.clip.width,
762
+ presentation.clip.height,
763
+ radii.topLeft,
764
+ radii.topRight,
765
+ radii.bottomRight,
766
+ radii.bottomLeft,
767
+ SmoothClipPresentationCurve(presentation.clip),
768
+ &target)) {
769
+ return 0;
770
+ }
771
+ return [self inheritedVelocityToRect:target.rect
772
+ radii:target.radii
773
+ contentTranslation:CGPointMake(
774
+ presentation.contentTranslateX,
775
+ presentation.contentTranslateY)
776
+ contentScale:presentation.contentScale];
777
+ }
778
+
328
779
  - (void)smoothClipApplyPresentation:(smoothclip::Presentation)presentation {
329
780
  [self smoothClipApplyPresentation:presentation recordVelocitySample:YES];
330
781
  }
@@ -334,33 +785,72 @@ static std::array<double, 7> SmoothClipVelocityChannels(
334
785
  [self stopLayerAnimationWithoutCallback];
335
786
  [self storeRequestedPresentation:presentation];
336
787
  [self applyRequestedClip];
337
- // Skipped for writes that are not interactive motion (a latched
788
+ // Skipped for writes that are not interactive motion (a pending-run
338
789
  // cancel-to-target): they must not enter the 'inherit' history.
339
790
  if (recordVelocitySample && _hasLayout) {
340
- [self recordInteractiveRect:_normalizedClip
341
- radius:_normalizedRadius
342
- contentTranslation:_normalizedContentTranslation];
791
+ [self recordInteractiveRect:_canonicalClip
792
+ radii:_canonicalRadii
793
+ contentTranslation:_canonicalContentTranslation
794
+ contentScale:_canonicalContentScale];
343
795
  }
344
796
  }
345
797
 
346
798
  - (smoothclip::Presentation)smoothClipCurrentPresentation {
347
- CGFloat visibleRadius = _normalizedRadius;
799
+ SmoothClipCornerRadii visibleRadii = _canonicalRadii;
348
800
  const CGRect visibleRect = _activeAnimationId != 0
349
- ? [self presentationRectWithRadius:&visibleRadius]
350
- : _normalizedClip;
801
+ ? [self presentationRectWithRadii:&visibleRadii]
802
+ : _canonicalClip;
351
803
  CALayer *contentLayer = _activeAnimationId != 0
352
804
  ? (CALayer *)_contentContainer.layer.presentationLayer
353
805
  : _contentContainer.layer;
354
806
  if (contentLayer == nil) contentLayer = _contentContainer.layer;
355
807
  const CGAffineTransform transform = contentLayer.affineTransform;
356
- return {
357
- {CGRectGetMinX(visibleRect),
358
- CGRectGetMinY(visibleRect),
359
- CGRectGetWidth(visibleRect),
360
- CGRectGetHeight(visibleRect),
361
- visibleRadius},
362
- transform.tx,
363
- transform.ty};
808
+ const BOOL uniform = SmoothClipCornerRadiiAreUniform(visibleRadii);
809
+ smoothclip::Geometry geometry{
810
+ CGRectGetMinX(visibleRect),
811
+ CGRectGetMinY(visibleRect),
812
+ CGRectGetWidth(visibleRect),
813
+ CGRectGetHeight(visibleRect),
814
+ uniform ? visibleRadii.topLeft : 0};
815
+ if (_canonicalHasExplicitRadii || !uniform) {
816
+ geometry.topLeftRadius = visibleRadii.topLeft;
817
+ geometry.topRightRadius = visibleRadii.topRight;
818
+ geometry.bottomRightRadius = visibleRadii.bottomRight;
819
+ geometry.bottomLeftRadius = visibleRadii.bottomLeft;
820
+ }
821
+ geometry.curve = _canonicalCurve == SmoothClipCornerCurveContinuous
822
+ ? smoothclip::ClipCurve::Continuous
823
+ : smoothclip::ClipCurve::Circular;
824
+ const CGFloat contentScale = hypot(transform.a, transform.b);
825
+ smoothclip::Shadow shadow = _canonicalShadow;
826
+ CALayer *shadowLayer = _activeAnimationId != 0 && _shadowLayer != nil
827
+ ? (CALayer *)_shadowLayer.presentationLayer
828
+ : _shadowLayer;
829
+ if (shadowLayer == nil) return SmoothClipCanonicalSnapshot({
830
+ geometry, transform.tx, transform.ty, contentScale, shadow});
831
+ shadow.enabled = _canonicalShadow.enabled || shadowLayer.shadowOpacity > 0;
832
+ shadow.blurRadius = shadowLayer.shadowRadius * 2.0;
833
+ shadow.offsetX = shadowLayer.shadowOffset.width;
834
+ shadow.offsetY = shadowLayer.shadowOffset.height;
835
+ CGColorRef color = shadowLayer.shadowColor;
836
+ if (color != nil) {
837
+ UIColor *uiColor = [UIColor colorWithCGColor:color];
838
+ CGFloat red = 0, green = 0, blue = 0, alpha = 0;
839
+ if ([uiColor getRed:&red green:&green blue:&blue alpha:&alpha]) {
840
+ shadow.red = red;
841
+ shadow.green = green;
842
+ shadow.blue = blue;
843
+ shadow.alpha = alpha;
844
+ }
845
+ }
846
+ CGPathRef shadowPath = shadowLayer.shadowPath;
847
+ if (shadowPath != nil && !CGRectIsEmpty(visibleRect)) {
848
+ const CGRect shadowBounds = CGPathGetBoundingBox(shadowPath);
849
+ shadow.spreadDistance =
850
+ CGRectGetMinX(visibleRect) - CGRectGetMinX(shadowBounds);
851
+ }
852
+ return SmoothClipCanonicalSnapshot(
853
+ {geometry, transform.tx, transform.ty, contentScale, shadow});
364
854
  }
365
855
 
366
856
  - (BOOL)smoothClipIsJoinable {
@@ -373,64 +863,54 @@ static std::array<double, 7> SmoothClipVelocityChannels(
373
863
  - (BOOL)smoothClipCanDisplay {
374
864
  // A CA animation committed while this view's layer tree is detached from
375
865
  // the render tree does not survive the later attach commit; installs and
376
- // latch starts must wait until a frame can actually be produced.
866
+ // pre-ready runs must wait until a frame can actually be produced.
377
867
  return [self smoothClipIsJoinable] && self.window != nil &&
378
868
  smoothclip::applicationIsActive();
379
869
  }
380
870
 
381
- - (BOOL)smoothClipHasPendingInstall {
382
- return _pendingAnimationInstall;
383
- }
384
-
385
871
  - (void)smoothClipClearVelocitySamples {
386
872
  smoothclip::clearVelocitySamples(_velocitySamples);
387
873
  }
388
874
 
389
- - (double)smoothClipSpringContinuationVelocity {
390
- if (_activeAnimationId == 0 || _activeAnimationKind != 2 ||
391
- _pendingAnimationInstall) {
392
- return 0;
393
- }
394
- const double elapsed = MAX(0, CACurrentMediaTime() - _animationStartedAt);
395
- return smoothclip::springContinuationVelocity(_springAnimation, elapsed);
396
- }
397
-
398
875
  - (smoothclip::Presentation)smoothClipFreezePresentation {
399
876
  const smoothclip::Presentation visible =
400
877
  [self smoothClipCurrentPresentation];
401
- const CGRect visibleRect = CGRectMake(
402
- visible.clip.x,
403
- visible.clip.y,
404
- visible.clip.width,
405
- visible.clip.height);
406
- const CGPoint visibleContentTranslation = CGPointMake(
407
- visible.contentTranslateX,
408
- visible.contentTranslateY);
409
878
  [self stopLayerAnimationWithoutCallback];
410
- _requestedClip = visibleRect;
411
- _requestedRadius = visible.clip.radius;
412
- _requestedContentTranslation = visibleContentTranslation;
413
- [self writeLayerRect:visibleRect radius:visible.clip.radius];
414
- [self writeContentTranslation:visibleContentTranslation];
415
- [self syncVisibilityForRect:visibleRect];
879
+ [self storeRequestedPresentation:visible];
880
+ [self applyRequestedClip];
416
881
  return visible;
417
882
  }
418
883
 
419
- - (CGRect)presentationRectWithRadius:(CGFloat *)radius {
884
+ - (CGRect)presentationRectWithRadii:(SmoothClipCornerRadii *)radii {
420
885
  CALayer *layer = (CALayer *)_clipContainer.layer.presentationLayer;
421
886
  if (layer == nil) layer = _clipContainer.layer;
422
887
  const CGRect bounds = layer.bounds;
423
888
  const CGPoint position = layer.position;
424
- if (radius != nullptr) *radius = layer.cornerRadius;
425
- return CGRectMake(
889
+ const CGRect rect = CGRectMake(
426
890
  position.x - bounds.size.width * layer.anchorPoint.x,
427
891
  position.y - bounds.size.height * layer.anchorPoint.y,
428
892
  bounds.size.width,
429
893
  bounds.size.height);
894
+ if (radii != nullptr) {
895
+ if (_clipContainer.layer.mask == _unequalCornerMask) {
896
+ CAShapeLayer *mask =
897
+ (CAShapeLayer *)_unequalCornerMask.presentationLayer;
898
+ if (mask == nil) mask = _unequalCornerMask;
899
+ *radii = SmoothClipRadiiFromFixedPath(
900
+ mask.path, rect, _canonicalRadii);
901
+ } else {
902
+ *radii = {
903
+ layer.cornerRadius,
904
+ layer.cornerRadius,
905
+ layer.cornerRadius,
906
+ layer.cornerRadius,
907
+ };
908
+ }
909
+ }
910
+ return rect;
430
911
  }
431
912
 
432
913
  - (void)stopLayerAnimationWithoutCallback {
433
- _pendingAnimationInstall = NO;
434
914
  if (_activeAnimationId == 0) return;
435
915
  // Invalidate before removing: CA may deliver didStop for the removed
436
916
  // animation after this method returns, when _ignoreAnimationCallback has
@@ -440,6 +920,9 @@ static std::array<double, 7> SmoothClipVelocityChannels(
440
920
  _ignoreAnimationCallback = YES;
441
921
  [_clipContainer.layer removeAnimationForKey:@"smoothClip.geometry"];
442
922
  [_contentContainer.layer removeAnimationForKey:@"smoothClip.content"];
923
+ [_unequalCornerMask removeAnimationForKey:@"smoothClip.mask"];
924
+ [_shadowLayer removeAnimationForKey:@"smoothClip.shadow"];
925
+ [self applyStaticCornerRepresentation:[self canonicalGeometryValue]];
443
926
  _ignoreAnimationCallback = NO;
444
927
  _activeAnimationId = 0;
445
928
  _activeAnimationKind = 0;
@@ -458,15 +941,19 @@ static std::array<double, 7> SmoothClipVelocityChannels(
458
941
  }
459
942
 
460
943
  - (CFTimeInterval)springSettlingDurationWithVelocity:(double)velocity {
461
- // Every key path in the group shares mass/stiffness/damping and the same
462
- // normalized initialVelocity, so one settling solve covers all of them.
463
- CASpringAnimation *probe =
464
- [CASpringAnimation animationWithKeyPath:@"bounds.origin.x"];
465
- probe.mass = _springAnimation.mass;
466
- probe.stiffness = _springAnimation.stiffness;
467
- probe.damping = _springAnimation.damping;
468
- probe.initialVelocity = velocity;
469
- return probe.settlingDuration;
944
+ // Every key path shares one normalized trajectory. Derive the terminal
945
+ // duration from the same relative-energy rule as Android/Reanimated instead
946
+ // of asking each Core Animation property for an independent settle time.
947
+ smoothclip::ScalarSpringState state{0, velocity};
948
+ constexpr double step = 1.0 / 120.0;
949
+ double elapsed = 0;
950
+ while (elapsed < 10.0 &&
951
+ smoothclip::relativeSpringEnergy(state, _springAnimation) >
952
+ _springAnimation.energyThreshold) {
953
+ state = smoothclip::advanceScalarSpring(state, _springAnimation, step);
954
+ elapsed += step;
955
+ }
956
+ return elapsed;
470
957
  }
471
958
 
472
959
  - (CASpringAnimation *)springAnimationForKeyPath:(NSString *)keyPath
@@ -486,13 +973,19 @@ static std::array<double, 7> SmoothClipVelocityChannels(
486
973
  return animation;
487
974
  }
488
975
 
489
- - (void)installAnimationFromRect:(CGRect)fromRect
490
- fromRadius:(CGFloat)fromRadius
491
- fromContentTranslation:(CGPoint)fromContentTranslation
492
- toRect:(CGRect)toRect
493
- toRadius:(CGFloat)toRadius
494
- toContentTranslation:(CGPoint)toContentTranslation
495
- duration:(CFTimeInterval)duration {
976
+ - (void)installAnimationFromGeometry:
977
+ (SmoothClipCanonicalGeometry)fromGeometry
978
+ fromContentTranslation:(CGPoint)fromContentTranslation
979
+ fromContentScale:(CGFloat)fromContentScale
980
+ fromShadow:(smoothclip::Shadow)fromShadow
981
+ toGeometry:
982
+ (SmoothClipCanonicalGeometry)toGeometry
983
+ hasExplicitToRadii:(BOOL)hasExplicitToRadii
984
+ toContentTranslation:(CGPoint)toContentTranslation
985
+ toContentScale:(CGFloat)toContentScale
986
+ toShadow:(smoothclip::Shadow)toShadow
987
+ duration:(CFTimeInterval)duration
988
+ sharedBeginTime:(CFTimeInterval)sharedBeginTime {
496
989
  const int32_t animationId = _activeAnimationId;
497
990
  const NSInteger animationKind = _activeAnimationKind;
498
991
  [self stopLayerAnimationWithoutCallback];
@@ -500,50 +993,133 @@ static std::array<double, 7> SmoothClipVelocityChannels(
500
993
  _activeAnimationKind = animationKind;
501
994
  // Unhide while any animated frame can show content; an empty-to-empty
502
995
  // transition must stay hidden for accessibility.
503
- if (!CGRectIsEmpty(toRect) || !CGRectIsEmpty(fromRect)) {
996
+ if (!CGRectIsEmpty(toGeometry.rect) ||
997
+ !CGRectIsEmpty(fromGeometry.rect)) {
504
998
  [self setClipContainerHidden:NO];
505
999
  }
1000
+ // Autonomous native motion has no per-frame accessibility layout updates.
1001
+ // Hide moving descendants until the endpoint is committed.
1002
+ _clipContainer.accessibilityElementsHidden = YES;
506
1003
 
507
1004
  CALayer *layer = _clipContainer.layer;
508
- const CGRect toBounds = toRect;
1005
+ const CGRect toBounds = toGeometry.rect;
509
1006
  const CGPoint fromPosition =
510
- CGPointMake(CGRectGetMidX(fromRect), CGRectGetMidY(fromRect));
1007
+ CGPointMake(
1008
+ CGRectGetMidX(fromGeometry.rect),
1009
+ CGRectGetMidY(fromGeometry.rect));
511
1010
  const CGPoint toPosition =
512
- CGPointMake(CGRectGetMidX(toRect), CGRectGetMidY(toRect));
513
- layer.bounds = toBounds;
514
- layer.position = toPosition;
515
- layer.cornerRadius = toRadius;
516
- _contentContainer.layer.affineTransform = CGAffineTransformMakeTranslation(
517
- toContentTranslation.x,
518
- toContentTranslation.y);
519
- _normalizedClip = toRect;
520
- _normalizedRadius = toRadius;
521
- _normalizedContentTranslation = toContentTranslation;
1011
+ CGPointMake(
1012
+ CGRectGetMidX(toGeometry.rect),
1013
+ CGRectGetMidY(toGeometry.rect));
1014
+ const BOOL usesMask =
1015
+ !SmoothClipCornerRadiiAreUniform(fromGeometry.radii) ||
1016
+ !SmoothClipCornerRadiiAreUniform(toGeometry.radii) ||
1017
+ fromGeometry.curve != toGeometry.curve;
1018
+ [self writeLayerGeometry:toGeometry
1019
+ hasExplicitRadii:hasExplicitToRadii];
1020
+ [self writeContentTranslation:toContentTranslation scale:toContentScale];
1021
+ _canonicalShadow = toShadow;
1022
+ SmoothBoxShadowNormalizeAnimationEndpoints(
1023
+ fromGeometry, &fromShadow, toGeometry, &toShadow);
1024
+ // The target model layer is complete before any presentation animations are
1025
+ // attached. Core Animation may remove the terminal presentation animation
1026
+ // before its delegate runs, so correctness cannot depend on completion-time
1027
+ // cleanup hiding a stale path or opaque color.
1028
+ [self writeShadowModel:toShadow geometry:toGeometry];
1029
+ if (usesMask) {
1030
+ // Keep one mask representation for the full interval, including an
1031
+ // unequal→uniform transition. Switching layer.mask mid-animation would
1032
+ // create a one-frame seam even though the underlying paths are compatible.
1033
+ [self configureUnequalCornerMaskForGeometry:toGeometry];
1034
+ layer.cornerRadius = 0;
1035
+ layer.mask = _unequalCornerMask;
1036
+ }
522
1037
 
1038
+ CGPathRef fromMaskPath = usesMask
1039
+ ? SmoothClipCreateRoundedRectPath(
1040
+ fromGeometry.rect, fromGeometry.radii, fromGeometry.curve)
1041
+ : nil;
1042
+ CGPathRef toMaskPath = usesMask
1043
+ ? SmoothClipCreateRoundedRectPath(
1044
+ toGeometry.rect, toGeometry.radii, toGeometry.curve)
1045
+ : nil;
1046
+ const BOOL fromShadowVisible =
1047
+ SmoothBoxShadowVisible(fromGeometry, fromShadow);
1048
+ const BOOL toShadowVisible =
1049
+ SmoothBoxShadowVisible(toGeometry, toShadow);
1050
+ const BOOL hasVisibleShadow = fromShadowVisible || toShadowVisible;
1051
+ const BOOL animatesShadowPath = hasVisibleShadow &&
1052
+ !SmoothBoxShadowPathInputEqual(
1053
+ fromGeometry, fromShadow, toGeometry, toShadow);
1054
+ const BOOL animatesShadowColor = hasVisibleShadow &&
1055
+ !SmoothBoxShadowColorEqual(fromShadow, toShadow);
1056
+ const BOOL animatesShadowRadius = hasVisibleShadow &&
1057
+ fromShadow.blurRadius != toShadow.blurRadius;
1058
+ const BOOL animatesShadowOffset = hasVisibleShadow &&
1059
+ (fromShadow.offsetX != toShadow.offsetX ||
1060
+ fromShadow.offsetY != toShadow.offsetY);
1061
+ const BOOL animatesShadow = animatesShadowPath || animatesShadowColor ||
1062
+ animatesShadowRadius || animatesShadowOffset;
1063
+ if (hasVisibleShadow) {
1064
+ [self ensureShadowLayer];
1065
+ // Reveal opacity is encoded in shadowColor alpha. Animating layer opacity
1066
+ // too would multiply both curves and produce a quadratic-looking fade.
1067
+ _shadowLayer.shadowOpacity = 1;
1068
+ }
1069
+ CGPathRef fromShadowPath = animatesShadowPath
1070
+ ? SmoothClipCreateShadowPath(fromGeometry, fromShadow)
1071
+ : nil;
1072
+ CGPathRef toShadowPath = animatesShadowPath
1073
+ ? SmoothClipCreateShadowPath(toGeometry, toShadow)
1074
+ : nil;
523
1075
  CAAnimationGroup *group = [CAAnimationGroup animation];
524
1076
  CAAnimationGroup *contentGroup = [CAAnimationGroup animation];
1077
+ CAAnimationGroup *shadowGroup = animatesShadow
1078
+ ? [CAAnimationGroup animation]
1079
+ : nil;
1080
+ CAAnimationGroup *maskAnimation = nil;
525
1081
  if (_activeAnimationKind == 1) {
526
1082
  CAMediaTimingFunction *timing = [CAMediaTimingFunction
527
1083
  functionWithControlPoints:_timingAnimation.controlPoint1X
528
1084
  :_timingAnimation.controlPoint1Y
529
1085
  :_timingAnimation.controlPoint2X
530
1086
  :_timingAnimation.controlPoint2Y];
531
- group.animations = @[
1087
+ NSMutableArray<CAAnimation *> *geometryAnimations = [NSMutableArray arrayWithArray:@[
532
1088
  [self basicAnimationForKeyPath:@"bounds"
533
- fromValue:[NSValue valueWithCGRect:fromRect]
1089
+ fromValue:[NSValue valueWithCGRect:fromGeometry.rect]
534
1090
  toValue:[NSValue valueWithCGRect:toBounds]
535
1091
  timingFunction:timing],
536
1092
  [self basicAnimationForKeyPath:@"position"
537
1093
  fromValue:[NSValue valueWithCGPoint:fromPosition]
538
1094
  toValue:[NSValue valueWithCGPoint:toPosition]
539
1095
  timingFunction:timing],
540
- [self basicAnimationForKeyPath:@"cornerRadius"
541
- fromValue:@(fromRadius)
542
- toValue:@(toRadius)
543
- timingFunction:timing],
544
- ];
1096
+ ]];
1097
+ if (usesMask) {
1098
+ maskAnimation = [CAAnimationGroup animation];
1099
+ maskAnimation.animations = @[
1100
+ [self basicAnimationForKeyPath:@"path"
1101
+ fromValue:(__bridge id)fromMaskPath
1102
+ toValue:(__bridge id)toMaskPath
1103
+ timingFunction:timing],
1104
+ [self basicAnimationForKeyPath:@"bounds"
1105
+ fromValue:[NSValue valueWithCGRect:fromGeometry.rect]
1106
+ toValue:[NSValue valueWithCGRect:toGeometry.rect]
1107
+ timingFunction:timing],
1108
+ [self basicAnimationForKeyPath:@"position"
1109
+ fromValue:[NSValue valueWithCGPoint:fromPosition]
1110
+ toValue:[NSValue valueWithCGPoint:toPosition]
1111
+ timingFunction:timing],
1112
+ ];
1113
+ } else {
1114
+ [geometryAnimations addObject:
1115
+ [self basicAnimationForKeyPath:@"cornerRadius"
1116
+ fromValue:@(fromGeometry.radii.topLeft)
1117
+ toValue:@(toGeometry.radii.topLeft)
1118
+ timingFunction:timing]];
1119
+ }
1120
+ group.animations = geometryAnimations;
545
1121
  group.duration = duration;
546
- contentGroup.animations = @[
1122
+ NSMutableArray<CAAnimation *> *contentAnimations = [NSMutableArray arrayWithArray:@[
547
1123
  [self basicAnimationForKeyPath:@"transform.translation.x"
548
1124
  fromValue:@(fromContentTranslation.x)
549
1125
  toValue:@(toContentTranslation.x)
@@ -552,8 +1128,43 @@ static std::array<double, 7> SmoothClipVelocityChannels(
552
1128
  fromValue:@(fromContentTranslation.y)
553
1129
  toValue:@(toContentTranslation.y)
554
1130
  timingFunction:timing],
555
- ];
1131
+ ]];
1132
+ if (fromContentScale != 1 || toContentScale != 1) {
1133
+ [contentAnimations addObject:
1134
+ [self basicAnimationForKeyPath:@"transform.scale"
1135
+ fromValue:@(fromContentScale)
1136
+ toValue:@(toContentScale)
1137
+ timingFunction:timing]];
1138
+ }
1139
+ contentGroup.animations = contentAnimations;
556
1140
  contentGroup.duration = duration;
1141
+ if (animatesShadow) {
1142
+ NSMutableArray<CAAnimation *> *shadowAnimations = [NSMutableArray array];
1143
+ if (animatesShadowPath) [shadowAnimations addObject:
1144
+ [self basicAnimationForKeyPath:@"shadowPath"
1145
+ fromValue:(__bridge id)fromShadowPath
1146
+ toValue:(__bridge id)toShadowPath
1147
+ timingFunction:timing]];
1148
+ if (animatesShadowColor) [shadowAnimations addObject:
1149
+ [self basicAnimationForKeyPath:@"shadowColor"
1150
+ fromValue:(__bridge id)[self colorForShadow:fromShadow]
1151
+ toValue:(__bridge id)[self colorForShadow:toShadow]
1152
+ timingFunction:timing]];
1153
+ if (animatesShadowRadius) [shadowAnimations addObject:
1154
+ [self basicAnimationForKeyPath:@"shadowRadius"
1155
+ fromValue:@(MAX(0, fromShadow.blurRadius) / 2.0)
1156
+ toValue:@(MAX(0, toShadow.blurRadius) / 2.0)
1157
+ timingFunction:timing]];
1158
+ if (animatesShadowOffset) [shadowAnimations addObject:
1159
+ [self basicAnimationForKeyPath:@"shadowOffset"
1160
+ fromValue:[NSValue valueWithCGSize:CGSizeMake(
1161
+ fromShadow.offsetX, fromShadow.offsetY)]
1162
+ toValue:[NSValue valueWithCGSize:CGSizeMake(
1163
+ toShadow.offsetX, toShadow.offsetY)]
1164
+ timingFunction:timing]];
1165
+ shadowGroup.animations = shadowAnimations;
1166
+ shadowGroup.duration = duration;
1167
+ }
557
1168
  } else {
558
1169
  // `_springAnimation.initialVelocity` is the interactive motion projected
559
1170
  // onto the current-to-target trajectory, normalized by that distance
@@ -565,24 +1176,24 @@ static std::array<double, 7> SmoothClipVelocityChannels(
565
1176
  const double velocity = _springAnimation.initialVelocity;
566
1177
  const CFTimeInterval springDuration =
567
1178
  [self springSettlingDurationWithVelocity:velocity];
568
- group.animations = @[
1179
+ NSMutableArray<CAAnimation *> *geometryAnimations = [NSMutableArray arrayWithArray:@[
569
1180
  [self springAnimationForKeyPath:@"bounds.origin.x"
570
- fromValue:@(fromRect.origin.x)
1181
+ fromValue:@(fromGeometry.rect.origin.x)
571
1182
  toValue:@(toBounds.origin.x)
572
1183
  velocity:velocity
573
1184
  duration:springDuration],
574
1185
  [self springAnimationForKeyPath:@"bounds.origin.y"
575
- fromValue:@(fromRect.origin.y)
1186
+ fromValue:@(fromGeometry.rect.origin.y)
576
1187
  toValue:@(toBounds.origin.y)
577
1188
  velocity:velocity
578
1189
  duration:springDuration],
579
1190
  [self springAnimationForKeyPath:@"bounds.size.width"
580
- fromValue:@(fromRect.size.width)
1191
+ fromValue:@(fromGeometry.rect.size.width)
581
1192
  toValue:@(toBounds.size.width)
582
1193
  velocity:velocity
583
1194
  duration:springDuration],
584
1195
  [self springAnimationForKeyPath:@"bounds.size.height"
585
- fromValue:@(fromRect.size.height)
1196
+ fromValue:@(fromGeometry.rect.size.height)
586
1197
  toValue:@(toBounds.size.height)
587
1198
  velocity:velocity
588
1199
  duration:springDuration],
@@ -596,17 +1207,59 @@ static std::array<double, 7> SmoothClipVelocityChannels(
596
1207
  toValue:@(toPosition.y)
597
1208
  velocity:velocity
598
1209
  duration:springDuration],
599
- [self springAnimationForKeyPath:@"cornerRadius"
600
- fromValue:@(fromRadius)
601
- toValue:@(toRadius)
602
- velocity:velocity
603
- duration:springDuration],
604
- ];
605
- // A rebuilt spring (layout change, foreground resume) must run its own
606
- // settling time; clamping to the interrupted run's remaining wall-clock
607
- // time would truncate the oscillation and snap to the target.
1210
+ ]];
1211
+ if (usesMask) {
1212
+ maskAnimation = [CAAnimationGroup animation];
1213
+ maskAnimation.animations = @[
1214
+ [self springAnimationForKeyPath:@"path"
1215
+ fromValue:(__bridge id)fromMaskPath
1216
+ toValue:(__bridge id)toMaskPath
1217
+ velocity:velocity
1218
+ duration:springDuration],
1219
+ [self springAnimationForKeyPath:@"bounds.origin.x"
1220
+ fromValue:@(fromGeometry.rect.origin.x)
1221
+ toValue:@(toGeometry.rect.origin.x)
1222
+ velocity:velocity
1223
+ duration:springDuration],
1224
+ [self springAnimationForKeyPath:@"bounds.origin.y"
1225
+ fromValue:@(fromGeometry.rect.origin.y)
1226
+ toValue:@(toGeometry.rect.origin.y)
1227
+ velocity:velocity
1228
+ duration:springDuration],
1229
+ [self springAnimationForKeyPath:@"bounds.size.width"
1230
+ fromValue:@(fromGeometry.rect.size.width)
1231
+ toValue:@(toGeometry.rect.size.width)
1232
+ velocity:velocity
1233
+ duration:springDuration],
1234
+ [self springAnimationForKeyPath:@"bounds.size.height"
1235
+ fromValue:@(fromGeometry.rect.size.height)
1236
+ toValue:@(toGeometry.rect.size.height)
1237
+ velocity:velocity
1238
+ duration:springDuration],
1239
+ [self springAnimationForKeyPath:@"position.x"
1240
+ fromValue:@(fromPosition.x)
1241
+ toValue:@(toPosition.x)
1242
+ velocity:velocity
1243
+ duration:springDuration],
1244
+ [self springAnimationForKeyPath:@"position.y"
1245
+ fromValue:@(fromPosition.y)
1246
+ toValue:@(toPosition.y)
1247
+ velocity:velocity
1248
+ duration:springDuration],
1249
+ ];
1250
+ } else {
1251
+ [geometryAnimations addObject:
1252
+ [self springAnimationForKeyPath:@"cornerRadius"
1253
+ fromValue:@(fromGeometry.radii.topLeft)
1254
+ toValue:@(toGeometry.radii.topLeft)
1255
+ velocity:velocity
1256
+ duration:springDuration]];
1257
+ }
1258
+ group.animations = geometryAnimations;
1259
+ // Springs use their analytic settling time; a wall-clock duration would
1260
+ // truncate oscillation and snap to the target.
608
1261
  group.duration = springDuration;
609
- contentGroup.animations = @[
1262
+ NSMutableArray<CAAnimation *> *contentAnimations = [NSMutableArray arrayWithArray:@[
610
1263
  [self springAnimationForKeyPath:@"transform.translation.x"
611
1264
  fromValue:@(fromContentTranslation.x)
612
1265
  toValue:@(toContentTranslation.x)
@@ -617,12 +1270,74 @@ static std::array<double, 7> SmoothClipVelocityChannels(
617
1270
  toValue:@(toContentTranslation.y)
618
1271
  velocity:velocity
619
1272
  duration:springDuration],
620
- ];
1273
+ ]];
1274
+ if (fromContentScale != 1 || toContentScale != 1) {
1275
+ [contentAnimations addObject:
1276
+ [self springAnimationForKeyPath:@"transform.scale"
1277
+ fromValue:@(fromContentScale)
1278
+ toValue:@(toContentScale)
1279
+ velocity:velocity
1280
+ duration:springDuration]];
1281
+ }
1282
+ contentGroup.animations = contentAnimations;
621
1283
  contentGroup.duration = springDuration;
1284
+ if (animatesShadow) {
1285
+ NSMutableArray<CAAnimation *> *shadowAnimations = [NSMutableArray array];
1286
+ if (animatesShadowPath) [shadowAnimations addObject:
1287
+ [self springAnimationForKeyPath:@"shadowPath"
1288
+ fromValue:(__bridge id)fromShadowPath
1289
+ toValue:(__bridge id)toShadowPath
1290
+ velocity:velocity
1291
+ duration:springDuration]];
1292
+ if (animatesShadowColor) [shadowAnimations addObject:
1293
+ [self springAnimationForKeyPath:@"shadowColor"
1294
+ fromValue:(__bridge id)[self colorForShadow:fromShadow]
1295
+ toValue:(__bridge id)[self colorForShadow:toShadow]
1296
+ velocity:velocity
1297
+ duration:springDuration]];
1298
+ if (animatesShadowRadius) [shadowAnimations addObject:
1299
+ [self springAnimationForKeyPath:@"shadowRadius"
1300
+ fromValue:@(MAX(0, fromShadow.blurRadius) / 2.0)
1301
+ toValue:@(MAX(0, toShadow.blurRadius) / 2.0)
1302
+ velocity:velocity
1303
+ duration:springDuration]];
1304
+ if (animatesShadowOffset) [shadowAnimations addObject:
1305
+ [self springAnimationForKeyPath:@"shadowOffset"
1306
+ fromValue:[NSValue valueWithCGSize:CGSizeMake(
1307
+ fromShadow.offsetX, fromShadow.offsetY)]
1308
+ toValue:[NSValue valueWithCGSize:CGSizeMake(
1309
+ toShadow.offsetX, toShadow.offsetY)]
1310
+ velocity:velocity
1311
+ duration:springDuration]];
1312
+ shadowGroup.animations = shadowAnimations;
1313
+ shadowGroup.duration = springDuration;
1314
+ }
1315
+ }
1316
+
1317
+ if (maskAnimation != nil) {
1318
+ maskAnimation.duration = group.duration;
1319
+ }
1320
+
1321
+ if (sharedBeginTime > 0) {
1322
+ // Each layer receives the same absolute media timestamp translated into
1323
+ // its local clock. Group participants therefore advance from one epoch
1324
+ // even though their animations are installed sequentially.
1325
+ group.beginTime = [layer convertTime:sharedBeginTime fromLayer:nil];
1326
+ contentGroup.beginTime = [_contentContainer.layer
1327
+ convertTime:sharedBeginTime
1328
+ fromLayer:nil];
1329
+ if (animatesShadow) {
1330
+ shadowGroup.beginTime = [_shadowLayer
1331
+ convertTime:sharedBeginTime
1332
+ fromLayer:nil];
1333
+ }
1334
+ if (maskAnimation != nil) {
1335
+ maskAnimation.beginTime = [_unequalCornerMask
1336
+ convertTime:sharedBeginTime
1337
+ fromLayer:nil];
1338
+ }
622
1339
  }
623
1340
 
624
- _animationDuration = group.duration;
625
- _animationStartedAt = CACurrentMediaTime();
626
1341
  _animationDelegate = [SmoothClipAnimationDelegate new];
627
1342
  _animationDelegate.view = self;
628
1343
  _animationDelegate.driverId = _driverId;
@@ -631,189 +1346,122 @@ static std::array<double, 7> SmoothClipVelocityChannels(
631
1346
  [layer addAnimation:group forKey:@"smoothClip.geometry"];
632
1347
  [_contentContainer.layer addAnimation:contentGroup
633
1348
  forKey:@"smoothClip.content"];
634
- _animationInstallGeneration += 1;
1349
+ if (animatesShadow) {
1350
+ [_shadowLayer addAnimation:shadowGroup forKey:@"smoothClip.shadow"];
1351
+ }
1352
+ if (maskAnimation != nil) {
1353
+ [_unequalCornerMask addAnimation:maskAnimation forKey:@"smoothClip.mask"];
1354
+ }
1355
+ if (fromMaskPath != nil) CGPathRelease(fromMaskPath);
1356
+ if (toMaskPath != nil) CGPathRelease(toMaskPath);
1357
+ if (fromShadowPath != nil) CGPathRelease(fromShadowPath);
1358
+ if (toShadowPath != nil) CGPathRelease(toShadowPath);
635
1359
  }
636
1360
 
637
- - (BOOL)startAnimationToRequestedGeometryWithDuration:(CFTimeInterval)duration {
1361
+ - (BOOL)startAnimationToRequestedGeometryWithDuration:(CFTimeInterval)duration
1362
+ sharedBeginTime:
1363
+ (CFTimeInterval)sharedBeginTime {
638
1364
  if (![self smoothClipCanDisplay]) {
639
- _pendingAnimationInstall = YES;
640
1365
  return NO;
641
1366
  }
642
- SmoothNormalizedClipGeometry target;
643
- if (![self normalizedRequestedGeometry:&target]) return NO;
1367
+ SmoothClipCanonicalGeometry target;
1368
+ if (![self canonicalRequestedGeometry:&target]) return NO;
644
1369
  const smoothclip::Presentation current = [self smoothClipCurrentPresentation];
645
- [self installAnimationFromRect:CGRectMake(
646
- current.clip.x,
647
- current.clip.y,
648
- current.clip.width,
649
- current.clip.height)
650
- fromRadius:current.clip.radius
651
- fromContentTranslation:CGPointMake(
652
- current.contentTranslateX,
653
- current.contentTranslateY)
654
- toRect:target.rect
655
- toRadius:target.radius
656
- toContentTranslation:_requestedContentTranslation
657
- duration:duration];
1370
+ const SmoothClipCornerRadii currentRadii =
1371
+ SmoothClipPresentationRadii(current.clip);
1372
+ const SmoothClipCanonicalGeometry fromGeometry = {
1373
+ .rect = CGRectMake(
1374
+ current.clip.x,
1375
+ current.clip.y,
1376
+ current.clip.width,
1377
+ current.clip.height),
1378
+ .radius = SmoothClipCornerRadiiAreUniform(currentRadii)
1379
+ ? currentRadii.topLeft
1380
+ : 0,
1381
+ .radii = currentRadii,
1382
+ .curve = SmoothClipPresentationCurve(current.clip),
1383
+ };
1384
+ [self installAnimationFromGeometry:fromGeometry
1385
+ fromContentTranslation:CGPointMake(
1386
+ current.contentTranslateX,
1387
+ current.contentTranslateY)
1388
+ fromContentScale:current.contentScale
1389
+ fromShadow:current.shadow
1390
+ toGeometry:target
1391
+ hasExplicitToRadii:_requestedHasExplicitRadii
1392
+ toContentTranslation:_requestedContentTranslation
1393
+ toContentScale:_requestedContentScale
1394
+ toShadow:_requestedShadow
1395
+ duration:duration
1396
+ sharedBeginTime:sharedBeginTime];
658
1397
  return YES;
659
1398
  }
660
1399
 
661
1400
  - (BOOL)smoothClipAnimateTiming:(smoothclip::Presentation)presentation
662
1401
  animation:(smoothclip::TimingAnimation)animation
663
1402
  animationId:(int32_t)animationId {
1403
+ return [self smoothClipAnimateTiming:presentation
1404
+ animation:animation
1405
+ animationId:animationId
1406
+ sharedBeginTime:0];
1407
+ }
1408
+
1409
+ - (BOOL)smoothClipAnimateTiming:(smoothclip::Presentation)presentation
1410
+ animation:(smoothclip::TimingAnimation)animation
1411
+ animationId:(int32_t)animationId
1412
+ sharedBeginTime:(CFTimeInterval)sharedBeginTime {
664
1413
  [self storeRequestedPresentation:presentation];
665
1414
  _timingAnimation = animation;
666
1415
  _activeAnimationKind = 1;
667
1416
  _activeAnimationId = animationId;
668
1417
  return [self startAnimationToRequestedGeometryWithDuration:
669
- MAX(0, animation.durationMs) / 1000.0];
1418
+ MAX(0, animation.durationMs) / 1000.0
1419
+ sharedBeginTime:sharedBeginTime];
670
1420
  }
671
1421
 
672
1422
  - (BOOL)smoothClipAnimateSpring:(smoothclip::Presentation)presentation
673
1423
  animation:(smoothclip::SpringAnimation)animation
674
1424
  animationId:(int32_t)animationId {
1425
+ return [self smoothClipAnimateSpring:presentation
1426
+ animation:animation
1427
+ animationId:animationId
1428
+ sharedBeginTime:0];
1429
+ }
1430
+
1431
+ - (BOOL)smoothClipAnimateSpring:(smoothclip::Presentation)presentation
1432
+ animation:(smoothclip::SpringAnimation)animation
1433
+ animationId:(int32_t)animationId
1434
+ sharedBeginTime:(CFTimeInterval)sharedBeginTime {
675
1435
  [self storeRequestedPresentation:presentation];
676
1436
  _springAnimation = animation;
677
1437
  if (animation.inheritVelocity) {
678
- SmoothNormalizedClipGeometry target;
679
- if ([self normalizedRequestedGeometry:&target]) {
1438
+ SmoothClipCanonicalGeometry target;
1439
+ if ([self canonicalRequestedGeometry:&target]) {
680
1440
  _springAnimation.initialVelocity =
681
1441
  [self inheritedVelocityToRect:target.rect
682
- radius:target.radius
683
- contentTranslation:_requestedContentTranslation];
1442
+ radii:target.radii
1443
+ contentTranslation:_requestedContentTranslation
1444
+ contentScale:_requestedContentScale];
684
1445
  } else {
685
1446
  _springAnimation.initialVelocity = 0;
686
1447
  }
687
1448
  }
688
1449
  _activeAnimationKind = 2;
689
1450
  _activeAnimationId = animationId;
690
- return [self startAnimationToRequestedGeometryWithDuration:0];
691
- }
692
-
693
- - (CAKeyframeAnimation *)keyframeAnimationForKeyPath:(NSString *)keyPath
694
- values:(NSArray *)values
695
- keyTimes:(NSArray<NSNumber *> *)keyTimes {
696
- CAKeyframeAnimation *animation =
697
- [CAKeyframeAnimation animationWithKeyPath:keyPath];
698
- animation.values = values;
699
- animation.keyTimes = keyTimes;
700
- animation.calculationMode = kCAAnimationLinear;
701
- return animation;
702
- }
703
-
704
- - (BOOL)smoothClipAnimateKeyframes:(smoothclip::Presentation)presentation
705
- keyframes:(const std::vector<smoothclip::Keyframe> &)keyframes
706
- durationMs:(double)durationMs
707
- animationId:(int32_t)animationId {
708
- if (keyframes.size() < 2) return NO;
709
- [self storeRequestedPresentation:presentation];
710
- _activeAnimationKind = 3;
711
- _activeAnimationId = animationId;
712
- if (![self smoothClipCanDisplay]) {
713
- _pendingAnimationInstall = YES;
714
- return NO;
715
- }
716
- [self stopLayerAnimationWithoutCallback];
717
- _activeAnimationKind = 3;
718
- _activeAnimationId = animationId;
719
-
720
- const NSUInteger frameCount = keyframes.size();
721
- const CGSize hostSize = self.bounds.size;
722
- NSMutableArray<NSNumber *> *times =
723
- [NSMutableArray arrayWithCapacity:frameCount];
724
- NSMutableArray *boundsValues = [NSMutableArray arrayWithCapacity:frameCount];
725
- NSMutableArray *positionValues =
726
- [NSMutableArray arrayWithCapacity:frameCount];
727
- NSMutableArray *radiusValues = [NSMutableArray arrayWithCapacity:frameCount];
728
- NSMutableArray *translateXValues =
729
- [NSMutableArray arrayWithCapacity:frameCount];
730
- NSMutableArray *translateYValues =
731
- [NSMutableArray arrayWithCapacity:frameCount];
732
- for (const smoothclip::Keyframe &frame : keyframes) {
733
- SmoothNormalizedClipGeometry normalized;
734
- if (!SmoothClipNormalizeGeometry(
735
- frame.presentation.clip.x,
736
- frame.presentation.clip.y,
737
- frame.presentation.clip.width,
738
- frame.presentation.clip.height,
739
- frame.presentation.clip.radius,
740
- hostSize,
741
- &normalized)) return NO;
742
- [times addObject:@(frame.offset)];
743
- [boundsValues addObject:[NSValue valueWithCGRect:normalized.rect]];
744
- [positionValues addObject:[NSValue valueWithCGPoint:CGPointMake(
745
- CGRectGetMidX(normalized.rect), CGRectGetMidY(normalized.rect))]];
746
- [radiusValues addObject:@(normalized.radius)];
747
- [translateXValues addObject:@(frame.presentation.contentTranslateX)];
748
- [translateYValues addObject:@(frame.presentation.contentTranslateY)];
749
- }
750
-
751
- SmoothNormalizedClipGeometry target;
752
- if (![self normalizedRequestedGeometry:&target]) return NO;
753
- [self writeLayerRect:target.rect radius:target.radius];
754
- [self writeContentTranslation:_requestedContentTranslation];
755
- [self setClipContainerHidden:NO];
756
-
757
- CAAnimationGroup *group = [CAAnimationGroup animation];
758
- group.animations = @[
759
- [self keyframeAnimationForKeyPath:@"bounds"
760
- values:boundsValues
761
- keyTimes:times],
762
- [self keyframeAnimationForKeyPath:@"position"
763
- values:positionValues
764
- keyTimes:times],
765
- [self keyframeAnimationForKeyPath:@"cornerRadius"
766
- values:radiusValues
767
- keyTimes:times],
768
- ];
769
- group.duration = MAX(0, durationMs) / 1000.0;
770
- CAAnimationGroup *contentGroup = [CAAnimationGroup animation];
771
- contentGroup.animations = @[
772
- [self keyframeAnimationForKeyPath:@"transform.translation.x"
773
- values:translateXValues
774
- keyTimes:times],
775
- [self keyframeAnimationForKeyPath:@"transform.translation.y"
776
- values:translateYValues
777
- keyTimes:times],
778
- ];
779
- contentGroup.duration = group.duration;
780
-
781
- _animationDuration = group.duration;
782
- _animationStartedAt = CACurrentMediaTime();
783
- _animationDelegate = [SmoothClipAnimationDelegate new];
784
- _animationDelegate.view = self;
785
- _animationDelegate.driverId = _driverId;
786
- _animationDelegate.animationId = _activeAnimationId;
787
- group.delegate = _animationDelegate;
788
- [_clipContainer.layer addAnimation:group forKey:@"smoothClip.geometry"];
789
- [_contentContainer.layer addAnimation:contentGroup
790
- forKey:@"smoothClip.content"];
791
- _animationInstallGeneration += 1;
792
- return YES;
1451
+ return [self startAnimationToRequestedGeometryWithDuration:0
1452
+ sharedBeginTime:sharedBeginTime];
793
1453
  }
794
1454
 
795
1455
  - (void)smoothClipCancelAnimationUsingTarget:(BOOL)useTarget {
796
1456
  if (_activeAnimationId == 0) return;
797
1457
  const smoothclip::Presentation visible =
798
1458
  [self smoothClipCurrentPresentation];
799
- const CGRect visibleRect = CGRectMake(
800
- visible.clip.x,
801
- visible.clip.y,
802
- visible.clip.width,
803
- visible.clip.height);
804
- const CGPoint visibleContentTranslation = CGPointMake(
805
- visible.contentTranslateX,
806
- visible.contentTranslateY);
807
1459
  [self stopLayerAnimationWithoutCallback];
808
1460
  if (useTarget) {
809
1461
  [self applyRequestedClip];
810
1462
  } else {
811
- _requestedClip = visibleRect;
812
- _requestedRadius = visible.clip.radius;
813
- _requestedContentTranslation = visibleContentTranslation;
814
- [self writeLayerRect:visibleRect radius:visible.clip.radius];
815
- [self writeContentTranslation:visibleContentTranslation];
816
- [self syncVisibilityForRect:visibleRect];
1463
+ [self storeRequestedPresentation:visible];
1464
+ [self applyRequestedClip];
817
1465
  }
818
1466
  }
819
1467
 
@@ -824,7 +1472,12 @@ static std::array<double, 7> SmoothClipVelocityChannels(
824
1472
  _activeAnimationId = 0;
825
1473
  _activeAnimationKind = 0;
826
1474
  _animationDelegate = nil;
827
- [self syncVisibilityForRect:_normalizedClip];
1475
+ [_unequalCornerMask removeAnimationForKey:@"smoothClip.mask"];
1476
+ [_shadowLayer removeAnimationForKey:@"smoothClip.shadow"];
1477
+ [self applyStaticCornerRepresentation:[self canonicalGeometryValue]];
1478
+ [self writeShadow:_requestedShadow
1479
+ geometry:[self canonicalGeometryValue]];
1480
+ [self syncVisibilityForRect:_canonicalClip];
828
1481
  smoothclip::viewAnimationDidStop(
829
1482
  driverId, animationId, self, finished);
830
1483
  }
@@ -834,19 +1487,40 @@ static std::array<double, 7> SmoothClipVelocityChannels(
834
1487
  const auto &newProps =
835
1488
  *std::static_pointer_cast<const SmoothClipViewProps>(props);
836
1489
  [super updateProps:props oldProps:oldProps];
1490
+ self.clipsToBounds = YES;
837
1491
 
838
1492
  const uint64_t nextDriverId =
839
1493
  isfinite(newProps.driverId) && newProps.driverId > 0
840
1494
  ? static_cast<uint64_t>(newProps.driverId)
841
1495
  : 0;
842
- const smoothclip::Presentation initial{
843
- {newProps.initialClipX,
844
- newProps.initialClipY,
845
- newProps.initialClipWidth,
846
- newProps.initialClipHeight,
847
- newProps.initialClipRadius},
848
- newProps.initialContentTranslateX,
849
- newProps.initialContentTranslateY};
1496
+ smoothclip::Presentation initial{};
1497
+ if (!SmoothClipBuildPresentation(
1498
+ newProps.initialClipX,
1499
+ newProps.initialClipY,
1500
+ newProps.initialClipWidth,
1501
+ newProps.initialClipHeight,
1502
+ newProps.initialClipTopLeftRadius,
1503
+ newProps.initialClipTopRightRadius,
1504
+ newProps.initialClipBottomRightRadius,
1505
+ newProps.initialClipBottomLeftRadius,
1506
+ newProps.initialClipCurve,
1507
+ newProps.initialContentTranslateX,
1508
+ newProps.initialContentTranslateY,
1509
+ newProps.initialContentScale,
1510
+ newProps.initialClipBoxShadowEnabled,
1511
+ newProps.initialClipBoxShadowRed,
1512
+ newProps.initialClipBoxShadowGreen,
1513
+ newProps.initialClipBoxShadowBlue,
1514
+ newProps.initialClipBoxShadowAlpha,
1515
+ newProps.initialClipBoxShadowOffsetX,
1516
+ newProps.initialClipBoxShadowOffsetY,
1517
+ newProps.initialClipBoxShadowBlurRadius,
1518
+ newProps.initialClipBoxShadowSpreadDistance,
1519
+ &initial)) {
1520
+ // The initial presentation is atomic. Never apply a valid subset when one
1521
+ // field rejects.
1522
+ return;
1523
+ }
850
1524
 
851
1525
  if (nextDriverId != _driverId) {
852
1526
  if (_driverId != 0) {
@@ -872,160 +1546,95 @@ static std::array<double, 7> SmoothClipVelocityChannels(
872
1546
 
873
1547
  - (void)updateLayoutMetrics:(const LayoutMetrics &)layoutMetrics
874
1548
  oldLayoutMetrics:(const LayoutMetrics &)oldLayoutMetrics {
875
- CGFloat visibleRadius = 0;
876
- const BOOL wasAnimating = _activeAnimationId != 0;
877
- const BOOL wasPending = _pendingAnimationInstall;
878
- const uint32_t installGenerationAtEntry = _animationInstallGeneration;
879
- const smoothclip::Presentation visible = wasAnimating && !wasPending
880
- ? [self smoothClipCurrentPresentation]
881
- : smoothclip::Presentation{{0, 0, 0, 0, 0}, 0, 0};
882
- const CGRect visibleRect = CGRectMake(
883
- visible.clip.x,
884
- visible.clip.y,
885
- visible.clip.width,
886
- visible.clip.height);
887
- visibleRadius = visible.clip.radius;
888
- const CGPoint visibleContentTranslation = CGPointMake(
889
- visible.contentTranslateX,
890
- visible.contentTranslateY);
891
- CFTimeInterval remaining = wasAnimating
892
- ? MAX(0, _animationDuration -
893
- (CACurrentMediaTime() - _animationStartedAt))
894
- : 0;
895
- const double springContinuationVelocity =
896
- wasAnimating && !wasPending && _activeAnimationKind == 2
897
- ? [self smoothClipSpringContinuationVelocity]
898
- : 0;
899
-
900
1549
  [super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:oldLayoutMetrics];
901
1550
  _hasLayout = YES;
902
1551
  [self layoutContentContainer];
903
1552
  if (_driverId != 0) {
904
- // Both edges matter: zero size/detach/background suspend an installed
905
- // participant; the first positive visible layout resumes the held latch.
906
1553
  smoothclip::viewDisplayabilityChanged(_driverId, self);
907
1554
  }
908
- if (wasPending) {
909
- // The registry either kept the install deferred or installed the rebased
910
- // latch. Relayout must not independently restart a background-held id.
911
- if (_activeAnimationId == 0) [self applyRequestedClip];
912
- return;
913
- }
914
- if (wasAnimating) {
915
- // A negative transition may have frozen this host and re-latched the
916
- // registry above. Its non-recording freeze is already the correct model
917
- // state for the new lifecycle; do not resurrect a local timer.
918
- if (_activeAnimationId == 0 || ![self smoothClipCanDisplay]) return;
919
- // The lifecycle notification above may itself have installed this
920
- // animation from the registry's trimmed remainder (a latch resume inside
921
- // this very layout pass). That install rebased the clocks; the `remaining`
922
- // captured on entry predates the suspension and would rebuild a shorter —
923
- // possibly zero-duration — run on top of the fresh one. The id alone
924
- // cannot detect this (a resume keeps the animation id), so compare the
925
- // install generation.
926
- if (_animationInstallGeneration != installGenerationAtEntry) return;
927
- const int32_t animationId = _activeAnimationId;
928
- const NSInteger animationKind = _activeAnimationKind;
929
- [self stopLayerAnimationWithoutCallback];
930
- _activeAnimationId = animationId;
931
- _activeAnimationKind = animationKind;
932
- if (_activeAnimationKind == 1 && _animationDuration > 0) {
933
- const double progress = smoothclip::clamp01(
934
- 1 - remaining / _animationDuration);
935
- _timingAnimation =
936
- smoothclip::timingRemainder(_timingAnimation, progress).animation;
937
- remaining = _timingAnimation.durationMs / 1000.0;
938
- } else if (_activeAnimationKind == 2) {
939
- _springAnimation.initialVelocity = springContinuationVelocity;
940
- _springAnimation.inheritVelocity = false;
941
- } else if (_activeAnimationKind == 3) {
942
- _activeAnimationKind = 1;
943
- _timingAnimation = {remaining * 1000.0, 0, 0, 1, 1, 2};
944
- }
945
- SmoothNormalizedClipGeometry current;
946
- SmoothClipNormalizeGeometry(
947
- CGRectGetMinX(visibleRect),
948
- CGRectGetMinY(visibleRect),
949
- CGRectGetWidth(visibleRect),
950
- CGRectGetHeight(visibleRect),
951
- visibleRadius,
952
- self.bounds.size,
953
- &current);
954
- SmoothNormalizedClipGeometry target;
955
- if ([self normalizedRequestedGeometry:&target]) {
956
- [self installAnimationFromRect:current.rect
957
- fromRadius:current.radius
958
- fromContentTranslation:visibleContentTranslation
959
- toRect:target.rect
960
- toRadius:target.radius
961
- toContentTranslation:_requestedContentTranslation
962
- duration:remaining];
963
- }
964
- } else {
965
- if (_driverId != 0 && smoothclip::hasActiveAnimation(_driverId)) {
966
- // Registry-level lifecycle pause owns the frozen model. A layout pass
967
- // while its latch is held must not apply the requested target or start a
968
- // per-view timer; foreground/reattach will normalize and resume it once.
969
- return;
970
- }
1555
+ if (_activeAnimationId == 0 &&
1556
+ (_driverId == 0 || !smoothclip::hasActiveAnimation(_driverId))) {
971
1557
  [self applyRequestedClip];
972
1558
  }
973
1559
  }
974
1560
 
975
1561
  - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
976
1562
  if (![super pointInside:point withEvent:event]) return NO;
977
- CGFloat radius = 0;
1563
+ SmoothClipCornerRadii radii = _canonicalRadii;
978
1564
  const CGRect clip = _activeAnimationId != 0
979
- ? [self presentationRectWithRadius:&radius]
980
- : _normalizedClip;
981
- if (_activeAnimationId == 0) radius = _normalizedRadius;
1565
+ ? [self presentationRectWithRadii:&radii]
1566
+ : _canonicalClip;
982
1567
  if (CGRectIsEmpty(clip) || !CGRectContainsPoint(clip, point)) return NO;
983
- if (radius <= 0) return YES;
984
-
985
- const CGFloat innerLeft = CGRectGetMinX(clip) + radius;
986
- const CGFloat innerRight = CGRectGetMaxX(clip) - radius;
987
- const CGFloat innerTop = CGRectGetMinY(clip) + radius;
988
- const CGFloat innerBottom = CGRectGetMaxY(clip) - radius;
989
- if ((point.x >= innerLeft && point.x <= innerRight) ||
990
- (point.y >= innerTop && point.y <= innerBottom)) {
991
- return YES;
1568
+ CGPathRef path = nil;
1569
+ if (_clipContainer.layer.mask == _unequalCornerMask) {
1570
+ CAShapeLayer *mask = _activeAnimationId != 0
1571
+ ? (CAShapeLayer *)_unequalCornerMask.presentationLayer
1572
+ : _unequalCornerMask;
1573
+ if (mask == nil) mask = _unequalCornerMask;
1574
+ path = mask.path;
1575
+ return path != nil && CGPathContainsPoint(path, NULL, point, NO);
992
1576
  }
993
-
994
- const CGFloat centerX = point.x < innerLeft ? innerLeft : innerRight;
995
- const CGFloat centerY = point.y < innerTop ? innerTop : innerBottom;
996
- const CGFloat dx = point.x - centerX;
997
- const CGFloat dy = point.y - centerY;
998
- return dx * dx + dy * dy <= radius * radius;
1577
+ path = SmoothClipCreateRoundedRectPath(clip, radii, _canonicalCurve);
1578
+ const BOOL contains = CGPathContainsPoint(path, NULL, point, NO);
1579
+ CGPathRelease(path);
1580
+ return contains;
999
1581
  }
1000
1582
 
1001
- #pragma mark - Legacy native command (Android parity and benchmark baseline)
1583
+ #pragma mark - Native commands
1002
1584
 
1003
1585
  - (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args {
1004
1586
  RCTSmoothClipViewHandleCommand(self, commandName, args);
1005
1587
  }
1006
1588
 
1007
- - (void)setClipGeometry:(double)x
1008
- y:(double)y
1009
- width:(double)width
1010
- height:(double)height
1011
- radius:(double)radius {
1012
- _commandIsAuthoritative = YES;
1013
- [self smoothClipApplyPresentation:
1014
- {{x, y, width, height, radius}, 0, 0}];
1015
- }
1016
-
1017
1589
  - (void)setClipPresentation:(double)x
1018
- y:(double)y
1019
- width:(double)width
1020
- height:(double)height
1021
- radius:(double)radius
1022
- contentTranslateX:(double)contentTranslateX
1023
- contentTranslateY:(double)contentTranslateY {
1590
+ y:(double)y
1591
+ width:(double)width
1592
+ height:(double)height
1593
+ topLeftRadius:(double)topLeftRadius
1594
+ topRightRadius:(double)topRightRadius
1595
+ bottomRightRadius:(double)bottomRightRadius
1596
+ bottomLeftRadius:(double)bottomLeftRadius
1597
+ curveCode:(NSInteger)curveCode
1598
+ contentTranslateX:(double)contentTranslateX
1599
+ contentTranslateY:(double)contentTranslateY
1600
+ contentScale:(double)contentScale
1601
+ shadowEnabled:(BOOL)shadowEnabled
1602
+ shadowRed:(double)shadowRed
1603
+ shadowGreen:(double)shadowGreen
1604
+ shadowBlue:(double)shadowBlue
1605
+ shadowAlpha:(double)shadowAlpha
1606
+ shadowOffsetX:(double)shadowOffsetX
1607
+ shadowOffsetY:(double)shadowOffsetY
1608
+ shadowBlurRadius:(double)shadowBlurRadius
1609
+ shadowSpreadDistance:(double)shadowSpreadDistance {
1610
+ smoothclip::Presentation presentation{};
1611
+ if (!SmoothClipBuildPresentation(
1612
+ x,
1613
+ y,
1614
+ width,
1615
+ height,
1616
+ topLeftRadius,
1617
+ topRightRadius,
1618
+ bottomRightRadius,
1619
+ bottomLeftRadius,
1620
+ curveCode,
1621
+ contentTranslateX,
1622
+ contentTranslateY,
1623
+ contentScale,
1624
+ shadowEnabled,
1625
+ shadowRed,
1626
+ shadowGreen,
1627
+ shadowBlue,
1628
+ shadowAlpha,
1629
+ shadowOffsetX,
1630
+ shadowOffsetY,
1631
+ shadowBlurRadius,
1632
+ shadowSpreadDistance,
1633
+ &presentation)) {
1634
+ return;
1635
+ }
1024
1636
  _commandIsAuthoritative = YES;
1025
- [self smoothClipApplyPresentation:
1026
- {{x, y, width, height, radius},
1027
- contentTranslateX,
1028
- contentTranslateY}];
1637
+ [self smoothClipApplyPresentation:presentation];
1029
1638
  }
1030
1639
 
1031
1640
  - (void)prepareForRecycle {
@@ -1034,15 +1643,24 @@ static std::array<double, 7> SmoothClipVelocityChannels(
1034
1643
  _driverId = 0;
1035
1644
  }
1036
1645
  [self stopLayerAnimationWithoutCallback];
1037
- _pendingAnimationInstall = NO;
1038
1646
  smoothclip::clearVelocitySamples(_velocitySamples);
1039
1647
  [super prepareForRecycle];
1040
1648
  _requestedClip = CGRectZero;
1041
- _normalizedClip = CGRectZero;
1649
+ _canonicalClip = CGRectZero;
1042
1650
  _requestedRadius = 0;
1043
- _normalizedRadius = 0;
1651
+ _canonicalRadius = 0;
1652
+ _requestedRadii = {0, 0, 0, 0};
1653
+ _canonicalRadii = {0, 0, 0, 0};
1654
+ _requestedCurve = SmoothClipCornerCurveCircular;
1655
+ _canonicalCurve = SmoothClipCornerCurveCircular;
1656
+ _requestedHasExplicitRadii = NO;
1657
+ _canonicalHasExplicitRadii = NO;
1044
1658
  _requestedContentTranslation = CGPointZero;
1045
- _normalizedContentTranslation = CGPointZero;
1659
+ _canonicalContentTranslation = CGPointZero;
1660
+ _requestedContentScale = 1;
1661
+ _canonicalContentScale = 1;
1662
+ _requestedShadow = {};
1663
+ _canonicalShadow = {};
1046
1664
  _hasLayout = NO;
1047
1665
  _commandIsAuthoritative = NO;
1048
1666
 
@@ -1050,7 +1668,14 @@ static std::array<double, 7> SmoothClipVelocityChannels(
1050
1668
  layer.bounds = CGRectZero;
1051
1669
  layer.position = CGPointZero;
1052
1670
  layer.cornerRadius = 0;
1671
+ layer.cornerCurve = kCACornerCurveCircular;
1672
+ layer.mask = nil;
1673
+ _unequalCornerMask.path = nil;
1053
1674
  _contentContainer.layer.affineTransform = CGAffineTransformIdentity;
1675
+ _shadowLayer.shadowPath = nil;
1676
+ _shadowLayer.shadowOpacity = 0;
1677
+ _shadowLayer.shadowRadius = 0;
1678
+ _shadowLayer.shadowOffset = CGSizeZero;
1054
1679
  [self setClipContainerHidden:YES];
1055
1680
  }
1056
1681