@onlynative/inertia-gradients 0.0.1-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,107 @@
1
+ import * as react from 'react';
2
+ import { LinearGradientProps } from 'expo-linear-gradient';
3
+ import { TransitionConfig } from '@onlynative/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 };
@@ -0,0 +1,107 @@
1
+ import * as react from 'react';
2
+ import { LinearGradientProps } from 'expo-linear-gradient';
3
+ import { TransitionConfig } from '@onlynative/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('@onlynative/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 '@onlynative/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 '@onlynative/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 '@onlynative/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/package.json ADDED
@@ -0,0 +1,87 @@
1
+ {
2
+ "name": "@onlynative/inertia-gradients",
3
+ "version": "0.0.1-alpha.3",
4
+ "description": "Animated linear gradient primitive for @onlynative/inertia, built on expo-linear-gradient.",
5
+ "license": "MIT",
6
+ "author": "OnlyNative",
7
+ "homepage": "https://github.com/onlynative/inertia",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/onlynative/inertia.git",
11
+ "directory": "packages/gradients"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/onlynative/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
+ "README.md",
45
+ "LICENSE",
46
+ "CHANGELOG.md",
47
+ "!**/__tests__",
48
+ "!**/*.test.*"
49
+ ],
50
+ "scripts": {
51
+ "build": "tsup",
52
+ "dev": "tsup --watch",
53
+ "typecheck": "tsc --noEmit",
54
+ "test": "jest",
55
+ "size": "size-limit",
56
+ "size:why": "size-limit --why",
57
+ "lint": "eslint src",
58
+ "clean": "rm -rf dist .turbo *.tsbuildinfo"
59
+ },
60
+ "peerDependencies": {
61
+ "@onlynative/inertia": "workspace:*",
62
+ "expo-linear-gradient": ">=14.0.0",
63
+ "react": ">=19.0.0",
64
+ "react-native": ">=0.81.0",
65
+ "react-native-reanimated": ">=4.0.0"
66
+ },
67
+ "devDependencies": {
68
+ "@onlynative/inertia": "workspace:*",
69
+ "@react-native/babel-preset": "^0.81.5",
70
+ "@size-limit/preset-small-lib": "^11.1.0",
71
+ "@testing-library/react-native": "^13.3.3",
72
+ "@types/jest": "^29.5.14",
73
+ "@types/react": "^19.1.0",
74
+ "expo-linear-gradient": "~14.0.2",
75
+ "jest": "^29.7.0",
76
+ "react": "19.1.0",
77
+ "react-native": "0.81.5",
78
+ "react-native-reanimated": "~4.1.1",
79
+ "react-test-renderer": "19.1.0",
80
+ "size-limit": "^11.1.0",
81
+ "tsup": "^8.3.5",
82
+ "typescript": "^5.7.3"
83
+ },
84
+ "publishConfig": {
85
+ "access": "public"
86
+ }
87
+ }
@@ -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 '@onlynative/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
+ * `@onlynative/inertia-gradients` — animated gradient primitives for
3
+ * `@onlynative/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 '@onlynative/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