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
package/src/geometry.ts CHANGED
@@ -1,42 +1,186 @@
1
+ /* eslint-disable no-bitwise -- React Native colors are converted to RRGGBBAA */
2
+ import type { ColorValue } from 'react-native';
3
+ import { processColor } from 'react-native-reanimated';
4
+
5
+ export type ClipCurve = 'circular' | 'continuous';
6
+
7
+ /** One numeric outset shadow following React Native's `boxShadow` field names. */
8
+ export type ClipBoxShadow = Readonly<{
9
+ color?: ColorValue;
10
+ offsetX: number;
11
+ offsetY: number;
12
+ /** CSS box-shadow blur radius. iOS converts this to CALayer radius / 2. */
13
+ blurRadius?: number;
14
+ spreadDistance?: number;
15
+ }>;
16
+
17
+ export type CanonicalClipBoxShadow = Readonly<{
18
+ /** React Native numeric RRGGBBAA color; canonicalization is idempotent. */
19
+ color: ColorValue;
20
+ offsetX: number;
21
+ offsetY: number;
22
+ blurRadius: number;
23
+ spreadDistance: number;
24
+ }>;
25
+
1
26
  export type ClipGeometry = Readonly<{
2
27
  x: number;
3
28
  y: number;
4
29
  width: number;
5
30
  height: number;
31
+ /** Default for every corner without an explicit override. */
6
32
  radius: number;
33
+ topLeftRadius?: number;
34
+ topRightRadius?: number;
35
+ bottomRightRadius?: number;
36
+ bottomLeftRadius?: number;
37
+ curve?: ClipCurve;
7
38
  }>;
8
39
 
