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.
package/Ease.podspec ADDED
@@ -0,0 +1,20 @@
1
+ require "json"
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, "package.json")))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = "Ease"
7
+ s.version = package["version"]
8
+ s.summary = package["description"]
9
+ s.homepage = package["homepage"]
10
+ s.license = package["license"]
11
+ s.authors = package["author"]
12
+
13
+ s.platforms = { :ios => min_ios_version_supported }
14
+ s.source = { :git => "https://github.com/janicduplessis/react-native-ease.git", :tag => "#{s.version}" }
15
+
16
+ s.source_files = "ios/**/*.{h,m,mm,swift,cpp}"
17
+ s.private_header_files = "ios/**/*.h"
18
+
19
+ install_modules_dependencies(s)
20
+ end
package/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Janic Duplessis
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ of this software and associated documentation files (the "Software"), to deal
6
+ in the Software without restriction, including without limitation the rights
7
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the Software is
9
+ furnished to do so, subject to the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all
12
+ copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,411 @@
1
+ # 🍃 react-native-ease
2
+
3
+ Lightweight declarative animations powered by platform APIs. Uses Core Animation on iOS and Animator on Android — zero JS overhead.
4
+
5
+ ## Goals
6
+
7
+ - **Fast** — Animations run entirely on native platform APIs (CAAnimation, ObjectAnimator/SpringAnimation). No JS animation loop, no worklets, no shared values.
8
+ - **Simple** — CSS-transition-like API. Set target values, get smooth animations. One component, a few props.
9
+ - **Lightweight** — Minimal native code, no C++ runtime, no custom animation engine. Just a thin declarative wrapper around what the OS already provides.
10
+ - **Interruptible** — Changing values mid-animation smoothly redirects to the new target. No jumps.
11
+
12
+ ## Non-Goals
13
+
14
+ - **Complex gesture-driven animations** — If you need pan/pinch-driven animations, animation worklets, or shared values across components, use [react-native-reanimated](https://github.com/software-mansion/react-native-reanimated).
15
+ - **Layout animations** — Animating width/height/layout changes is not supported.
16
+ - **Shared element transitions** — Use Reanimated or React Navigation's shared element transitions.
17
+ - **Old architecture** — Fabric (new architecture) only.
18
+
19
+ ## When to use this vs Reanimated
20
+
21
+ | Use react-native-ease | Use Reanimated |
22
+ |---|---|
23
+ | Fade in a view | Gesture-driven animations |
24
+ | Slide/translate on state change | Complex interpolations |
25
+ | Scale/rotate on press | Shared values across components |
26
+ | Simple enter animations | Layout animations |
27
+ | You want zero config | You need animation worklets |
28
+
29
+ ## Installation
30
+
31
+ ```bash
32
+ npm install react-native-ease
33
+ # or
34
+ yarn add react-native-ease
35
+ ```
36
+
37
+ ## Quick Start
38
+
39
+ ```tsx
40
+ import { EaseView } from 'react-native-ease';
41
+
42
+ function FadeCard({ visible, children }) {
43
+ return (
44
+ <EaseView
45
+ animate={{ opacity: visible ? 1 : 0 }}
46
+ transition={{ type: 'timing', duration: 300 }}
47
+ style={styles.card}
48
+ >
49
+ {children}
50
+ </EaseView>
51
+ );
52
+ }
53
+ ```
54
+
55
+ `EaseView` works like a regular `View` — it accepts children, styles, and all standard view props. When values in `animate` change, it smoothly transitions to the new values using native platform animations.
56
+
57
+ ## Guide
58
+
59
+ ### Timing Animations
60
+
61
+ Timing animations transition from one value to another over a fixed duration with an easing curve.
62
+
63
+ ```tsx
64
+ <EaseView
65
+ animate={{ opacity: isVisible ? 1 : 0 }}
66
+ transition={{ type: 'timing', duration: 300, easing: 'easeOut' }}
67
+ />
68
+ ```
69
+
70
+ | Parameter | Type | Default | Description |
71
+ |---|---|---|---|
72
+ | `duration` | `number` | `300` | Duration in milliseconds |
73
+ | `easing` | `EasingType` | `'easeInOut'` | Easing curve (preset name or `[x1, y1, x2, y2]` cubic bezier) |
74
+ | `loop` | `string` | — | `'repeat'` restarts from the beginning, `'reverse'` alternates direction |
75
+
76
+ Available easing curves:
77
+
78
+ - `'linear'` — constant speed
79
+ - `'easeIn'` — starts slow, accelerates
80
+ - `'easeOut'` — starts fast, decelerates
81
+ - `'easeInOut'` — slow start and end, fast middle
82
+ - `[x1, y1, x2, y2]` — custom cubic bezier (same as CSS `cubic-bezier()`)
83
+
84
+ ### Custom Easing
85
+
86
+ Pass a `[x1, y1, x2, y2]` tuple for custom cubic bezier curves. The values correspond to the two control points of the bezier curve, matching the CSS `cubic-bezier()` function.
87
+
88
+ ```tsx
89
+ // Standard Material Design easing
90
+ <EaseView
91
+ animate={{ opacity: isVisible ? 1 : 0 }}
92
+ transition={{ type: 'timing', duration: 300, easing: [0.4, 0, 0.2, 1] }}
93
+ />
94
+
95
+ // Overshoot (y-values can exceed 0–1)
96
+ <EaseView
97
+ animate={{ scale: active ? 1.2 : 1 }}
98
+ transition={{ type: 'timing', duration: 500, easing: [0.68, -0.55, 0.265, 1.55] }}
99
+ />
100
+ ```
101
+
102
+ x-values (x1, x2) must be between 0 and 1. y-values can exceed this range to create overshoot effects.
103
+
104
+ ### Spring Animations
105
+
106
+ Spring animations use a physics-based model for natural-feeling motion. Great for interactive elements.
107
+
108
+ ```tsx
109
+ <EaseView
110
+ animate={{ translateX: isOpen ? 200 : 0 }}
111
+ transition={{ type: 'spring', damping: 15, stiffness: 120, mass: 1 }}
112
+ />
113
+ ```
114
+
115
+ | Parameter | Type | Default | Description |
116
+ |---|---|---|---|
117
+ | `damping` | `number` | `15` | Friction — higher values reduce oscillation |
118
+ | `stiffness` | `number` | `120` | Spring constant — higher values mean faster animation |
119
+ | `mass` | `number` | `1` | Mass of the object — higher values mean slower, more momentum |
120
+
121
+ Spring presets for common feels:
122
+
123
+ ```tsx
124
+ // Snappy (no bounce)
125
+ { type: 'spring', damping: 20, stiffness: 300, mass: 1 }
126
+
127
+ // Gentle bounce
128
+ { type: 'spring', damping: 12, stiffness: 120, mass: 1 }
129
+
130
+ // Bouncy
131
+ { type: 'spring', damping: 8, stiffness: 200, mass: 1 }
132
+
133
+ // Slow and heavy
134
+ { type: 'spring', damping: 20, stiffness: 60, mass: 2 }
135
+ ```
136
+
137
+ ### Disabling Animations
138
+
139
+ Use `{ type: 'none' }` to apply values immediately without animation. Useful for skipping animations in reduced-motion modes or when you need an instant state change.
140
+
141
+ ```tsx
142
+ <EaseView
143
+ animate={{ opacity: isVisible ? 1 : 0 }}
144
+ transition={{ type: 'none' }}
145
+ />
146
+ ```
147
+
148
+ `onTransitionEnd` fires immediately with `{ finished: true }`.
149
+
150
+ ### Border Radius
151
+
152
+ `borderRadius` can be animated just like other properties. It uses hardware-accelerated platform APIs — `ViewOutlineProvider` + `clipToOutline` on Android and `layer.cornerRadius` + `layer.masksToBounds` on iOS. Unlike RN's style-based `borderRadius` (which uses a Canvas drawable on Android), this clips children properly and is GPU-accelerated.
153
+
154
+ ```tsx
155
+ <EaseView
156
+ animate={{ borderRadius: expanded ? 0 : 16 }}
157
+ transition={{ type: 'timing', duration: 300 }}
158
+ style={styles.card}
159
+ >
160
+ <Image source={heroImage} style={styles.image} />
161
+ <Text>Content is clipped to rounded corners</Text>
162
+ </EaseView>
163
+ ```
164
+
165
+ When `borderRadius` is in `animate`, any `borderRadius` in `style` is automatically stripped to avoid conflicts.
166
+
167
+ ### Animatable Properties
168
+
169
+ All properties are set in the `animate` prop as flat values (no transform array).
170
+
171
+ ```tsx
172
+ <EaseView
173
+ animate={{
174
+ opacity: 1, // 0 to 1
175
+ translateX: 0, // pixels
176
+ translateY: 0, // pixels
177
+ scale: 1, // 1 = normal size (shorthand for scaleX + scaleY)
178
+ scaleX: 1, // horizontal scale
179
+ scaleY: 1, // vertical scale
180
+ rotate: 0, // Z-axis rotation in degrees
181
+ rotateX: 0, // X-axis rotation in degrees (3D)
182
+ rotateY: 0, // Y-axis rotation in degrees (3D)
183
+ borderRadius: 0, // pixels (hardware-accelerated, clips children)
184
+ }}
185
+ />
186
+ ```
187
+
188
+ `scale` is a shorthand that sets both `scaleX` and `scaleY`. When `scaleX` or `scaleY` is also specified, it overrides the `scale` value for that axis.
189
+
190
+ You can animate any combination of properties simultaneously. All properties share the same transition config.
191
+
192
+ ### Looping Animations
193
+
194
+ Timing animations can loop infinitely. Use `'repeat'` to restart from the beginning or `'reverse'` to alternate direction.
195
+
196
+ ```tsx
197
+ // Pulsing opacity
198
+ <EaseView
199
+ initialAnimate={{ opacity: 0.3 }}
200
+ animate={{ opacity: 1 }}
201
+ transition={{ type: 'timing', duration: 1000, easing: 'easeInOut', loop: 'reverse' }}
202
+ />
203
+
204
+ // Marquee-style scroll
205
+ <EaseView
206
+ initialAnimate={{ translateX: 0 }}
207
+ animate={{ translateX: -300 }}
208
+ transition={{ type: 'timing', duration: 3000, easing: 'linear', loop: 'repeat' }}
209
+ />
210
+ ```
211
+
212
+ Loop requires `initialAnimate` to define the starting value. Spring animations do not support looping.
213
+
214
+ ### Enter Animations
215
+
216
+ Use `initialAnimate` to set starting values. On mount, the view starts at `initialAnimate` values and animates to `animate` values.
217
+
218
+ ```tsx
219
+ // Fade in and slide up on mount
220
+ <EaseView
221
+ initialAnimate={{ opacity: 0, translateY: 20 }}
222
+ animate={{ opacity: 1, translateY: 0 }}
223
+ transition={{ type: 'spring', damping: 15, stiffness: 120, mass: 1 }}
224
+ />
225
+ ```
226
+
227
+ Without `initialAnimate`, the view renders at the `animate` values immediately with no animation on mount.
228
+
229
+ ### Interruption
230
+
231
+ Animations are interruptible by default. If you change `animate` values while an animation is running, it smoothly redirects to the new target from wherever it currently is — no jumping or restarting.
232
+
233
+ ```tsx
234
+ // Rapidly toggling this is fine — each toggle smoothly
235
+ // redirects the animation from its current position
236
+ <EaseView
237
+ animate={{ translateX: isLeft ? 0 : 200 }}
238
+ transition={{ type: 'spring', damping: 15, stiffness: 120, mass: 1 }}
239
+ />
240
+ ```
241
+
242
+ ### Transform Origin
243
+
244
+ By default, scale and rotation animate from the view's center. Use `transformOrigin` to change the pivot point with 0–1 fractions.
245
+
246
+ ```tsx
247
+ // Rotate from top-left corner
248
+ <EaseView
249
+ animate={{ rotate: isOpen ? 45 : 0 }}
250
+ transformOrigin={{ x: 0, y: 0 }}
251
+ transition={{ type: 'spring', damping: 12, stiffness: 200, mass: 1 }}
252
+ style={styles.card}
253
+ />
254
+
255
+ // Scale from bottom-right
256
+ <EaseView
257
+ animate={{ scale: active ? 1.2 : 1 }}
258
+ transformOrigin={{ x: 1, y: 1 }}
259
+ transition={{ type: 'spring', damping: 15, stiffness: 120, mass: 1 }}
260
+ style={styles.card}
261
+ />
262
+ ```
263
+
264
+ | Value | Position |
265
+ |---|---|
266
+ | `{ x: 0, y: 0 }` | Top-left |
267
+ | `{ x: 0.5, y: 0.5 }` | Center (default) |
268
+ | `{ x: 1, y: 1 }` | Bottom-right |
269
+
270
+ ### Style Handling
271
+
272
+ `EaseView` accepts all standard `ViewStyle` properties. If a property appears in both `style` and `animate`, the animated value takes priority and the style value is stripped. A dev warning is logged when this happens.
273
+
274
+ ```tsx
275
+ // opacity in style works because only translateY is animated
276
+ <EaseView
277
+ animate={{ translateY: moved ? -10 : 0 }}
278
+ transition={{ type: 'spring', damping: 15, stiffness: 120, mass: 1 }}
279
+ style={{
280
+ opacity: 0.9,
281
+ backgroundColor: 'white',
282
+ borderRadius: 16,
283
+ padding: 16,
284
+ }}
285
+ >
286
+ <Text>Notification card</Text>
287
+ </EaseView>
288
+
289
+ // ⚠️ opacity is in both — animate wins, style opacity is stripped, dev warning logged
290
+ <EaseView
291
+ animate={{ opacity: 1 }}
292
+ style={{ opacity: 0.5, backgroundColor: 'white' }}
293
+ />
294
+ ```
295
+
296
+ ## API Reference
297
+
298
+ ### `<EaseView>`
299
+
300
+ A `View` that animates property changes using native platform APIs.
301
+
302
+ | Prop | Type | Description |
303
+ |---|---|---|
304
+ | `animate` | `AnimateProps` | Target values for animated properties |
305
+ | `initialAnimate` | `AnimateProps` | Starting values for enter animations (animates to `animate` on mount) |
306
+ | `transition` | `Transition` | Animation configuration (timing, spring, or none) |
307
+ | `onTransitionEnd` | `(event) => void` | Called when all animations complete with `{ finished: boolean }` |
308
+ | `transformOrigin` | `{ x?: number; y?: number }` | Pivot point for scale/rotation as 0–1 fractions. Default: `{ x: 0.5, y: 0.5 }` (center) |
309
+ | `useHardwareLayer` | `boolean` | Android only — rasterize to GPU texture during animations. See [Hardware Layers](#hardware-layers-android). Default: `false` |
310
+ | `style` | `ViewStyle` | Non-animated styles (layout, colors, borders, etc.) |
311
+ | `children` | `ReactNode` | Child elements |
312
+ | ...rest | `ViewProps` | All other standard View props |
313
+
314
+ ### `AnimateProps`
315
+
316
+ | Property | Type | Default | Description |
317
+ |---|---|---|---|
318
+ | `opacity` | `number` | `1` | View opacity (0–1) |
319
+ | `translateX` | `number` | `0` | Horizontal translation in pixels |
320
+ | `translateY` | `number` | `0` | Vertical translation in pixels |
321
+ | `scale` | `number` | `1` | Uniform scale factor (shorthand for `scaleX` + `scaleY`) |
322
+ | `scaleX` | `number` | `1` | Horizontal scale factor (overrides `scale` for X axis) |
323
+ | `scaleY` | `number` | `1` | Vertical scale factor (overrides `scale` for Y axis) |
324
+ | `rotate` | `number` | `0` | Z-axis rotation in degrees |
325
+ | `rotateX` | `number` | `0` | X-axis rotation in degrees (3D) |
326
+ | `rotateY` | `number` | `0` | Y-axis rotation in degrees (3D) |
327
+ | `borderRadius` | `number` | `0` | Border radius in pixels (hardware-accelerated, clips children) |
328
+
329
+ Properties not specified in `animate` default to their identity values.
330
+
331
+ ### `TimingTransition`
332
+
333
+ ```tsx
334
+ {
335
+ type: 'timing';
336
+ duration?: number; // default: 300 (ms)
337
+ easing?: EasingType; // default: 'easeInOut' — preset name or [x1, y1, x2, y2]
338
+ loop?: 'repeat' | 'reverse'; // default: none
339
+ }
340
+ ```
341
+
342
+ ### `SpringTransition`
343
+
344
+ ```tsx
345
+ {
346
+ type: 'spring';
347
+ damping?: number; // default: 15
348
+ stiffness?: number; // default: 120
349
+ mass?: number; // default: 1
350
+ }
351
+ ```
352
+
353
+ ### `NoneTransition`
354
+
355
+ ```tsx
356
+ {
357
+ type: 'none';
358
+ }
359
+ ```
360
+
361
+ Applies values instantly with no animation. `onTransitionEnd` fires immediately with `{ finished: true }`.
362
+
363
+ ## Hardware Layers (Android)
364
+
365
+ Setting `useHardwareLayer` rasterizes the view into a GPU texture for the duration of the animation. This means animated property changes (opacity, scale, rotation) are composited on the RenderThread without redrawing the view hierarchy — useful for complex views with many children.
366
+
367
+ ```tsx
368
+ <EaseView
369
+ animate={{ opacity: isVisible ? 1 : 0 }}
370
+ useHardwareLayer
371
+ />
372
+ ```
373
+
374
+ **Trade-offs:**
375
+
376
+ - Faster rendering for opacity, scale, and rotation animations (RenderThread compositing).
377
+ - Uses additional GPU memory for the off-screen texture (proportional to view size).
378
+ - Children that overflow the view's layout bounds are **clipped** by the texture. This causes visual artifacts when animating `translateX`/`translateY` on views with overflowing content.
379
+
380
+ No-op on iOS where Core Animation already composites off the main thread.
381
+
382
+ ## How It Works
383
+
384
+ `EaseView` is a native Fabric component. The JS side flattens your `animate` and `transition` props into flat native props. When those props change, the native view:
385
+
386
+ 1. **Diffs** previous vs new values to find what changed
387
+ 2. **Reads** the current in-flight value (for smooth interruption)
388
+ 3. **Creates** a platform-native animation from the current value to the new target
389
+ 4. **Sets** the final value immediately on the model layer
390
+
391
+ On iOS, this uses `CABasicAnimation`/`CASpringAnimation` on `CALayer` key paths. On Android, this uses `ObjectAnimator`/`SpringAnimation` on `View` properties. No JS thread involvement during the animation.
392
+
393
+ ## Requirements
394
+
395
+ - React Native 0.76+ (new architecture / Fabric)
396
+ - iOS 15.1+
397
+ - Android minSdk 24+
398
+
399
+ ## Contributing
400
+
401
+ - [Development workflow](CONTRIBUTING.md#development-workflow)
402
+ - [Sending a pull request](CONTRIBUTING.md#sending-a-pull-request)
403
+ - [Code of conduct](CODE_OF_CONDUCT.md)
404
+
405
+ ## License
406
+
407
+ MIT
408
+
409
+ ---
410
+
411
+ Made with [create-react-native-library](https://github.com/callstack/react-native-builder-bob)
@@ -0,0 +1,68 @@
1
+ buildscript {
2
+ ext.Ease = [
3
+ kotlinVersion: "2.0.21",
4
+ minSdkVersion: 24,
5
+ compileSdkVersion: 36,
6
+ targetSdkVersion: 36
7
+ ]
8
+
9
+ ext.getExtOrDefault = { prop ->
10
+ if (rootProject.ext.has(prop)) {
11
+ return rootProject.ext.get(prop)
12
+ }
13
+
14
+ return Ease[prop]
15
+ }
16
+
17
+ repositories {
18
+ google()
19
+ mavenCentral()
20
+ }
21
+
22
+ dependencies {
23
+ classpath "com.android.tools.build:gradle:8.7.2"
24
+ // noinspection DifferentKotlinGradleVersion
25
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion')}"
26
+ }
27
+ }
28
+
29
+
30
+ apply plugin: "com.android.library"
31
+ apply plugin: "kotlin-android"
32
+
33
+ apply plugin: "com.facebook.react"
34
+
35
+ android {
36
+ namespace "com.ease"
37
+
38
+ compileSdkVersion getExtOrDefault("compileSdkVersion")
39
+
40
+ defaultConfig {
41
+ minSdkVersion getExtOrDefault("minSdkVersion")
42
+ targetSdkVersion getExtOrDefault("targetSdkVersion")
43
+ }
44
+
45
+ buildFeatures {
46
+ buildConfig true
47
+ }
48
+
49
+ buildTypes {
50
+ release {
51
+ minifyEnabled false
52
+ }
53
+ }
54
+
55
+ lint {
56
+ disable "GradleCompatible"
57
+ }
58
+
59
+ compileOptions {
60
+ sourceCompatibility JavaVersion.VERSION_1_8
61
+ targetCompatibility JavaVersion.VERSION_1_8
62
+ }
63
+ }
64
+
65
+ dependencies {
66
+ implementation "com.facebook.react:react-android"
67
+ implementation "androidx.dynamicanimation:dynamicanimation:1.0.0"
68
+ }
@@ -0,0 +1,2 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ </manifest>
@@ -0,0 +1,17 @@
1
+ package com.ease
2
+
3
+ import com.facebook.react.BaseReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.module.model.ReactModuleInfoProvider
7
+ import com.facebook.react.uimanager.ViewManager
8
+
9
+ class EaseViewPackage : BaseReactPackage() {
10
+ override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
11
+ return listOf(EaseViewManager())
12
+ }
13
+
14
+ override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? = null
15
+
16
+ override fun getReactModuleInfoProvider() = ReactModuleInfoProvider { emptyMap() }
17
+ }