@rootnative/inertia-gradients 0.0.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/CHANGELOG.md +16 -0
- package/LICENSE +21 -0
- package/README.md +96 -0
- package/dist/index.d.mts +107 -0
- package/dist/index.d.ts +107 -0
- package/dist/index.js +142 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +136 -0
- package/dist/index.mjs.map +1 -0
- package/llms.txt +95 -0
- package/package.json +88 -0
- package/src/MotionLinearGradient.tsx +261 -0
- package/src/index.ts +21 -0
- package/src/types.ts +54 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@rootnative/inertia-gradients` are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Pre-`1.0`, breaking changes may land in minor versions and are called out under their release.
|
|
4
|
+
|
|
5
|
+
This package ships in lockstep with `@rootnative/inertia` — version numbers track the core release that introduced or last touched the adapter.
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.0.0-alpha.0]
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `MotionLinearGradient` over `expo-linear-gradient`. Accepts the same `initial` / `animate` / `transition` shape as the core `Motion.*` primitives, with animatable keys for `colors`, `start`, `end`, and `locations`.
|
|
14
|
+
|
|
15
|
+
[unreleased]: https://github.com/rootnative/inertia/compare/v0.0.0-alpha.0...HEAD
|
|
16
|
+
[0.0.0-alpha.0]: https://github.com/rootnative/inertia/releases/tag/v0.0.0-alpha.0
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 RootNative
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# @rootnative/inertia-gradients
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@rootnative/inertia-gradients)
|
|
4
|
+
[](./LICENSE)
|
|
5
|
+
|
|
6
|
+
Animatable linear gradient primitive for [`@rootnative/inertia`](../core), built on [`expo-linear-gradient`](https://docs.expo.dev/versions/latest/sdk/linear-gradient/).
|
|
7
|
+
|
|
8
|
+
`MotionLinearGradient` accepts the same `initial` / `animate` / `transition` shape as the core `Motion.*` primitives, with animatable keys for `colors`, `start`, `end`, and `locations`.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
pnpm add @rootnative/inertia-gradients expo-linear-gradient
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
`expo-linear-gradient` works in bare React Native projects as well as Expo — no `expo-modules-core` runtime is required.
|
|
17
|
+
|
|
18
|
+
**Peer dependencies:** `@rootnative/inertia` (workspace or installed), `react >=19.0.0`, `react-native >=0.81.0`, `react-native-reanimated >=4.0.0`, `expo-linear-gradient >=14.0.0`.
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
```tsx
|
|
23
|
+
import { StyleSheet } from 'react-native'
|
|
24
|
+
import { MotionLinearGradient } from '@rootnative/inertia-gradients'
|
|
25
|
+
|
|
26
|
+
export function Hero() {
|
|
27
|
+
return (
|
|
28
|
+
<MotionLinearGradient
|
|
29
|
+
colors={['#0f172a', '#1e293b']}
|
|
30
|
+
animate={{ colors: ['#7c3aed', '#0ea5e9'] }}
|
|
31
|
+
transition={{ type: 'timing', duration: 600 }}
|
|
32
|
+
style={StyleSheet.absoluteFill}
|
|
33
|
+
/>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The static `colors` prop sets the visual on first render and **locks the slot count** for the component's lifetime. To resize the gradient (e.g. swap a 2-stop for a 3-stop), remount via `key={...}`. The component throws in dev if `colors.length` changes between renders.
|
|
39
|
+
|
|
40
|
+
## Animatable props
|
|
41
|
+
|
|
42
|
+
| Key | Shape | Notes |
|
|
43
|
+
| ----------- | -------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
|
|
44
|
+
| `colors` | `readonly string[]` | Element-wise color interpolation via Reanimated's color setter. Length must match the static `colors` prop. |
|
|
45
|
+
| `start` | `{ x: number, y: number }` | Normalized `[0, 1]` coordinates. `x` and `y` animate independently. |
|
|
46
|
+
| `end` | `{ x: number, y: number }` | Same shape as `start`. |
|
|
47
|
+
| `locations` | `readonly number[]` | Optional stop positions. If supplied at mount, must remain supplied and same-length as `colors` for the component's lifetime. |
|
|
48
|
+
|
|
49
|
+
Per-property transitions work just like the core primitives:
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
<MotionLinearGradient
|
|
53
|
+
colors={['#000', '#000']}
|
|
54
|
+
animate={{
|
|
55
|
+
colors: ['#7c3aed', '#0ea5e9'],
|
|
56
|
+
start: { x: 0, y: 0 },
|
|
57
|
+
end: { x: 1, y: 1 },
|
|
58
|
+
}}
|
|
59
|
+
transition={{
|
|
60
|
+
colors: { type: 'timing', duration: 600 },
|
|
61
|
+
start: { type: 'spring', tension: 80, friction: 14 },
|
|
62
|
+
end: { type: 'spring', tension: 80, friction: 14 },
|
|
63
|
+
}}
|
|
64
|
+
/>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## `initial`
|
|
68
|
+
|
|
69
|
+
Pass `initial` to override the mount-frame values (so the component starts somewhere other than the static props), or `initial={false}` to start at the `animate` target with no mount animation.
|
|
70
|
+
|
|
71
|
+
```tsx
|
|
72
|
+
<MotionLinearGradient
|
|
73
|
+
colors={['#111', '#222']}
|
|
74
|
+
initial={{ colors: ['#000', '#000'] }} // fade up from black
|
|
75
|
+
animate={{ colors: ['#7c3aed', '#0ea5e9'] }}
|
|
76
|
+
/>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Reduced motion
|
|
80
|
+
|
|
81
|
+
`MotionLinearGradient` participates in `<MotionConfig reducedMotion>` the same way the core primitives do — when the OS reduce-motion setting is on, transitions resolve as direct assignment instead of `withSpring` / `withTiming`.
|
|
82
|
+
|
|
83
|
+
## What this primitive doesn't do (v0.2)
|
|
84
|
+
|
|
85
|
+
- **Radial / conic gradients** — linear-only for v0.2.
|
|
86
|
+
- **Slot-count resize** — the colors array length is locked at mount.
|
|
87
|
+
- **Per-stop sequence keyframes** — `animate.colors` accepts a single target array, not nested arrays. For chained gradient transitions, drive the target through state.
|
|
88
|
+
|
|
89
|
+
## Documentation
|
|
90
|
+
|
|
91
|
+
- Full docs: [https://rootnative.github.io/inertia/docs/gradients](https://rootnative.github.io/inertia/docs/gradients)
|
|
92
|
+
- Core library: [`@rootnative/inertia`](../core)
|
|
93
|
+
|
|
94
|
+
## License
|
|
95
|
+
|
|
96
|
+
[MIT](./LICENSE) © RootNative
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { LinearGradientProps } from 'expo-linear-gradient';
|
|
3
|
+
import { TransitionConfig } from '@rootnative/inertia';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A 2D point on the gradient's `[0, 1]` square. `{ x: 0, y: 0 }` is the
|
|
7
|
+
* top-left corner; `{ x: 1, y: 1 }` is the bottom-right.
|
|
8
|
+
*/
|
|
9
|
+
interface GradientPoint {
|
|
10
|
+
x: number;
|
|
11
|
+
y: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Animatable target snapshot for a linear gradient. Every field is optional —
|
|
15
|
+
* include only the dimensions you want to animate; the rest fall back to the
|
|
16
|
+
* static props on the component.
|
|
17
|
+
*
|
|
18
|
+
* `colors` and `locations` arrays must keep the same length as the static
|
|
19
|
+
* `colors` prop. Slot count is locked at first render so the shared-value
|
|
20
|
+
* table is stable across the animation's lifetime.
|
|
21
|
+
*/
|
|
22
|
+
interface LinearGradientAnimate {
|
|
23
|
+
colors?: readonly string[];
|
|
24
|
+
start?: GradientPoint;
|
|
25
|
+
end?: GradientPoint;
|
|
26
|
+
locations?: readonly number[];
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The four animatable dimensions of a linear gradient. Per-key transitions on
|
|
30
|
+
* `transition` are keyed against this shape.
|
|
31
|
+
*/
|
|
32
|
+
interface LinearGradientStateShape {
|
|
33
|
+
colors: readonly string[];
|
|
34
|
+
start: GradientPoint;
|
|
35
|
+
end: GradientPoint;
|
|
36
|
+
locations: readonly number[];
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Per-property transition map. Top-level entries on `transition` apply to all
|
|
40
|
+
* properties unless overridden by a per-key entry here.
|
|
41
|
+
*/
|
|
42
|
+
type LinearGradientPerPropertyTransition = {
|
|
43
|
+
[K in keyof LinearGradientStateShape]?: TransitionConfig;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Transition shape accepted by `MotionLinearGradient`. Either a single
|
|
47
|
+
* top-level transition applied to every animated dimension, or a per-property
|
|
48
|
+
* map.
|
|
49
|
+
*/
|
|
50
|
+
type LinearGradientTransition = TransitionConfig | LinearGradientPerPropertyTransition;
|
|
51
|
+
|
|
52
|
+
type AtLeastTwoStrings = readonly [string, string, ...string[]];
|
|
53
|
+
interface MotionLinearGradientProps extends Omit<LinearGradientProps, 'colors' | 'start' | 'end' | 'locations'> {
|
|
54
|
+
/**
|
|
55
|
+
* Initial color stops, in order. At least two are required. The array's
|
|
56
|
+
* length is **locked at first render** — to change the number of stops,
|
|
57
|
+
* remount with a new `key`.
|
|
58
|
+
*/
|
|
59
|
+
colors: AtLeastTwoStrings;
|
|
60
|
+
/** Start point in normalized `[0, 1]` coordinates. Defaults to `{x:0,y:0}`. */
|
|
61
|
+
start?: GradientPoint;
|
|
62
|
+
/** End point in normalized `[0, 1]` coordinates. Defaults to `{x:1,y:0}`. */
|
|
63
|
+
end?: GradientPoint;
|
|
64
|
+
/**
|
|
65
|
+
* Optional stop positions. If supplied at mount, must remain supplied (and
|
|
66
|
+
* the same length as `colors`) for the lifetime of the component.
|
|
67
|
+
*/
|
|
68
|
+
locations?: readonly number[];
|
|
69
|
+
/**
|
|
70
|
+
* Initial frame override. When present, the component mounts displaying
|
|
71
|
+
* these values, then animates to `animate` on the next effect. Pass `false`
|
|
72
|
+
* to skip the initial-mount animation entirely.
|
|
73
|
+
*/
|
|
74
|
+
initial?: LinearGradientAnimate | false;
|
|
75
|
+
/** Target animation state. */
|
|
76
|
+
animate?: LinearGradientAnimate;
|
|
77
|
+
/**
|
|
78
|
+
* Transition config — either a single `TransitionConfig` applied to every
|
|
79
|
+
* animated dimension, or a per-property map (`{ colors, start, end,
|
|
80
|
+
* locations }`). Per-property entries win over the top-level transition.
|
|
81
|
+
*/
|
|
82
|
+
transition?: LinearGradientTransition;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Animatable `LinearGradient`. Wraps `expo-linear-gradient`'s `LinearGradient`
|
|
86
|
+
* with declarative `initial` / `animate` / `transition` props.
|
|
87
|
+
*
|
|
88
|
+
* Animatable dimensions:
|
|
89
|
+
* - `colors` — array of color strings, element-wise interpolated. Slot count
|
|
90
|
+
* is locked at mount.
|
|
91
|
+
* - `start` / `end` — `{ x, y }` points; x and y animate independently.
|
|
92
|
+
* - `locations` — array of stop positions, element-wise interpolated. Locked
|
|
93
|
+
* to the same length as `colors` (and to its presence at mount).
|
|
94
|
+
*
|
|
95
|
+
* Example:
|
|
96
|
+
* ```tsx
|
|
97
|
+
* <MotionLinearGradient
|
|
98
|
+
* colors={['#0f172a', '#1e293b']}
|
|
99
|
+
* animate={{ colors: ['#7c3aed', '#0ea5e9'] }}
|
|
100
|
+
* transition={{ type: 'timing', duration: 600 }}
|
|
101
|
+
* style={StyleSheet.absoluteFill}
|
|
102
|
+
* />
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
declare function MotionLinearGradient(props: MotionLinearGradientProps): react.JSX.Element;
|
|
106
|
+
|
|
107
|
+
export { type GradientPoint, type LinearGradientAnimate, type LinearGradientPerPropertyTransition, type LinearGradientStateShape, type LinearGradientTransition, MotionLinearGradient, type MotionLinearGradientProps };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { LinearGradientProps } from 'expo-linear-gradient';
|
|
3
|
+
import { TransitionConfig } from '@rootnative/inertia';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A 2D point on the gradient's `[0, 1]` square. `{ x: 0, y: 0 }` is the
|
|
7
|
+
* top-left corner; `{ x: 1, y: 1 }` is the bottom-right.
|
|
8
|
+
*/
|
|
9
|
+
interface GradientPoint {
|
|
10
|
+
x: number;
|
|
11
|
+
y: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Animatable target snapshot for a linear gradient. Every field is optional —
|
|
15
|
+
* include only the dimensions you want to animate; the rest fall back to the
|
|
16
|
+
* static props on the component.
|
|
17
|
+
*
|
|
18
|
+
* `colors` and `locations` arrays must keep the same length as the static
|
|
19
|
+
* `colors` prop. Slot count is locked at first render so the shared-value
|
|
20
|
+
* table is stable across the animation's lifetime.
|
|
21
|
+
*/
|
|
22
|
+
interface LinearGradientAnimate {
|
|
23
|
+
colors?: readonly string[];
|
|
24
|
+
start?: GradientPoint;
|
|
25
|
+
end?: GradientPoint;
|
|
26
|
+
locations?: readonly number[];
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The four animatable dimensions of a linear gradient. Per-key transitions on
|
|
30
|
+
* `transition` are keyed against this shape.
|
|
31
|
+
*/
|
|
32
|
+
interface LinearGradientStateShape {
|
|
33
|
+
colors: readonly string[];
|
|
34
|
+
start: GradientPoint;
|
|
35
|
+
end: GradientPoint;
|
|
36
|
+
locations: readonly number[];
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Per-property transition map. Top-level entries on `transition` apply to all
|
|
40
|
+
* properties unless overridden by a per-key entry here.
|
|
41
|
+
*/
|
|
42
|
+
type LinearGradientPerPropertyTransition = {
|
|
43
|
+
[K in keyof LinearGradientStateShape]?: TransitionConfig;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Transition shape accepted by `MotionLinearGradient`. Either a single
|
|
47
|
+
* top-level transition applied to every animated dimension, or a per-property
|
|
48
|
+
* map.
|
|
49
|
+
*/
|
|
50
|
+
type LinearGradientTransition = TransitionConfig | LinearGradientPerPropertyTransition;
|
|
51
|
+
|
|
52
|
+
type AtLeastTwoStrings = readonly [string, string, ...string[]];
|
|
53
|
+
interface MotionLinearGradientProps extends Omit<LinearGradientProps, 'colors' | 'start' | 'end' | 'locations'> {
|
|
54
|
+
/**
|
|
55
|
+
* Initial color stops, in order. At least two are required. The array's
|
|
56
|
+
* length is **locked at first render** — to change the number of stops,
|
|
57
|
+
* remount with a new `key`.
|
|
58
|
+
*/
|
|
59
|
+
colors: AtLeastTwoStrings;
|
|
60
|
+
/** Start point in normalized `[0, 1]` coordinates. Defaults to `{x:0,y:0}`. */
|
|
61
|
+
start?: GradientPoint;
|
|
62
|
+
/** End point in normalized `[0, 1]` coordinates. Defaults to `{x:1,y:0}`. */
|
|
63
|
+
end?: GradientPoint;
|
|
64
|
+
/**
|
|
65
|
+
* Optional stop positions. If supplied at mount, must remain supplied (and
|
|
66
|
+
* the same length as `colors`) for the lifetime of the component.
|
|
67
|
+
*/
|
|
68
|
+
locations?: readonly number[];
|
|
69
|
+
/**
|
|
70
|
+
* Initial frame override. When present, the component mounts displaying
|
|
71
|
+
* these values, then animates to `animate` on the next effect. Pass `false`
|
|
72
|
+
* to skip the initial-mount animation entirely.
|
|
73
|
+
*/
|
|
74
|
+
initial?: LinearGradientAnimate | false;
|
|
75
|
+
/** Target animation state. */
|
|
76
|
+
animate?: LinearGradientAnimate;
|
|
77
|
+
/**
|
|
78
|
+
* Transition config — either a single `TransitionConfig` applied to every
|
|
79
|
+
* animated dimension, or a per-property map (`{ colors, start, end,
|
|
80
|
+
* locations }`). Per-property entries win over the top-level transition.
|
|
81
|
+
*/
|
|
82
|
+
transition?: LinearGradientTransition;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Animatable `LinearGradient`. Wraps `expo-linear-gradient`'s `LinearGradient`
|
|
86
|
+
* with declarative `initial` / `animate` / `transition` props.
|
|
87
|
+
*
|
|
88
|
+
* Animatable dimensions:
|
|
89
|
+
* - `colors` — array of color strings, element-wise interpolated. Slot count
|
|
90
|
+
* is locked at mount.
|
|
91
|
+
* - `start` / `end` — `{ x, y }` points; x and y animate independently.
|
|
92
|
+
* - `locations` — array of stop positions, element-wise interpolated. Locked
|
|
93
|
+
* to the same length as `colors` (and to its presence at mount).
|
|
94
|
+
*
|
|
95
|
+
* Example:
|
|
96
|
+
* ```tsx
|
|
97
|
+
* <MotionLinearGradient
|
|
98
|
+
* colors={['#0f172a', '#1e293b']}
|
|
99
|
+
* animate={{ colors: ['#7c3aed', '#0ea5e9'] }}
|
|
100
|
+
* transition={{ type: 'timing', duration: 600 }}
|
|
101
|
+
* style={StyleSheet.absoluteFill}
|
|
102
|
+
* />
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
declare function MotionLinearGradient(props: MotionLinearGradientProps): react.JSX.Element;
|
|
106
|
+
|
|
107
|
+
export { type GradientPoint, type LinearGradientAnimate, type LinearGradientPerPropertyTransition, type LinearGradientStateShape, type LinearGradientTransition, MotionLinearGradient, type MotionLinearGradientProps };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var react = require('react');
|
|
4
|
+
var expoLinearGradient = require('expo-linear-gradient');
|
|
5
|
+
var Animated = require('react-native-reanimated');
|
|
6
|
+
var inertia = require('@rootnative/inertia');
|
|
7
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
8
|
+
|
|
9
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
10
|
+
|
|
11
|
+
var Animated__default = /*#__PURE__*/_interopDefault(Animated);
|
|
12
|
+
|
|
13
|
+
// src/MotionLinearGradient.tsx
|
|
14
|
+
var AnimatedLinearGradient = Animated__default.default.createAnimatedComponent(expoLinearGradient.LinearGradient);
|
|
15
|
+
var NO_ANIMATION = { type: "no-animation" };
|
|
16
|
+
var DEFAULT_START = { x: 0, y: 0 };
|
|
17
|
+
var DEFAULT_END = { x: 1, y: 0 };
|
|
18
|
+
function pickTransition(per, key) {
|
|
19
|
+
if (!per) return void 0;
|
|
20
|
+
if ("type" in per) return per;
|
|
21
|
+
return per[key];
|
|
22
|
+
}
|
|
23
|
+
function MotionLinearGradient(props) {
|
|
24
|
+
const {
|
|
25
|
+
colors,
|
|
26
|
+
start = DEFAULT_START,
|
|
27
|
+
end = DEFAULT_END,
|
|
28
|
+
locations,
|
|
29
|
+
initial,
|
|
30
|
+
animate,
|
|
31
|
+
transition,
|
|
32
|
+
...rest
|
|
33
|
+
} = props;
|
|
34
|
+
const slotCountRef = react.useRef(colors.length);
|
|
35
|
+
const hasLocationsRef = react.useRef(locations !== void 0);
|
|
36
|
+
if (__DEV__) {
|
|
37
|
+
if (slotCountRef.current !== colors.length) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
`[inertia-gradients] colors length changed from ${slotCountRef.current} to ${colors.length} \u2014 colors length is locked at mount; remount via key={...} to resize.`
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
if (hasLocationsRef.current !== (locations !== void 0)) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
`[inertia-gradients] locations presence changed \u2014 locations must be either always present or always absent (locked at mount).`
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
if (locations !== void 0 && locations.length !== slotCountRef.current) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
`[inertia-gradients] locations length (${locations.length}) must match colors length (${slotCountRef.current}).`
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const seedSource = initial === false ? animate : initial ?? void 0;
|
|
54
|
+
const seedColors = seedSource?.colors ?? colors;
|
|
55
|
+
const seedStart = seedSource?.start ?? start;
|
|
56
|
+
const seedEnd = seedSource?.end ?? end;
|
|
57
|
+
const seedLocations = seedSource?.locations ?? locations;
|
|
58
|
+
const colorSvs = [];
|
|
59
|
+
for (let i = 0; i < slotCountRef.current; i++) {
|
|
60
|
+
const seed = seedColors[i] ?? colors[i] ?? "";
|
|
61
|
+
colorSvs.push(Animated.useSharedValue(seed));
|
|
62
|
+
}
|
|
63
|
+
const startX = Animated.useSharedValue(seedStart.x);
|
|
64
|
+
const startY = Animated.useSharedValue(seedStart.y);
|
|
65
|
+
const endX = Animated.useSharedValue(seedEnd.x);
|
|
66
|
+
const endY = Animated.useSharedValue(seedEnd.y);
|
|
67
|
+
const locationSvs = [];
|
|
68
|
+
for (let i = 0; i < slotCountRef.current; i++) {
|
|
69
|
+
locationSvs.push(Animated.useSharedValue(seedLocations?.[i] ?? 0));
|
|
70
|
+
}
|
|
71
|
+
const reduce = inertia.useShouldReduceMotion();
|
|
72
|
+
const colorsKey = animate?.colors ? animate.colors.join("|") : "";
|
|
73
|
+
const startKey = animate?.start ? `${animate.start.x},${animate.start.y}` : "";
|
|
74
|
+
const endKey = animate?.end ? `${animate.end.x},${animate.end.y}` : "";
|
|
75
|
+
const locationsKey = animate?.locations ? animate.locations.join("|") : "";
|
|
76
|
+
const animateColors = animate?.colors;
|
|
77
|
+
const animateStart = animate?.start;
|
|
78
|
+
const animateEnd = animate?.end;
|
|
79
|
+
const animateLocations = animate?.locations;
|
|
80
|
+
react.useEffect(() => {
|
|
81
|
+
if (!animateColors) return;
|
|
82
|
+
const cfg = reduce ? NO_ANIMATION : pickTransition(transition, "colors");
|
|
83
|
+
for (let i = 0; i < colorSvs.length; i++) {
|
|
84
|
+
const target = animateColors[i] ?? colors[i] ?? "";
|
|
85
|
+
colorSvs[i].value = inertia.resolveTransition(cfg, target);
|
|
86
|
+
}
|
|
87
|
+
}, [colorsKey, reduce, transition]);
|
|
88
|
+
react.useEffect(() => {
|
|
89
|
+
if (!animateStart) return;
|
|
90
|
+
const cfg = reduce ? NO_ANIMATION : pickTransition(transition, "start");
|
|
91
|
+
startX.value = inertia.resolveTransition(cfg, animateStart.x);
|
|
92
|
+
startY.value = inertia.resolveTransition(cfg, animateStart.y);
|
|
93
|
+
}, [startKey, reduce, transition]);
|
|
94
|
+
react.useEffect(() => {
|
|
95
|
+
if (!animateEnd) return;
|
|
96
|
+
const cfg = reduce ? NO_ANIMATION : pickTransition(transition, "end");
|
|
97
|
+
endX.value = inertia.resolveTransition(cfg, animateEnd.x);
|
|
98
|
+
endY.value = inertia.resolveTransition(cfg, animateEnd.y);
|
|
99
|
+
}, [endKey, reduce, transition]);
|
|
100
|
+
react.useEffect(() => {
|
|
101
|
+
if (!animateLocations) return;
|
|
102
|
+
const cfg = reduce ? NO_ANIMATION : pickTransition(transition, "locations");
|
|
103
|
+
for (let i = 0; i < locationSvs.length; i++) {
|
|
104
|
+
const target = animateLocations[i];
|
|
105
|
+
if (target !== void 0) {
|
|
106
|
+
locationSvs[i].value = inertia.resolveTransition(cfg, target);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}, [locationsKey, reduce, transition]);
|
|
110
|
+
const animatedProps = Animated.useAnimatedProps(() => {
|
|
111
|
+
"worklet";
|
|
112
|
+
const colorsOut = new Array(colorSvs.length);
|
|
113
|
+
for (let i = 0; i < colorSvs.length; i++) colorsOut[i] = colorSvs[i].value;
|
|
114
|
+
const out = {
|
|
115
|
+
colors: colorsOut,
|
|
116
|
+
start: { x: startX.value, y: startY.value },
|
|
117
|
+
end: { x: endX.value, y: endY.value }
|
|
118
|
+
};
|
|
119
|
+
if (hasLocationsRef.current) {
|
|
120
|
+
const locsOut = new Array(locationSvs.length);
|
|
121
|
+
for (let i = 0; i < locationSvs.length; i++)
|
|
122
|
+
locsOut[i] = locationSvs[i].value;
|
|
123
|
+
out.locations = locsOut;
|
|
124
|
+
}
|
|
125
|
+
return out;
|
|
126
|
+
});
|
|
127
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
128
|
+
AnimatedLinearGradient,
|
|
129
|
+
{
|
|
130
|
+
animatedProps,
|
|
131
|
+
colors,
|
|
132
|
+
start,
|
|
133
|
+
end,
|
|
134
|
+
locations,
|
|
135
|
+
...rest
|
|
136
|
+
}
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
exports.MotionLinearGradient = MotionLinearGradient;
|
|
141
|
+
//# sourceMappingURL=index.js.map
|
|
142
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/MotionLinearGradient.tsx"],"names":["Animated","LinearGradient","useRef","useSharedValue","useShouldReduceMotion","useEffect","resolveTransition","useAnimatedProps","jsx"],"mappings":"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","file":"index.js","sourcesContent":["import { useEffect, useRef } from 'react'\nimport { LinearGradient, type LinearGradientProps } from 'expo-linear-gradient'\nimport Animated, {\n useAnimatedProps,\n useSharedValue,\n type SharedValue,\n} from 'react-native-reanimated'\nimport {\n resolveTransition,\n useShouldReduceMotion,\n type TransitionConfig,\n} from '@rootnative/inertia'\nimport type {\n GradientPoint,\n LinearGradientAnimate,\n LinearGradientPerPropertyTransition,\n LinearGradientTransition,\n} from './types'\n\nconst AnimatedLinearGradient = Animated.createAnimatedComponent(LinearGradient)\n\nconst NO_ANIMATION: TransitionConfig = { type: 'no-animation' }\nconst DEFAULT_START: GradientPoint = { x: 0, y: 0 }\nconst DEFAULT_END: GradientPoint = { x: 1, y: 0 }\n\n/**\n * Extract the per-key transition for `key`, falling back to the top-level\n * transition if the user passed `transition` as a single `TransitionConfig`\n * rather than a per-property map. Mirrors the precedence rule used by the\n * core factory: per-key wins, top-level fills, library default below that.\n */\nfunction pickTransition(\n per: LinearGradientTransition | undefined,\n key: keyof LinearGradientPerPropertyTransition,\n): TransitionConfig | undefined {\n if (!per) return undefined\n if ('type' in per) return per as TransitionConfig\n return (per as LinearGradientPerPropertyTransition)[key]\n}\n\ntype AtLeastTwoStrings = readonly [string, string, ...string[]]\n\nexport interface MotionLinearGradientProps extends Omit<\n LinearGradientProps,\n 'colors' | 'start' | 'end' | 'locations'\n> {\n /**\n * Initial color stops, in order. At least two are required. The array's\n * length is **locked at first render** — to change the number of stops,\n * remount with a new `key`.\n */\n colors: AtLeastTwoStrings\n /** Start point in normalized `[0, 1]` coordinates. Defaults to `{x:0,y:0}`. */\n start?: GradientPoint\n /** End point in normalized `[0, 1]` coordinates. Defaults to `{x:1,y:0}`. */\n end?: GradientPoint\n /**\n * Optional stop positions. If supplied at mount, must remain supplied (and\n * the same length as `colors`) for the lifetime of the component.\n */\n locations?: readonly number[]\n /**\n * Initial frame override. When present, the component mounts displaying\n * these values, then animates to `animate` on the next effect. Pass `false`\n * to skip the initial-mount animation entirely.\n */\n initial?: LinearGradientAnimate | false\n /** Target animation state. */\n animate?: LinearGradientAnimate\n /**\n * Transition config — either a single `TransitionConfig` applied to every\n * animated dimension, or a per-property map (`{ colors, start, end,\n * locations }`). Per-property entries win over the top-level transition.\n */\n transition?: LinearGradientTransition\n}\n\n/**\n * Animatable `LinearGradient`. Wraps `expo-linear-gradient`'s `LinearGradient`\n * with declarative `initial` / `animate` / `transition` props.\n *\n * Animatable dimensions:\n * - `colors` — array of color strings, element-wise interpolated. Slot count\n * is locked at mount.\n * - `start` / `end` — `{ x, y }` points; x and y animate independently.\n * - `locations` — array of stop positions, element-wise interpolated. Locked\n * to the same length as `colors` (and to its presence at mount).\n *\n * Example:\n * ```tsx\n * <MotionLinearGradient\n * colors={['#0f172a', '#1e293b']}\n * animate={{ colors: ['#7c3aed', '#0ea5e9'] }}\n * transition={{ type: 'timing', duration: 600 }}\n * style={StyleSheet.absoluteFill}\n * />\n * ```\n */\nexport function MotionLinearGradient(props: MotionLinearGradientProps) {\n const {\n colors,\n start = DEFAULT_START,\n end = DEFAULT_END,\n locations,\n initial,\n animate,\n transition,\n ...rest\n } = props\n\n // Slot count is locked at first render; subsequent renders must keep\n // `colors.length` constant so the hook order (one `useSharedValue` per slot\n // below) stays stable. `locations` presence is similarly locked because we\n // allocate its shared-value table on the same path.\n const slotCountRef = useRef(colors.length)\n const hasLocationsRef = useRef(locations !== undefined)\n\n if (__DEV__) {\n if (slotCountRef.current !== colors.length) {\n throw new Error(\n `[inertia-gradients] colors length changed from ${slotCountRef.current} to ${colors.length} — colors length is locked at mount; remount via key={...} to resize.`,\n )\n }\n if (hasLocationsRef.current !== (locations !== undefined)) {\n throw new Error(\n `[inertia-gradients] locations presence changed — locations must be either always present or always absent (locked at mount).`,\n )\n }\n if (locations !== undefined && locations.length !== slotCountRef.current) {\n throw new Error(\n `[inertia-gradients] locations length (${locations.length}) must match colors length (${slotCountRef.current}).`,\n )\n }\n }\n\n // `initial: false` means \"start at the animate target — no mount animation\".\n // `initial: {...}` overrides the static prop with explicit initial values.\n // `initial: undefined` (default) seeds from the static props.\n const seedSource = initial === false ? animate : (initial ?? undefined)\n const seedColors = seedSource?.colors ?? colors\n const seedStart = seedSource?.start ?? start\n const seedEnd = seedSource?.end ?? end\n const seedLocations = seedSource?.locations ?? locations\n\n // Loop-of-hooks pattern: safe because slotCountRef enforces a constant\n // length. ESLint can't see the invariant, so we suppress per call site.\n const colorSvs: SharedValue<string>[] = []\n for (let i = 0; i < slotCountRef.current; i++) {\n // Slot `i` is in-bounds for `colors` by the length lock, but TS sees a\n // generic `readonly string[]` index so coerce via a non-null fallback.\n const seed = seedColors[i] ?? colors[i] ?? ''\n // eslint-disable-next-line react-hooks/rules-of-hooks\n colorSvs.push(useSharedValue<string>(seed))\n }\n\n const startX = useSharedValue(seedStart.x)\n const startY = useSharedValue(seedStart.y)\n const endX = useSharedValue(seedEnd.x)\n const endY = useSharedValue(seedEnd.y)\n\n const locationSvs: SharedValue<number>[] = []\n for (let i = 0; i < slotCountRef.current; i++) {\n // eslint-disable-next-line react-hooks/rules-of-hooks\n locationSvs.push(useSharedValue<number>(seedLocations?.[i] ?? 0))\n }\n\n const reduce = useShouldReduceMotion()\n\n // Serialize targets into scalar keys so effects re-run on value change, not\n // on every parent re-render (a fresh `animate` literal each render is the\n // common case in callers).\n const colorsKey = animate?.colors ? animate.colors.join('|') : ''\n const startKey = animate?.start ? `${animate.start.x},${animate.start.y}` : ''\n const endKey = animate?.end ? `${animate.end.x},${animate.end.y}` : ''\n const locationsKey = animate?.locations ? animate.locations.join('|') : ''\n\n const animateColors = animate?.colors\n const animateStart = animate?.start\n const animateEnd = animate?.end\n const animateLocations = animate?.locations\n\n useEffect(() => {\n if (!animateColors) return\n const cfg = reduce ? NO_ANIMATION : pickTransition(transition, 'colors')\n for (let i = 0; i < colorSvs.length; i++) {\n const target = animateColors[i] ?? colors[i] ?? ''\n colorSvs[i]!.value = resolveTransition(cfg, target) as string\n }\n // colorSvs / colors are stable across renders by the length-lock above.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [colorsKey, reduce, transition])\n\n useEffect(() => {\n if (!animateStart) return\n const cfg = reduce ? NO_ANIMATION : pickTransition(transition, 'start')\n startX.value = resolveTransition(cfg, animateStart.x) as number\n startY.value = resolveTransition(cfg, animateStart.y) as number\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [startKey, reduce, transition])\n\n useEffect(() => {\n if (!animateEnd) return\n const cfg = reduce ? NO_ANIMATION : pickTransition(transition, 'end')\n endX.value = resolveTransition(cfg, animateEnd.x) as number\n endY.value = resolveTransition(cfg, animateEnd.y) as number\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [endKey, reduce, transition])\n\n useEffect(() => {\n if (!animateLocations) return\n const cfg = reduce ? NO_ANIMATION : pickTransition(transition, 'locations')\n for (let i = 0; i < locationSvs.length; i++) {\n const target = animateLocations[i]\n if (target !== undefined) {\n locationSvs[i]!.value = resolveTransition(cfg, target) as number\n }\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [locationsKey, reduce, transition])\n\n const animatedProps = useAnimatedProps(() => {\n 'worklet'\n const colorsOut = new Array<string>(colorSvs.length)\n for (let i = 0; i < colorSvs.length; i++) colorsOut[i] = colorSvs[i]!.value\n const out: {\n colors: string[]\n start: GradientPoint\n end: GradientPoint\n locations?: number[]\n } = {\n colors: colorsOut,\n start: { x: startX.value, y: startY.value },\n end: { x: endX.value, y: endY.value },\n }\n if (hasLocationsRef.current) {\n const locsOut = new Array<number>(locationSvs.length)\n for (let i = 0; i < locationSvs.length; i++)\n locsOut[i] = locationSvs[i]!.value\n out.locations = locsOut\n }\n return out\n })\n\n return (\n <AnimatedLinearGradient\n // `animatedProps` overrides the static `colors` / `start` / `end` /\n // `locations` each frame; the static props below are the first-render\n // seeds so the gradient renders before the first effect tick. The cast\n // sheds Reanimated's strict-tuple constraint that the worklet's return\n // type can't express — the runtime value is the same shape.\n animatedProps={animatedProps as never}\n colors={colors}\n start={start}\n end={end}\n locations={locations as never}\n {...rest}\n />\n )\n}\n\ndeclare const __DEV__: boolean\n"]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { useRef, useEffect } from 'react';
|
|
2
|
+
import { LinearGradient } from 'expo-linear-gradient';
|
|
3
|
+
import Animated, { useSharedValue, useAnimatedProps } from 'react-native-reanimated';
|
|
4
|
+
import { useShouldReduceMotion, resolveTransition } from '@rootnative/inertia';
|
|
5
|
+
import { jsx } from 'react/jsx-runtime';
|
|
6
|
+
|
|
7
|
+
// src/MotionLinearGradient.tsx
|
|
8
|
+
var AnimatedLinearGradient = Animated.createAnimatedComponent(LinearGradient);
|
|
9
|
+
var NO_ANIMATION = { type: "no-animation" };
|
|
10
|
+
var DEFAULT_START = { x: 0, y: 0 };
|
|
11
|
+
var DEFAULT_END = { x: 1, y: 0 };
|
|
12
|
+
function pickTransition(per, key) {
|
|
13
|
+
if (!per) return void 0;
|
|
14
|
+
if ("type" in per) return per;
|
|
15
|
+
return per[key];
|
|
16
|
+
}
|
|
17
|
+
function MotionLinearGradient(props) {
|
|
18
|
+
const {
|
|
19
|
+
colors,
|
|
20
|
+
start = DEFAULT_START,
|
|
21
|
+
end = DEFAULT_END,
|
|
22
|
+
locations,
|
|
23
|
+
initial,
|
|
24
|
+
animate,
|
|
25
|
+
transition,
|
|
26
|
+
...rest
|
|
27
|
+
} = props;
|
|
28
|
+
const slotCountRef = useRef(colors.length);
|
|
29
|
+
const hasLocationsRef = useRef(locations !== void 0);
|
|
30
|
+
if (__DEV__) {
|
|
31
|
+
if (slotCountRef.current !== colors.length) {
|
|
32
|
+
throw new Error(
|
|
33
|
+
`[inertia-gradients] colors length changed from ${slotCountRef.current} to ${colors.length} \u2014 colors length is locked at mount; remount via key={...} to resize.`
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
if (hasLocationsRef.current !== (locations !== void 0)) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
`[inertia-gradients] locations presence changed \u2014 locations must be either always present or always absent (locked at mount).`
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
if (locations !== void 0 && locations.length !== slotCountRef.current) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
`[inertia-gradients] locations length (${locations.length}) must match colors length (${slotCountRef.current}).`
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const seedSource = initial === false ? animate : initial ?? void 0;
|
|
48
|
+
const seedColors = seedSource?.colors ?? colors;
|
|
49
|
+
const seedStart = seedSource?.start ?? start;
|
|
50
|
+
const seedEnd = seedSource?.end ?? end;
|
|
51
|
+
const seedLocations = seedSource?.locations ?? locations;
|
|
52
|
+
const colorSvs = [];
|
|
53
|
+
for (let i = 0; i < slotCountRef.current; i++) {
|
|
54
|
+
const seed = seedColors[i] ?? colors[i] ?? "";
|
|
55
|
+
colorSvs.push(useSharedValue(seed));
|
|
56
|
+
}
|
|
57
|
+
const startX = useSharedValue(seedStart.x);
|
|
58
|
+
const startY = useSharedValue(seedStart.y);
|
|
59
|
+
const endX = useSharedValue(seedEnd.x);
|
|
60
|
+
const endY = useSharedValue(seedEnd.y);
|
|
61
|
+
const locationSvs = [];
|
|
62
|
+
for (let i = 0; i < slotCountRef.current; i++) {
|
|
63
|
+
locationSvs.push(useSharedValue(seedLocations?.[i] ?? 0));
|
|
64
|
+
}
|
|
65
|
+
const reduce = useShouldReduceMotion();
|
|
66
|
+
const colorsKey = animate?.colors ? animate.colors.join("|") : "";
|
|
67
|
+
const startKey = animate?.start ? `${animate.start.x},${animate.start.y}` : "";
|
|
68
|
+
const endKey = animate?.end ? `${animate.end.x},${animate.end.y}` : "";
|
|
69
|
+
const locationsKey = animate?.locations ? animate.locations.join("|") : "";
|
|
70
|
+
const animateColors = animate?.colors;
|
|
71
|
+
const animateStart = animate?.start;
|
|
72
|
+
const animateEnd = animate?.end;
|
|
73
|
+
const animateLocations = animate?.locations;
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
if (!animateColors) return;
|
|
76
|
+
const cfg = reduce ? NO_ANIMATION : pickTransition(transition, "colors");
|
|
77
|
+
for (let i = 0; i < colorSvs.length; i++) {
|
|
78
|
+
const target = animateColors[i] ?? colors[i] ?? "";
|
|
79
|
+
colorSvs[i].value = resolveTransition(cfg, target);
|
|
80
|
+
}
|
|
81
|
+
}, [colorsKey, reduce, transition]);
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
if (!animateStart) return;
|
|
84
|
+
const cfg = reduce ? NO_ANIMATION : pickTransition(transition, "start");
|
|
85
|
+
startX.value = resolveTransition(cfg, animateStart.x);
|
|
86
|
+
startY.value = resolveTransition(cfg, animateStart.y);
|
|
87
|
+
}, [startKey, reduce, transition]);
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
if (!animateEnd) return;
|
|
90
|
+
const cfg = reduce ? NO_ANIMATION : pickTransition(transition, "end");
|
|
91
|
+
endX.value = resolveTransition(cfg, animateEnd.x);
|
|
92
|
+
endY.value = resolveTransition(cfg, animateEnd.y);
|
|
93
|
+
}, [endKey, reduce, transition]);
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
if (!animateLocations) return;
|
|
96
|
+
const cfg = reduce ? NO_ANIMATION : pickTransition(transition, "locations");
|
|
97
|
+
for (let i = 0; i < locationSvs.length; i++) {
|
|
98
|
+
const target = animateLocations[i];
|
|
99
|
+
if (target !== void 0) {
|
|
100
|
+
locationSvs[i].value = resolveTransition(cfg, target);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}, [locationsKey, reduce, transition]);
|
|
104
|
+
const animatedProps = useAnimatedProps(() => {
|
|
105
|
+
"worklet";
|
|
106
|
+
const colorsOut = new Array(colorSvs.length);
|
|
107
|
+
for (let i = 0; i < colorSvs.length; i++) colorsOut[i] = colorSvs[i].value;
|
|
108
|
+
const out = {
|
|
109
|
+
colors: colorsOut,
|
|
110
|
+
start: { x: startX.value, y: startY.value },
|
|
111
|
+
end: { x: endX.value, y: endY.value }
|
|
112
|
+
};
|
|
113
|
+
if (hasLocationsRef.current) {
|
|
114
|
+
const locsOut = new Array(locationSvs.length);
|
|
115
|
+
for (let i = 0; i < locationSvs.length; i++)
|
|
116
|
+
locsOut[i] = locationSvs[i].value;
|
|
117
|
+
out.locations = locsOut;
|
|
118
|
+
}
|
|
119
|
+
return out;
|
|
120
|
+
});
|
|
121
|
+
return /* @__PURE__ */ jsx(
|
|
122
|
+
AnimatedLinearGradient,
|
|
123
|
+
{
|
|
124
|
+
animatedProps,
|
|
125
|
+
colors,
|
|
126
|
+
start,
|
|
127
|
+
end,
|
|
128
|
+
locations,
|
|
129
|
+
...rest
|
|
130
|
+
}
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export { MotionLinearGradient };
|
|
135
|
+
//# sourceMappingURL=index.mjs.map
|
|
136
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/MotionLinearGradient.tsx"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["import { useEffect, useRef } from 'react'\nimport { LinearGradient, type LinearGradientProps } from 'expo-linear-gradient'\nimport Animated, {\n useAnimatedProps,\n useSharedValue,\n type SharedValue,\n} from 'react-native-reanimated'\nimport {\n resolveTransition,\n useShouldReduceMotion,\n type TransitionConfig,\n} from '@rootnative/inertia'\nimport type {\n GradientPoint,\n LinearGradientAnimate,\n LinearGradientPerPropertyTransition,\n LinearGradientTransition,\n} from './types'\n\nconst AnimatedLinearGradient = Animated.createAnimatedComponent(LinearGradient)\n\nconst NO_ANIMATION: TransitionConfig = { type: 'no-animation' }\nconst DEFAULT_START: GradientPoint = { x: 0, y: 0 }\nconst DEFAULT_END: GradientPoint = { x: 1, y: 0 }\n\n/**\n * Extract the per-key transition for `key`, falling back to the top-level\n * transition if the user passed `transition` as a single `TransitionConfig`\n * rather than a per-property map. Mirrors the precedence rule used by the\n * core factory: per-key wins, top-level fills, library default below that.\n */\nfunction pickTransition(\n per: LinearGradientTransition | undefined,\n key: keyof LinearGradientPerPropertyTransition,\n): TransitionConfig | undefined {\n if (!per) return undefined\n if ('type' in per) return per as TransitionConfig\n return (per as LinearGradientPerPropertyTransition)[key]\n}\n\ntype AtLeastTwoStrings = readonly [string, string, ...string[]]\n\nexport interface MotionLinearGradientProps extends Omit<\n LinearGradientProps,\n 'colors' | 'start' | 'end' | 'locations'\n> {\n /**\n * Initial color stops, in order. At least two are required. The array's\n * length is **locked at first render** — to change the number of stops,\n * remount with a new `key`.\n */\n colors: AtLeastTwoStrings\n /** Start point in normalized `[0, 1]` coordinates. Defaults to `{x:0,y:0}`. */\n start?: GradientPoint\n /** End point in normalized `[0, 1]` coordinates. Defaults to `{x:1,y:0}`. */\n end?: GradientPoint\n /**\n * Optional stop positions. If supplied at mount, must remain supplied (and\n * the same length as `colors`) for the lifetime of the component.\n */\n locations?: readonly number[]\n /**\n * Initial frame override. When present, the component mounts displaying\n * these values, then animates to `animate` on the next effect. Pass `false`\n * to skip the initial-mount animation entirely.\n */\n initial?: LinearGradientAnimate | false\n /** Target animation state. */\n animate?: LinearGradientAnimate\n /**\n * Transition config — either a single `TransitionConfig` applied to every\n * animated dimension, or a per-property map (`{ colors, start, end,\n * locations }`). Per-property entries win over the top-level transition.\n */\n transition?: LinearGradientTransition\n}\n\n/**\n * Animatable `LinearGradient`. Wraps `expo-linear-gradient`'s `LinearGradient`\n * with declarative `initial` / `animate` / `transition` props.\n *\n * Animatable dimensions:\n * - `colors` — array of color strings, element-wise interpolated. Slot count\n * is locked at mount.\n * - `start` / `end` — `{ x, y }` points; x and y animate independently.\n * - `locations` — array of stop positions, element-wise interpolated. Locked\n * to the same length as `colors` (and to its presence at mount).\n *\n * Example:\n * ```tsx\n * <MotionLinearGradient\n * colors={['#0f172a', '#1e293b']}\n * animate={{ colors: ['#7c3aed', '#0ea5e9'] }}\n * transition={{ type: 'timing', duration: 600 }}\n * style={StyleSheet.absoluteFill}\n * />\n * ```\n */\nexport function MotionLinearGradient(props: MotionLinearGradientProps) {\n const {\n colors,\n start = DEFAULT_START,\n end = DEFAULT_END,\n locations,\n initial,\n animate,\n transition,\n ...rest\n } = props\n\n // Slot count is locked at first render; subsequent renders must keep\n // `colors.length` constant so the hook order (one `useSharedValue` per slot\n // below) stays stable. `locations` presence is similarly locked because we\n // allocate its shared-value table on the same path.\n const slotCountRef = useRef(colors.length)\n const hasLocationsRef = useRef(locations !== undefined)\n\n if (__DEV__) {\n if (slotCountRef.current !== colors.length) {\n throw new Error(\n `[inertia-gradients] colors length changed from ${slotCountRef.current} to ${colors.length} — colors length is locked at mount; remount via key={...} to resize.`,\n )\n }\n if (hasLocationsRef.current !== (locations !== undefined)) {\n throw new Error(\n `[inertia-gradients] locations presence changed — locations must be either always present or always absent (locked at mount).`,\n )\n }\n if (locations !== undefined && locations.length !== slotCountRef.current) {\n throw new Error(\n `[inertia-gradients] locations length (${locations.length}) must match colors length (${slotCountRef.current}).`,\n )\n }\n }\n\n // `initial: false` means \"start at the animate target — no mount animation\".\n // `initial: {...}` overrides the static prop with explicit initial values.\n // `initial: undefined` (default) seeds from the static props.\n const seedSource = initial === false ? animate : (initial ?? undefined)\n const seedColors = seedSource?.colors ?? colors\n const seedStart = seedSource?.start ?? start\n const seedEnd = seedSource?.end ?? end\n const seedLocations = seedSource?.locations ?? locations\n\n // Loop-of-hooks pattern: safe because slotCountRef enforces a constant\n // length. ESLint can't see the invariant, so we suppress per call site.\n const colorSvs: SharedValue<string>[] = []\n for (let i = 0; i < slotCountRef.current; i++) {\n // Slot `i` is in-bounds for `colors` by the length lock, but TS sees a\n // generic `readonly string[]` index so coerce via a non-null fallback.\n const seed = seedColors[i] ?? colors[i] ?? ''\n // eslint-disable-next-line react-hooks/rules-of-hooks\n colorSvs.push(useSharedValue<string>(seed))\n }\n\n const startX = useSharedValue(seedStart.x)\n const startY = useSharedValue(seedStart.y)\n const endX = useSharedValue(seedEnd.x)\n const endY = useSharedValue(seedEnd.y)\n\n const locationSvs: SharedValue<number>[] = []\n for (let i = 0; i < slotCountRef.current; i++) {\n // eslint-disable-next-line react-hooks/rules-of-hooks\n locationSvs.push(useSharedValue<number>(seedLocations?.[i] ?? 0))\n }\n\n const reduce = useShouldReduceMotion()\n\n // Serialize targets into scalar keys so effects re-run on value change, not\n // on every parent re-render (a fresh `animate` literal each render is the\n // common case in callers).\n const colorsKey = animate?.colors ? animate.colors.join('|') : ''\n const startKey = animate?.start ? `${animate.start.x},${animate.start.y}` : ''\n const endKey = animate?.end ? `${animate.end.x},${animate.end.y}` : ''\n const locationsKey = animate?.locations ? animate.locations.join('|') : ''\n\n const animateColors = animate?.colors\n const animateStart = animate?.start\n const animateEnd = animate?.end\n const animateLocations = animate?.locations\n\n useEffect(() => {\n if (!animateColors) return\n const cfg = reduce ? NO_ANIMATION : pickTransition(transition, 'colors')\n for (let i = 0; i < colorSvs.length; i++) {\n const target = animateColors[i] ?? colors[i] ?? ''\n colorSvs[i]!.value = resolveTransition(cfg, target) as string\n }\n // colorSvs / colors are stable across renders by the length-lock above.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [colorsKey, reduce, transition])\n\n useEffect(() => {\n if (!animateStart) return\n const cfg = reduce ? NO_ANIMATION : pickTransition(transition, 'start')\n startX.value = resolveTransition(cfg, animateStart.x) as number\n startY.value = resolveTransition(cfg, animateStart.y) as number\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [startKey, reduce, transition])\n\n useEffect(() => {\n if (!animateEnd) return\n const cfg = reduce ? NO_ANIMATION : pickTransition(transition, 'end')\n endX.value = resolveTransition(cfg, animateEnd.x) as number\n endY.value = resolveTransition(cfg, animateEnd.y) as number\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [endKey, reduce, transition])\n\n useEffect(() => {\n if (!animateLocations) return\n const cfg = reduce ? NO_ANIMATION : pickTransition(transition, 'locations')\n for (let i = 0; i < locationSvs.length; i++) {\n const target = animateLocations[i]\n if (target !== undefined) {\n locationSvs[i]!.value = resolveTransition(cfg, target) as number\n }\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [locationsKey, reduce, transition])\n\n const animatedProps = useAnimatedProps(() => {\n 'worklet'\n const colorsOut = new Array<string>(colorSvs.length)\n for (let i = 0; i < colorSvs.length; i++) colorsOut[i] = colorSvs[i]!.value\n const out: {\n colors: string[]\n start: GradientPoint\n end: GradientPoint\n locations?: number[]\n } = {\n colors: colorsOut,\n start: { x: startX.value, y: startY.value },\n end: { x: endX.value, y: endY.value },\n }\n if (hasLocationsRef.current) {\n const locsOut = new Array<number>(locationSvs.length)\n for (let i = 0; i < locationSvs.length; i++)\n locsOut[i] = locationSvs[i]!.value\n out.locations = locsOut\n }\n return out\n })\n\n return (\n <AnimatedLinearGradient\n // `animatedProps` overrides the static `colors` / `start` / `end` /\n // `locations` each frame; the static props below are the first-render\n // seeds so the gradient renders before the first effect tick. The cast\n // sheds Reanimated's strict-tuple constraint that the worklet's return\n // type can't express — the runtime value is the same shape.\n animatedProps={animatedProps as never}\n colors={colors}\n start={start}\n end={end}\n locations={locations as never}\n {...rest}\n />\n )\n}\n\ndeclare const __DEV__: boolean\n"]}
|
package/llms.txt
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
> @rootnative/inertia-gradients — adapter package for @rootnative/inertia.
|
|
2
|
+
> This file is generated from the matching docs page by scripts/build-llms.mjs — do not edit by hand.
|
|
3
|
+
|
|
4
|
+
> Full docs: https://rootnative.github.io/inertia/
|
|
5
|
+
> Core overview: see @rootnative/inertia/llms.txt (or docs/static/llms.txt in the repo)
|
|
6
|
+
> Source: https://github.com/rootnative/inertia
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
# Gradients
|
|
10
|
+
|
|
11
|
+
`@rootnative/inertia-gradients` adds an animatable linear gradient built on `expo-linear-gradient`. It is an **optional** sibling package — install it only when you need to animate gradient stops. The core library has no required `expo-linear-gradient` dependency.
|
|
12
|
+
|
|
13
|
+
`MotionLinearGradient` accepts the same `initial` / `animate` / `transition` shape as the core `Motion.*` primitives, with animatable keys for `colors`, `start`, `end`, and `locations`.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
yarn add @rootnative/inertia-gradients expo-linear-gradient
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
`expo-linear-gradient` works in bare React Native projects as well as Expo, but it is an Expo Module — bare projects need the Expo modules runtime installed (see [Installing Expo modules](https://docs.expo.dev/bare/installing-expo-modules/)).
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
```tsx
|
|
26
|
+
import { MotionLinearGradient } from '@rootnative/inertia-gradients'
|
|
27
|
+
|
|
28
|
+
function Hero() {
|
|
29
|
+
return (
|
|
30
|
+
<MotionLinearGradient
|
|
31
|
+
colors={['#0f172a', '#1e293b']}
|
|
32
|
+
animate={{ colors: ['#7c3aed', '#0ea5e9'] }}
|
|
33
|
+
transition={{ type: 'timing', duration: 600 }}
|
|
34
|
+
style={StyleSheet.absoluteFill}
|
|
35
|
+
/>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The static `colors` prop is required — it sets the visual on first render and **locks the slot count** for the lifetime of the component. To resize the gradient (e.g. swap a 2-stop for a 3-stop), remount via `key={...}`. The component throws in dev if `colors.length` changes between renders.
|
|
41
|
+
|
|
42
|
+
## Animatable props
|
|
43
|
+
|
|
44
|
+
| Key | Shape | Notes |
|
|
45
|
+
| ----------- | -------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
|
|
46
|
+
| `colors` | `readonly string[]` | Element-wise color interpolation via Reanimated's color setter. Length must match the static `colors` prop. |
|
|
47
|
+
| `start` | `{ x: number, y: number }` | Normalized `[0, 1]` coordinates. `x` and `y` animate independently — useful for rotating gradient direction. |
|
|
48
|
+
| `end` | `{ x: number, y: number }` | Same shape as `start`. |
|
|
49
|
+
| `locations` | `readonly number[]` | Optional stop positions. If supplied at mount, must remain supplied and same-length as `colors` for the component's lifetime. |
|
|
50
|
+
|
|
51
|
+
Per-property transitions work just like the core primitives:
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
<MotionLinearGradient
|
|
55
|
+
colors={['#000', '#000']}
|
|
56
|
+
animate={{
|
|
57
|
+
colors: ['#7c3aed', '#0ea5e9'],
|
|
58
|
+
start: { x: 0, y: 0 },
|
|
59
|
+
end: { x: 1, y: 1 },
|
|
60
|
+
}}
|
|
61
|
+
transition={{
|
|
62
|
+
colors: { type: 'timing', duration: 600 },
|
|
63
|
+
start: { type: 'spring', tension: 80, friction: 14 },
|
|
64
|
+
end: { type: 'spring', tension: 80, friction: 14 },
|
|
65
|
+
}}
|
|
66
|
+
/>
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## `initial`
|
|
70
|
+
|
|
71
|
+
Pass `initial` to override the mount-frame values (so the component starts somewhere other than the static props), or `initial={false}` to start at the `animate` target with no mount animation.
|
|
72
|
+
|
|
73
|
+
```tsx
|
|
74
|
+
<MotionLinearGradient
|
|
75
|
+
colors={['#111', '#222']}
|
|
76
|
+
initial={{ colors: ['#000', '#000'] }} // fade up from black
|
|
77
|
+
animate={{ colors: ['#7c3aed', '#0ea5e9'] }}
|
|
78
|
+
/>
|
|
79
|
+
|
|
80
|
+
<MotionLinearGradient
|
|
81
|
+
colors={['#111', '#222']}
|
|
82
|
+
initial={false} // skip the mount animation
|
|
83
|
+
animate={{ colors: ['#7c3aed', '#0ea5e9'] }}
|
|
84
|
+
/>
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Reduced motion
|
|
88
|
+
|
|
89
|
+
`MotionLinearGradient` participates in [`<MotionConfig reducedMotion>`](./motion-config.md) the same way the core primitives do — when the OS reduce-motion setting is on (or you pass `reducedMotion="always"`), transitions resolve as direct assignment instead of `withSpring` / `withTiming`.
|
|
90
|
+
|
|
91
|
+
## What this primitive doesn't do (v0.2)
|
|
92
|
+
|
|
93
|
+
- **Radial / conic gradients** — linear-only for v0.2. Radial lands in v0.3 once the linear API is validated.
|
|
94
|
+
- **Slot-count resize** — the colors array length is locked at mount. To change it, remount via `key={...}`.
|
|
95
|
+
- **Per-stop sequence keyframes** — `animate.colors` accepts a single target array, not a nested array of arrays. For chained gradient transitions, drive the target through state and let React re-render.
|
package/package.json
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rootnative/inertia-gradients",
|
|
3
|
+
"version": "0.0.0-alpha.0",
|
|
4
|
+
"description": "Animated linear gradient primitive for @rootnative/inertia, built on expo-linear-gradient.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "RootNative",
|
|
7
|
+
"homepage": "https://github.com/rootnative/inertia",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/rootnative/inertia.git",
|
|
11
|
+
"directory": "packages/gradients"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/rootnative/inertia/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"react-native",
|
|
18
|
+
"reanimated",
|
|
19
|
+
"gradient",
|
|
20
|
+
"linear-gradient",
|
|
21
|
+
"animation",
|
|
22
|
+
"inertia",
|
|
23
|
+
"expo"
|
|
24
|
+
],
|
|
25
|
+
"sideEffects": false,
|
|
26
|
+
"main": "./dist/index.js",
|
|
27
|
+
"module": "./dist/index.mjs",
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"react-native": "./src/index.ts",
|
|
30
|
+
"source": "./src/index.ts",
|
|
31
|
+
"exports": {
|
|
32
|
+
".": {
|
|
33
|
+
"types": "./dist/index.d.ts",
|
|
34
|
+
"react-native": "./src/index.ts",
|
|
35
|
+
"source": "./src/index.ts",
|
|
36
|
+
"import": "./dist/index.mjs",
|
|
37
|
+
"require": "./dist/index.js"
|
|
38
|
+
},
|
|
39
|
+
"./package.json": "./package.json"
|
|
40
|
+
},
|
|
41
|
+
"files": [
|
|
42
|
+
"dist",
|
|
43
|
+
"src",
|
|
44
|
+
"llms.txt",
|
|
45
|
+
"README.md",
|
|
46
|
+
"LICENSE",
|
|
47
|
+
"CHANGELOG.md",
|
|
48
|
+
"!**/__tests__",
|
|
49
|
+
"!**/*.test.*"
|
|
50
|
+
],
|
|
51
|
+
"peerDependencies": {
|
|
52
|
+
"@rootnative/inertia": ">=0.0.0-alpha.0",
|
|
53
|
+
"expo-linear-gradient": ">=14.0.0",
|
|
54
|
+
"react": ">=19.0.0",
|
|
55
|
+
"react-native": ">=0.81.0",
|
|
56
|
+
"react-native-reanimated": ">=4.0.0"
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@react-native/babel-preset": "^0.81.5",
|
|
60
|
+
"@size-limit/preset-small-lib": "^11.1.0",
|
|
61
|
+
"@testing-library/react-native": "^13.3.3",
|
|
62
|
+
"@types/jest": "^29.5.14",
|
|
63
|
+
"@types/react": "^19.1.0",
|
|
64
|
+
"expo-linear-gradient": "~14.0.2",
|
|
65
|
+
"jest": "^29.7.0",
|
|
66
|
+
"react": "19.1.0",
|
|
67
|
+
"react-native": "0.81.5",
|
|
68
|
+
"react-native-reanimated": "~4.1.1",
|
|
69
|
+
"react-test-renderer": "19.1.0",
|
|
70
|
+
"size-limit": "^11.1.0",
|
|
71
|
+
"tsup": "^8.3.5",
|
|
72
|
+
"typescript": "^5.7.3",
|
|
73
|
+
"@rootnative/inertia": "0.0.0-alpha.0"
|
|
74
|
+
},
|
|
75
|
+
"publishConfig": {
|
|
76
|
+
"access": "public"
|
|
77
|
+
},
|
|
78
|
+
"scripts": {
|
|
79
|
+
"build": "tsup",
|
|
80
|
+
"dev": "tsup --watch",
|
|
81
|
+
"typecheck": "tsc --noEmit",
|
|
82
|
+
"test": "jest",
|
|
83
|
+
"size": "size-limit",
|
|
84
|
+
"size:why": "size-limit --why",
|
|
85
|
+
"lint": "eslint .",
|
|
86
|
+
"clean": "rm -rf dist .turbo *.tsbuildinfo"
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react'
|
|
2
|
+
import { LinearGradient, type LinearGradientProps } from 'expo-linear-gradient'
|
|
3
|
+
import Animated, {
|
|
4
|
+
useAnimatedProps,
|
|
5
|
+
useSharedValue,
|
|
6
|
+
type SharedValue,
|
|
7
|
+
} from 'react-native-reanimated'
|
|
8
|
+
import {
|
|
9
|
+
resolveTransition,
|
|
10
|
+
useShouldReduceMotion,
|
|
11
|
+
type TransitionConfig,
|
|
12
|
+
} from '@rootnative/inertia'
|
|
13
|
+
import type {
|
|
14
|
+
GradientPoint,
|
|
15
|
+
LinearGradientAnimate,
|
|
16
|
+
LinearGradientPerPropertyTransition,
|
|
17
|
+
LinearGradientTransition,
|
|
18
|
+
} from './types'
|
|
19
|
+
|
|
20
|
+
const AnimatedLinearGradient = Animated.createAnimatedComponent(LinearGradient)
|
|
21
|
+
|
|
22
|
+
const NO_ANIMATION: TransitionConfig = { type: 'no-animation' }
|
|
23
|
+
const DEFAULT_START: GradientPoint = { x: 0, y: 0 }
|
|
24
|
+
const DEFAULT_END: GradientPoint = { x: 1, y: 0 }
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Extract the per-key transition for `key`, falling back to the top-level
|
|
28
|
+
* transition if the user passed `transition` as a single `TransitionConfig`
|
|
29
|
+
* rather than a per-property map. Mirrors the precedence rule used by the
|
|
30
|
+
* core factory: per-key wins, top-level fills, library default below that.
|
|
31
|
+
*/
|
|
32
|
+
function pickTransition(
|
|
33
|
+
per: LinearGradientTransition | undefined,
|
|
34
|
+
key: keyof LinearGradientPerPropertyTransition,
|
|
35
|
+
): TransitionConfig | undefined {
|
|
36
|
+
if (!per) return undefined
|
|
37
|
+
if ('type' in per) return per as TransitionConfig
|
|
38
|
+
return (per as LinearGradientPerPropertyTransition)[key]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type AtLeastTwoStrings = readonly [string, string, ...string[]]
|
|
42
|
+
|
|
43
|
+
export interface MotionLinearGradientProps extends Omit<
|
|
44
|
+
LinearGradientProps,
|
|
45
|
+
'colors' | 'start' | 'end' | 'locations'
|
|
46
|
+
> {
|
|
47
|
+
/**
|
|
48
|
+
* Initial color stops, in order. At least two are required. The array's
|
|
49
|
+
* length is **locked at first render** — to change the number of stops,
|
|
50
|
+
* remount with a new `key`.
|
|
51
|
+
*/
|
|
52
|
+
colors: AtLeastTwoStrings
|
|
53
|
+
/** Start point in normalized `[0, 1]` coordinates. Defaults to `{x:0,y:0}`. */
|
|
54
|
+
start?: GradientPoint
|
|
55
|
+
/** End point in normalized `[0, 1]` coordinates. Defaults to `{x:1,y:0}`. */
|
|
56
|
+
end?: GradientPoint
|
|
57
|
+
/**
|
|
58
|
+
* Optional stop positions. If supplied at mount, must remain supplied (and
|
|
59
|
+
* the same length as `colors`) for the lifetime of the component.
|
|
60
|
+
*/
|
|
61
|
+
locations?: readonly number[]
|
|
62
|
+
/**
|
|
63
|
+
* Initial frame override. When present, the component mounts displaying
|
|
64
|
+
* these values, then animates to `animate` on the next effect. Pass `false`
|
|
65
|
+
* to skip the initial-mount animation entirely.
|
|
66
|
+
*/
|
|
67
|
+
initial?: LinearGradientAnimate | false
|
|
68
|
+
/** Target animation state. */
|
|
69
|
+
animate?: LinearGradientAnimate
|
|
70
|
+
/**
|
|
71
|
+
* Transition config — either a single `TransitionConfig` applied to every
|
|
72
|
+
* animated dimension, or a per-property map (`{ colors, start, end,
|
|
73
|
+
* locations }`). Per-property entries win over the top-level transition.
|
|
74
|
+
*/
|
|
75
|
+
transition?: LinearGradientTransition
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Animatable `LinearGradient`. Wraps `expo-linear-gradient`'s `LinearGradient`
|
|
80
|
+
* with declarative `initial` / `animate` / `transition` props.
|
|
81
|
+
*
|
|
82
|
+
* Animatable dimensions:
|
|
83
|
+
* - `colors` — array of color strings, element-wise interpolated. Slot count
|
|
84
|
+
* is locked at mount.
|
|
85
|
+
* - `start` / `end` — `{ x, y }` points; x and y animate independently.
|
|
86
|
+
* - `locations` — array of stop positions, element-wise interpolated. Locked
|
|
87
|
+
* to the same length as `colors` (and to its presence at mount).
|
|
88
|
+
*
|
|
89
|
+
* Example:
|
|
90
|
+
* ```tsx
|
|
91
|
+
* <MotionLinearGradient
|
|
92
|
+
* colors={['#0f172a', '#1e293b']}
|
|
93
|
+
* animate={{ colors: ['#7c3aed', '#0ea5e9'] }}
|
|
94
|
+
* transition={{ type: 'timing', duration: 600 }}
|
|
95
|
+
* style={StyleSheet.absoluteFill}
|
|
96
|
+
* />
|
|
97
|
+
* ```
|
|
98
|
+
*/
|
|
99
|
+
export function MotionLinearGradient(props: MotionLinearGradientProps) {
|
|
100
|
+
const {
|
|
101
|
+
colors,
|
|
102
|
+
start = DEFAULT_START,
|
|
103
|
+
end = DEFAULT_END,
|
|
104
|
+
locations,
|
|
105
|
+
initial,
|
|
106
|
+
animate,
|
|
107
|
+
transition,
|
|
108
|
+
...rest
|
|
109
|
+
} = props
|
|
110
|
+
|
|
111
|
+
// Slot count is locked at first render; subsequent renders must keep
|
|
112
|
+
// `colors.length` constant so the hook order (one `useSharedValue` per slot
|
|
113
|
+
// below) stays stable. `locations` presence is similarly locked because we
|
|
114
|
+
// allocate its shared-value table on the same path.
|
|
115
|
+
const slotCountRef = useRef(colors.length)
|
|
116
|
+
const hasLocationsRef = useRef(locations !== undefined)
|
|
117
|
+
|
|
118
|
+
if (__DEV__) {
|
|
119
|
+
if (slotCountRef.current !== colors.length) {
|
|
120
|
+
throw new Error(
|
|
121
|
+
`[inertia-gradients] colors length changed from ${slotCountRef.current} to ${colors.length} — colors length is locked at mount; remount via key={...} to resize.`,
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
if (hasLocationsRef.current !== (locations !== undefined)) {
|
|
125
|
+
throw new Error(
|
|
126
|
+
`[inertia-gradients] locations presence changed — locations must be either always present or always absent (locked at mount).`,
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
if (locations !== undefined && locations.length !== slotCountRef.current) {
|
|
130
|
+
throw new Error(
|
|
131
|
+
`[inertia-gradients] locations length (${locations.length}) must match colors length (${slotCountRef.current}).`,
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// `initial: false` means "start at the animate target — no mount animation".
|
|
137
|
+
// `initial: {...}` overrides the static prop with explicit initial values.
|
|
138
|
+
// `initial: undefined` (default) seeds from the static props.
|
|
139
|
+
const seedSource = initial === false ? animate : (initial ?? undefined)
|
|
140
|
+
const seedColors = seedSource?.colors ?? colors
|
|
141
|
+
const seedStart = seedSource?.start ?? start
|
|
142
|
+
const seedEnd = seedSource?.end ?? end
|
|
143
|
+
const seedLocations = seedSource?.locations ?? locations
|
|
144
|
+
|
|
145
|
+
// Loop-of-hooks pattern: safe because slotCountRef enforces a constant
|
|
146
|
+
// length. ESLint can't see the invariant, so we suppress per call site.
|
|
147
|
+
const colorSvs: SharedValue<string>[] = []
|
|
148
|
+
for (let i = 0; i < slotCountRef.current; i++) {
|
|
149
|
+
// Slot `i` is in-bounds for `colors` by the length lock, but TS sees a
|
|
150
|
+
// generic `readonly string[]` index so coerce via a non-null fallback.
|
|
151
|
+
const seed = seedColors[i] ?? colors[i] ?? ''
|
|
152
|
+
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
153
|
+
colorSvs.push(useSharedValue<string>(seed))
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const startX = useSharedValue(seedStart.x)
|
|
157
|
+
const startY = useSharedValue(seedStart.y)
|
|
158
|
+
const endX = useSharedValue(seedEnd.x)
|
|
159
|
+
const endY = useSharedValue(seedEnd.y)
|
|
160
|
+
|
|
161
|
+
const locationSvs: SharedValue<number>[] = []
|
|
162
|
+
for (let i = 0; i < slotCountRef.current; i++) {
|
|
163
|
+
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
164
|
+
locationSvs.push(useSharedValue<number>(seedLocations?.[i] ?? 0))
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const reduce = useShouldReduceMotion()
|
|
168
|
+
|
|
169
|
+
// Serialize targets into scalar keys so effects re-run on value change, not
|
|
170
|
+
// on every parent re-render (a fresh `animate` literal each render is the
|
|
171
|
+
// common case in callers).
|
|
172
|
+
const colorsKey = animate?.colors ? animate.colors.join('|') : ''
|
|
173
|
+
const startKey = animate?.start ? `${animate.start.x},${animate.start.y}` : ''
|
|
174
|
+
const endKey = animate?.end ? `${animate.end.x},${animate.end.y}` : ''
|
|
175
|
+
const locationsKey = animate?.locations ? animate.locations.join('|') : ''
|
|
176
|
+
|
|
177
|
+
const animateColors = animate?.colors
|
|
178
|
+
const animateStart = animate?.start
|
|
179
|
+
const animateEnd = animate?.end
|
|
180
|
+
const animateLocations = animate?.locations
|
|
181
|
+
|
|
182
|
+
useEffect(() => {
|
|
183
|
+
if (!animateColors) return
|
|
184
|
+
const cfg = reduce ? NO_ANIMATION : pickTransition(transition, 'colors')
|
|
185
|
+
for (let i = 0; i < colorSvs.length; i++) {
|
|
186
|
+
const target = animateColors[i] ?? colors[i] ?? ''
|
|
187
|
+
colorSvs[i]!.value = resolveTransition(cfg, target) as string
|
|
188
|
+
}
|
|
189
|
+
// colorSvs / colors are stable across renders by the length-lock above.
|
|
190
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
191
|
+
}, [colorsKey, reduce, transition])
|
|
192
|
+
|
|
193
|
+
useEffect(() => {
|
|
194
|
+
if (!animateStart) return
|
|
195
|
+
const cfg = reduce ? NO_ANIMATION : pickTransition(transition, 'start')
|
|
196
|
+
startX.value = resolveTransition(cfg, animateStart.x) as number
|
|
197
|
+
startY.value = resolveTransition(cfg, animateStart.y) as number
|
|
198
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
199
|
+
}, [startKey, reduce, transition])
|
|
200
|
+
|
|
201
|
+
useEffect(() => {
|
|
202
|
+
if (!animateEnd) return
|
|
203
|
+
const cfg = reduce ? NO_ANIMATION : pickTransition(transition, 'end')
|
|
204
|
+
endX.value = resolveTransition(cfg, animateEnd.x) as number
|
|
205
|
+
endY.value = resolveTransition(cfg, animateEnd.y) as number
|
|
206
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
207
|
+
}, [endKey, reduce, transition])
|
|
208
|
+
|
|
209
|
+
useEffect(() => {
|
|
210
|
+
if (!animateLocations) return
|
|
211
|
+
const cfg = reduce ? NO_ANIMATION : pickTransition(transition, 'locations')
|
|
212
|
+
for (let i = 0; i < locationSvs.length; i++) {
|
|
213
|
+
const target = animateLocations[i]
|
|
214
|
+
if (target !== undefined) {
|
|
215
|
+
locationSvs[i]!.value = resolveTransition(cfg, target) as number
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
219
|
+
}, [locationsKey, reduce, transition])
|
|
220
|
+
|
|
221
|
+
const animatedProps = useAnimatedProps(() => {
|
|
222
|
+
'worklet'
|
|
223
|
+
const colorsOut = new Array<string>(colorSvs.length)
|
|
224
|
+
for (let i = 0; i < colorSvs.length; i++) colorsOut[i] = colorSvs[i]!.value
|
|
225
|
+
const out: {
|
|
226
|
+
colors: string[]
|
|
227
|
+
start: GradientPoint
|
|
228
|
+
end: GradientPoint
|
|
229
|
+
locations?: number[]
|
|
230
|
+
} = {
|
|
231
|
+
colors: colorsOut,
|
|
232
|
+
start: { x: startX.value, y: startY.value },
|
|
233
|
+
end: { x: endX.value, y: endY.value },
|
|
234
|
+
}
|
|
235
|
+
if (hasLocationsRef.current) {
|
|
236
|
+
const locsOut = new Array<number>(locationSvs.length)
|
|
237
|
+
for (let i = 0; i < locationSvs.length; i++)
|
|
238
|
+
locsOut[i] = locationSvs[i]!.value
|
|
239
|
+
out.locations = locsOut
|
|
240
|
+
}
|
|
241
|
+
return out
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
return (
|
|
245
|
+
<AnimatedLinearGradient
|
|
246
|
+
// `animatedProps` overrides the static `colors` / `start` / `end` /
|
|
247
|
+
// `locations` each frame; the static props below are the first-render
|
|
248
|
+
// seeds so the gradient renders before the first effect tick. The cast
|
|
249
|
+
// sheds Reanimated's strict-tuple constraint that the worklet's return
|
|
250
|
+
// type can't express — the runtime value is the same shape.
|
|
251
|
+
animatedProps={animatedProps as never}
|
|
252
|
+
colors={colors}
|
|
253
|
+
start={start}
|
|
254
|
+
end={end}
|
|
255
|
+
locations={locations as never}
|
|
256
|
+
{...rest}
|
|
257
|
+
/>
|
|
258
|
+
)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
declare const __DEV__: boolean
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@rootnative/inertia-gradients` — animated gradient primitives for
|
|
3
|
+
* `@rootnative/inertia`.
|
|
4
|
+
*
|
|
5
|
+
* v0.2 surface:
|
|
6
|
+
* - `MotionLinearGradient` — animatable linear gradient over
|
|
7
|
+
* `expo-linear-gradient`. Animates `colors`, `start`, `end`, and
|
|
8
|
+
* `locations` with the same `initial` / `animate` / `transition` shape
|
|
9
|
+
* as the core Motion primitives.
|
|
10
|
+
*
|
|
11
|
+
* Radial / conic gradients land in v0.3 once the linear API is validated.
|
|
12
|
+
*/
|
|
13
|
+
export { MotionLinearGradient } from './MotionLinearGradient'
|
|
14
|
+
export type { MotionLinearGradientProps } from './MotionLinearGradient'
|
|
15
|
+
export type {
|
|
16
|
+
GradientPoint,
|
|
17
|
+
LinearGradientAnimate,
|
|
18
|
+
LinearGradientPerPropertyTransition,
|
|
19
|
+
LinearGradientStateShape,
|
|
20
|
+
LinearGradientTransition,
|
|
21
|
+
} from './types'
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { TransitionConfig } from '@rootnative/inertia'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A 2D point on the gradient's `[0, 1]` square. `{ x: 0, y: 0 }` is the
|
|
5
|
+
* top-left corner; `{ x: 1, y: 1 }` is the bottom-right.
|
|
6
|
+
*/
|
|
7
|
+
export interface GradientPoint {
|
|
8
|
+
x: number
|
|
9
|
+
y: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Animatable target snapshot for a linear gradient. Every field is optional —
|
|
14
|
+
* include only the dimensions you want to animate; the rest fall back to the
|
|
15
|
+
* static props on the component.
|
|
16
|
+
*
|
|
17
|
+
* `colors` and `locations` arrays must keep the same length as the static
|
|
18
|
+
* `colors` prop. Slot count is locked at first render so the shared-value
|
|
19
|
+
* table is stable across the animation's lifetime.
|
|
20
|
+
*/
|
|
21
|
+
export interface LinearGradientAnimate {
|
|
22
|
+
colors?: readonly string[]
|
|
23
|
+
start?: GradientPoint
|
|
24
|
+
end?: GradientPoint
|
|
25
|
+
locations?: readonly number[]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The four animatable dimensions of a linear gradient. Per-key transitions on
|
|
30
|
+
* `transition` are keyed against this shape.
|
|
31
|
+
*/
|
|
32
|
+
export interface LinearGradientStateShape {
|
|
33
|
+
colors: readonly string[]
|
|
34
|
+
start: GradientPoint
|
|
35
|
+
end: GradientPoint
|
|
36
|
+
locations: readonly number[]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Per-property transition map. Top-level entries on `transition` apply to all
|
|
41
|
+
* properties unless overridden by a per-key entry here.
|
|
42
|
+
*/
|
|
43
|
+
export type LinearGradientPerPropertyTransition = {
|
|
44
|
+
[K in keyof LinearGradientStateShape]?: TransitionConfig
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Transition shape accepted by `MotionLinearGradient`. Either a single
|
|
49
|
+
* top-level transition applied to every animated dimension, or a per-property
|
|
50
|
+
* map.
|
|
51
|
+
*/
|
|
52
|
+
export type LinearGradientTransition =
|
|
53
|
+
| TransitionConfig
|
|
54
|
+
| LinearGradientPerPropertyTransition
|