react-native-ease 0.1.0-alpha.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.
@@ -0,0 +1,256 @@
1
+ import { StyleSheet, type ViewProps, type ViewStyle } from 'react-native';
2
+ import NativeEaseView from './EaseViewNativeComponent';
3
+ import type {
4
+ AnimateProps,
5
+ CubicBezier,
6
+ Transition,
7
+ TransitionEndEvent,
8
+ TransformOrigin,
9
+ } from './types';
10
+
11
+ /** Identity values used as defaults for animate/initialAnimate. */
12
+ const IDENTITY = {
13
+ opacity: 1,
14
+ translateX: 0,
15
+ translateY: 0,
16
+ scaleX: 1,
17
+ scaleY: 1,
18
+ rotate: 0,
19
+ rotateX: 0,
20
+ rotateY: 0,
21
+ borderRadius: 0,
22
+ };
23
+
24
+ /** Bitmask flags — must match native constants. */
25
+ /* eslint-disable no-bitwise */
26
+ const MASK_OPACITY = 1 << 0;
27
+ const MASK_TRANSLATE_X = 1 << 1;
28
+ const MASK_TRANSLATE_Y = 1 << 2;
29
+ const MASK_SCALE_X = 1 << 3;
30
+ const MASK_SCALE_Y = 1 << 4;
31
+ const MASK_ROTATE = 1 << 5;
32
+ const MASK_ROTATE_X = 1 << 6;
33
+ const MASK_ROTATE_Y = 1 << 7;
34
+ const MASK_BORDER_RADIUS = 1 << 8;
35
+ /* eslint-enable no-bitwise */
36
+
37
+ /** Maps animate prop keys to style keys that conflict. */
38
+ const ANIMATE_TO_STYLE_KEYS: Record<keyof AnimateProps, string> = {
39
+ opacity: 'opacity',
40
+ translateX: 'transform',
41
+ translateY: 'transform',
42
+ scale: 'transform',
43
+ scaleX: 'transform',
44
+ scaleY: 'transform',
45
+ rotate: 'transform',
46
+ rotateX: 'transform',
47
+ rotateY: 'transform',
48
+ borderRadius: 'borderRadius',
49
+ };
50
+
51
+ /** Preset easing curves as cubic bezier control points. */
52
+ const EASING_PRESETS: Record<string, CubicBezier> = {
53
+ linear: [0, 0, 1, 1],
54
+ easeIn: [0.42, 0, 1, 1],
55
+ easeOut: [0, 0, 0.58, 1],
56
+ easeInOut: [0.42, 0, 0.58, 1],
57
+ };
58
+
59
+ export type EaseViewProps = ViewProps & {
60
+ /** Target values for animated properties. */
61
+ animate?: AnimateProps;
62
+ /** Starting values for enter animations. Animates to `animate` on mount. */
63
+ initialAnimate?: AnimateProps;
64
+ /** Animation configuration (timing or spring). */
65
+ transition?: Transition;
66
+ /** Called when all animations complete. Reports whether they finished naturally or were interrupted. */
67
+ onTransitionEnd?: (event: TransitionEndEvent) => void;
68
+ /**
69
+ * Enable Android hardware layer during animations. The view is rasterized to
70
+ * a GPU texture so animated property changes (opacity, scale, rotation) are
71
+ * composited on the RenderThread without redrawing the view hierarchy.
72
+ *
73
+ * **Trade-offs:**
74
+ * - Faster rendering of opacity/scale/rotation animations.
75
+ * - Uses additional GPU memory for the off-screen texture.
76
+ * - Children that overflow the view's layout bounds are clipped by the
77
+ * texture, which can cause visual artifacts with `translateX`/`translateY`.
78
+ *
79
+ * Best suited for views that animate opacity, scale, or rotation without
80
+ * overflowing children. No-op on iOS where Core Animation already composites
81
+ * off the main thread.
82
+ * @default false
83
+ */
84
+ useHardwareLayer?: boolean;
85
+ /** Pivot point for scale and rotation as 0–1 fractions. @default { x: 0.5, y: 0.5 } (center) */
86
+ transformOrigin?: TransformOrigin;
87
+ };
88
+
89
+ export function EaseView({
90
+ animate,
91
+ initialAnimate,
92
+ transition,
93
+ onTransitionEnd,
94
+ useHardwareLayer = false,
95
+ transformOrigin,
96
+ style,
97
+ ...rest
98
+ }: EaseViewProps) {
99
+ // Compute bitmask of which properties are animated.
100
+ // Native uses this to skip non-animated properties (lets style handle them).
101
+ /* eslint-disable no-bitwise */
102
+ let animatedProperties = 0;
103
+ if (animate?.opacity != null) animatedProperties |= MASK_OPACITY;
104
+ if (animate?.translateX != null) animatedProperties |= MASK_TRANSLATE_X;
105
+ if (animate?.translateY != null) animatedProperties |= MASK_TRANSLATE_Y;
106
+ if (animate?.scaleX != null || animate?.scale != null)
107
+ animatedProperties |= MASK_SCALE_X;
108
+ if (animate?.scaleY != null || animate?.scale != null)
109
+ animatedProperties |= MASK_SCALE_Y;
110
+ if (animate?.rotate != null) animatedProperties |= MASK_ROTATE;
111
+ if (animate?.rotateX != null) animatedProperties |= MASK_ROTATE_X;
112
+ if (animate?.rotateY != null) animatedProperties |= MASK_ROTATE_Y;
113
+ if (animate?.borderRadius != null) animatedProperties |= MASK_BORDER_RADIUS;
114
+ /* eslint-enable no-bitwise */
115
+
116
+ // Resolve animate values (identity defaults for non-animated — safe values).
117
+ const resolved = {
118
+ ...IDENTITY,
119
+ ...animate,
120
+ scaleX: animate?.scaleX ?? animate?.scale ?? IDENTITY.scaleX,
121
+ scaleY: animate?.scaleY ?? animate?.scale ?? IDENTITY.scaleY,
122
+ rotateX: animate?.rotateX ?? IDENTITY.rotateX,
123
+ rotateY: animate?.rotateY ?? IDENTITY.rotateY,
124
+ };
125
+
126
+ // Resolve initialAnimate:
127
+ // - No initialAnimate: same as resolved (no enter animation)
128
+ // - With initialAnimate: use initial values for animated properties,
129
+ // falling back to identity defaults.
130
+ const initial = initialAnimate ?? animate;
131
+ const resolvedInitial = {
132
+ ...IDENTITY,
133
+ ...initial,
134
+ scaleX: initial?.scaleX ?? initial?.scale ?? IDENTITY.scaleX,
135
+ scaleY: initial?.scaleY ?? initial?.scale ?? IDENTITY.scaleY,
136
+ rotateX: initial?.rotateX ?? IDENTITY.rotateX,
137
+ rotateY: initial?.rotateY ?? IDENTITY.rotateY,
138
+ };
139
+
140
+ // Strip style keys that conflict with animated properties
141
+ let cleanStyle: ViewProps['style'] = style;
142
+ if (animate && style) {
143
+ const flat = StyleSheet.flatten(style) as Record<string, unknown>;
144
+ if (flat) {
145
+ const conflicting = new Set<string>();
146
+ for (const key of Object.keys(animate) as (keyof AnimateProps)[]) {
147
+ if (animate[key] != null) {
148
+ const styleKey = ANIMATE_TO_STYLE_KEYS[key];
149
+ if (styleKey && styleKey in flat) {
150
+ conflicting.add(styleKey);
151
+ }
152
+ }
153
+ }
154
+ if (conflicting.size > 0) {
155
+ if (__DEV__) {
156
+ console.warn(
157
+ `react-native-ease: ${[...conflicting].join(
158
+ ', ',
159
+ )} found in both style and animate. ` +
160
+ 'The animated value takes priority; the style value will be ignored.',
161
+ );
162
+ }
163
+ const cleaned: Record<string, unknown> = {};
164
+ for (const [k, v] of Object.entries(flat)) {
165
+ if (!conflicting.has(k)) {
166
+ cleaned[k] = v;
167
+ }
168
+ }
169
+ cleanStyle = cleaned as ViewStyle;
170
+ }
171
+ }
172
+ }
173
+
174
+ // Resolve transition config
175
+ const transitionType = transition?.type ?? 'timing';
176
+ const transitionDuration =
177
+ transition?.type === 'timing' ? transition.duration ?? 300 : 300;
178
+ const rawEasing =
179
+ transition?.type === 'timing'
180
+ ? transition.easing ?? 'easeInOut'
181
+ : 'easeInOut';
182
+ if (__DEV__) {
183
+ if (Array.isArray(rawEasing)) {
184
+ if ((rawEasing as number[]).length !== 4) {
185
+ console.warn(
186
+ 'react-native-ease: Custom easing must be a [x1, y1, x2, y2] tuple (got length ' +
187
+ (rawEasing as number[]).length +
188
+ ').',
189
+ );
190
+ }
191
+ if (
192
+ rawEasing[0] < 0 ||
193
+ rawEasing[0] > 1 ||
194
+ rawEasing[2] < 0 ||
195
+ rawEasing[2] > 1
196
+ ) {
197
+ console.warn(
198
+ 'react-native-ease: Easing x-values (x1, x2) must be between 0 and 1.',
199
+ );
200
+ }
201
+ }
202
+ }
203
+ const bezier: CubicBezier = Array.isArray(rawEasing)
204
+ ? rawEasing
205
+ : EASING_PRESETS[rawEasing]!;
206
+ const transitionDamping =
207
+ transition?.type === 'spring' ? transition.damping ?? 15 : 15;
208
+ const transitionStiffness =
209
+ transition?.type === 'spring' ? transition.stiffness ?? 120 : 120;
210
+ const transitionMass =
211
+ transition?.type === 'spring' ? transition.mass ?? 1 : 1;
212
+ const transitionLoop =
213
+ transition?.type === 'timing' ? transition.loop ?? 'none' : 'none';
214
+
215
+ const handleTransitionEnd = onTransitionEnd
216
+ ? (event: { nativeEvent: { finished: boolean } }) =>
217
+ onTransitionEnd(event.nativeEvent)
218
+ : undefined;
219
+
220
+ return (
221
+ <NativeEaseView
222
+ style={cleanStyle}
223
+ onTransitionEnd={handleTransitionEnd}
224
+ animatedProperties={animatedProperties}
225
+ animateOpacity={resolved.opacity}
226
+ animateTranslateX={resolved.translateX}
227
+ animateTranslateY={resolved.translateY}
228
+ animateScaleX={resolved.scaleX}
229
+ animateScaleY={resolved.scaleY}
230
+ animateRotate={resolved.rotate}
231
+ animateRotateX={resolved.rotateX}
232
+ animateRotateY={resolved.rotateY}
233
+ animateBorderRadius={resolved.borderRadius}
234
+ initialAnimateOpacity={resolvedInitial.opacity}
235
+ initialAnimateTranslateX={resolvedInitial.translateX}
236
+ initialAnimateTranslateY={resolvedInitial.translateY}
237
+ initialAnimateScaleX={resolvedInitial.scaleX}
238
+ initialAnimateScaleY={resolvedInitial.scaleY}
239
+ initialAnimateRotate={resolvedInitial.rotate}
240
+ initialAnimateRotateX={resolvedInitial.rotateX}
241
+ initialAnimateRotateY={resolvedInitial.rotateY}
242
+ initialAnimateBorderRadius={resolvedInitial.borderRadius}
243
+ transitionType={transitionType}
244
+ transitionDuration={transitionDuration}
245
+ transitionEasingBezier={bezier}
246
+ transitionDamping={transitionDamping}
247
+ transitionStiffness={transitionStiffness}
248
+ transitionMass={transitionMass}
249
+ transitionLoop={transitionLoop}
250
+ useHardwareLayer={useHardwareLayer}
251
+ transformOriginX={transformOrigin?.x ?? 0.5}
252
+ transformOriginY={transformOrigin?.y ?? 0.5}
253
+ {...rest}
254
+ />
255
+ );
256
+ }
@@ -0,0 +1,68 @@
1
+ import {
2
+ codegenNativeComponent,
3
+ type CodegenTypes,
4
+ type ViewProps,
5
+ type HostComponent,
6
+ } from 'react-native';
7
+
8
+ export interface NativeProps extends ViewProps {
9
+ // Bitmask of which properties are animated (0 = none, let style handle all)
10
+ animatedProperties?: CodegenTypes.WithDefault<CodegenTypes.Int32, 0>;
11
+
12
+ // Animate target values
13
+ animateOpacity?: CodegenTypes.WithDefault<CodegenTypes.Float, 1.0>;
14
+ animateTranslateX?: CodegenTypes.WithDefault<CodegenTypes.Float, 0.0>;
15
+ animateTranslateY?: CodegenTypes.WithDefault<CodegenTypes.Float, 0.0>;
16
+ animateScaleX?: CodegenTypes.WithDefault<CodegenTypes.Float, 1.0>;
17
+ animateScaleY?: CodegenTypes.WithDefault<CodegenTypes.Float, 1.0>;
18
+ animateRotate?: CodegenTypes.WithDefault<CodegenTypes.Float, 0.0>;
19
+ animateRotateX?: CodegenTypes.WithDefault<CodegenTypes.Float, 0.0>;
20
+ animateRotateY?: CodegenTypes.WithDefault<CodegenTypes.Float, 0.0>;
21
+ animateBorderRadius?: CodegenTypes.WithDefault<CodegenTypes.Float, 0.0>;
22
+
23
+ // Initial values for enter animations
24
+ initialAnimateOpacity?: CodegenTypes.WithDefault<CodegenTypes.Float, 1.0>;
25
+ initialAnimateTranslateX?: CodegenTypes.WithDefault<CodegenTypes.Float, 0.0>;
26
+ initialAnimateTranslateY?: CodegenTypes.WithDefault<CodegenTypes.Float, 0.0>;
27
+ initialAnimateScaleX?: CodegenTypes.WithDefault<CodegenTypes.Float, 1.0>;
28
+ initialAnimateScaleY?: CodegenTypes.WithDefault<CodegenTypes.Float, 1.0>;
29
+ initialAnimateRotate?: CodegenTypes.WithDefault<CodegenTypes.Float, 0.0>;
30
+ initialAnimateRotateX?: CodegenTypes.WithDefault<CodegenTypes.Float, 0.0>;
31
+ initialAnimateRotateY?: CodegenTypes.WithDefault<CodegenTypes.Float, 0.0>;
32
+ initialAnimateBorderRadius?: CodegenTypes.WithDefault<
33
+ CodegenTypes.Float,
34
+ 0.0
35
+ >;
36
+
37
+ // Transition config
38
+ transitionType?: CodegenTypes.WithDefault<
39
+ 'timing' | 'spring' | 'none',
40
+ 'timing'
41
+ >;
42
+ transitionDuration?: CodegenTypes.WithDefault<CodegenTypes.Int32, 300>;
43
+ // Easing cubic bezier control points [x1, y1, x2, y2] (default: easeInOut)
44
+ transitionEasingBezier?: ReadonlyArray<CodegenTypes.Float>;
45
+ transitionDamping?: CodegenTypes.WithDefault<CodegenTypes.Float, 15.0>;
46
+ transitionStiffness?: CodegenTypes.WithDefault<CodegenTypes.Float, 120.0>;
47
+ transitionMass?: CodegenTypes.WithDefault<CodegenTypes.Float, 1.0>;
48
+ transitionLoop?: CodegenTypes.WithDefault<
49
+ 'none' | 'repeat' | 'reverse',
50
+ 'none'
51
+ >;
52
+
53
+ // Transform origin (0–1 fractions, default center)
54
+ transformOriginX?: CodegenTypes.WithDefault<CodegenTypes.Float, 0.5>;
55
+ transformOriginY?: CodegenTypes.WithDefault<CodegenTypes.Float, 0.5>;
56
+
57
+ // Events
58
+ onTransitionEnd?: CodegenTypes.DirectEventHandler<
59
+ Readonly<{ finished: boolean }>
60
+ >;
61
+
62
+ // Android hardware layer optimization (no-op on iOS)
63
+ useHardwareLayer?: CodegenTypes.WithDefault<boolean, false>;
64
+ }
65
+
66
+ export default codegenNativeComponent<NativeProps>(
67
+ 'EaseView',
68
+ ) as HostComponent<NativeProps>;
package/src/index.tsx ADDED
@@ -0,0 +1,13 @@
1
+ export { EaseView } from './EaseView';
2
+ export type { EaseViewProps } from './EaseView';
3
+ export type {
4
+ AnimateProps,
5
+ CubicBezier,
6
+ Transition,
7
+ TimingTransition,
8
+ SpringTransition,
9
+ NoneTransition,
10
+ EasingType,
11
+ TransitionEndEvent,
12
+ TransformOrigin,
13
+ } from './types';
package/src/types.ts ADDED
@@ -0,0 +1,78 @@
1
+ /** Cubic bezier control points: [x1, y1, x2, y2]. */
2
+ export type CubicBezier = [number, number, number, number];
3
+
4
+ /** Easing curve for timing animations. */
5
+ export type EasingType =
6
+ | 'linear'
7
+ | 'easeIn'
8
+ | 'easeOut'
9
+ | 'easeInOut'
10
+ | CubicBezier;
11
+
12
+ /** Timing-based transition with fixed duration and easing curve. */
13
+ export type TimingTransition = {
14
+ type: 'timing';
15
+ /** Duration in milliseconds. @default 300 */
16
+ duration?: number;
17
+ /** Easing curve. @default 'easeInOut' */
18
+ easing?: EasingType;
19
+ /** Loop mode — 'repeat' restarts from the beginning, 'reverse' alternates direction. */
20
+ loop?: 'repeat' | 'reverse';
21
+ };
22
+
23
+ /** Physics-based spring transition. */
24
+ export type SpringTransition = {
25
+ type: 'spring';
26
+ /** Friction — higher values reduce oscillation. @default 15 */
27
+ damping?: number;
28
+ /** Spring constant — higher values mean faster animation. @default 120 */
29
+ stiffness?: number;
30
+ /** Mass of the object — higher values mean slower, more momentum. @default 1 */
31
+ mass?: number;
32
+ };
33
+
34
+ /** No transition — values are applied immediately without animation. */
35
+ export type NoneTransition = {
36
+ type: 'none';
37
+ };
38
+
39
+ /** Animation transition configuration. */
40
+ export type Transition = TimingTransition | SpringTransition | NoneTransition;
41
+
42
+ /** Event fired when the animation ends. */
43
+ export type TransitionEndEvent = {
44
+ /** True if the animation completed naturally, false if interrupted. */
45
+ finished: boolean;
46
+ };
47
+
48
+ /** Transform origin as 0–1 fractions. Default is center (0.5, 0.5). */
49
+ export type TransformOrigin = {
50
+ /** Horizontal origin. 0 = left, 0.5 = center, 1 = right. @default 0.5 */
51
+ x?: number;
52
+ /** Vertical origin. 0 = top, 0.5 = center, 1 = bottom. @default 0.5 */
53
+ y?: number;
54
+ };
55
+
56
+ /** Animatable view properties. Unspecified properties default to their identity values. */
57
+ export type AnimateProps = {
58
+ /** View opacity (0–1). @default 1 */
59
+ opacity?: number;
60
+ /** Horizontal translation in pixels. @default 0 */
61
+ translateX?: number;
62
+ /** Vertical translation in pixels. @default 0 */
63
+ translateY?: number;
64
+ /** Uniform scale factor (shorthand for scaleX + scaleY). @default 1 */
65
+ scale?: number;
66
+ /** Horizontal scale factor. Overrides `scale` for the X axis. @default 1 */
67
+ scaleX?: number;
68
+ /** Vertical scale factor. Overrides `scale` for the Y axis. @default 1 */
69
+ scaleY?: number;
70
+ /** Z-axis rotation in degrees. @default 0 */
71
+ rotate?: number;
72
+ /** X-axis rotation in degrees (3D). @default 0 */
73
+ rotateX?: number;
74
+ /** Y-axis rotation in degrees (3D). @default 0 */
75
+ rotateY?: number;
76
+ /** Border radius in pixels. Uses hardware-accelerated clipping (ViewOutlineProvider on Android, layer.cornerRadius on iOS). @default 0 */
77
+ borderRadius?: number;
78
+ };