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
@@ -1,68 +1,18 @@
1
1
  import NativeSmoothClipModule from './smoothClipNative';
2
- import {
3
- SMOOTH_CLIP_V1_CAPABILITIES,
4
- type SmoothClipCapabilities,
5
- } from './capabilityTypes';
6
-
7
- type VersionedModule = typeof NativeSmoothClipModule & {
8
- getPresentationProtocolVersion?: () => number;
9
- supportsAutonomousComplexPathAnimation?: () => boolean;
10
- };
2
+ import type { SmoothClipCapabilities } from './capabilityTypes';
11
3
 
12
4
  let cached: SmoothClipCapabilities | undefined;
13
5
 
14
6
  export function getSmoothClipCapabilities(): SmoothClipCapabilities {
15
7
  if (cached) return cached;
16
- const module = NativeSmoothClipModule as VersionedModule;
17
- let version = 1;
18
- try {
19
- version = module.getPresentationProtocolVersion?.() === 2 ? 2 : 1;
20
- } catch {
21
- version = 1;
22
- }
23
- if (version !== 2) {
24
- cached = SMOOTH_CLIP_V1_CAPABILITIES;
25
- return cached;
26
- }
27
8
  let autonomousComplexPathAnimation = false;
28
9
  try {
29
10
  autonomousComplexPathAnimation =
30
- module.supportsAutonomousComplexPathAnimation?.() === true;
11
+ NativeSmoothClipModule.supportsAutonomousComplexPathAnimation() === true;
31
12
  } catch {
32
13
  autonomousComplexPathAnimation = false;
33
14
  }
34
- const groups =
35
- typeof (
36
- module as Partial<{
37
- beginGroupInteractionV2: unknown;
38
- snapshotGroupV2: unknown;
39
- setClipPresentationBatchV2: unknown;
40
- animateTimingGroupV2: unknown;
41
- animateSpringGroupV2: unknown;
42
- animateKeyframesGroupV2: unknown;
43
- cancelAnimationGroupV2: unknown;
44
- }>
45
- ).beginGroupInteractionV2 === 'function' &&
46
- typeof (module as Partial<{ snapshotGroupV2: unknown }>).snapshotGroupV2 ===
47
- 'function' &&
48
- typeof (module as Partial<{ setClipPresentationBatchV2: unknown }>)
49
- .setClipPresentationBatchV2 === 'function' &&
50
- typeof (module as Partial<{ animateTimingGroupV2: unknown }>)
51
- .animateTimingGroupV2 === 'function' &&
52
- typeof (module as Partial<{ animateSpringGroupV2: unknown }>)
53
- .animateSpringGroupV2 === 'function' &&
54
- typeof (module as Partial<{ animateKeyframesGroupV2: unknown }>)
55
- .animateKeyframesGroupV2 === 'function' &&
56
- typeof (module as Partial<{ cancelAnimationGroupV2: unknown }>)
57
- .cancelAnimationGroupV2 === 'function' &&
58
- typeof (module as Partial<{ onClipGroupAnimationComplete: unknown }>)
59
- .onClipGroupAnimationComplete === 'function';
60
15
  cached = {
61
- presentationProtocolVersion: 2,
62
- groups,
63
- perCornerRadii: true,
64
- continuousCurve: true,
65
- contentScale: true,
66
16
  autonomousComplexPathAnimation,
67
17
  };
68
18
  return cached;
@@ -1,17 +1,3 @@
1
1
  export type SmoothClipCapabilities = Readonly<{
2
- presentationProtocolVersion: 1 | 2;
3
- groups: boolean;
4
- perCornerRadii: boolean;
5
- continuousCurve: boolean;
6
- contentScale: boolean;
7
2
  autonomousComplexPathAnimation: boolean;
8
3
  }>;
9
-
10
- export const SMOOTH_CLIP_V1_CAPABILITIES: SmoothClipCapabilities = {
11
- presentationProtocolVersion: 1,
12
- groups: false,
13
- perCornerRadii: false,
14
- continuousCurve: false,
15
- contentScale: false,
16
- autonomousComplexPathAnimation: false,
17
- };
@@ -0,0 +1,52 @@
1
+ import type { SmoothClipController, SmoothClipRef } from './controllerTypes';
2
+ import type { CanonicalSmoothClipPresentation } from './geometry';
3
+
4
+ export type InternalSmoothClipController = Readonly<{
5
+ ref: SmoothClipRef;
6
+ initialFrame: CanonicalSmoothClipPresentation;
7
+ }>;
8
+
9
+ export type InternalSmoothClipRef = SmoothClipRef & Readonly<{ id: number }>;
10
+
11
+ const controllers = new WeakMap<
12
+ SmoothClipController,
13
+ InternalSmoothClipController
14
+ >();
15
+
16
+ export function setControllerRef(
17
+ controller: SmoothClipController,
18
+ value: InternalSmoothClipController
19
+ ): void {
20
+ controllers.set(controller, value);
21
+ }
22
+
23
+ export function getControllerRef(
24
+ controller: SmoothClipController
25
+ ): InternalSmoothClipController {
26
+ const value = controllers.get(controller);
27
+ if (!value) {
28
+ throw new Error(
29
+ '[SmoothClipView] Controllers must be created by useSmoothClipController.'
30
+ );
31
+ }
32
+ return value;
33
+ }
34
+
35
+ export function unwrapSmoothClipRef(
36
+ ref: SmoothClipRef | undefined
37
+ ): InternalSmoothClipRef | undefined {
38
+ 'worklet';
39
+ const internal = ref as InternalSmoothClipRef | undefined;
40
+ if (
41
+ internal === undefined ||
42
+ !Number.isSafeInteger(internal.id) ||
43
+ internal.id <= 0
44
+ ) {
45
+ return undefined;
46
+ }
47
+ return internal;
48
+ }
49
+
50
+ export function createSmoothClipRef(id: number): SmoothClipRef {
51
+ return { id } as InternalSmoothClipRef;
52
+ }
@@ -0,0 +1,6 @@
1
+ import NativeSmoothClipModule from './NativeSmoothClipModule';
2
+
3
+ /** Destroy from the React Native runtime; native forwards to the UI thread. */
4
+ export function destroyController(controllerId: number): void {
5
+ NativeSmoothClipModule.destroyDriver(controllerId);
6
+ }
@@ -0,0 +1,87 @@
1
+ import type {
2
+ CanonicalSmoothClipPresentation,
3
+ ClipGeometry,
4
+ SmoothClipPresentation,
5
+ } from './geometry';
6
+
7
+ export type ClipReduceMotion = 'system' | 'always' | 'never';
8
+
9
+ type SmoothClipAnimationBase = Readonly<{
10
+ /** Resolved per transaction; it is never retained by the controller. */
11
+ reduceMotion?: ClipReduceMotion;
12
+ }>;
13
+
14
+ export type TimingClipAnimation = SmoothClipAnimationBase &
15
+ Readonly<{
16
+ type: 'timing';
17
+ duration: number;
18
+ controlPoints: readonly [number, number, number, number];
19
+ }>;
20
+
21
+ export type SpringClipAnimation = SmoothClipAnimationBase &
22
+ Readonly<{
23
+ type: 'spring';
24
+ mass?: number;
25
+ stiffness?: number;
26
+ damping?: number;
27
+ /** Normalized progress velocity in inverse seconds. */
28
+ velocity?: number;
29
+ /** Relative mechanical energy used to determine settlement. */
30
+ energyThreshold?: number;
31
+ }>;
32
+
33
+ export type SmoothClipAnimation = TimingClipAnimation | SpringClipAnimation;
34
+
35
+ declare const smoothClipRefBrand: unique symbol;
36
+ /** Opaque, worklet-safe identity for one mounted clip host. */
37
+ export type SmoothClipRef = Readonly<{
38
+ readonly [smoothClipRefBrand]: true;
39
+ }>;
40
+
41
+ declare const smoothClipRunBrand: unique symbol;
42
+ /** Opaque identity for a native transaction started on the UI runtime. */
43
+ export type SmoothClipRunHandle = Readonly<{
44
+ readonly [smoothClipRunBrand]: true;
45
+ }>;
46
+
47
+ export type SmoothClipCompletion = Readonly<{
48
+ finished: boolean;
49
+ /** Present only when supplied by the UI-runtime caller. */
50
+ completionTag?: number;
51
+ }>;
52
+
53
+ export type SmoothClipReactRun = Readonly<{
54
+ finished: Promise<boolean>;
55
+ cancel(): void;
56
+ }>;
57
+
58
+ export type SmoothClipControllerOptions = Readonly<{
59
+ /** Stable React-runtime callback for UI- and React-runtime transactions. */
60
+ onAnimationComplete?: (result: SmoothClipCompletion) => void;
61
+ }>;
62
+
63
+ export type SmoothClipControllerUI = Readonly<{
64
+ beginInteraction(): CanonicalSmoothClipPresentation;
65
+ setFrame(frame: SmoothClipPresentation): void;
66
+ animateTo(
67
+ target: SmoothClipPresentation,
68
+ animation: SmoothClipAnimation,
69
+ completionTag?: number
70
+ ): SmoothClipRunHandle | null;
71
+ cancel(run: SmoothClipRunHandle): CanonicalSmoothClipPresentation;
72
+ }>;
73
+
74
+ export type SmoothClipControllerReact = Readonly<{
75
+ animateTo(
76
+ target: SmoothClipPresentation,
77
+ animation: SmoothClipAnimation
78
+ ): SmoothClipReactRun;
79
+ }>;
80
+
81
+ export type SmoothClipController = Readonly<{
82
+ ref: SmoothClipRef;
83
+ ui: SmoothClipControllerUI;
84
+ react: SmoothClipControllerReact;
85
+ }>;
86
+
87
+ export type SmoothClipInitialFrame = ClipGeometry | SmoothClipPresentation;
@@ -0,0 +1 @@
1
+ export { useSmoothClipController } from './controllers.native';
@@ -0,0 +1 @@
1
+ export { useSmoothClipController } from './controllers.native';
@@ -0,0 +1,104 @@
1
+ import { useEffect, useRef } from 'react';
2
+ import { scheduleOnUI } from 'react-native-worklets';
3
+ import type {
4
+ SmoothClipController,
5
+ SmoothClipControllerOptions,
6
+ SmoothClipInitialFrame,
7
+ } from './controllerTypes';
8
+ import { createSmoothClipRef, setControllerRef } from './controllerInternals';
9
+ import { destroyController } from './controllerLifecycle';
10
+ import { allocateSmoothClipId } from './ids';
11
+ import {
12
+ canonicalizeClipPresentation,
13
+ createClipPresentation,
14
+ type CanonicalSmoothClipPresentation,
15
+ } from './geometry';
16
+ import { useSmoothClipGroup } from './groups.native';
17
+ import { presentationPacket } from './presentationCodec';
18
+ import NativeSmoothClipModule from './smoothClipNative';
19
+
20
+ const setClipPresentationHostFunction =
21
+ NativeSmoothClipModule.setClipPresentation;
22
+
23
+ export function useSmoothClipController(
24
+ initialValue: SmoothClipInitialFrame,
25
+ options: SmoothClipControllerOptions = {}
26
+ ): SmoothClipController {
27
+ // A controller is intentionally a one-member transaction group. This keeps
28
+ // validation, native dispatch, cancellation, completion, and lifecycle races
29
+ // on the same path as multi-member transitions.
30
+ const group = useSmoothClipGroup(options);
31
+ const initialRequested =
32
+ 'clip' in initialValue
33
+ ? initialValue
34
+ : createClipPresentation(initialValue);
35
+ const canonicalInitial = canonicalizeClipPresentation(initialRequested);
36
+ if (canonicalInitial === null) {
37
+ throw new Error('[SmoothClipView] Initial presentation must be finite.');
38
+ }
39
+ const initialFrameRef = useRef<CanonicalSmoothClipPresentation | null>(null);
40
+ initialFrameRef.current ??= canonicalInitial;
41
+ const initialFrame = initialFrameRef.current;
42
+
43
+ const idRef = useRef(0);
44
+ const controllerRef = useRef<SmoothClipController | null>(null);
45
+ if (idRef.current === 0) idRef.current = allocateSmoothClipId();
46
+ const controllerId = idRef.current;
47
+
48
+ if (controllerRef.current === null) {
49
+ const ref = createSmoothClipRef(controllerId);
50
+ const controller: SmoothClipController = {
51
+ ref,
52
+ ui: {
53
+ beginInteraction() {
54
+ 'worklet';
55
+ return group.ui.beginInteraction([ref])[0]?.frame ?? initialFrame;
56
+ },
57
+ setFrame(frame) {
58
+ 'worklet';
59
+ group.ui.setFrames([{ clip: ref, frame }]);
60
+ },
61
+ animateTo(target, animation, completionTag = 0) {
62
+ 'worklet';
63
+ return group.ui.animateTo(
64
+ [{ clip: ref, target }],
65
+ animation,
66
+ completionTag
67
+ );
68
+ },
69
+ cancel(run) {
70
+ 'worklet';
71
+ return group.ui.cancel(run)[0]?.frame ?? initialFrame;
72
+ },
73
+ },
74
+ react: {
75
+ animateTo(target, animation) {
76
+ return group.react.animateTo([{ clip: ref, target }], animation);
77
+ },
78
+ },
79
+ };
80
+ setControllerRef(controller, { ref, initialFrame });
81
+ controllerRef.current = controller;
82
+ }
83
+
84
+ useEffect(() => {
85
+ scheduleOnUI(
86
+ (id: number, frame: CanonicalSmoothClipPresentation) => {
87
+ 'worklet';
88
+ setClipPresentationHostFunction(
89
+ id,
90
+ presentationPacket(frame),
91
+ true,
92
+ false
93
+ );
94
+ },
95
+ controllerId,
96
+ initialFrame
97
+ );
98
+ return () => {
99
+ destroyController(controllerId);
100
+ };
101
+ }, [controllerId, initialFrame]);
102
+
103
+ return controllerRef.current;
104
+ }
@@ -0,0 +1 @@
1
+ export { useSmoothClipController } from './controllers.native';
package/src/geometry.ts CHANGED
@@ -1,5 +1,28 @@
1
+ /* eslint-disable no-bitwise -- React Native colors are converted to RRGGBBAA */
2
+ import type { ColorValue } from 'react-native';
3
+ import { processColor } from 'react-native-reanimated';
4
+
1
5
  export type ClipCurve = 'circular' | 'continuous';
2
6
 
7
+ /** One numeric outset shadow following React Native's `boxShadow` field names. */
8
+ export type ClipBoxShadow = Readonly<{
9
+ color?: ColorValue;
10
+ offsetX: number;
11
+ offsetY: number;
12
+ /** CSS box-shadow blur radius. iOS converts this to CALayer radius / 2. */
13
+ blurRadius?: number;
14
+ spreadDistance?: number;
15
+ }>;
16
+
17
+ export type CanonicalClipBoxShadow = Readonly<{
18
+ /** React Native numeric RRGGBBAA color; canonicalization is idempotent. */
19
+ color: ColorValue;
20
+ offsetX: number;
21
+ offsetY: number;
22
+ blurRadius: number;
23
+ spreadDistance: number;
24
+ }>;
25
+
3
26
  export type ClipGeometry = Readonly<{
4
27
  x: number;
5
28
  y: number;
@@ -19,7 +42,7 @@ export type CanonicalClipGeometry = Readonly<{
19
42
  y: number;
20
43
  width: number;
21
44
  height: number;
22
- /** Normalized legacy all-corners value retained for V1 compatibility. */
45
+ /** Canonical all-corners value; zero when the corner radii differ. */
23
46
  radius: number;
24
47
  topLeftRadius: number;
25
48
  topRightRadius: number;
@@ -28,17 +51,13 @@ export type CanonicalClipGeometry = Readonly<{
28
51
  curve: ClipCurve;
29
52
  }>;
30
53
 
31
- export type ClipBounds = Readonly<{
32
- width: number;
33
- height: number;
34
- }>;
35
-
36
54
  export type SmoothClipPresentation = Readonly<{
37
55
  clip: ClipGeometry;
38
56
  contentTranslateX: number;
39
57
  contentTranslateY: number;
40
58
  /** Defaults to 1 when omitted. */
41
59
  contentScale?: number;
60
+ boxShadow?: ClipBoxShadow;
42
61
  }>;
43
62
 
44
63
  export type CanonicalSmoothClipPresentation = Readonly<{
@@ -46,8 +65,11 @@ export type CanonicalSmoothClipPresentation = Readonly<{
46
65
  contentTranslateX: number;
47
66
  contentTranslateY: number;
48
67
  contentScale: number;
68
+ boxShadow?: CanonicalClipBoxShadow;
49
69
  }>;
50
70
 
71
+ const DEFAULT_SHADOW_COLOR = '#000000ff';
72
+
51
73
  function isClipCurve(curve: ClipCurve | undefined): boolean {
52
74
  'worklet';
53
75
  return curve === undefined || curve === 'circular' || curve === 'continuous';
@@ -58,14 +80,74 @@ function isFiniteOptionalNumber(value: number | undefined): boolean {
58
80
  return value === undefined || Number.isFinite(value);
59
81
  }
60
82
 
83
+ function canonicalizeClipBoxShadow(
84
+ shadow: ClipBoxShadow | undefined
85
+ ): CanonicalClipBoxShadow | null | undefined {
86
+ 'worklet';
87
+ if (shadow === undefined) return undefined;
88
+ const { offsetX, offsetY } = shadow;
89
+ const blurRadius = shadow.blurRadius ?? 0;
90
+ const spreadDistance = shadow.spreadDistance ?? 0;
91
+ if (
92
+ !Number.isFinite(offsetX) ||
93
+ !Number.isFinite(offsetY) ||
94
+ !Number.isFinite(blurRadius) ||
95
+ !Number.isFinite(spreadDistance)
96
+ ) {
97
+ return null;
98
+ }
99
+ const processedColor =
100
+ shadow.color === undefined
101
+ ? processColor(DEFAULT_SHADOW_COLOR)
102
+ : processColor(shadow.color);
103
+ // Platform and dynamic colors cannot be represented by the scalar native
104
+ // animation protocol. Callers can resolve those colors before presenting.
105
+ if (typeof processedColor !== 'number') return null;
106
+ const unsignedProcessedColor = processedColor >>> 0;
107
+ const color =
108
+ ((unsignedProcessedColor << 8) | (unsignedProcessedColor >>> 24)) >>> 0;
109
+ return {
110
+ color: color as unknown as ColorValue,
111
+ offsetX,
112
+ offsetY,
113
+ blurRadius: Math.max(0, blurRadius),
114
+ spreadDistance,
115
+ };
116
+ }
117
+
118
+ function clipBoxShadowEquals(
119
+ first: ClipBoxShadow | undefined,
120
+ second: ClipBoxShadow | undefined
121
+ ): boolean {
122
+ 'worklet';
123
+ if (first === undefined || second === undefined) return first === second;
124
+ const canonicalFirst = canonicalizeClipBoxShadow(first);
125
+ const canonicalSecond = canonicalizeClipBoxShadow(second);
126
+ return (
127
+ canonicalFirst !== null &&
128
+ canonicalFirst !== undefined &&
129
+ canonicalSecond !== null &&
130
+ canonicalSecond !== undefined &&
131
+ canonicalFirst.color === canonicalSecond.color &&
132
+ canonicalFirst.offsetX === canonicalSecond.offsetX &&
133
+ canonicalFirst.offsetY === canonicalSecond.offsetY &&
134
+ canonicalFirst.blurRadius === canonicalSecond.blurRadius &&
135
+ canonicalFirst.spreadDistance === canonicalSecond.spreadDistance
136
+ );
137
+ }
138
+
61
139
  export function isFiniteClipGeometry(geometry: ClipGeometry): boolean {
62
140
  'worklet';
141
+ const width = Math.max(0, geometry.width);
142
+ const height = Math.max(0, geometry.height);
63
143
  return (
64
144
  Number.isFinite(geometry.x) &&
65
145
  Number.isFinite(geometry.y) &&
66
146
  Number.isFinite(geometry.width) &&
67
147
  Number.isFinite(geometry.height) &&
68
148
  Number.isFinite(geometry.radius) &&
149
+ Number.isFinite(geometry.x + width) &&
150
+ Number.isFinite(geometry.y + height) &&
69
151
  isFiniteOptionalNumber(geometry.topLeftRadius) &&
70
152
  isFiniteOptionalNumber(geometry.topRightRadius) &&
71
153
  isFiniteOptionalNumber(geometry.bottomRightRadius) &&
@@ -129,12 +211,14 @@ export function isFiniteClipPresentation(
129
211
  ): boolean {
130
212
  'worklet';
131
213
  const contentScale = resolvedContentScale(presentation);
214
+ const shadow = canonicalizeClipBoxShadow(presentation.boxShadow);
132
215
  return (
133
216
  isFiniteClipGeometry(presentation.clip) &&
134
217
  Number.isFinite(presentation.contentTranslateX) &&
135
218
  Number.isFinite(presentation.contentTranslateY) &&
136
219
  Number.isFinite(contentScale) &&
137
- contentScale > 0
220
+ contentScale > 0 &&
221
+ shadow !== null
138
222
  );
139
223
  }
140
224
 
@@ -148,7 +232,8 @@ export function clipPresentationEquals(
148
232
  clipGeometryEquals(first.clip, second.clip) &&
149
233
  first.contentTranslateX === second.contentTranslateX &&
150
234
  first.contentTranslateY === second.contentTranslateY &&
151
- resolvedContentScale(first) === resolvedContentScale(second)
235
+ resolvedContentScale(first) === resolvedContentScale(second) &&
236
+ clipBoxShadowEquals(first.boxShadow, second.boxShadow)
152
237
  );
153
238
  }
154
239
 
@@ -179,15 +264,15 @@ function createCanonicalClipGeometry(
179
264
  if (left > 0) scale = Math.min(scale, height / left);
180
265
  scale = Math.max(0, Math.min(1, scale));
181
266
 
182
- const normalizedTopLeftRadius = topLeftRadius * scale;
183
- const normalizedTopRightRadius = topRightRadius * scale;
184
- const normalizedBottomRightRadius = bottomRightRadius * scale;
185
- const normalizedBottomLeftRadius = bottomLeftRadius * scale;
267
+ const canonicalTopLeftRadius = topLeftRadius * scale;
268
+ const canonicalTopRightRadius = topRightRadius * scale;
269
+ const canonicalBottomRightRadius = bottomRightRadius * scale;
270
+ const canonicalBottomLeftRadius = bottomLeftRadius * scale;
186
271
  const radius =
187
- normalizedTopLeftRadius === normalizedTopRightRadius &&
188
- normalizedTopRightRadius === normalizedBottomRightRadius &&
189
- normalizedBottomRightRadius === normalizedBottomLeftRadius
190
- ? normalizedTopLeftRadius
272
+ canonicalTopLeftRadius === canonicalTopRightRadius &&
273
+ canonicalTopRightRadius === canonicalBottomRightRadius &&
274
+ canonicalBottomRightRadius === canonicalBottomLeftRadius
275
+ ? canonicalTopLeftRadius
191
276
  : 0;
192
277
 
193
278
  return {
@@ -196,10 +281,10 @@ function createCanonicalClipGeometry(
196
281
  width,
197
282
  height,
198
283
  radius,
199
- topLeftRadius: normalizedTopLeftRadius,
200
- topRightRadius: normalizedTopRightRadius,
201
- bottomRightRadius: normalizedBottomRightRadius,
202
- bottomLeftRadius: normalizedBottomLeftRadius,
284
+ topLeftRadius: canonicalTopLeftRadius,
285
+ topRightRadius: canonicalTopRightRadius,
286
+ bottomRightRadius: canonicalBottomRightRadius,
287
+ bottomLeftRadius: canonicalBottomLeftRadius,
203
288
  curve: resolvedCurve(geometry),
204
289
  };
205
290
  }
@@ -232,12 +317,15 @@ export function canonicalizeClipPresentation(
232
317
 
233
318
  const clip = canonicalizeClipGeometry(presentation.clip);
234
319
  if (clip === null) return null;
320
+ const boxShadow = canonicalizeClipBoxShadow(presentation.boxShadow);
321
+ if (boxShadow === null) return null;
235
322
 
236
323
  return {
237
324
  clip,
238
325
  contentTranslateX: presentation.contentTranslateX,
239
326
  contentTranslateY: presentation.contentTranslateY,
240
327
  contentScale: resolvedContentScale(presentation),
328
+ ...(boxShadow === undefined ? {} : { boxShadow }),
241
329
  };
242
330
  }
243
331
 
@@ -247,10 +335,12 @@ export function createClipPresentation(
247
335
  clip: ClipGeometry,
248
336
  contentTranslateX = 0,
249
337
  contentTranslateY = 0,
250
- contentScale = 1
338
+ contentScale = 1,
339
+ boxShadow?: ClipBoxShadow
251
340
  ): CanonicalSmoothClipPresentation {
252
341
  'worklet';
253
342
  const canonicalClip = canonicalizeClipGeometry(clip);
343
+ const canonicalShadow = canonicalizeClipBoxShadow(boxShadow);
254
344
  return {
255
345
  // Keep invalid input observably invalid so downstream validation remains
256
346
  // atomic instead of accidentally sanitizing a required scalar.
@@ -269,58 +359,12 @@ export function createClipPresentation(
269
359
  contentTranslateX,
270
360
  contentTranslateY,
271
361
  contentScale,
272
- };
273
- }
274
-
275
- /**
276
- * Mirrors the native geometry contract for tests and non-native fallbacks.
277
- * Native remains authoritative because its bounds are the actual rendered size.
278
- */
279
- export function normalizeClipGeometry(
280
- geometry: ClipGeometry,
281
- bounds: ClipBounds
282
- ): CanonicalClipGeometry | null {
283
- 'worklet';
284
- if (
285
- !isFiniteClipGeometry(geometry) ||
286
- !Number.isFinite(bounds.width) ||
287
- !Number.isFinite(bounds.height)
288
- ) {
289
- return null;
290
- }
291
-
292
- const boundsWidth = Math.max(0, bounds.width);
293
- const boundsHeight = Math.max(0, bounds.height);
294
- const requestedWidth = Math.max(0, geometry.width);
295
- const requestedHeight = Math.max(0, geometry.height);
296
-
297
- const left = Math.min(boundsWidth, Math.max(0, geometry.x));
298
- const top = Math.min(boundsHeight, Math.max(0, geometry.y));
299
- const right = Math.min(boundsWidth, Math.max(0, geometry.x + requestedWidth));
300
- const bottom = Math.min(
301
- boundsHeight,
302
- Math.max(0, geometry.y + requestedHeight)
303
- );
304
- const width = Math.max(0, right - left);
305
- const height = Math.max(0, bottom - top);
306
-
307
- return createCanonicalClipGeometry(geometry, left, top, width, height);
308
- }
309
-
310
- export function normalizeClipPresentation(
311
- presentation: SmoothClipPresentation,
312
- bounds: ClipBounds
313
- ): CanonicalSmoothClipPresentation | null {
314
- 'worklet';
315
- if (!isFiniteClipPresentation(presentation)) return null;
316
-
317
- const clip = normalizeClipGeometry(presentation.clip, bounds);
318
- if (clip === null) return null;
319
-
320
- return {
321
- clip,
322
- contentTranslateX: presentation.contentTranslateX,
323
- contentTranslateY: presentation.contentTranslateY,
324
- contentScale: resolvedContentScale(presentation),
362
+ ...(canonicalShadow === undefined
363
+ ? {}
364
+ : {
365
+ // Match the geometry constructor's invalid-input behavior: retain the
366
+ // bad source value so the driver's atomic validation rejects it.
367
+ boxShadow: (canonicalShadow ?? boxShadow) as CanonicalClipBoxShadow,
368
+ }),
325
369
  };
326
370
  }