react-native-smooth-clip-view 0.4.2 → 0.4.4

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 (46) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/README.md +74 -0
  3. package/android/src/main/cpp/SmoothClipAndroid.h +1 -1
  4. package/android/src/main/cpp/SmoothClipBindings.cpp +14 -8
  5. package/android/src/main/cpp/SmoothClipRegistry.cpp +6 -6
  6. package/android/src/main/java/com/smoothclipview/ClipGeometryNormalizer.kt +23 -0
  7. package/android/src/main/java/com/smoothclipview/SmoothClipBindings.kt +2 -0
  8. package/android/src/main/java/com/smoothclipview/SmoothClipModule.kt +2 -0
  9. package/android/src/main/java/com/smoothclipview/SmoothClipView.kt +124 -32
  10. package/android/src/main/java/com/smoothclipview/SmoothClipViewManager.kt +16 -0
  11. package/cpp/SmoothClipAnimationCurve.h +13 -10
  12. package/cpp/SmoothClipRegistry.h +2 -0
  13. package/cpp/SmoothClipSharedGeometry.h +20 -0
  14. package/cpp/SmoothClipVelocityTracker.h +6 -0
  15. package/ios/SmoothClipTurboModule.cpp +17 -6
  16. package/ios/SmoothClipTurboModule.h +3 -1
  17. package/ios/SmoothClipView.mm +241 -22
  18. package/lib/module/NativeSmoothClipModule.js.map +1 -1
  19. package/lib/module/SmoothClipView.ios.js +3 -0
  20. package/lib/module/SmoothClipView.ios.js.map +1 -1
  21. package/lib/module/SmoothClipViewNativeComponent.ts +5 -1
  22. package/lib/module/geometry.js +22 -2
  23. package/lib/module/geometry.js.map +1 -1
  24. package/lib/module/presentationCodec.js +5 -3
  25. package/lib/module/presentationCodec.js.map +1 -1
  26. package/lib/typescript/src/NativeSmoothClipModule.d.ts +1 -1
  27. package/lib/typescript/src/NativeSmoothClipModule.d.ts.map +1 -1
  28. package/lib/typescript/src/SmoothClipView.ios.d.ts.map +1 -1
  29. package/lib/typescript/src/SmoothClipViewNativeComponent.d.ts +3 -1
  30. package/lib/typescript/src/SmoothClipViewNativeComponent.d.ts.map +1 -1
  31. package/lib/typescript/src/controllerTypes.d.ts +5 -1
  32. package/lib/typescript/src/controllerTypes.d.ts.map +1 -1
  33. package/lib/typescript/src/geometry.d.ts +9 -0
  34. package/lib/typescript/src/geometry.d.ts.map +1 -1
  35. package/lib/typescript/src/index.d.ts +1 -1
  36. package/lib/typescript/src/index.d.ts.map +1 -1
  37. package/lib/typescript/src/presentationCodec.d.ts +1 -1
  38. package/lib/typescript/src/presentationCodec.d.ts.map +1 -1
  39. package/package.json +5 -4
  40. package/src/NativeSmoothClipModule.ts +3 -1
  41. package/src/SmoothClipView.ios.tsx +3 -0
  42. package/src/SmoothClipViewNativeComponent.ts +5 -1
  43. package/src/controllerTypes.ts +5 -1
  44. package/src/geometry.ts +35 -0
  45. package/src/index.ts +1 -0
  46. package/src/presentationCodec.ts +8 -2