9
- export type ClipBounds = Readonly<{
40
+ export type CanonicalClipGeometry = Readonly<{
41
+ x: number;
42
+ y: number;
10
43
  width: number;
11
44
  height: number;
45
+ /** Canonical all-corners value; zero when the corner radii differ. */
46
+ radius: number;
47
+ topLeftRadius: number;
48
+ topRightRadius: number;
49
+ bottomRightRadius: number;
50
+ bottomLeftRadius: number;
51
+ curve: ClipCurve;
12
52
  }>;
13
53
 
14
54
  export type SmoothClipPresentation = Readonly<{
15
55
  clip: ClipGeometry;
16
56
  contentTranslateX: number;
17
57
  contentTranslateY: number;
58
+ /** Defaults to 1 when omitted. */
59
+ contentScale?: number;
60
+ boxShadow?: ClipBoxShadow;
18
61
  }>;
19
62
 
20
- export function createClipPresentation(
21
- clip: ClipGeometry,
22
- contentTranslateX = 0,
23
- contentTranslateY = 0
24
- ): SmoothClipPresentation {
63
+ export type CanonicalSmoothClipPresentation = Readonly<{
64
+ clip: CanonicalClipGeometry;
65
+ contentTranslateX: number;
66
+ contentTranslateY: number;
67
+ contentScale: number;
68
+ boxShadow?: CanonicalClipBoxShadow;
69
+ }>;
70
+
71
+ const DEFAULT_SHADOW_COLOR = '#000000ff';
72
+
73
+ function isClipCurve(curve: ClipCurve | undefined): boolean {
74
+ 'worklet';
75
+ return curve === undefined || curve === 'circular' || curve === 'continuous';
76
+ }
77
+
78
+ function isFiniteOptionalNumber(value: number | undefined): boolean {
25
79
  'worklet';
26
- return { clip, contentTranslateX, contentTranslateY };
80
+ return value === undefined || Number.isFinite(value);
81
+ }
82
+
83
+ function canonicalizeClipBoxShadow(
84
+ shadow: ClipBoxShadow | undefined
85
+ ): CanonicalClipBoxShadow | null | undefined {
86
+ 'worklet';
87
+ if (shadow === undefined) return undefined;
88
+ const { offsetX, offsetY } = shadow;
89
+ const blurRadius = shadow.blurRadius ?? 0;
90
+ const spreadDistance = shadow.spreadDistance ?? 0;
91
+ if (
92
+ !Number.isFinite(offsetX) ||
93
+ !Number.isFinite(offsetY) ||
94
+ !Number.isFinite(blurRadius) ||
95
+ !Number.isFinite(spreadDistance)
96
+ ) {
97
+ return null;
98
+ }
99
+ const processedColor =
100
+ shadow.color === undefined
101
+ ? processColor(DEFAULT_SHADOW_COLOR)
102
+ : processColor(shadow.color);
103
+ // Platform and dynamic colors cannot be represented by the scalar native
104
+ // animation protocol. Callers can resolve those colors before presenting.
105
+ if (typeof processedColor !== 'number') return null;
106
+ const unsignedProcessedColor = processedColor >>> 0;
107
+ const color =
108
+ ((unsignedProcessedColor << 8) | (unsignedProcessedColor >>> 24)) >>> 0;
109
+ return {
110
+ color: color as unknown as ColorValue,
111
+ offsetX,
112
+ offsetY,
113
+ blurRadius: Math.max(0, blurRadius),
114
+ spreadDistance,
115
+ };
116
+ }
117
+
118
+ function clipBoxShadowEquals(
119
+ first: ClipBoxShadow | undefined,
120
+ second: ClipBoxShadow | undefined
121
+ ): boolean {
122
+ 'worklet';
123
+ if (first === undefined || second === undefined) return first === second;
124
+ const canonicalFirst = canonicalizeClipBoxShadow(first);
125
+ const canonicalSecond = canonicalizeClipBoxShadow(second);
126
+ return (
127
+ canonicalFirst !== null &&
128
+ canonicalFirst !== undefined &&
129
+ canonicalSecond !== null &&
130
+ canonicalSecond !== undefined &&
131
+ canonicalFirst.color === canonicalSecond.color &&
132
+ canonicalFirst.offsetX === canonicalSecond.offsetX &&
133
+ canonicalFirst.offsetY === canonicalSecond.offsetY &&
134
+ canonicalFirst.blurRadius === canonicalSecond.blurRadius &&
135
+ canonicalFirst.spreadDistance === canonicalSecond.spreadDistance
136
+ );
27
137
  }
28
138
 
29
139
  export function isFiniteClipGeometry(geometry: ClipGeometry): boolean {
30
140
  'worklet';
141
+ const width = Math.max(0, geometry.width);
142
+ const height = Math.max(0, geometry.height);
31
143
  return (
32
144
  Number.isFinite(geometry.x) &&
33
145
  Number.isFinite(geometry.y) &&
34
146
  Number.isFinite(geometry.width) &&
35
147
  Number.isFinite(geometry.height) &&
36
- Number.isFinite(geometry.radius)
148
+ Number.isFinite(geometry.radius) &&
149
+ Number.isFinite(geometry.x + width) &&
150
+ Number.isFinite(geometry.y + height) &&
151
+ isFiniteOptionalNumber(geometry.topLeftRadius) &&
152
+ isFiniteOptionalNumber(geometry.topRightRadius) &&
153
+ isFiniteOptionalNumber(geometry.bottomRightRadius) &&
154
+ isFiniteOptionalNumber(geometry.bottomLeftRadius) &&
155
+ isClipCurve(geometry.curve)
37
156
  );
38
157
  }
39
158
 
159
+ function resolvedTopLeftRadius(geometry: ClipGeometry): number {
160
+ 'worklet';
161
+ return geometry.topLeftRadius ?? geometry.radius;
162
+ }
163
+
164
+ function resolvedTopRightRadius(geometry: ClipGeometry): number {
165
+ 'worklet';
166
+ return geometry.topRightRadius ?? geometry.radius;
167
+ }
168
+
169
+ function resolvedBottomRightRadius(geometry: ClipGeometry): number {
170
+ 'worklet';
171
+ return geometry.bottomRightRadius ?? geometry.radius;
172
+ }
173
+
174
+ function resolvedBottomLeftRadius(geometry: ClipGeometry): number {
175
+ 'worklet';
176
+ return geometry.bottomLeftRadius ?? geometry.radius;
177
+ }
178
+
179
+ function resolvedCurve(geometry: ClipGeometry): ClipCurve {
180
+ 'worklet';
181
+ return geometry.curve ?? 'circular';
182
+ }
183
+
40
184
  export function clipGeometryEquals(
41
185
  first: ClipGeometry | null,
42
186
  second: ClipGeometry
@@ -48,18 +192,33 @@ export function clipGeometryEquals(
48
192
  first.y === second.y &&
49
193
  first.width === second.width &&
50
194
  first.height === second.height &&
51
- first.radius === second.radius
195
+ first.radius === second.radius &&
196
+ resolvedTopLeftRadius(first) === resolvedTopLeftRadius(second) &&
197
+ resolvedTopRightRadius(first) === resolvedTopRightRadius(second) &&
198
+ resolvedBottomRightRadius(first) === resolvedBottomRightRadius(second) &&
199
+ resolvedBottomLeftRadius(first) === resolvedBottomLeftRadius(second) &&
200
+ resolvedCurve(first) === resolvedCurve(second)
52
201
  );
53
202
  }
54
203
 
204
+ function resolvedContentScale(presentation: SmoothClipPresentation): number {
205
+ 'worklet';
206
+ return presentation.contentScale ?? 1;
207
+ }
208
+
55
209
  export function isFiniteClipPresentation(
56
210
  presentation: SmoothClipPresentation
57
211
  ): boolean {
58
212
  'worklet';
213
+ const contentScale = resolvedContentScale(presentation);
214
+ const shadow = canonicalizeClipBoxShadow(presentation.boxShadow);
59
215
  return (
60
216
  isFiniteClipGeometry(presentation.clip) &&
61
217
  Number.isFinite(presentation.contentTranslateX) &&
62
- Number.isFinite(presentation.contentTranslateY)
218
+ Number.isFinite(presentation.contentTranslateY) &&
219
+ Number.isFinite(contentScale) &&
220
+ contentScale > 0 &&
221
+ shadow !== null
63
222
  );
64
223
  }
65
224
 
@@ -72,51 +231,140 @@ export function clipPresentationEquals(
72
231
  first !== null &&
73
232
  clipGeometryEquals(first.clip, second.clip) &&
74
233
  first.contentTranslateX === second.contentTranslateX &&
75
- first.contentTranslateY === second.contentTranslateY
234
+ first.contentTranslateY === second.contentTranslateY &&
235
+ resolvedContentScale(first) === resolvedContentScale(second) &&
236
+ clipBoxShadowEquals(first.boxShadow, second.boxShadow)
76
237
  );
77
238
  }
78
239
 
240
+ function createCanonicalClipGeometry(
241
+ geometry: ClipGeometry,
242
+ x: number,
243
+ y: number,
244
+ width: number,
245
+ height: number
246
+ ): CanonicalClipGeometry {
247
+ 'worklet';
248
+ const topLeftRadius = Math.max(0, resolvedTopLeftRadius(geometry));
249
+ const topRightRadius = Math.max(0, resolvedTopRightRadius(geometry));
250
+ const bottomRightRadius = Math.max(0, resolvedBottomRightRadius(geometry));
251
+ const bottomLeftRadius = Math.max(0, resolvedBottomLeftRadius(geometry));
252
+
253
+ // CSS corner-overlap reduction uses one common factor so all four corners
254
+ // retain their proportions when either pair exceeds the available edge.
255
+ let scale = 1;
256
+ const top = topLeftRadius + topRightRadius;
257
+ const right = topRightRadius + bottomRightRadius;
258
+ const bottom = bottomLeftRadius + bottomRightRadius;
259
+ const left = topLeftRadius + bottomLeftRadius;
260
+
261
+ if (top > 0) scale = Math.min(scale, width / top);
262
+ if (right > 0) scale = Math.min(scale, height / right);
263
+ if (bottom > 0) scale = Math.min(scale, width / bottom);
264
+ if (left > 0) scale = Math.min(scale, height / left);
265
+ scale = Math.max(0, Math.min(1, scale));
266
+
267
+ const canonicalTopLeftRadius = topLeftRadius * scale;
268
+ const canonicalTopRightRadius = topRightRadius * scale;
269
+ const canonicalBottomRightRadius = bottomRightRadius * scale;
270
+ const canonicalBottomLeftRadius = bottomLeftRadius * scale;
271
+ const radius =
272
+ canonicalTopLeftRadius === canonicalTopRightRadius &&
273
+ canonicalTopRightRadius === canonicalBottomRightRadius &&
274
+ canonicalBottomRightRadius === canonicalBottomLeftRadius
275
+ ? canonicalTopLeftRadius
276
+ : 0;
277
+
278
+ return {
279
+ x,
280
+ y,
281
+ width,
282
+ height,
283
+ radius,
284
+ topLeftRadius: canonicalTopLeftRadius,
285
+ topRightRadius: canonicalTopRightRadius,
286
+ bottomRightRadius: canonicalBottomRightRadius,
287
+ bottomLeftRadius: canonicalBottomLeftRadius,
288
+ curve: resolvedCurve(geometry),
289
+ };
290
+ }
291
+
79
292
  /**
80
- * Mirrors the native geometry contract for tests and non-native fallbacks.
81
- * Native remains authoritative because its bounds are the actual rendered size.
293
+ * Expands presentation defaults and applies CSS proportional corner scaling.
82
294
  */
83
- export function normalizeClipGeometry(
84
- geometry: ClipGeometry,
85
- bounds: ClipBounds
86
- ): ClipGeometry | null {
295
+ export function canonicalizeClipGeometry(
296
+ geometry: ClipGeometry
297
+ ): CanonicalClipGeometry | null {
87
298
  'worklet';
88
- const values = [
299
+ if (!isFiniteClipGeometry(geometry)) return null;
300
+
301
+ const width = Math.max(0, geometry.width);
302
+ const height = Math.max(0, geometry.height);
303
+ return createCanonicalClipGeometry(
304
+ geometry,
89
305
  geometry.x,
90
306
  geometry.y,
91
- geometry.width,
92
- geometry.height,
93
- geometry.radius,
94
- bounds.width,
95
- bounds.height,
96
- ];
97
-
98
- if (!values.every(Number.isFinite)) return null;
99
-
100
- const boundsWidth = Math.max(0, bounds.width);
101
- const boundsHeight = Math.max(0, bounds.height);
102
- const requestedWidth = Math.max(0, geometry.width);
103
- const requestedHeight = Math.max(0, geometry.height);
104
-
105
- const left = Math.min(boundsWidth, Math.max(0, geometry.x));
106
- const top = Math.min(boundsHeight, Math.max(0, geometry.y));
107
- const right = Math.min(boundsWidth, Math.max(0, geometry.x + requestedWidth));
108
- const bottom = Math.min(
109
- boundsHeight,
110
- Math.max(0, geometry.y + requestedHeight)
307
+ width,
308
+ height
111
309
  );
112
- const width = Math.max(0, right - left);
113
- const height = Math.max(0, bottom - top);
310
+ }
311
+
312
+ export function canonicalizeClipPresentation(
313
+ presentation: SmoothClipPresentation
314
+ ): CanonicalSmoothClipPresentation | null {
315
+ 'worklet';
316
+ if (!isFiniteClipPresentation(presentation)) return null;
317
+
318
+ const clip = canonicalizeClipGeometry(presentation.clip);
319
+ if (clip === null) return null;
320
+ const boxShadow = canonicalizeClipBoxShadow(presentation.boxShadow);
321
+ if (boxShadow === null) return null;
114
322
 
115
323
  return {
116
- x: left,
117
- y: top,
118
- width,
119
- height,
120
- radius: Math.min(Math.max(0, geometry.radius), Math.min(width, height) / 2),
324
+ clip,
325
+ contentTranslateX: presentation.contentTranslateX,
326
+ contentTranslateY: presentation.contentTranslateY,
327
+ contentScale: resolvedContentScale(presentation),
328
+ ...(boxShadow === undefined ? {} : { boxShadow }),
329
+ };
330
+ }
331
+
332
+ // Keep this below the canonicalization helpers so the worklet never closes
333
+ // over a helper declared later in this module.
334
+ export function createClipPresentation(
335
+ clip: ClipGeometry,
336
+ contentTranslateX = 0,
337
+ contentTranslateY = 0,
338
+ contentScale = 1,
339
+ boxShadow?: ClipBoxShadow
340
+ ): CanonicalSmoothClipPresentation {
341
+ 'worklet';
342
+ const canonicalClip = canonicalizeClipGeometry(clip);
343
+ const canonicalShadow = canonicalizeClipBoxShadow(boxShadow);
344
+ return {
345
+ // Keep invalid input observably invalid so downstream validation remains
346
+ // atomic instead of accidentally sanitizing a required scalar.
347
+ clip: canonicalClip ?? {
348
+ x: clip.x,
349
+ y: clip.y,
350
+ width: clip.width,
351
+ height: clip.height,
352
+ radius: clip.radius,
353
+ topLeftRadius: resolvedTopLeftRadius(clip),
354
+ topRightRadius: resolvedTopRightRadius(clip),
355
+ bottomRightRadius: resolvedBottomRightRadius(clip),
356
+ bottomLeftRadius: resolvedBottomLeftRadius(clip),
357
+ curve: resolvedCurve(clip),
358
+ },
359
+ contentTranslateX,
360
+ contentTranslateY,
361
+ contentScale,
362
+ ...(canonicalShadow === undefined
363
+ ? {}
364
+ : {
365
+ // Match the geometry constructor's invalid-input behavior: retain the
366
+ // bad source value so the driver's atomic validation rejects it.
367
+ boxShadow: (canonicalShadow ?? boxShadow) as CanonicalClipBoxShadow,
368
+ }),
121
369
  };
122
370
  }
@@ -0,0 +1,53 @@
1
+ import type {
2
+ SmoothClipAnimation,
3
+ SmoothClipCompletion,
4
+ SmoothClipReactRun,
5
+ SmoothClipRef,
6
+ SmoothClipRunHandle,
7
+ } from './controllerTypes';
8
+ import type {
9
+ CanonicalSmoothClipPresentation,
10
+ SmoothClipPresentation,
11
+ } from './geometry';
12
+
13
+ export type SmoothClipGroupFrame = Readonly<{
14
+ clip: SmoothClipRef;
15
+ frame: SmoothClipPresentation;
16
+ }>;
17
+
18
+ export type SmoothClipGroupTarget = Readonly<{
19
+ clip: SmoothClipRef;
20
+ target: SmoothClipPresentation;
21
+ }>;
22
+
23
+ export type SmoothClipGroupSnapshot = Readonly<{
24
+ clip: SmoothClipRef;
25
+ frame: CanonicalSmoothClipPresentation;
26
+ ready: boolean;
27
+ }>;
28
+
29
+ export type SmoothClipGroupOptions = Readonly<{
30
+ /** Stable React-runtime callback for UI- and React-runtime transactions. */
31
+ onAnimationComplete?: (result: SmoothClipCompletion) => void;
32
+ }>;
33
+
34
+ export type SmoothClipGroup = Readonly<{
35
+ ui: Readonly<{
36
+ setFrames(entries: readonly SmoothClipGroupFrame[]): void;
37
+ beginInteraction(
38
+ clips: readonly SmoothClipRef[]
39
+ ): readonly SmoothClipGroupSnapshot[];
40
+ animateTo(
41
+ entries: readonly SmoothClipGroupTarget[],
42
+ animation: SmoothClipAnimation,
43
+ completionTag?: number
44
+ ): SmoothClipRunHandle | null;
45
+ cancel(run: SmoothClipRunHandle): readonly SmoothClipGroupSnapshot[];
46
+ }>;
47
+ react: Readonly<{
48
+ animateTo(
49
+ entries: readonly SmoothClipGroupTarget[],
50
+ animation: SmoothClipAnimation
51
+ ): SmoothClipReactRun;
52
+ }>;
53
+ }>;
@@ -0,0 +1 @@
1
+ export { useSmoothClipGroup } from './groups.native';
@@ -0,0 +1 @@
1
+ export { useSmoothClipGroup } from './groups.native';