react-native-smooth-clip-view 0.3.0 → 0.4.1

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 (199) hide show
  1. package/README.md +135 -386
  2. package/android/build.gradle +4 -7
  3. package/android/proguard-rules.pro +2 -2
  4. package/android/src/main/cpp/SmoothClipAndroid.h +9 -12
  5. package/android/src/main/cpp/SmoothClipBindings.cpp +300 -704
  6. package/android/src/main/cpp/SmoothClipRegistry.cpp +348 -594
  7. package/android/src/main/java/com/smoothclipview/ClipGeometryNormalizer.kt +36 -227
  8. package/android/src/main/java/com/smoothclipview/SmoothClipBindings.kt +16 -19
  9. package/android/src/main/java/com/smoothclipview/SmoothClipModule.kt +36 -294
  10. package/android/src/main/java/com/smoothclipview/SmoothClipView.kt +369 -486
  11. package/android/src/main/java/com/smoothclipview/SmoothClipViewManager.kt +75 -77
  12. package/cpp/SmoothClipAnimationCurve.h +237 -346
  13. package/cpp/SmoothClipRegistry.h +24 -52
  14. package/cpp/SmoothClipSharedGeometry.h +64 -94
  15. package/cpp/SmoothClipVelocityTracker.h +5 -5
  16. package/ios/SmoothClipGeometry.h +23 -34
  17. package/ios/SmoothClipRegistry.mm +206 -711
  18. package/ios/SmoothClipTurboModule.cpp +217 -715
  19. package/ios/SmoothClipTurboModule.h +54 -297
  20. package/ios/SmoothClipView.mm +599 -505
  21. package/ios/SmoothClipViewRegistry.h +2 -7
  22. package/lib/module/NativeSmoothClipModule.js.map +1 -1
  23. package/lib/module/SmoothClipView.ios.js +84 -17
  24. package/lib/module/SmoothClipView.ios.js.map +1 -1
  25. package/lib/module/SmoothClipView.js +1 -1
  26. package/lib/module/SmoothClipView.js.map +1 -1
  27. package/lib/module/SmoothClipViewNativeComponent.ts +20 -26
  28. package/lib/module/capabilities.native.js +1 -19
  29. package/lib/module/capabilities.native.js.map +1 -1
  30. package/lib/module/capabilityTypes.js +0 -9
  31. package/lib/module/capabilityTypes.js.map +1 -1
  32. package/lib/module/controllerInternals.js +28 -0
  33. package/lib/module/controllerInternals.js.map +1 -0
  34. package/lib/module/controllerLifecycle.js +9 -0
  35. package/lib/module/controllerLifecycle.js.map +1 -0
  36. package/lib/module/controllerTypes.js +4 -0
  37. package/lib/module/controllerTypes.js.map +1 -0
  38. package/lib/module/controllers.android.js +4 -0
  39. package/lib/module/controllers.android.js.map +1 -0
  40. package/lib/module/controllers.ios.js +4 -0
  41. package/lib/module/controllers.ios.js.map +1 -0
  42. package/lib/module/controllers.js +4 -0
  43. package/lib/module/controllers.js.map +1 -0
  44. package/lib/module/controllers.native.js +89 -0
  45. package/lib/module/controllers.native.js.map +1 -0
  46. package/lib/module/geometry.js +71 -51
  47. package/lib/module/geometry.js.map +1 -1
  48. package/lib/module/groupTypes.js +4 -0
  49. package/lib/module/{groupDriverTypes.js.map → groupTypes.js.map} +1 -1
  50. package/lib/module/groups.android.js +4 -0
  51. package/lib/module/groups.android.js.map +1 -0
  52. package/lib/module/groups.ios.js +4 -0
  53. package/lib/module/groups.ios.js.map +1 -0
  54. package/lib/module/groups.js +4 -0
  55. package/lib/module/groups.js.map +1 -0
  56. package/lib/module/groups.native.js +399 -0
  57. package/lib/module/groups.native.js.map +1 -0
  58. package/lib/module/ids.js +8 -0
  59. package/lib/module/ids.js.map +1 -0
  60. package/lib/module/index.js +3 -3
  61. package/lib/module/index.js.map +1 -1
  62. package/lib/module/nativeCompletion.js +37 -16
  63. package/lib/module/nativeCompletion.js.map +1 -1
  64. package/lib/module/presentationCodec.js +81 -0
  65. package/lib/module/presentationCodec.js.map +1 -0
  66. package/lib/typescript/src/NativeSmoothClipModule.d.ts +14 -23
  67. package/lib/typescript/src/NativeSmoothClipModule.d.ts.map +1 -1
  68. package/lib/typescript/src/SmoothClipView.d.ts +1 -1
  69. package/lib/typescript/src/SmoothClipView.d.ts.map +1 -1
  70. package/lib/typescript/src/SmoothClipView.ios.d.ts +18 -14
  71. package/lib/typescript/src/SmoothClipView.ios.d.ts.map +1 -1
  72. package/lib/typescript/src/SmoothClipViewNativeComponent.d.ts +10 -5
  73. package/lib/typescript/src/SmoothClipViewNativeComponent.d.ts.map +1 -1
  74. package/lib/typescript/src/capabilities.native.d.ts +1 -1
  75. package/lib/typescript/src/capabilities.native.d.ts.map +1 -1
  76. package/lib/typescript/src/capabilityTypes.d.ts +0 -6
  77. package/lib/typescript/src/capabilityTypes.d.ts.map +1 -1
  78. package/lib/typescript/src/controllerInternals.d.ts +14 -0
  79. package/lib/typescript/src/controllerInternals.d.ts.map +1 -0
  80. package/lib/typescript/src/controllerLifecycle.d.ts +3 -0
  81. package/lib/typescript/src/controllerLifecycle.d.ts.map +1 -0
  82. package/lib/typescript/src/controllerTypes.d.ts +62 -0
  83. package/lib/typescript/src/controllerTypes.d.ts.map +1 -0
  84. package/lib/typescript/src/controllers.android.d.ts +2 -0
  85. package/lib/typescript/src/controllers.android.d.ts.map +1 -0
  86. package/lib/typescript/src/controllers.d.ts +2 -0
  87. package/lib/typescript/src/controllers.d.ts.map +1 -0
  88. package/lib/typescript/src/controllers.ios.d.ts +2 -0
  89. package/lib/typescript/src/controllers.ios.d.ts.map +1 -0
  90. package/lib/typescript/src/controllers.native.d.ts +3 -0
  91. package/lib/typescript/src/controllers.native.d.ts.map +1 -0
  92. package/lib/typescript/src/geometry.d.ts +22 -12
  93. package/lib/typescript/src/geometry.d.ts.map +1 -1
  94. package/lib/typescript/src/groupTypes.d.ts +31 -0
  95. package/lib/typescript/src/groupTypes.d.ts.map +1 -0
  96. package/lib/typescript/src/groups.android.d.ts +2 -0
  97. package/lib/typescript/src/groups.android.d.ts.map +1 -0
  98. package/lib/typescript/src/groups.d.ts +2 -0
  99. package/lib/typescript/src/groups.d.ts.map +1 -0
  100. package/lib/typescript/src/groups.ios.d.ts +2 -0
  101. package/lib/typescript/src/groups.ios.d.ts.map +1 -0
  102. package/lib/typescript/src/groups.native.d.ts +3 -0
  103. package/lib/typescript/src/groups.native.d.ts.map +1 -0
  104. package/lib/typescript/src/ids.d.ts +2 -0
  105. package/lib/typescript/src/ids.d.ts.map +1 -0
  106. package/lib/typescript/src/index.d.ts +6 -6
  107. package/lib/typescript/src/index.d.ts.map +1 -1
  108. package/lib/typescript/src/nativeCompletion.d.ts +16 -6
  109. package/lib/typescript/src/nativeCompletion.d.ts.map +1 -1
  110. package/lib/typescript/src/presentationCodec.d.ts +10 -0
  111. package/lib/typescript/src/presentationCodec.d.ts.map +1 -0
  112. package/package.json +8 -3
  113. package/src/NativeSmoothClipModule.ts +41 -292
  114. package/src/SmoothClipView.ios.tsx +130 -23
  115. package/src/SmoothClipView.tsx +5 -1
  116. package/src/SmoothClipViewNativeComponent.ts +20 -26
  117. package/src/capabilities.native.ts +2 -52
  118. package/src/capabilityTypes.ts +0 -14
  119. package/src/controllerInternals.ts +52 -0
  120. package/src/controllerLifecycle.ts +6 -0
  121. package/src/controllerTypes.ts +87 -0
  122. package/src/controllers.android.ts +1 -0
  123. package/src/controllers.ios.ts +1 -0
  124. package/src/controllers.native.ts +104 -0
  125. package/src/controllers.ts +1 -0
  126. package/src/geometry.ts +118 -74
  127. package/src/groupTypes.ts +53 -0
  128. package/src/groups.android.ts +1 -0
  129. package/src/groups.ios.ts +1 -0
  130. package/src/groups.native.ts +604 -0
  131. package/src/groups.ts +1 -0
  132. package/src/ids.ts +6 -0
  133. package/src/index.ts +20 -28
  134. package/src/nativeCompletion.ts +67 -32
  135. package/src/presentationCodec.ts +134 -0
  136. package/lib/module/driverState.js +0 -82
  137. package/lib/module/driverState.js.map +0 -1
  138. package/lib/module/driverTypes.js +0 -4
  139. package/lib/module/driverTypes.js.map +0 -1
  140. package/lib/module/drivers.android.js +0 -6
  141. package/lib/module/drivers.android.js.map +0 -1
  142. package/lib/module/drivers.ios.js +0 -6
  143. package/lib/module/drivers.ios.js.map +0 -1
  144. package/lib/module/drivers.js +0 -4
  145. package/lib/module/drivers.js.map +0 -1
  146. package/lib/module/drivers.native.js +0 -756
  147. package/lib/module/drivers.native.js.map +0 -1
  148. package/lib/module/groupDriverTypes.js +0 -4
  149. package/lib/module/groupDrivers.android.js +0 -4
  150. package/lib/module/groupDrivers.android.js.map +0 -1
  151. package/lib/module/groupDrivers.ios.js +0 -4
  152. package/lib/module/groupDrivers.ios.js.map +0 -1
  153. package/lib/module/groupDrivers.js +0 -4
  154. package/lib/module/groupDrivers.js.map +0 -1
  155. package/lib/module/groupDrivers.native.js +0 -799
  156. package/lib/module/groupDrivers.native.js.map +0 -1
  157. package/lib/module/presentationProtocol.js +0 -15
  158. package/lib/module/presentationProtocol.js.map +0 -1
  159. package/lib/module/reactRequests.js +0 -56
  160. package/lib/module/reactRequests.js.map +0 -1
  161. package/lib/typescript/src/driverState.d.ts +0 -26
  162. package/lib/typescript/src/driverState.d.ts.map +0 -1
  163. package/lib/typescript/src/driverTypes.d.ts +0 -121
  164. package/lib/typescript/src/driverTypes.d.ts.map +0 -1
  165. package/lib/typescript/src/drivers.android.d.ts +0 -3
  166. package/lib/typescript/src/drivers.android.d.ts.map +0 -1
  167. package/lib/typescript/src/drivers.d.ts +0 -3
  168. package/lib/typescript/src/drivers.d.ts.map +0 -1
  169. package/lib/typescript/src/drivers.ios.d.ts +0 -3
  170. package/lib/typescript/src/drivers.ios.d.ts.map +0 -1
  171. package/lib/typescript/src/drivers.native.d.ts +0 -5
  172. package/lib/typescript/src/drivers.native.d.ts.map +0 -1
  173. package/lib/typescript/src/groupDriverTypes.d.ts +0 -66
  174. package/lib/typescript/src/groupDriverTypes.d.ts.map +0 -1
  175. package/lib/typescript/src/groupDrivers.android.d.ts +0 -2
  176. package/lib/typescript/src/groupDrivers.android.d.ts.map +0 -1
  177. package/lib/typescript/src/groupDrivers.d.ts +0 -2
  178. package/lib/typescript/src/groupDrivers.d.ts.map +0 -1
  179. package/lib/typescript/src/groupDrivers.ios.d.ts +0 -2
  180. package/lib/typescript/src/groupDrivers.ios.d.ts.map +0 -1
  181. package/lib/typescript/src/groupDrivers.native.d.ts +0 -4
  182. package/lib/typescript/src/groupDrivers.native.d.ts.map +0 -1
  183. package/lib/typescript/src/presentationProtocol.d.ts +0 -4
  184. package/lib/typescript/src/presentationProtocol.d.ts.map +0 -1
  185. package/lib/typescript/src/reactRequests.d.ts +0 -9
  186. package/lib/typescript/src/reactRequests.d.ts.map +0 -1
  187. package/src/driverState.ts +0 -118
  188. package/src/driverTypes.ts +0 -171
  189. package/src/drivers.android.ts +0 -8
  190. package/src/drivers.ios.ts +0 -8
  191. package/src/drivers.native.ts +0 -1599
  192. package/src/drivers.ts +0 -6
  193. package/src/groupDriverTypes.ts +0 -117
  194. package/src/groupDrivers.android.ts +0 -1
  195. package/src/groupDrivers.ios.ts +0 -1
  196. package/src/groupDrivers.native.ts +0 -1189
  197. package/src/groupDrivers.ts +0 -1
  198. package/src/presentationProtocol.ts +0 -28
  199. package/src/reactRequests.ts +0 -76
