@onlynative/inertia-svg 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.
package/README.md ADDED
@@ -0,0 +1,96 @@
1
+ # @onlynative/inertia-svg
2
+
3
+ [![npm](https://img.shields.io/npm/v/@onlynative/inertia-svg.svg)](https://www.npmjs.com/package/@onlynative/inertia-svg)
4
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
5
+
6
+ Animatable SVG primitives for [`@onlynative/inertia`](../core), built on [`react-native-svg`](https://github.com/software-mansion/react-native-svg).
7
+
8
+ `MotionPath` accepts the same `initial` / `animate` / `transition` shape as the core `Motion.*` primitives, with animatable keys for the path data (`d`), color paint (`fill`, `stroke`), and numeric paint (`strokeWidth`, opacities, `strokeDashoffset`).
9
+
10
+ ## Install
11
+
12
+ ```sh
13
+ pnpm add @onlynative/inertia-svg react-native-svg
14
+ ```
15
+
16
+ `react-native-svg` works in bare React Native projects as well as Expo.
17
+
18
+ **Peer dependencies:** `@onlynative/inertia` (workspace or installed), `react >=19.0.0`, `react-native >=0.81.0`, `react-native-reanimated >=4.0.0`, `react-native-svg >=15.0.0`.
19
+
20
+ ## Usage
21
+
22
+ ```tsx
23
+ import Svg from 'react-native-svg'
24
+ import { MotionPath } from '@onlynative/inertia-svg'
25
+
26
+ export function Heart({ beating }) {
27
+ return (
28
+ <Svg viewBox="0 0 100 100" width={120} height={120}>
29
+ <MotionPath
30
+ d="M 50 30 L 70 12 L 90 30 L 50 88 L 10 30 L 30 12 Z"
31
+ fill="#ef4444"
32
+ stroke="#991b1b"
33
+ strokeWidth={3}
34
+ animate={{
35
+ d: beating
36
+ ? 'M 50 28 L 71 10 L 92 28 L 50 92 L 8 28 L 29 10 Z'
37
+ : 'M 50 30 L 70 12 L 90 30 L 50 88 L 10 30 L 30 12 Z',
38
+ }}
39
+ transition={{ type: 'spring', tension: 200, friction: 10 }}
40
+ />
41
+ </Svg>
42
+ )
43
+ }
44
+ ```
45
+
46
+ The static `d` prop sets the visual on first render and **locks the command sequence** for the component's lifetime. Every target `d` (via `initial` or `animate`) must produce the same command letters in the same order after implicit-repeat expansion. To switch between structurally different shapes, remount with a new `key`.
47
+
48
+ ## Animatable props
49
+
50
+ | Key | Type | Notes |
51
+ | ------------------ | -------- | ------------------------------------------------------------------------------------------------- |
52
+ | `d` | `string` | Path morph via element-wise scalar interpolation. Source and target must share the same template. |
53
+ | `fill` | `string` | Color, interpolated by Reanimated's color setter. |
54
+ | `stroke` | `string` | Color. |
55
+ | `strokeWidth` | `number` | Numeric. |
56
+ | `strokeOpacity` | `number` | 0–1. |
57
+ | `fillOpacity` | `number` | 0–1. |
58
+ | `opacity` | `number` | 0–1. |
59
+ | `strokeDashoffset` | `number` | Useful for "draw-in" animations on a dashed stroke. |
60
+
61
+ Per-property transitions are supported — pass a `{ [key]: TransitionConfig }` shape to `transition` instead of a single config, e.g.
62
+
63
+ ```tsx
64
+ transition={{
65
+ d: { type: 'spring', tension: 160, friction: 14 },
66
+ fill: { type: 'timing', duration: 300 },
67
+ }}
68
+ ```
69
+
70
+ ## Structural compatibility
71
+
72
+ ```
73
+ ✅ M 0 0 L 10 10 Z ↔ M 50 50 L 80 80 Z same template: M L Z
74
+ ✅ M 0 0 10 10 20 20 ↔ M 0 0 L 30 30 L 40 40 same after implicit-repeat expansion (M → L)
75
+ ❌ M 0 0 L 10 10 Z ↔ M 0 0 L 10 10 segment count differs
76
+ ❌ M 0 0 L 10 10 ↔ M 0 0 l 10 10 absolute vs relative are distinct templates
77
+ ❌ M 0 0 L 10 10 ↔ M 0 0 C 1 1 2 2 3 3 command letters differ
78
+ ```
79
+
80
+ In dev, the component throws on template mismatches at mount, when `animate.d` changes shape, or when the static `d` prop itself changes shape between renders. In production those errors degrade to a no-op snap so a single bad target doesn't crash the screen.
81
+
82
+ Path resampling between structurally different shapes (flubber-style) is out of scope for v0.2. For arbitrary shape swaps, remount with `key={...}`.
83
+
84
+ ## Reduced motion
85
+
86
+ `MotionPath` participates in `<MotionConfig reducedMotion>` just like the core primitives — when the OS reduce-motion setting is on (or you pass `reducedMotion="always"`), every animated property snaps directly to its target.
87
+
88
+ ## What this package doesn't do (v0.2)
89
+
90
+ - Other SVG shapes (`Circle`, `Rect`, `Line`, `Ellipse`) — land in a follow-up once the `MotionPath` API is validated.
91
+ - Path resampling between arbitrary shapes.
92
+ - Morphing an `L` into a `C` (or other across-command interpolation). Element-wise scalar interpolation is intentional.
93
+
94
+ ## License
95
+
96
+ MIT
@@ -0,0 +1,188 @@
1
+ import * as react from 'react';
2
+ import { PathProps } from 'react-native-svg';
3
+ import { TransitionConfig } from '@onlynative/inertia';
4
+
5
+ /**
6
+ * Animatable target snapshot for a `MotionSvg.Path`. Every field is optional
7
+ * — include only the dimensions you want to animate; the rest fall back to
8
+ * the static props on the component.
9
+ *
10
+ * `d` morphs the path geometry. The target path **must produce the same
11
+ * command sequence** as the source after implicit-repeat expansion (same
12
+ * letters in the same order, e.g. `M L L L Z`). Element-wise numeric
13
+ * interpolation is the morphing model — see `parsePathD` for the
14
+ * normalization rules.
15
+ */
16
+ interface PathAnimate {
17
+ d?: string;
18
+ fill?: string;
19
+ stroke?: string;
20
+ strokeWidth?: number;
21
+ strokeOpacity?: number;
22
+ fillOpacity?: number;
23
+ opacity?: number;
24
+ strokeDashoffset?: number;
25
+ }
26
+ /** The animatable dimensions of a `MotionSvg.Path`. */
27
+ type PathStateShape = {
28
+ [K in keyof Required<PathAnimate>]: PathAnimate[K];
29
+ };
30
+ /**
31
+ * Per-property transition map. Top-level entries on `transition` apply to all
32
+ * properties unless overridden by a per-key entry here.
33
+ */
34
+ type PathPerPropertyTransition = {
35
+ [K in keyof PathStateShape]?: TransitionConfig;
36
+ };
37
+ /**
38
+ * Transition shape accepted by `MotionSvg.Path`. Either a single top-level
39
+ * transition applied to every animated dimension, or a per-property map.
40
+ */
41
+ type PathTransition = TransitionConfig | PathPerPropertyTransition;
42
+
43
+ interface MotionPathProps extends Omit<PathProps, 'd' | 'fill' | 'stroke' | 'strokeWidth' | 'strokeOpacity' | 'fillOpacity' | 'opacity' | 'strokeDashoffset'> {
44
+ /**
45
+ * Initial path data. **The command sequence is locked at first render** —
46
+ * every target `d` passed via `animate` / `initial` must produce the same
47
+ * command letters in the same order after implicit-repeat expansion. To
48
+ * morph between structurally different paths, remount with a new `key`.
49
+ */
50
+ d: string;
51
+ fill?: string;
52
+ stroke?: string;
53
+ strokeWidth?: number;
54
+ strokeOpacity?: number;
55
+ fillOpacity?: number;
56
+ opacity?: number;
57
+ strokeDashoffset?: number;
58
+ /**
59
+ * Initial frame override. When present, the component mounts displaying
60
+ * these values, then animates to `animate` on the next effect. Pass `false`
61
+ * to skip the initial-mount animation entirely.
62
+ */
63
+ initial?: PathAnimate | false;
64
+ /** Target animation state. */
65
+ animate?: PathAnimate;
66
+ /**
67
+ * Transition config — either a single `TransitionConfig` applied to every
68
+ * animated dimension, or a per-property map. Per-property entries win over
69
+ * the top-level transition.
70
+ */
71
+ transition?: PathTransition;
72
+ }
73
+ /**
74
+ * Animatable `<Path>` from `react-native-svg`. Wraps `Path` with declarative
75
+ * `initial` / `animate` / `transition` props.
76
+ *
77
+ * Animatable dimensions:
78
+ * - `d` — path morph via element-wise scalar interpolation. Source and target
79
+ * must share the same command sequence (e.g. both `M L L L Z`).
80
+ * - `fill`, `stroke` — color strings, interpolated via Reanimated's native
81
+ * color animation.
82
+ * - `strokeWidth`, `strokeOpacity`, `fillOpacity`, `opacity`,
83
+ * `strokeDashoffset` — numeric, spring or timing-driven.
84
+ *
85
+ * Example:
86
+ * ```tsx
87
+ * <Svg viewBox="0 0 100 100">
88
+ * <MotionPath
89
+ * d="M 50 20 L 80 80 L 20 80 Z"
90
+ * animate={{ d: "M 50 80 L 80 20 L 20 20 Z", fill: '#7c3aed' }}
91
+ * transition={{ type: 'spring', tension: 140, friction: 12 }}
92
+ * fill="#0ea5e9"
93
+ * />
94
+ * </Svg>
95
+ * ```
96
+ */
97
+ declare function MotionPath(props: MotionPathProps): react.JSX.Element;
98
+
99
+ /**
100
+ * SVG path-string utilities used by `MotionSvg.Path`. Everything here runs on
101
+ * the JS thread — paths are tokenized into a normalized command list at mount
102
+ * and when `animate.d` changes; the worklet only ever consumes flat number
103
+ * arrays + a frozen command template.
104
+ *
105
+ * Path morphing in v0.2 requires **structural compatibility**: the source and
106
+ * every target `d` must produce the same command sequence (same command
107
+ * letters, in the same order, after implicit-repeat expansion). Element-wise
108
+ * numeric interpolation is the entire morphing model — we do not resample
109
+ * paths or insert/remove commands. Same-shape morphs (e.g. a heart breathing,
110
+ * a chevron flipping, a check mark tracing in) are the supported use case.
111
+ */
112
+ /**
113
+ * A single normalized path command after implicit-repeat expansion. The cmd
114
+ * letter is preserved (absolute vs relative — case is meaningful to the SVG
115
+ * renderer). `args` always has exactly `CMD_ARGS[cmd]` entries.
116
+ */
117
+ interface PathSegment {
118
+ cmd: string;
119
+ args: number[];
120
+ }
121
+ /**
122
+ * Parse a path `d` string into a flat list of normalized segments. Implicit
123
+ * repeats are expanded — `M 0 0 10 10 20 20` becomes three segments
124
+ * (`M 0 0`, `L 10 10`, `L 20 20`) so the segment list can be compared and
125
+ * interpolated 1:1 against another path.
126
+ */
127
+ declare function parsePathD(d: string): PathSegment[];
128
+ /**
129
+ * The frozen "shape" of a path — just command letters and arg widths. Two
130
+ * paths are morphable iff their templates are equal.
131
+ */
132
+ interface PathTemplate {
133
+ cmds: ReadonlyArray<string>;
134
+ /** Flat width per segment, indexed parallel to `cmds`. */
135
+ widths: ReadonlyArray<number>;
136
+ /** Total scalar count across all segments — `widths.reduce((a,b)=>a+b,0)`. */
137
+ size: number;
138
+ }
139
+ declare function templateOf(segments: ReadonlyArray<PathSegment>): PathTemplate;
140
+ /** Flatten a parsed segment list into a single number array (length === size). */
141
+ declare function flattenParams(segments: ReadonlyArray<PathSegment>): number[];
142
+ /**
143
+ * Verify a target template matches the source. Returns `null` on match or a
144
+ * descriptive error string on mismatch — callers throw in `__DEV__` and
145
+ * silently snap to the target in production.
146
+ */
147
+ declare function diffTemplate(source: PathTemplate, target: PathTemplate): string | null;
148
+ /**
149
+ * Build a path `d` string from a template + flat param array. Runs inside the
150
+ * worklet on the UI thread, so it must not capture any JS-thread closures or
151
+ * use Array.prototype helpers that allocate intermediates the Hermes runtime
152
+ * boxes into JS objects. Manual loops + `+=` string concat keep the worklet
153
+ * cheap.
154
+ *
155
+ * MUST be a worklet — call sites in `MotionPath` wrap it with `'worklet'` via
156
+ * `useAnimatedProps`.
157
+ */
158
+ declare function serializePath(template: PathTemplate, params: ReadonlyArray<number>): string;
159
+
160
+ /**
161
+ * `@onlynative/inertia-svg` — animatable SVG primitives for
162
+ * `@onlynative/inertia`.
163
+ *
164
+ * v0.2 surface:
165
+ * - `MotionPath` / `MotionSvg.Path` — animatable `<Path>` over
166
+ * `react-native-svg`. Supports path morphing on the `d` attribute (source
167
+ * and target must share the same command sequence) plus animatable
168
+ * `fill`, `stroke`, `strokeWidth`, `strokeOpacity`, `fillOpacity`,
169
+ * `opacity`, and `strokeDashoffset` with the same `initial` /
170
+ * `animate` / `transition` shape as the core `Motion.*` primitives.
171
+ *
172
+ * Additional shape primitives (`Circle`, `Rect`, `Line`, `Ellipse`) land in
173
+ * a follow-up once the path morphing API is validated. Path normalization
174
+ * (resampling between structurally different paths) is out of scope for
175
+ * v0.2 — use structurally-compatible source/target paths and remount with
176
+ * `key={...}` to switch shape.
177
+ */
178
+
179
+ /**
180
+ * Namespace bundling every animatable SVG primitive. Use `MotionSvg.Path` for
181
+ * autocomplete-friendly grouping or import `MotionPath` directly — both
182
+ * point at the same component.
183
+ */
184
+ declare const MotionSvg: {
185
+ readonly Path: typeof MotionPath;
186
+ };
187
+
188
+ export { MotionPath, type MotionPathProps, MotionSvg, type PathAnimate, type PathPerPropertyTransition, type PathSegment, type PathStateShape, type PathTemplate, type PathTransition, diffTemplate, flattenParams, parsePathD, serializePath, templateOf };
@@ -0,0 +1,188 @@
1
+ import * as react from 'react';
2
+ import { PathProps } from 'react-native-svg';
3
+ import { TransitionConfig } from '@onlynative/inertia';
4
+
5
+ /**
6
+ * Animatable target snapshot for a `MotionSvg.Path`. Every field is optional
7
+ * — include only the dimensions you want to animate; the rest fall back to
8
+ * the static props on the component.
9
+ *
10
+ * `d` morphs the path geometry. The target path **must produce the same
11
+ * command sequence** as the source after implicit-repeat expansion (same
12
+ * letters in the same order, e.g. `M L L L Z`). Element-wise numeric
13
+ * interpolation is the morphing model — see `parsePathD` for the
14
+ * normalization rules.
15
+ */
16
+ interface PathAnimate {
17
+ d?: string;
18
+ fill?: string;
19
+ stroke?: string;
20
+ strokeWidth?: number;
21
+ strokeOpacity?: number;
22
+ fillOpacity?: number;
23
+ opacity?: number;
24
+ strokeDashoffset?: number;
25
+ }
26
+ /** The animatable dimensions of a `MotionSvg.Path`. */
27
+ type PathStateShape = {
28
+ [K in keyof Required<PathAnimate>]: PathAnimate[K];
29
+ };
30
+ /**
31
+ * Per-property transition map. Top-level entries on `transition` apply to all
32
+ * properties unless overridden by a per-key entry here.
33
+ */
34
+ type PathPerPropertyTransition = {
35
+ [K in keyof PathStateShape]?: TransitionConfig;
36
+ };
37
+ /**
38
+ * Transition shape accepted by `MotionSvg.Path`. Either a single top-level
39
+ * transition applied to every animated dimension, or a per-property map.
40
+ */
41
+ type PathTransition = TransitionConfig | PathPerPropertyTransition;
42
+
43
+ interface MotionPathProps extends Omit<PathProps, 'd' | 'fill' | 'stroke' | 'strokeWidth' | 'strokeOpacity' | 'fillOpacity' | 'opacity' | 'strokeDashoffset'> {
44
+ /**
45
+ * Initial path data. **The command sequence is locked at first render** —
46
+ * every target `d` passed via `animate` / `initial` must produce the same
47
+ * command letters in the same order after implicit-repeat expansion. To
48
+ * morph between structurally different paths, remount with a new `key`.
49
+ */
50
+ d: string;
51
+ fill?: string;
52
+ stroke?: string;
53
+ strokeWidth?: number;
54
+ strokeOpacity?: number;
55
+ fillOpacity?: number;
56
+ opacity?: number;
57
+ strokeDashoffset?: number;
58
+ /**
59
+ * Initial frame override. When present, the component mounts displaying
60
+ * these values, then animates to `animate` on the next effect. Pass `false`
61
+ * to skip the initial-mount animation entirely.
62
+ */
63
+ initial?: PathAnimate | false;
64
+ /** Target animation state. */
65
+ animate?: PathAnimate;
66
+ /**
67
+ * Transition config — either a single `TransitionConfig` applied to every
68
+ * animated dimension, or a per-property map. Per-property entries win over
69
+ * the top-level transition.
70
+ */
71
+ transition?: PathTransition;
72
+ }
73
+ /**
74
+ * Animatable `<Path>` from `react-native-svg`. Wraps `Path` with declarative
75
+ * `initial` / `animate` / `transition` props.
76
+ *
77
+ * Animatable dimensions:
78
+ * - `d` — path morph via element-wise scalar interpolation. Source and target
79
+ * must share the same command sequence (e.g. both `M L L L Z`).
80
+ * - `fill`, `stroke` — color strings, interpolated via Reanimated's native
81
+ * color animation.
82
+ * - `strokeWidth`, `strokeOpacity`, `fillOpacity`, `opacity`,
83
+ * `strokeDashoffset` — numeric, spring or timing-driven.
84
+ *
85
+ * Example:
86
+ * ```tsx
87
+ * <Svg viewBox="0 0 100 100">
88
+ * <MotionPath
89
+ * d="M 50 20 L 80 80 L 20 80 Z"
90
+ * animate={{ d: "M 50 80 L 80 20 L 20 20 Z", fill: '#7c3aed' }}
91
+ * transition={{ type: 'spring', tension: 140, friction: 12 }}
92
+ * fill="#0ea5e9"
93
+ * />
94
+ * </Svg>
95
+ * ```
96
+ */
97
+ declare function MotionPath(props: MotionPathProps): react.JSX.Element;
98
+
99
+ /**
100
+ * SVG path-string utilities used by `MotionSvg.Path`. Everything here runs on
101
+ * the JS thread — paths are tokenized into a normalized command list at mount
102
+ * and when `animate.d` changes; the worklet only ever consumes flat number
103
+ * arrays + a frozen command template.
104
+ *
105
+ * Path morphing in v0.2 requires **structural compatibility**: the source and
106
+ * every target `d` must produce the same command sequence (same command
107
+ * letters, in the same order, after implicit-repeat expansion). Element-wise
108
+ * numeric interpolation is the entire morphing model — we do not resample
109
+ * paths or insert/remove commands. Same-shape morphs (e.g. a heart breathing,
110
+ * a chevron flipping, a check mark tracing in) are the supported use case.
111
+ */
112
+ /**
113
+ * A single normalized path command after implicit-repeat expansion. The cmd
114
+ * letter is preserved (absolute vs relative — case is meaningful to the SVG
115
+ * renderer). `args` always has exactly `CMD_ARGS[cmd]` entries.
116
+ */
117
+ interface PathSegment {
118
+ cmd: string;
119
+ args: number[];
120
+ }
121
+ /**
122
+ * Parse a path `d` string into a flat list of normalized segments. Implicit
123
+ * repeats are expanded — `M 0 0 10 10 20 20` becomes three segments
124
+ * (`M 0 0`, `L 10 10`, `L 20 20`) so the segment list can be compared and
125
+ * interpolated 1:1 against another path.
126
+ */
127
+ declare function parsePathD(d: string): PathSegment[];
128
+ /**
129
+ * The frozen "shape" of a path — just command letters and arg widths. Two
130
+ * paths are morphable iff their templates are equal.
131
+ */
132
+ interface PathTemplate {
133
+ cmds: ReadonlyArray<string>;
134
+ /** Flat width per segment, indexed parallel to `cmds`. */
135
+ widths: ReadonlyArray<number>;
136
+ /** Total scalar count across all segments — `widths.reduce((a,b)=>a+b,0)`. */
137
+ size: number;
138
+ }
139
+ declare function templateOf(segments: ReadonlyArray<PathSegment>): PathTemplate;
140
+ /** Flatten a parsed segment list into a single number array (length === size). */
141
+ declare function flattenParams(segments: ReadonlyArray<PathSegment>): number[];
142
+ /**
143
+ * Verify a target template matches the source. Returns `null` on match or a
144
+ * descriptive error string on mismatch — callers throw in `__DEV__` and
145
+ * silently snap to the target in production.
146
+ */
147
+ declare function diffTemplate(source: PathTemplate, target: PathTemplate): string | null;
148
+ /**
149
+ * Build a path `d` string from a template + flat param array. Runs inside the
150
+ * worklet on the UI thread, so it must not capture any JS-thread closures or
151
+ * use Array.prototype helpers that allocate intermediates the Hermes runtime
152
+ * boxes into JS objects. Manual loops + `+=` string concat keep the worklet
153
+ * cheap.
154
+ *
155
+ * MUST be a worklet — call sites in `MotionPath` wrap it with `'worklet'` via
156
+ * `useAnimatedProps`.
157
+ */
158
+ declare function serializePath(template: PathTemplate, params: ReadonlyArray<number>): string;
159
+
160
+ /**
161
+ * `@onlynative/inertia-svg` — animatable SVG primitives for
162
+ * `@onlynative/inertia`.
163
+ *
164
+ * v0.2 surface:
165
+ * - `MotionPath` / `MotionSvg.Path` — animatable `<Path>` over
166
+ * `react-native-svg`. Supports path morphing on the `d` attribute (source
167
+ * and target must share the same command sequence) plus animatable
168
+ * `fill`, `stroke`, `strokeWidth`, `strokeOpacity`, `fillOpacity`,
169
+ * `opacity`, and `strokeDashoffset` with the same `initial` /
170
+ * `animate` / `transition` shape as the core `Motion.*` primitives.
171
+ *
172
+ * Additional shape primitives (`Circle`, `Rect`, `Line`, `Ellipse`) land in
173
+ * a follow-up once the path morphing API is validated. Path normalization
174
+ * (resampling between structurally different paths) is out of scope for
175
+ * v0.2 — use structurally-compatible source/target paths and remount with
176
+ * `key={...}` to switch shape.
177
+ */
178
+
179
+ /**
180
+ * Namespace bundling every animatable SVG primitive. Use `MotionSvg.Path` for
181
+ * autocomplete-friendly grouping or import `MotionPath` directly — both
182
+ * point at the same component.
183
+ */
184
+ declare const MotionSvg: {
185
+ readonly Path: typeof MotionPath;
186
+ };
187
+
188
+ export { MotionPath, type MotionPathProps, MotionSvg, type PathAnimate, type PathPerPropertyTransition, type PathSegment, type PathStateShape, type PathTemplate, type PathTransition, diffTemplate, flattenParams, parsePathD, serializePath, templateOf };