react-native-smooth-clip-view 0.1.0 → 0.2.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 (113) hide show
  1. package/README.md +171 -43
  2. package/SmoothClipView.podspec +8 -2
  3. package/android/CMakeLists.txt +39 -0
  4. package/android/build.gradle +69 -2
  5. package/android/proguard-rules.pro +6 -0
  6. package/android/src/main/cpp/SmoothClipAndroid.h +59 -0
  7. package/android/src/main/cpp/SmoothClipBindings.cpp +428 -0
  8. package/android/src/main/cpp/SmoothClipRegistry.cpp +945 -0
  9. package/android/src/main/java/com/smoothclipview/ClipGeometryNormalizer.kt +25 -0
  10. package/android/src/main/java/com/smoothclipview/SmoothClipBindings.kt +55 -0
  11. package/android/src/main/java/com/smoothclipview/SmoothClipModule.kt +128 -0
  12. package/android/src/main/java/com/smoothclipview/SmoothClipView.kt +164 -30
  13. package/android/src/main/java/com/smoothclipview/SmoothClipViewManager.kt +107 -10
  14. package/android/src/main/java/com/smoothclipview/SmoothClipViewPackage.kt +18 -2
  15. package/cpp/SmoothClipRegistry.h +93 -0
  16. package/cpp/SmoothClipSharedGeometry.h +50 -0
  17. package/ios/SmoothClipModuleProvider.h +9 -0
  18. package/ios/SmoothClipModuleProvider.mm +16 -0
  19. package/ios/SmoothClipRegistry.mm +754 -0
  20. package/ios/SmoothClipTurboModule.cpp +290 -0
  21. package/ios/SmoothClipTurboModule.h +132 -0
  22. package/ios/SmoothClipView.mm +976 -85
  23. package/ios/SmoothClipViewRegistry.h +27 -0
  24. package/lib/module/NativeSmoothClipModule.js +5 -0
  25. package/lib/module/NativeSmoothClipModule.js.map +1 -0
  26. package/lib/module/SmoothClipView.android.js +4 -0
  27. package/lib/module/SmoothClipView.android.js.map +1 -0
  28. package/lib/module/SmoothClipView.ios.js +34 -0
  29. package/lib/module/SmoothClipView.ios.js.map +1 -0
  30. package/lib/module/SmoothClipView.js +26 -12
  31. package/lib/module/SmoothClipView.js.map +1 -1
  32. package/lib/module/SmoothClipView.web.js +28 -12
  33. package/lib/module/SmoothClipView.web.js.map +1 -1
  34. package/lib/module/SmoothClipViewNativeComponent.ts +14 -1
  35. package/lib/module/driverState.js +102 -0
  36. package/lib/module/driverState.js.map +1 -0
  37. package/lib/module/driverTypes.js +4 -0
  38. package/lib/module/driverTypes.js.map +1 -0
  39. package/lib/module/drivers.android.js +4 -0
  40. package/lib/module/drivers.android.js.map +1 -0
  41. package/lib/module/drivers.ios.js +380 -0
  42. package/lib/module/drivers.ios.js.map +1 -0
  43. package/lib/module/drivers.js +276 -0
  44. package/lib/module/drivers.js.map +1 -0
  45. package/lib/module/geometry.js +19 -0
  46. package/lib/module/geometry.js.map +1 -1
  47. package/lib/module/index.js +2 -0
  48. package/lib/module/index.js.map +1 -1
  49. package/lib/module/index.web.js +2 -0
  50. package/lib/module/index.web.js.map +1 -1
  51. package/lib/module/nativeCompletion.js +21 -0
  52. package/lib/module/nativeCompletion.js.map +1 -0
  53. package/lib/module/reactRequests.js +50 -0
  54. package/lib/module/reactRequests.js.map +1 -0
  55. package/lib/module/smoothClipNative.android.js +15 -0
  56. package/lib/module/smoothClipNative.android.js.map +1 -0
  57. package/lib/module/smoothClipNative.js +8 -0
  58. package/lib/module/smoothClipNative.js.map +1 -0
  59. package/lib/typescript/src/NativeSmoothClipModule.d.ts +19 -0
  60. package/lib/typescript/src/NativeSmoothClipModule.d.ts.map +1 -0
  61. package/lib/typescript/src/SmoothClipView.android.d.ts +3 -0
  62. package/lib/typescript/src/SmoothClipView.android.d.ts.map +1 -0
  63. package/lib/typescript/src/SmoothClipView.d.ts +5 -6
  64. package/lib/typescript/src/SmoothClipView.d.ts.map +1 -1
  65. package/lib/typescript/src/SmoothClipView.ios.d.ts +9 -0
  66. package/lib/typescript/src/SmoothClipView.ios.d.ts.map +1 -0
  67. package/lib/typescript/src/SmoothClipView.web.d.ts +5 -7
  68. package/lib/typescript/src/SmoothClipView.web.d.ts.map +1 -1
  69. package/lib/typescript/src/SmoothClipViewNativeComponent.d.ts +4 -0
  70. package/lib/typescript/src/SmoothClipViewNativeComponent.d.ts.map +1 -1
  71. package/lib/typescript/src/driverState.d.ts +31 -0
  72. package/lib/typescript/src/driverState.d.ts.map +1 -0
  73. package/lib/typescript/src/driverTypes.d.ts +61 -0
  74. package/lib/typescript/src/driverTypes.d.ts.map +1 -0
  75. package/lib/typescript/src/drivers.android.d.ts +3 -0
  76. package/lib/typescript/src/drivers.android.d.ts.map +1 -0
  77. package/lib/typescript/src/drivers.d.ts +5 -0
  78. package/lib/typescript/src/drivers.d.ts.map +1 -0
  79. package/lib/typescript/src/drivers.ios.d.ts +5 -0
  80. package/lib/typescript/src/drivers.ios.d.ts.map +1 -0
  81. package/lib/typescript/src/geometry.d.ts +8 -0
  82. package/lib/typescript/src/geometry.d.ts.map +1 -1
  83. package/lib/typescript/src/index.d.ts +4 -1
  84. package/lib/typescript/src/index.d.ts.map +1 -1
  85. package/lib/typescript/src/index.web.d.ts +4 -1
  86. package/lib/typescript/src/index.web.d.ts.map +1 -1
  87. package/lib/typescript/src/nativeCompletion.d.ts +7 -0
  88. package/lib/typescript/src/nativeCompletion.d.ts.map +1 -0
  89. package/lib/typescript/src/reactRequests.d.ts +7 -0
  90. package/lib/typescript/src/reactRequests.d.ts.map +1 -0
  91. package/lib/typescript/src/smoothClipNative.android.d.ts +4 -0
  92. package/lib/typescript/src/smoothClipNative.android.d.ts.map +1 -0
  93. package/lib/typescript/src/smoothClipNative.d.ts +3 -0
  94. package/lib/typescript/src/smoothClipNative.d.ts.map +1 -0
  95. package/package.json +12 -7
  96. package/src/NativeSmoothClipModule.ts +104 -0
  97. package/src/SmoothClipView.android.tsx +2 -0
  98. package/src/SmoothClipView.ios.tsx +36 -0
  99. package/src/SmoothClipView.tsx +31 -27
  100. package/src/SmoothClipView.web.tsx +26 -22
  101. package/src/SmoothClipViewNativeComponent.ts +14 -1
  102. package/src/driverState.ts +150 -0
  103. package/src/driverTypes.ts +91 -0
  104. package/src/drivers.android.ts +6 -0
  105. package/src/drivers.ios.ts +665 -0
  106. package/src/drivers.ts +416 -0
  107. package/src/geometry.ts +39 -0
  108. package/src/index.ts +19 -1
  109. package/src/index.web.ts +19 -1
  110. package/src/nativeCompletion.ts +37 -0
  111. package/src/reactRequests.ts +64 -0
  112. package/src/smoothClipNative.android.ts +20 -0
  113. package/src/smoothClipNative.ts +5 -0