package/README.md CHANGED
@@ -1,449 +1,198 @@
1
1
  # react-native-smooth-clip-view
2
2
 
3
- [![npm version](https://img.shields.io/npm/v/react-native-smooth-clip-view.svg)](https://www.npmjs.com/package/react-native-smooth-clip-view)
3
+ Layout-free animated rounded clipping for React Native Fabric.
4
4
 
5
- https://github.com/user-attachments/assets/899235b3-de69-46d7-b6db-61bc54d80df8
6
-
7
- <sub>Card zoom and shared-element gallery transitions, captured at 60fps — [direct clip](https://github.com/kbrattli/react-native-smooth-clip-view/blob/main/docs/media/smooth-clip-demo.mp4).</sub>
8
-
9
- ## High-performance geometry animations for React Native
5
+ `SmoothClipView` is a fixed viewport. Its controller moves a rounded aperture
6
+ inside that viewport using raw host-local coordinates. Content is clipped by the
7
+ aperture, an optional outset shadow may extend outside the aperture, and the host
8
+ finally crops both. A full-screen host therefore accepts screen coordinates
9
+ directly, including negative and off-screen frames.
10
10
 
11
- `react-native-smooth-clip-view` lets you animate `x`, `y`, `width`, `height`, and `borderRadius` with Reanimated without triggering expensive layout work on every frame.
11
+ Use this library when a transition needs streamed gesture updates followed by
12
+ native-owned timing or spring motion without animating Yoga layout. A regular
13
+ Reanimated `View` with `overflow: 'hidden'` remains the simpler choice when
14
+ layout-thread work and interruption snapshots are not concerns.
12
15
 
13
- Instead of resizing the Yoga layout, `SmoothClipView` keeps a fixed footprint and updates only the native clipping layer. This makes geometry-heavy animations smooth and inexpensive—even for shared-element transitions, zoom transitions, expanding cards, reveals, sheets, maps, and media.
16
+ ## Demo
14
17
 
15
- Use it for transitions that previously struggled with performance when animating layout dimensions directly.
18
+ https://github.com/user-attachments/assets/899235b3-de69-46d7-b6db-61bc54d80df8
16
19
 
17
20
  ## Requirements
18
21
 
19
- - React Native 0.86 or newer with the New Architecture enabled
22
+ - React Native 0.86 or newer with the New Architecture
23
+ - React 19.2 or newer
20
24
  - React Native Reanimated 4.5 or newer
21
- - React Native Worklets 0.10 or newer
22
- - iOS 16.4 or newer
23
- - Android API 33 or newer
24
-
25
- The package supports iOS and Android. React Native Web and the legacy Paper
26
- architecture are not supported; keep imports and rendered usage behind native
27
- platform boundaries.
28
-
29
- ## Installation
30
-
31
- Available from [npm](https://www.npmjs.com/package/react-native-smooth-clip-view):
25
+ - React Native Worklets 0.10.1 or newer
26
+ - Android API 24 or newer
32
27
 
33
28
  ```sh
34
29
  npm install react-native-smooth-clip-view react-native-reanimated react-native-worklets
35
- ```
36
-
37
- For iOS, install pods after adding the package:
38
-
39
- ```sh
40
30
  cd ios && pod install
41
31
  ```
42
32
 
43
- Follow the Reanimated installation instructions for your React Native setup.
44
- Expo SDK 57 configures the required Babel plugin through `babel-preset-expo`.
45
-
46
- ## Usage
33
+ ## One clip
47
34
 
48
35
  ```tsx
49
- import { useState } from 'react';
50
- import { Button, StyleSheet, View } from 'react-native';
51
36
  import {
52
- type ClipGeometry,
37
+ ClipEasings,
53
38
  SmoothClipView,
54
- createClipPresentation,
55
- useSmoothClipDriver,
39
+ useSmoothClipController,
56
40
  } from 'react-native-smooth-clip-view';
57
41
 
58
- const initialClip: ClipGeometry = {
59
- x: 120,
60
- y: 180,
61
- width: 80,
62
- height: 80,
63
- radius: 40,
64
- };
65
-
66
- export function ClipExample() {
67
- const [expanded, setExpanded] = useState(false);
68
- const driver = useSmoothClipDriver(
69
- createClipPresentation(initialClip, -initialClip.x, -initialClip.y)
70
- );
42
+ function Reveal() {
43
+ const clip = useSmoothClipController({
44
+ clip: { x: 24, y: 80, width: 96, height: 96, radius: 24 },
45
+ contentTranslateX: -24,
46
+ contentTranslateY: -80,
47
+ contentScale: 1,
48
+ });
71
49
 
72
- const toggle = () => {
73
- const next = !expanded;
74
- setExpanded(next);
75
- const clip = next
76
- ? { x: 0, y: 0, width: 320, height: 480, radius: 24 }
77
- : initialClip;
78
- void driver.react.animateTo(
79
- createClipPresentation(clip, -clip.x, -clip.y),
50
+ const open = () => {
51
+ clip.react.animateTo(
52
+ {
53
+ clip: { x: 0, y: 0, width: 390, height: 844, radius: 0 },
54
+ contentTranslateX: 0,
55
+ contentTranslateY: 0,
56
+ contentScale: 1,
57
+ },
80
58
  {
81
59
  type: 'timing',
82
- duration: 450,
83
- controlPoints: [0.42, 0, 0.58, 1],
60
+ duration: 400,
61
+ controlPoints: ClipEasings.easeOutCubic,
84
62
  }
85
63
  );
86
64
  };
87
65
 
88
66
  return (
89
- <View>
90
- <SmoothClipView driver={driver} style={styles.host}>
91
- <View style={styles.content} />
92
- </SmoothClipView>
93
- <Button title="Toggle clip" onPress={toggle} />
94
- </View>
67
+ <SmoothClipView controller={clip} style={{ flex: 1 }}>
68
+ {/* screen-sized transition content */}
69
+ </SmoothClipView>
95
70
  );
96
71
  }
97
-
98
- const styles = StyleSheet.create({
99
- host: { width: 320, height: 480 },
100
- content: { width: 320, height: 480, backgroundColor: '#112743' },
101
- });
102
72
  ```
103
73
 
104
- The same driver can be passed to multiple hosts. One native registry update
105
- fans the presentation out to all mounted hosts. The original seven-scalar V1
106
- protocol remains unchanged; V2 adds per-corner radii, continuous curves, and
107
- centered content scale. Interactive updates avoid Yoga and ShadowTree commits.
74
+ One controller may have one mounted host at a time. Sequential unmount and
75
+ remount is supported. In development, mounting the same controller in two hosts
76
+ throws an error.
108
77
 
109
- For transitions whose endpoint is known, let Core Animation interpolate on the
110
- render server:
78
+ ## Worklet API
111
79
 
112
- ```tsx
113
- const expandFromReact = () =>
114
- driver.react.animateTo(
115
- createClipPresentation(
116
- { x: 0, y: 0, width: 320, height: 480, radius: 24 },
117
- 0,
118
- 0
119
- ),
120
- {
121
- type: 'timing',
122
- duration: 450,
123
- controlPoints: [0.42, 0, 0.58, 1],
124
- }
125
- );
126
- ```
80
+ Call `clip.ui` methods from a UI-runtime worklet:
127
81
 
128
- Native timing, spring, and keyframed transitions have no app callback between
129
- setup and completion on iOS. Core Animation interpolates the clip and content
130
- translation as one presentation. Android runs one C++ frame-loop driven by the
131
- vsync clock with registry fanout; outline calculation and invalidation still
132
- run on its UI thread. Springs are physically integrated per geometry channel
133
- and finish when they settle below sub-pixel thresholds on both platforms.
134
- Retargeting starts from visible state.
135
-
136
- Blocked-thread behavior: a blocked JS thread never stalls a running
137
- transition on either platform — no JS runs on the frame path. A blocked
138
- main thread stalls it on Android (the frame loop and the View property
139
- writes are main-thread-only by platform design; the RenderThread only
140
- replays what the main thread records) but not on iOS, where the render
141
- server advances installed Core Animations out of process. The Android
142
- behavior is not recoverable through public APIs — RenderThread-driven
143
- property animation (`RenderNodeAnimator`, what ripples use) is hidden API —
144
- and it is also the coherent choice for this library: parallel Reanimated
145
- content runs on the main thread on both platforms, so on Android the clip
146
- and its content stall and resume together, while on iOS a main-thread stall
147
- lets the natively animated channels keep moving past any Reanimated-driven
148
- ones.
149
- The same driver can be grabbed by a gesture without a visual jump:
82
+ ```ts
83
+ clip.ui.setFrame(presentation);
150
84
 
151
- ```tsx
152
- const gesture = Gesture.Pan()
153
- .onStart(() => {
154
- const visible = driver.ui.beginInteraction();
155
- dragStart.value = visible.clip.height;
156
- })
157
- .onUpdate((event) => {
158
- const clip = geometryForDrag(dragStart.value, event.translationY);
159
- // Per-frame hot path: writes straight to native without SharedValue
160
- // bookkeeping. Assigning driver.presentation.value also works.
161
- driver.ui.setScalars(clip.x, clip.y, clip.width, clip.height, clip.radius, 0, 0);
162
- })
163
- .onEnd((event) => {
164
- // The release event is fresher than the last onUpdate (on Android,
165
- // ACTION_UP carries a position no MOVE ever delivered). Pass the final
166
- // geometry as `from` so the animation starts from exactly that value —
167
- // it fuses a setScalars hot write with the handoff in one call.
168
- const clip = geometryForDrag(dragStart.value, event.translationY);
169
- // 'inherit' projects launch speed from the drag's last two samples. On
170
- // Android that sampling is opt-in — create the driver with
171
- // useSmoothClipDriver(initial, { velocityTracking: true }).
172
- driver.ui.animateTo(createClipPresentation(expandedClip), {
173
- type: 'spring',
174
- initialVelocity: 'inherit',
175
- from: createClipPresentation(clip),
176
- });
177
- });
85
+ const run = clip.ui.animateTo(target, spring, 1);
86
+
87
+ clip.ui.cancel(run); // optional; freezes at the visible frame
178
88
  ```
179
89
 
180
- ## API
181
-
182
- ### `SmoothClipView`
183
-
184
- `SmoothClipViewProps` extends React Native `ViewProps` and adds:
185
-
186
- | Prop | Type | Description |
187
- | ---------- | ------------------ | --------------------------------------- |
188
- | `driver` | `SmoothClipDriver` | Reusable hybrid clip driver. |
189
- | `children` | `ReactNode` | Content rendered inside the fixed host. |
190
-
191
- The host hides itself, drops out of the accessibility tree, and stops accepting
192
- touches while the clip is empty. The two platforms draw the boundary where they
193
- actually stop rendering, which differs below one pixel: Android emits an integer
194
- `Outline`, which collapses to nothing under half a physical pixel, so an extent
195
- in `(0, 0.5)` px counts as empty there; iOS masks in floats and treats only a
196
- zero-or-negative extent as empty. A clip animating through that band therefore
197
- turns non-interactive one frame earlier on Android — matching what each platform
198
- puts on screen, which is the property worth keeping identical.
199
-
200
- ### Driver
201
-
202
- - `useSmoothClipDriver(initialPresentation, options)` returns one hybrid driver
203
- whose writable `presentation` SharedValue contains clip geometry and content
204
- translation. Passing a `ClipGeometry` remains supported and initializes both
205
- translations to zero.
206
- - `driver.ui` is the synchronous UI-worklet interface. `beginInteraction()`
207
- freezes a transition at visible presentation state; `set()`, `animateTo()`,
208
- and `cancel()` change ownership atomically.
209
- - `driver.ui.setScalars(x, y, width, height, radius, tx, ty)` is the per-frame
210
- hot path: it writes geometry straight to native without touching
211
- `driver.presentation`, skipping SharedValue bookkeeping for high-frequency
212
- streams such as gestures. `driver.presentation.value` is stale after hot
213
- writes by design; `beginInteraction()` remains the source of truth for
214
- visible geometry, and `animateTo()` after hot writes starts from the native
215
- registry's latest value rather than the stale SharedValue. To start from a
216
- value fresher than the last hot write (a gesture's release sample), pass it
217
- as `animation.from` — `animateTo` then performs the hot write and the
218
- handoff in one call. Do not interleave `setScalars` with
219
- `presentation.value` writes on the same driver.
220
- - `driver.ui.setPresentationScalars(x, y, width, height, topLeft, topRight,
221
- bottomRight, bottomLeft, curveCode, tx, ty, scale)` is the V2 hot path.
222
- `curveCode` is `0` for circular and `1` for continuous. V2 values are
223
- validated as one transaction; widened values are never silently downgraded
224
- onto a V1 native binary.
225
- - Spring `initialVelocity` is one normalized scalar along the current-to-target
226
- trajectory, in units of the remaining distance per second (`1` covers the
227
- remaining distance in one second). Every geometry channel continues with the
228
- same normalized rate, so grab/release preserves the felt direction and
229
- speed. `'inherit'` (the default) estimates the scalar from the last two
230
- interactive samples on iOS and Android. On Android, sampling on the
231
- `setScalars` hot path is **opt-in**: pass
232
- `velocityTracking: true` to `useSmoothClipDriver` or `'inherit'` inherits
233
- zero after a hot-write drag (a dev-mode warning flags this). **Behavior
234
- change:** earlier releases always recorded on Android, so an existing
235
- `setScalars`-drag → inherit-spring handoff keeps its momentum only after
236
- adding the flag. The recording
237
- is a clock read plus channel copies on every per-frame write, so drivers
238
- that never hand off into an inherit spring skip it by default. iOS always
239
- records, and Android's declarative `presentation.value` channel always
240
- records too. How long the finger has been still since that last sample scales the
241
- result: full credit for one frame (16.7 ms), then a linear decay to zero at
242
- 100 ms. A release straight out of a drag is therefore untouched, and holding
243
- still before releasing bleeds the momentum off smoothly instead of keeping
244
- all of it until 99 ms and none at 101 ms. Two writes landing inside the same frame
245
- (< 4 ms apart, e.g. a release-sample `from` seed right after the last drag
246
- write) coalesce into one sample, and an identical re-write is ignored, so a
247
- fused handoff can neither zero nor inflate the inherited velocity.
248
- Only interactive writes contribute samples. Internal freeze/join/resume and
249
- static-finalization writes do not, so grabbing a native animation cannot
250
- manufacture velocity for a later `'inherit'` spring; a subsequent drag (or
251
- explicit `animation.from`) supplies the release samples instead.
252
- - `driver.react` exposes `beginInteraction`, `set`, `animateTo`, and `cancel`
253
- as Promises (`setScalars` is UI-worklet-only). React code never blocks
254
- waiting for main/UI-thread work. An immediate animation request resolves
255
- before its completion callback is delivered.
256
- - `animateTo()` transfers ownership to native animation. Timing uses
257
- cubic Bézier control points — `ClipEasings` exports exact-form presets
258
- (`easeOutCubic` = `Easing.out(Easing.cubic)` etc.) so a parallel Reanimated
259
- animation can run the identical curve without hand-deriving it; springs
260
- accept mass, stiffness, damping, and an
261
- explicit normalized velocity or `'inherit'` (the default). Keyframes accept
262
- validated, monotonically increasing offsets from zero through one; there is
263
- deliberately no keyframe easing field — playback is linear between offsets
264
- and the frames encode the curve, which also expresses per-channel-nonlinear
265
- paths that no single time-warp could reproduce. Every
266
- kind accepts an optional `from` presentation — a fused take-ownership hot
267
- write issued immediately before the handoff, so the animation starts from
268
- exactly that value (pass `frames[0].presentation` for keyframes, which
269
- interpolate absolutely). A non-finite `from` rejects the whole call; against
270
- a held pending-animation latch, explicit `from` is the newer intent: it
271
- cancels that latch once with `finished: false`, records/applies `from`, then
272
- starts the replacement from that native value. Passive hook seeds and public
273
- `set`/`setScalars` writes still leave a held latch intact. `from` behaves the
274
- same on both platforms (it is driver-layer, not native): on iOS the seed
275
- stops any running Core Animation, applies `from` to the model layer, and
276
- installs timing, spring, or keyframe playback from that exact presentation in
277
- the same start transaction.
278
- - `cancel()` freezes visible presentation by default. Pass `'target'` as its
279
- behavior to jump to the requested endpoint.
280
- - `options.onAnimationComplete` fires exactly once per animation with its ID
281
- and `finished` state, including cancellation, replacement, and native-side
282
- rejection (`animateTo` then returns a fresh non-zero id whose single
283
- `finished: false` completion follows — key completion handling by the
284
- returned id, never by `0`). A valid pre-registration request carries its
285
- authoritative interactive start, creates the missing driver state and
286
- returns a real id. `0` — with no completion — is reserved for off-main,
287
- invalid-id, or otherwise unsupported dispatch: a missing-state native request
288
- with no authoritative start, any `driver.ui.animateTo` issued after the
289
- driver's hook has unmounted, and an invalid-parameter request issued before
290
- the driver's first seed reached native (validation rejections mint their
291
- `finished: false` completion only once the native entry exists). (The
292
- post-unmount case is decided on the UI runtime,
293
- because a destroyed driver and a not-yet-seeded one are the same missing
294
- registry entry to native — accepting it would build a latch nothing can start
295
- and nothing can cancel.) A host is displayable only while it is attached,
296
- foreground/window-visible, laid out, and positive-sized (the window's own
297
- `hidden`/scene state is not consulted — RN's single always-visible window
298
- makes it moot). Losing the last
299
- displayable host mid-flight — including temporary detach, zero-size layout,
300
- or app background — freezes and re-latches the exact remainder instead of
301
- consuming duration offscreen. Foreground/reattach resumes the same animation
302
- ID from that stored timing/keyframe phase or spring state.
303
- Parallel Reanimated clocks do **not** pause with it: a `withTiming` started
304
- beside `animateTo` keeps its wall-clock start, so after backgrounding it
305
- completes on its first resumed frame while the native run still animates its
306
- preserved remainder. Key teardown and state transitions off
307
- `onAnimationComplete` (or re-synchronize on `AppState`) rather than off a
308
- duration-matched Reanimated callback.
309
- With multiple hosts on one driver, a host becomes an installed participant
310
- only after its native animation starts. Temporary loss moves it to suspended;
311
- a rejoin restores active participation. Unregistering an installed/suspended
312
- host, or reaching completion while it remains suspended, makes the eventual
313
- completion `finished: false`. A host that stayed deferred because it was
314
- detached, unlaid-out, or zero-sized never poisons completion. `finished: true`
315
- means every installed participant either ran to the end or rejoined and did so.
316
- - An `animateTo` issued before any host view can produce a visible frame (for
317
- example from an effect in the same commit that mounts the host, or inside a
318
- modal route whose subtree attaches to its window late) is held pending and
319
- starts with its full duration at the first moment a registered host can
320
- produce a frame — positive layout, window attach/visibility, and foreground
321
- state must all be present. This also covers an animation worklet that runs before the
322
- hook's seed worklet: the animation creates the state and the later passive
323
- seed cannot reset its ownership or active id. A pending animation owns the
324
- driver: ordinary take-ownership writes (`set`, `setScalars`, the hook's seed)
325
- are dropped while it is held. Replace it with another `animateTo`, override
326
- it with an explicit `animation.from`, or cancel it via `beginInteraction()`
327
- or `cancel()`. If no view ever becomes displayable, it survives until it is
328
- replaced, cancelled, or the driver is destroyed — at which point its single
329
- `finished: false` completion is delivered.
330
-
331
- Do not call `driver.ui` from React code — it throws on the React runtime; use
332
- `driver.react` there. During a native transition, `driver.presentation.value`
333
- is the requested target, not a per-frame mirror of the native presentation
334
- layer. `beginInteraction()` returns geometry normalized against the host
335
- bounds, so a clip that extended beyond the host comes back clamped. Start
336
- gestures with `beginInteraction()`: interactive writes issued while native
337
- still owns rendering are dropped.
338
-
339
- ### Group driver
340
-
341
- `useSmoothClipGroupDriver({ reduceMotion, onAnimationComplete })` coordinates
342
- multiple drivers with one native group ID and one completion. Its worklet-safe
343
- `ui` and Promise-based `react` interfaces expose:
344
-
345
- - `beginInteraction(drivers)` to freeze every overlapping group atomically and
346
- return canonical visible snapshots in input order.
347
- - `snapshotCurrent(drivers)` to sample presentation and readiness without
348
- changing ownership.
349
- - `setBatch(entries)` to validate every entry before committing one native
350
- transaction.
351
- - `animateTo(entries, animation)` for shared timing, spring, or linear
352
- keyframe progress. Membership is immutable for that group ID; a replacement
353
- is a new atomically installed group.
354
- - `cancel(groupId, 'freeze' | 'finish')` to return every participant snapshot.
355
-
356
- Groups wait until all participants have a visible, attached, laid-out,
357
- positive-size host. The default `suspensionPolicy: 'pause'` freezes and
358
- re-latches the whole group if any participant loses readiness; `'finish'`
359
- applies every target. Complex-path native settlement is capability-gated—use
360
- `getSmoothClipCapabilities()` and keep streaming with `setBatch` when
361
- `autonomousComplexPathAnimation` is false.
362
-
363
- ### `SmoothClipPresentation`
90
+ Native terminal events are delivered on the React JavaScript runtime through
91
+ one stable controller callback. Consumer callbacks are never retained in the
92
+ UI runtime:
364
93
 
365
94
  ```ts
366
- type SmoothClipPresentation = Readonly<{
367
- clip: ClipGeometry;
368
- contentTranslateX: number;
369
- contentTranslateY: number;
370
- contentScale?: number;
371
- }>;
95
+ const clip = useSmoothClipController(initialPresentation, {
96
+ onAnimationComplete(result) {
97
+ // result.completionTag is 1 for the tagged run above
98
+ // result.finished is true only when the target was reached
99
+ },
100
+ });
372
101
  ```
373
102
 
374
- Use the content translation fields when a clipped viewport must reveal a
375
- fixed-size child without a separate Reanimated transform. Native transitions
376
- animate clip position, bounds, radius, and content translation together.
103
+ The optional completion tag is a nonnegative 32-bit integer. Use it to identify
104
+ UI-runtime runs without retaining a consumer worklet for the animation lifetime.
377
105
 
378
- ### `ClipGeometry`
106
+ `setFrame` canonicalizes the object in the worklet and makes one scalar JSI call.
107
+ It does not update React state, animated props, Yoga, the ShadowTree, or a public
108
+ SharedValue.
379
109
 
380
- ```ts
381
- type ClipGeometry = Readonly<{
382
- x: number;
383
- y: number;
384
- width: number;
385
- height: number;
386
- radius: number;
387
- topLeftRadius?: number;
388
- topRightRadius?: number;
389
- bottomRightRadius?: number;
390
- bottomLeftRadius?: number;
391
- curve?: 'circular' | 'continuous';
392
- }>;
393
- ```
110
+ `beginInteraction()` atomically interrupts native motion and returns its visible
111
+ raw presentation. Apply the final gesture sample with `setFrame()` before calling
112
+ it when the release must start from that exact frame.
394
113
 
395
- Values use React Native points/DIPs. Native code rejects non-finite updates,
396
- intersects the requested rectangle with the actual host bounds, prevents
397
- negative sizes, and proportionally scales overlapping corners with the CSS
398
- corner-normalization rule. Content scale is centered on the native content
399
- container; translation is independent and is not multiplied by scale.
114
+ ## React API
400
115
 
401
- ### `normalizeClipGeometry`
116
+ React-thread animation starts synchronously return a run object:
402
117
 
403
- `normalizeClipGeometry(geometry, bounds)` mirrors the native normalization
404
- contract for tests and non-native calculations. Native bounds remain
405
- authoritative at render time.
118
+ ```ts
119
+ const run = clip.react.animateTo(target, {
120
+ type: 'spring',
121
+ stiffness: 180,
122
+ damping: 22,
123
+ velocity: 1.4,
124
+ });
406
125
 
407
- ## Migrating from 0.0.x
126
+ // run.cancel(); // optional; freezes the visible frame and resolves false
127
+ const finished = await run.finished;
128
+ ```
408
129
 
409
- Version 0.1.0 removes the `initialClip` and `animatedClip` props in favor of
410
- the driver API: create a driver with `useSmoothClipDriver(initialPresentation)`,
411
- pass it as the `driver` prop, and move per-frame updates from the
412
- `animatedClip` SharedValue to `driver.presentation.value` (or
413
- `driver.ui.setScalars`). Autonomous transitions move from Reanimated
414
- `withTiming`/`withSpring` to `driver.ui.animateTo` / `driver.react.animateTo`.
130
+ Timing accepts `duration` and cubic-Bezier `controlPoints`. Spring accepts
131
+ optional `mass`, `stiffness`, `damping`, normalized `velocity`, and
132
+ `energyThreshold`. Both specifications accept `reduceMotion`. The physical
133
+ defaults match Reanimated 4.5: mass `4`, stiffness `900`, damping `120`, and
134
+ relative energy threshold `6e-9`. A spring is rejected when its resolved native
135
+ trajectory could make `contentScale` nonpositive.
415
136
 
416
- ## Layout and styling contract
137
+ An animation requested before the host has a positive layout waits, then starts
138
+ with its full duration. Host loss after motion starts freezes every member and
139
+ resolves the transaction `false`. Replacement, cancellation, destruction, and
140
+ rejection also settle once with `false`.
417
141
 
418
- - Give the host its fixed maximum `width` and `height`; clipping never changes
419
- Yoga layout.
420
- - Put visual backgrounds inside `SmoothClipView`.
421
- - Keep borders, shadows, rotation, and anisotropic transforms on an outer
422
- visual carrier rather than the clip host.
423
- - Uniform circular corners use platform fast paths. Unequal or continuous
424
- corners use a fixed-topology portable path; Android intentionally does not
425
- claim pixel identity with Apple's proprietary continuous curve.
142
+ ## Atomic groups
426
143
 
427
- ## Example and development
144
+ Store `controller.ref` when multiple clips must update or settle atomically:
428
145
 
429
- The [`example`](./example) workspace is an Expo SDK 57 app that exercises the
430
- package through its public import on iOS and Android.
146
+ ```ts
147
+ const group = useSmoothClipGroup();
431
148
 
432
- ```sh
433
- npm install
434
- npm run example -- ios
435
- npm run example -- android
436
- ```
149
+ group.ui.setFrames([
150
+ { clip: first.ref, frame: firstFrame },
151
+ { clip: second.ref, frame: secondFrame },
152
+ ]);
437
153
 
438
- Run the repository checks with:
154
+ const snapshots = group.ui.beginInteraction([first.ref, second.ref]);
439
155
 
440
- ```sh
441
- npm run lint
442
- npm run typecheck
443
- npm test
444
- npm run prepare
156
+ const run = group.react.animateTo(
157
+ [
158
+ { clip: first.ref, target: firstTarget },
159
+ { clip: second.ref, target: secondTarget },
160
+ ],
161
+ { type: 'timing', duration: 320, controlPoints: ClipEasings.easeOutCubic }
162
+ );
163
+
164
+ const result = await run.finished;
445
165
  ```
446
166
 
167
+ `group.ui.setFrames` uses one native batch call. `beginInteraction` snapshots and
168
+ cancel snapshots preserve input order and report whether each clip currently has
169
+ a displayable host. Controllers are implemented as one-member groups, so both
170
+ APIs share validation, run ownership, and completion behavior.
171
+
172
+ ## Geometry and rendering
173
+
174
+ - `x` and `y` are never intersected with the host.
175
+ - Negative width and height canonicalize to zero.
176
+ - Corner-overlap scaling follows CSS rules against the requested rectangle.
177
+ - `contentTranslateX`, `contentTranslateY`, and positive `contentScale` animate
178
+ with the aperture.
179
+ - Circular and continuous curves and independent corner radii are supported.
180
+ - One outset `boxShadow` is supported. It escapes the aperture but not the host.
181
+ - A fully off-host aperture is not touchable, even when only its shadow overlaps.
182
+ - Descendant accessibility is hidden during autonomous native motion and restored
183
+ from aperture/host intersection at the endpoint.
184
+
185
+ Use `getSmoothClipCapabilities()` when a consumer needs to decide whether a
186
+ complex native path can be promoted on the current platform.
187
+
188
+ ## Performance contract
189
+
190
+ - One worklet-to-native call per `ui.setFrame`, or per group batch.
191
+ - Native timing and springs run without JS work between start and completion.
192
+ - The shadow-disabled rendering path keeps no shadow drawing resources.
193
+ - The fixed host is the maximum rendering viewport; consumers should size it to
194
+ the region in which content and shadow may appear.
195
+
447
196
  ## License
448
197
 
449
198
  MIT
@@ -1,7 +1,7 @@
1
1
  buildscript {
2
2
  ext.SmoothClipView = [
3
3
  kotlinVersion: "2.0.21",
4
- minSdkVersion: 33,
4
+ minSdkVersion: 24,
5
5
  compileSdkVersion: 36,
6
6
  targetSdkVersion: 36
7
7
  ]
@@ -76,14 +76,11 @@ android {
76
76
  }
77
77
 
78
78
  defaultConfig {
79
- // Outline.setPath only guarantees arbitrary path clipping from API 33.
80
- // Keep this as a hard library floor: allowing a consuming root project to
81
- // override it lower would make the V2 per-corner/continuous path silently
82
- // unsupported at runtime.
79
+ // API 24-32 clip child drawing directly; API 33+ use path outlines.
83
80
  minSdkVersion SmoothClipView.minSdkVersion
84
81
  targetSdkVersion getExtOrDefault("targetSdkVersion")
85
- // C++ resolves SmoothClipView by name and calls setClipPresentationDip via
86
- // a cached jmethodID; consumer minification must not rename or strip them.
82
+ // C++ resolves SmoothClipView by name and calls its presentation setters via
83
+ // cached jmethodIDs; consumer minification must not rename or strip them.
87
84
  consumerProguardFiles "proguard-rules.pro"
88
85
 
89
86
  externalNativeBuild {
@@ -1,6 +1,6 @@
1
1
  # SmoothClipView is resolved from C++ by name (fbjni findClassStatic) and its
2
- # V1/V2 setClipPresentation*Dip / setClipPresentation*Px are invoked through
3
- # cached jmethodIDs; SmoothClipBindings registers native methods by name. None
2
+ # presentation setters are invoked through cached jmethodIDs;
3
+ # SmoothClipBindings registers native methods by name. None
4
4
  # of them may be renamed or stripped in consumer release builds.
5
5
  -keep class com.smoothclipview.SmoothClipView { *; }
6
6
  -keep class com.smoothclipview.SmoothClipBindings { *; }