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