@@ -1,62 +1,66 @@
1
- import type { ReactNode } from 'react';
1
+ import { type ReactNode, useEffect } from 'react';
2
2
  import { type ViewProps, type ViewStyle } from 'react-native';
3
3
  import Animated, {
4
4
  useAnimatedReaction,
5
5
  useAnimatedStyle,
6
6
  useSharedValue,
7
- type SharedValue,
8
7
  } from 'react-native-reanimated';
9
- import {
10
- clipGeometryEquals,
11
- isFiniteClipGeometry,
12
- type ClipGeometry,
13
- } from './geometry';
8
+ import type { SmoothClipDriver } from './driverTypes';
9
+ import { getDriverState, registerDriverView } from './driverState';
10
+ import { clipPresentationEquals, isFiniteClipPresentation } from './geometry';
14
11
  import { createWebClipPath } from './webClipPath';
15
12
 
16
13
  export type SmoothClipViewProps = ViewProps & {
17
- initialClip: ClipGeometry;
18
- animatedClip: SharedValue<ClipGeometry>;
14
+ driver: SmoothClipDriver;
19
15
  children?: ReactNode;
20
16
  };
21
17
 
22
- /** Build-safe fallback. Web performance is intentionally outside the POC. */
18
+ /** Fixed-layout CSS fallback. */
23
19
  export function SmoothClipView({
24
- initialClip,
25
- animatedClip,
20
+ driver,
26
21
  children,
27
22
  style,
28
23
  ...viewProps
29
24
  }: SmoothClipViewProps) {
30
- const acceptedClip = useSharedValue(initialClip);
25
+ const { initialPresentation, source } = getDriverState(driver);
26
+ useEffect(() => registerDriverView(driver), [driver]);
27
+ const acceptedPresentation = useSharedValue(initialPresentation);
31
28
  const initialClipStyle = {
32
- clipPath: createWebClipPath(initialClip),
29
+ clipPath: createWebClipPath(initialPresentation.clip),
33
30
  overflow: 'hidden',
34
31
  } as unknown as ViewStyle;
35
32
 
36
33
  useAnimatedReaction(
37
- () => animatedClip.value,
38
- (clip, previousClip) => {
34
+ () => source.value,
35
+ (presentation, previousPresentation) => {
39
36
  if (
40
- isFiniteClipGeometry(clip) &&
41
- !clipGeometryEquals(previousClip, clip)
37
+ isFiniteClipPresentation(presentation) &&
38
+ !clipPresentationEquals(previousPresentation, presentation)
42
39
  ) {
43
- acceptedClip.value = clip;
40
+ acceptedPresentation.value = presentation;
44
41
  }
45
42
  },
46
- [animatedClip, acceptedClip]
43
+ [source, acceptedPresentation]
47
44
  );
48
45
 
49
46
  const clipStyle = useAnimatedStyle(() => {
50
- const clip = acceptedClip.value;
47
+ const clip = acceptedPresentation.value.clip;
51
48
  return {
52
49
  clipPath: createWebClipPath(clip),
53
50
  overflow: 'hidden',
54
51
  };
55
52
  });
56
53
 
54
+ const contentStyle = useAnimatedStyle(() => ({
55
+ transform: [
56
+ { translateX: acceptedPresentation.value.contentTranslateX },
57
+ { translateY: acceptedPresentation.value.contentTranslateY },
58
+ ],
59
+ }));
60
+
57
61
  return (
58
62
  <Animated.View {...viewProps} style={[style, initialClipStyle, clipStyle]}>
59
- {children}
63
+ <Animated.View style={contentStyle}>{children}</Animated.View>
60
64
  </Animated.View>
61
65
  );
62
66
  }
@@ -7,11 +7,14 @@ import {
7
7
  } from 'react-native';
8
8
 
9
9
  export interface NativeProps extends ViewProps {
10
+ driverId: CodegenTypes.Double;
10
11
  initialClipX: CodegenTypes.Double;
11
12
  initialClipY: CodegenTypes.Double;
12
13
  initialClipWidth: CodegenTypes.Double;
13
14
  initialClipHeight: CodegenTypes.Double;
14
15
  initialClipRadius: CodegenTypes.Double;
16
+ initialContentTranslateX: CodegenTypes.Double;
17
+ initialContentTranslateY: CodegenTypes.Double;
15
18
  }
16
19
 
17
20
  interface NativeCommands {
@@ -23,10 +26,20 @@ interface NativeCommands {
23
26
  height: CodegenTypes.Double,
24
27
  radius: CodegenTypes.Double
25
28
  ) => void;
29
+ setClipPresentation: (
30
+ viewRef: React.ElementRef<HostComponent<NativeProps>>,
31
+ x: CodegenTypes.Double,
32
+ y: CodegenTypes.Double,
33
+ width: CodegenTypes.Double,
34
+ height: CodegenTypes.Double,
35
+ radius: CodegenTypes.Double,
36
+ contentTranslateX: CodegenTypes.Double,
37
+ contentTranslateY: CodegenTypes.Double
38
+ ) => void;
26
39
  }
27
40
 
28
41
  export const Commands: NativeCommands = codegenNativeCommands<NativeCommands>({
29
- supportedCommands: ['setClipGeometry'],
42
+ supportedCommands: ['setClipGeometry', 'setClipPresentation'],
30
43
  });
31
44
 
32
45
  export default codegenNativeComponent<NativeProps>(
@@ -0,0 +1,150 @@
1
+ import type { SharedValue } from 'react-native-reanimated';
2
+ import type { ClipAnimationResult, SmoothClipDriver } from './driverTypes';
3
+ import type { SmoothClipPresentation } from './geometry';
4
+
5
+ export type DriverState = {
6
+ driverId: number;
7
+ initialPresentation: SmoothClipPresentation;
8
+ source: SharedValue<SmoothClipPresentation>;
9
+ activeAnimationId?: SharedValue<number>;
10
+ ownership?: SharedValue<number>;
11
+ onAnimationComplete: {
12
+ current: ((result: ClipAnimationResult) => void) | undefined;
13
+ };
14
+ mountedViews: Set<object>;
15
+ animationParticipants: Map<number, Set<object>>;
16
+ failedAnimations: Set<number>;
17
+ completionDeferrals: number;
18
+ deferredCompletions: ClipAnimationResult[];
19
+ };
20
+
21
+ const states = new WeakMap<SmoothClipDriver, DriverState>();
22
+ const statesById = new Map<number, DriverState>();
23
+ let nextDriverId = Date.now() * 1024;
24
+
25
+ export function allocateDriverId(): number {
26
+ nextDriverId += 1;
27
+ if (!Number.isSafeInteger(nextDriverId)) nextDriverId = 1;
28
+ return nextDriverId;
29
+ }
30
+
31
+ export function setDriverState(
32
+ driver: SmoothClipDriver,
33
+ state: DriverState
34
+ ): void {
35
+ // WeakMap-only: safe to call from render (discarded renders leave nothing
36
+ // reachable) and survives effect replays so getDriverState keeps working.
37
+ states.set(driver, state);
38
+ }
39
+
40
+ // The strong by-id index is attached in the driver hook's effect and detached
41
+ // in its cleanup, so discarded renders never leak entries and StrictMode /
42
+ // <Activity> effect replays can re-attach the same state.
43
+ export function attachDriverState(state: DriverState): void {
44
+ statesById.set(state.driverId, state);
45
+ }
46
+
47
+ export function detachDriverState(state: DriverState): void {
48
+ if (statesById.get(state.driverId) === state) {
49
+ statesById.delete(state.driverId);
50
+ }
51
+ }
52
+
53
+ export function createDriverState(
54
+ driverId: number,
55
+ initialPresentation: SmoothClipPresentation,
56
+ source: SharedValue<SmoothClipPresentation>,
57
+ onAnimationComplete: DriverState['onAnimationComplete'],
58
+ activeAnimationId?: SharedValue<number>,
59
+ ownership?: SharedValue<number>
60
+ ): DriverState {
61
+ return {
62
+ driverId,
63
+ initialPresentation,
64
+ source,
65
+ activeAnimationId,
66
+ ownership,
67
+ onAnimationComplete,
68
+ mountedViews: new Set(),
69
+ animationParticipants: new Map(),
70
+ failedAnimations: new Set(),
71
+ completionDeferrals: 0,
72
+ deferredCompletions: [],
73
+ };
74
+ }
75
+
76
+ export function registerDriverView(driver: SmoothClipDriver): () => void {
77
+ const state = getDriverState(driver);
78
+ const token = {};
79
+ state.mountedViews.add(token);
80
+ return () => {
81
+ state.mountedViews.delete(token);
82
+ for (const [animationId, participants] of state.animationParticipants) {
83
+ if (participants.has(token)) state.failedAnimations.add(animationId);
84
+ }
85
+ };
86
+ }
87
+
88
+ export function snapshotDriverViews(
89
+ driverId: number,
90
+ animationId: number
91
+ ): void {
92
+ const state = statesById.get(driverId);
93
+ if (state) {
94
+ state.animationParticipants.set(animationId, new Set(state.mountedViews));
95
+ }
96
+ }
97
+
98
+ export function finishDriverAnimation(
99
+ driverId: number,
100
+ animationId: number,
101
+ finished: boolean
102
+ ): boolean {
103
+ const state = statesById.get(driverId);
104
+ if (!state) return finished;
105
+ const result = finished && !state.failedAnimations.has(animationId);
106
+ state.animationParticipants.delete(animationId);
107
+ state.failedAnimations.delete(animationId);
108
+ return result;
109
+ }
110
+
111
+ export function deliverDriverCompletion(
112
+ driverId: number,
113
+ animationId: number,
114
+ finished: boolean
115
+ ): void {
116
+ const state = statesById.get(driverId);
117
+ if (!state) return;
118
+ const result = { animationId, finished };
119
+ if (state.completionDeferrals > 0) {
120
+ state.deferredCompletions.push(result);
121
+ return;
122
+ }
123
+ state.onAnimationComplete.current?.(result);
124
+ }
125
+
126
+ export function deferDriverCompletions(driverId: number): void {
127
+ const state = statesById.get(driverId);
128
+ if (state) state.completionDeferrals += 1;
129
+ }
130
+
131
+ export function releaseDriverCompletions(driverId: number): void {
132
+ const state = statesById.get(driverId);
133
+ if (!state || state.completionDeferrals === 0) return;
134
+ state.completionDeferrals -= 1;
135
+ if (state.completionDeferrals !== 0) return;
136
+ const deferred = state.deferredCompletions.splice(0);
137
+ for (const result of deferred) {
138
+ state.onAnimationComplete.current?.(result);
139
+ }
140
+ }
141
+
142
+ export function getDriverState(driver: SmoothClipDriver): DriverState {
143
+ const state = states.get(driver);
144
+ if (!state) {
145
+ throw new Error(
146
+ '[SmoothClipView] Drivers must be created by useSmoothClipDriver.'
147
+ );
148
+ }
149
+ return state;
150
+ }
@@ -0,0 +1,91 @@
1
+ import type { SharedValue } from 'react-native-reanimated';
2
+ import type { SmoothClipPresentation } from './geometry';
3
+
4
+ export type ClipReduceMotion = 'system' | 'always' | 'never';
5
+
6
+ export type TimingClipAnimation = Readonly<{
7
+ type: 'timing';
8
+ duration: number;
9
+ controlPoints: readonly [number, number, number, number];
10
+ }>;
11
+
12
+ export type SpringClipAnimation = Readonly<{
13
+ type: 'spring';
14
+ mass?: number;
15
+ stiffness?: number;
16
+ damping?: number;
17
+ initialVelocity?: number | 'inherit';
18
+ }>;
19
+
20
+ export type KeyframedClipAnimation = Readonly<{
21
+ type: 'keyframes';
22
+ duration: number;
23
+ frames: readonly Readonly<{
24
+ offset: number;
25
+ presentation: SmoothClipPresentation;
26
+ }>[];
27
+ }>;
28
+
29
+ export type SmoothClipAnimation =
30
+ TimingClipAnimation | SpringClipAnimation | KeyframedClipAnimation;
31
+
32
+ export type ClipAnimationResult = Readonly<{
33
+ animationId: number;
34
+ finished: boolean;
35
+ }>;
36
+
37
+ export type SmoothClipDriverOptions = Readonly<{
38
+ reduceMotion?: ClipReduceMotion;
39
+ onAnimationComplete?: (result: ClipAnimationResult) => void;
40
+ }>;
41
+
42
+ export type SmoothClipUIControls = Readonly<{
43
+ beginInteraction(): SmoothClipPresentation;
44
+ set(presentation: SmoothClipPresentation): void;
45
+ /**
46
+ * Per-frame hot path: writes geometry straight to native without touching
47
+ * `driver.presentation`. Skips SharedValue bookkeeping (dirty marking, RN
48
+ * mirror sync, listener dispatch) for high-frequency streams such as
49
+ * gestures. `driver.presentation.value` is stale after hot writes by design;
50
+ * `beginInteraction()` remains the source of truth for visible geometry.
51
+ * Do not interleave with `presentation.value` writes on the same driver —
52
+ * the listener's duplicate suppression may skip a later equal value.
53
+ */
54
+ setScalars(
55
+ x: number,
56
+ y: number,
57
+ width: number,
58
+ height: number,
59
+ radius: number,
60
+ contentTranslateX: number,
61
+ contentTranslateY: number
62
+ ): void;
63
+ animateTo(
64
+ presentation: SmoothClipPresentation,
65
+ animation: SmoothClipAnimation
66
+ ): number;
67
+ cancel(
68
+ animationId?: number,
69
+ behavior?: 'current' | 'target'
70
+ ): SmoothClipPresentation;
71
+ }>;
72
+
73
+ export type SmoothClipReactControls = Readonly<{
74
+ beginInteraction(): Promise<SmoothClipPresentation>;
75
+ set(presentation: SmoothClipPresentation): Promise<void>;
76
+ animateTo(
77
+ presentation: SmoothClipPresentation,
78
+ animation: SmoothClipAnimation
79
+ ): Promise<number>;
80
+ cancel(
81
+ animationId?: number,
82
+ behavior?: 'current' | 'target'
83
+ ): Promise<SmoothClipPresentation>;
84
+ }>;
85
+
86
+ export type SmoothClipDriver = Readonly<{
87
+ kind: 'hybrid';
88
+ presentation: SharedValue<SmoothClipPresentation>;
89
+ ui: SmoothClipUIControls;
90
+ react: SmoothClipReactControls;
91
+ }>;
@@ -0,0 +1,6 @@
1
+ export { useSmoothClipDriver } from './drivers.ios';
2
+ export type {
3
+ KeyframedClipAnimation,
4
+ SpringClipAnimation,
5
+ TimingClipAnimation,
6
+ } from './drivers.ios';