package/CHANGELOG.md ADDED
@@ -0,0 +1,48 @@
1
+ # Changelog
2
+
3
+ ## [0.4.4](https://github.com/kbrattli/react-native-smooth-clip-view/releases/tag/v0.4.4) — 2026-09-11
4
+
5
+ - Allow the Expo 56 runtime baseline (React Native 0.85.3, Reanimated 4.3.1, Worklets 0.8.3).
6
+
7
+ ## [0.4.3](https://github.com/kbrattli/react-native-smooth-clip-view/releases/tag/v0.4.3) — 2026-09-11
8
+
9
+ ### Rotate and fade the whole clip
10
+
11
+ Animate rotation and opacity alongside clip geometry on iOS and Android. The
12
+ aperture, its content, and its shadow move and fade together inside the fixed
13
+ host, using the existing controller and atomic group APIs.
14
+
15
+ - **Rotation:** pass degree or radian strings such as `'12deg'` or `'0.2rad'`.
16
+ Rotation is clockwise around the current aperture center, after content
17
+ translation and scale. Angles interpolate numerically, so `'720deg'` animates
18
+ two full turns from zero. Interruption snapshots preserve those turns.
19
+ - **Group opacity:** set `opacity` to fade content and shadow together, including
20
+ overlapping children. Finite values clamp to `[0, 1]`; spring rendering clamps
21
+ opacity while preserving the spring's internal motion state.
22
+ - **Animation continuity:** rotation and opacity participate in timing and
23
+ spring animations, cancellation snapshots, and background spring pause/resume.
24
+ Explicit spring velocity applies to these channels too.
25
+ - **Touch and accessibility:** hit testing follows the rotated aperture, including
26
+ clips rotated back into the viewport. Fully transparent content accepts no new
27
+ touches and is hidden from accessibility.
28
+ - **Rendering:** appearance updates use native compositor properties without
29
+ Yoga layout work or rebuilding unchanged clipping paths. Translucent groups
30
+ may require offscreen compositing.
31
+ - **Types and validation:** export `ClipRotation`, include rotation and opacity in
32
+ canonical presentations, and reject malformed angles or nonfinite values
33
+ atomically. Add JavaScript and native regression coverage for these behaviors.
34
+
35
+ ### Usage and upgrade notes
36
+
37
+ `rotation` defaults to `'0deg'` and `opacity` to `1`. Presentations are complete:
38
+ omitting either field resets it to its default. Snapshots return rotation in
39
+ radians. See [Rotate and fade a clip](./README.md#rotate-and-fade-a-clip) for a
40
+ controller example.
41
+
42
+ **Rebuild your native app after upgrading**, and run `pod install` on iOS. The
43
+ native presentation protocol has changed; JavaScript from 0.4.3 must run with the
44
+ matching native library, rather than an older binary updated only through OTA.
45
+
46
+ [All changes since 0.4.2](https://github.com/kbrattli/react-native-smooth-clip-view/compare/v0.4.2...v0.4.3)
47
+
48
+ Earlier release notes are available on [GitHub Releases](https://github.com/kbrattli/react-native-smooth-clip-view/releases).
package/README.md CHANGED
@@ -134,6 +134,21 @@ defaults match Reanimated 4.5: mass `4`, stiffness `900`, damping `120`, and
134
134
  relative energy threshold `6e-9`. A spring is rejected when its resolved native
135
135
  trajectory could make `contentScale` nonpositive.
136
136
 
137
+ `velocity` is a shared initial progress velocity in inverse seconds, defaulting
138
+ to `0`. Each changing channel starts with velocity proportional to its remaining
139
+ distance: `velocity * (target - start)`. This includes rotation (in radians) and
140
+ opacity. Public controllers and groups do not automatically inherit gesture
141
+ velocity; starting a new spring after `beginInteraction()` still defaults to `0`
142
+ unless `velocity` is supplied. Pausing and resuming an existing native spring
143
+ preserves its internal motion state; public snapshots return clamped opacity.
144
+
145
+ The internal native standalone path also supports inherited velocity. Its
146
+ projection uses only clip position, size, corner radii, and content translation.
147
+ Rotation, opacity, content scale, and shadow channels do not contribute to that
148
+ projection. The resulting normalized velocity can drive all changing channels,
149
+ including rotation and opacity, without mixing their units into the projection.
150
+ Rotation/opacity-only motion therefore supplies no inherited velocity.
151
+
137
152
  An animation requested before the host has a positive layout waits, then starts
138
153
  with its full duration. While the application is inactive, native animation
139
154
  transactions stay pending and do not report cancellation. Timing animations
@@ -189,6 +204,15 @@ APIs share validation, run ownership, and completion behavior.
189
204
  - Corner-overlap scaling follows CSS rules against the requested rectangle.
190
205
  - `contentTranslateX`, `contentTranslateY`, and positive `contentScale` animate
191
206
  with the aperture.
207
+ - `rotation` accepts degree or radian strings (for example, `'12deg'` or
208
+ `'0.2rad'`) and defaults to `'0deg'`. It rotates the aperture, content, and
209
+ shadow together, clockwise around the current aperture center. Existing
210
+ content translation and scale happen before this rotation. The host stays
211
+ fixed and crops the rotated result.
212
+ - `opacity` defaults to `1` and fades the aperture's content and shadow as one
213
+ group. Finite values clamp to `[0, 1]`; malformed angles and nonfinite values
214
+ reject the complete presentation. Spring opacity is clamped for rendering
215
+ without clamping its internal spring state.
192
216
  - Circular and continuous curves and independent corner radii are supported.
193
217
  - One outset `boxShadow` is supported. It escapes the aperture but not the host.
194
218
  - A fully off-host aperture is not touchable, even when only its shadow overlaps.
@@ -198,6 +222,52 @@ APIs share validation, run ownership, and completion behavior.
198
222
  Use `getSmoothClipCapabilities()` when a consumer needs to decide whether a
199
223
  complex native path can be promoted on the current platform.
200
224
 
225
+ ### Rotate and fade a clip
226
+
227
+ Rotation and opacity are presentation fields, so they use the same controller
228
+ and group methods as geometry. For example, start a tilted, translucent card
229
+ and animate it upright:
230
+
231
+ ```ts
232
+ const clip = useSmoothClipController({
233
+ clip: { x: 24, y: 80, width: 240, height: 320, radius: 24 },
234
+ contentTranslateX: 0,
235
+ contentTranslateY: 0,
236
+ rotation: '-12deg',
237
+ opacity: 0.4,
238
+ });
239
+
240
+ // Attach this controller to <SmoothClipView controller={clip}> as above.
241
+ const reveal = () =>
242
+ clip.react.animateTo(
243
+ {
244
+ clip: { x: 24, y: 80, width: 240, height: 320, radius: 24 },
245
+ contentTranslateX: 0,
246
+ contentTranslateY: 0,
247
+ rotation: '0deg',
248
+ opacity: 1,
249
+ },
250
+ { type: 'spring', stiffness: 180, damping: 22 }
251
+ );
252
+ ```
253
+
254
+ For gesture-driven updates, supply the same fields from a UI-runtime worklet:
255
+
256
+ ```ts
257
+ clip.ui.setFrame({ ...frame, rotation: '12deg', opacity: 0.5 });
258
+ ```
259
+
260
+ Angles interpolate numerically, so `'720deg'` requests two full turns from zero;
261
+ there is no automatic shortest-path wrapping. Interruption snapshots preserve
262
+ full turns and return rotation as a radian string. Each presentation is complete:
263
+ omitting rotation or opacity resets that field to its default. At zero opacity,
264
+ the content admits no new touches and is hidden from accessibility.
265
+
266
+ When upgrading to **0.4.3**, rebuild the native app (and run `pod install` on
267
+ iOS). The native presentation protocol includes new rotation and opacity
268
+ channels, so a JavaScript-only OTA update cannot upgrade an older native build.
269
+ See the [changelog](./CHANGELOG.md) for release details.
270
+
201
271
  ## Performance contract
202
272
 
203
273
  - One worklet-to-native call per `ui.setFrame`, or per group batch.
@@ -205,6 +275,10 @@ complex native path can be promoted on the current platform.
205
275
  - The shadow-disabled rendering path keeps no shadow drawing resources.
206
276
  - The fixed host is the maximum rendering viewport; consumers should size it to
207
277
  the region in which content and shadow may appear.
278
+ - Rotation and opacity update native compositor properties without Yoga work
279
+ or rebuilding unchanged clipping paths. Correctly fading overlapping children
280
+ may require offscreen compositing; large translucent groups can cost more GPU
281
+ time than opaque ones. No rasterization or software layer is forced.
208
282
 
209
283
  ## License
210
284
 
@@ -22,7 +22,7 @@ struct JSmoothClipView : facebook::jni::JavaClass<JSmoothClipView> {
22
22
  double contentTranslateXPx,
23
23
  double contentTranslateYPx,
24
24
  double contentScale,
25
- const Shadow &shadowPx) const;
25
+ const Shadow &shadowPx, double rotation, double opacity) const;
26
26
  void setAutonomousMotion(bool active) const;
27
27
  };
28
28
 
@@ -27,7 +27,7 @@ namespace smoothclip {
27
27
  namespace {
28
28
 
29
29
  constexpr double kMaxSafeJavaScriptInteger = 9007199254740991.0;
30
- constexpr size_t kPresentationStride = 21;
30
+ constexpr size_t kPresentationStride = 23;
31
31
  constexpr size_t kSnapshotStride = kPresentationStride + 1;
32
32
  constexpr size_t kMotionEntryStride = kPresentationStride * 2 + 2;
33
33
 
@@ -61,7 +61,8 @@ bool finiteBasePresentation(const Presentation &presentation) {
61
61
  std::isfinite(presentation.contentTranslateX) &&
62
62
  std::isfinite(presentation.contentTranslateY) &&
63
63
  std::isfinite(presentation.contentScale) &&
64
- presentation.contentScale > 0;
64
+ presentation.contentScale > 0 &&
65
+ std::isfinite(presentation.rotation) && std::isfinite(presentation.opacity);
65
66
  }
66
67
 
67
68
  bool finitePresentation(const Presentation &presentation) {
@@ -154,7 +155,7 @@ bool presentationAt(
154
155
  Shadow{
155
156
  packet[12] == 1.0,
156
157
  packet[13], packet[14], packet[15], packet[16],
157
- packet[17], packet[18], packet[19], packet[20]}};
158
+ packet[17], packet[18], packet[19], packet[20]}, packet[21], packet[22]};
158
159
  return finitePresentation(result);
159
160
  }
160
161
 
@@ -190,6 +191,8 @@ void appendPresentation(
190
191
  result.setValueAtIndex(runtime, offset++, presentation.shadow.offsetY);
191
192
  result.setValueAtIndex(runtime, offset++, presentation.shadow.blurRadius);
192
193
  result.setValueAtIndex(runtime, offset++, presentation.shadow.spreadDistance);
194
+ result.setValueAtIndex(runtime, offset++, presentation.rotation);
195
+ result.setValueAtIndex(runtime, offset++, presentation.opacity);
193
196
  }
194
197
 
195
198
  Array presentationArray(Runtime &runtime, const Presentation &presentation) {
@@ -551,9 +554,9 @@ void installBindings(
551
554
  });
552
555
 
553
556
  setHostFunction(
554
- runtime, bindings, "setClipFrameScalars", 22,
557
+ runtime, bindings, "setClipFrameScalars", 24,
555
558
  [](Runtime &, const Value &, const Value *args, size_t count) -> Value {
556
- if (count < 22) return Value::undefined();
559
+ if (count < 24) return Value::undefined();
557
560
  const double driverId = args[0].asNumber();
558
561
  Geometry geometry{
559
562
  args[1].asNumber(), args[2].asNumber(), args[3].asNumber(),
@@ -584,7 +587,7 @@ void installBindings(
584
587
  args[19].asNumber(),
585
588
  args[20].asNumber(),
586
589
  args[21].asNumber(),
587
- }};
590
+ }, args[22].asNumber(), args[23].asNumber()};
588
591
  if (validDriverId(driverId) && validCurve(args[9].asNumber()) &&
589
592
  finitePresentation(presentation)) {
590
593
  setPresentation(
@@ -828,6 +831,8 @@ void nativeRegisterView(
828
831
  jdouble shadowOffsetY,
829
832
  jdouble shadowBlurRadius,
830
833
  jdouble shadowSpreadDistance,
834
+ jdouble rotation,
835
+ jdouble opacity,
831
836
  jdouble density,
832
837
  jdouble widthPx,
833
838
  jdouble heightPx,
@@ -842,7 +847,8 @@ void nativeRegisterView(
842
847
  !std::isfinite(shadowBlue) || !std::isfinite(shadowAlpha) ||
843
848
  !std::isfinite(shadowOffsetX) ||
844
849
  !std::isfinite(shadowOffsetY) || !std::isfinite(shadowBlurRadius) ||
845
- !std::isfinite(shadowSpreadDistance) || contentScale <= 0 ||
850
+ !std::isfinite(shadowSpreadDistance) ||
851
+ !std::isfinite(rotation) || !std::isfinite(opacity) || contentScale <= 0 ||
846
852
  shadowRed < 0 || shadowRed > 1 || shadowGreen < 0 || shadowGreen > 1 ||
847
853
  shadowBlue < 0 || shadowBlue > 1 || shadowAlpha < 0 || shadowAlpha > 1 ||
848
854
  shadowBlurRadius < 0 ||
@@ -874,7 +880,7 @@ void nativeRegisterView(
874
880
  static_cast<uint64_t>(driverId),
875
881
  view,
876
882
  smoothclip::Presentation{
877
- geometry, contentTranslateX, contentTranslateY, contentScale, shadow},
883
+ geometry, contentTranslateX, contentTranslateY, contentScale, shadow, rotation, opacity},
878
884
  density,
879
885
  widthPx,
880
886
  heightPx,
@@ -458,7 +458,7 @@ void deliverToView(const ViewEntry &entry, const Presentation &presentation) {
458
458
  translateX,
459
459
  translateY,
460
460
  presentation.contentScale,
461
- shadowPx);
461
+ shadowPx, presentation.rotation, presentation.opacity);
462
462
  }
463
463
 
464
464
  void applyToViews(DriverState &state, const Presentation &presentation) {
@@ -1705,7 +1705,7 @@ void JSmoothClipView::applyClip(const Presentation &presentation) const {
1705
1705
  jdouble,
1706
1706
  jdouble,
1707
1707
  jdouble,
1708
- jdouble)>("setClipPresentationDip");
1708
+ jdouble, jdouble, jdouble)>("setClipPresentationDip");
1709
1709
  method(
1710
1710
  self(),
1711
1711
  presentation.clip.x,
@@ -1732,7 +1732,7 @@ void JSmoothClipView::applyClip(const Presentation &presentation) const {
1732
1732
  presentation.shadow.offsetX,
1733
1733
  presentation.shadow.offsetY,
1734
1734
  presentation.shadow.blurRadius,
1735
- presentation.shadow.spreadDistance);
1735
+ presentation.shadow.spreadDistance, presentation.rotation, presentation.opacity);
1736
1736
  }
1737
1737
 
1738
1738
  void JSmoothClipView::applyClipPx(
@@ -1740,7 +1740,7 @@ void JSmoothClipView::applyClipPx(
1740
1740
  double contentTranslateXPx,
1741
1741
  double contentTranslateYPx,
1742
1742
  double contentScale,
1743
- const Shadow &shadowPx) const {
1743
+ const Shadow &shadowPx, double rotation, double opacity) const {
1744
1744
  static const auto method =
1745
1745
  javaClassStatic()
1746
1746
  ->getMethod<void(
@@ -1764,7 +1764,7 @@ void JSmoothClipView::applyClipPx(
1764
1764
  jfloat,
1765
1765
  jfloat,
1766
1766
  jfloat,
1767
- jfloat)>("setClipPresentationPx");
1767
+ jfloat, jdouble, jfloat)>("setClipPresentationPx");
1768
1768
  method(
1769
1769
  self(),
1770
1770
  static_cast<jfloat>(clip.left),
@@ -1787,7 +1787,7 @@ void JSmoothClipView::applyClipPx(
1787
1787
  static_cast<jfloat>(shadowPx.offsetX),
1788
1788
  static_cast<jfloat>(shadowPx.offsetY),
1789
1789
  static_cast<jfloat>(shadowPx.blurRadius),
1790
- static_cast<jfloat>(shadowPx.spreadDistance));
1790
+ static_cast<jfloat>(shadowPx.spreadDistance), rotation, static_cast<jfloat>(opacity));
1791
1791
  }
1792
1792
 
1793
1793
  void JSmoothClipView::setAutonomousMotion(bool active) const {
@@ -240,3 +240,26 @@ internal fun clipAccessibility(
240
240
  } else {
241
241
  requestedAccessibility
242
242
  }
243
+
244
+ /** Conservative for rounded corners, exact for the rotated bounding rectangle. */
245
+ internal fun rotatedRectIntersectsHost(
246
+ left: Double, top: Double, right: Double, bottom: Double,
247
+ pivotX: Double, pivotY: Double, rotation: Double, hostWidth: Double, hostHeight: Double,
248
+ ): Boolean {
249
+ if (right <= left || bottom <= top || hostWidth <= 0 || hostHeight <= 0) return false
250
+ if (rotation == 0.0) return right > 0 && bottom > 0 && left < hostWidth && top < hostHeight
251
+ val c = kotlin.math.cos(rotation)
252
+ val s = kotlin.math.sin(rotation)
253
+ val dx = (left + right) / 2 - pivotX
254
+ val dy = (top + bottom) / 2 - pivotY
255
+ val x = pivotX + c * dx - s * dy - hostWidth / 2
256
+ val y = pivotY + s * dx + c * dy - hostHeight / 2
257
+ val hw = (right - left) / 2
258
+ val hh = (bottom - top) / 2
259
+ val ac = kotlin.math.abs(c)
260
+ val ass = kotlin.math.abs(s)
261
+ return kotlin.math.abs(x) < hostWidth / 2 + ac * hw + ass * hh &&
262
+ kotlin.math.abs(y) < hostHeight / 2 + ass * hw + ac * hh &&
263
+ kotlin.math.abs(c * x + s * y) < hw + ac * hostWidth / 2 + ass * hostHeight / 2 &&
264
+ kotlin.math.abs(-s * x + c * y) < hh + ass * hostWidth / 2 + ac * hostHeight / 2
265
+ }
@@ -43,6 +43,8 @@ internal object SmoothClipBindings {
43
43
  shadowOffsetY: Double,
44
44
  shadowBlurRadius: Double,
45
45
  shadowSpreadDistance: Double,
46
+ rotation: Double,
47
+ opacity: Double,
46
48
  density: Double,
47
49
  widthPx: Double,
48
50
  heightPx: Double,
@@ -140,6 +140,8 @@ class SmoothClipModule(context: ReactApplicationContext) :
140
140
  shadowOffsetY: Double,
141
141
  shadowBlurRadius: Double,
142
142
  shadowSpreadDistance: Double,
143
+ rotation: Double,
144
+ opacity: Double,
143
145
  ) = Unit
144
146
 
145
147
  override fun beginInteraction(driverId: Double): WritableArray =