@rootnative/inertia-svg 0.0.0-alpha.1 → 0.0.0-alpha.2

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/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
- import { useRef, useMemo, useEffect } from 'react';
2
- import { Path } from 'react-native-svg';
1
+ import { useRef, useEffect, useMemo } from 'react';
2
+ import { Path, Circle, Rect, Line } from 'react-native-svg';
3
3
  import Animated, { useSharedValue, useAnimatedProps } from 'react-native-reanimated';
4
- import { useShouldReduceMotion, resolveTransition } from '@rootnative/inertia';
4
+ import { useShouldReduceMotion, useNamedTransitions, resolveTransition, resolveNamedTransition } from '@rootnative/inertia';
5
5
  import { jsx } from 'react/jsx-runtime';
6
6
 
7
7
  // src/MotionPath.tsx
@@ -347,12 +347,241 @@ If you need to swap to a structurally different path, remount with key={...}.`
347
347
  }
348
348
  );
349
349
  }
350
+ var NO_ANIMATION2 = { type: "no-animation" };
351
+ function pickTransition2(transition, key, registry) {
352
+ if (!transition) return void 0;
353
+ if (typeof transition === "string" || "type" in transition) {
354
+ return resolveNamedTransition(transition, registry);
355
+ }
356
+ return resolveNamedTransition(
357
+ transition[key],
358
+ registry
359
+ );
360
+ }
361
+ function createMotionSvgComponent(Component, config) {
362
+ const AnimatedComponent = Animated.createAnimatedComponent(
363
+ Component
364
+ );
365
+ const numericKeys = config.animatableProps;
366
+ const colorKeys = config.colorProps ?? [];
367
+ const arrayKeys = config.arrayProps ?? [];
368
+ function MotionSvgComponent(props) {
369
+ const { initial, animate, transition, ...rest } = props;
370
+ const statics = rest;
371
+ const seedSource = initial === false ? animate : initial ?? void 0;
372
+ const reduce = useShouldReduceMotion();
373
+ const registry = useNamedTransitions();
374
+ const mountRef = useRef(null);
375
+ if (mountRef.current === null) {
376
+ const engaged2 = {};
377
+ const engagedNumeric2 = [];
378
+ const engagedColor2 = [];
379
+ const engagedArray2 = [];
380
+ const arrayLengths2 = {};
381
+ const anim2 = animate;
382
+ const seed2 = seedSource;
383
+ for (const k of numericKeys) {
384
+ engaged2[k] = seed2?.[k] !== void 0 || anim2?.[k] !== void 0 || statics[k] !== void 0;
385
+ if (engaged2[k]) engagedNumeric2.push(k);
386
+ }
387
+ for (const k of colorKeys) {
388
+ engaged2[k] = seed2?.[k] !== void 0 || anim2?.[k] !== void 0 || statics[k] !== void 0;
389
+ if (engaged2[k]) engagedColor2.push(k);
390
+ }
391
+ for (const k of arrayKeys) {
392
+ const src = seed2?.[k] ?? statics[k] ?? anim2?.[k];
393
+ arrayLengths2[k] = Array.isArray(src) ? src.length : 0;
394
+ engaged2[k] = arrayLengths2[k] > 0;
395
+ if (engaged2[k]) engagedArray2.push(k);
396
+ }
397
+ mountRef.current = {
398
+ engaged: engaged2,
399
+ engagedNumeric: engagedNumeric2,
400
+ engagedColor: engagedColor2,
401
+ engagedArray: engagedArray2,
402
+ arrayLengths: arrayLengths2
403
+ };
404
+ }
405
+ const {
406
+ engaged,
407
+ engagedNumeric,
408
+ engagedColor,
409
+ engagedArray,
410
+ arrayLengths
411
+ } = mountRef.current;
412
+ const seed = seedSource;
413
+ const anim = animate;
414
+ const numericSvs = {};
415
+ for (const k of numericKeys) {
416
+ numericSvs[k] = useSharedValue(
417
+ seed?.[k] ?? statics[k] ?? 0
418
+ );
419
+ }
420
+ const colorSvs = {};
421
+ for (const k of colorKeys) {
422
+ colorSvs[k] = useSharedValue(
423
+ seed?.[k] ?? statics[k] ?? "transparent"
424
+ );
425
+ }
426
+ const arraySvs = {};
427
+ for (const k of arrayKeys) {
428
+ const len = arrayLengths[k];
429
+ const seedArr = seed?.[k] ?? statics[k] ?? anim?.[k];
430
+ const svs = [];
431
+ for (let i = 0; i < len; i++) {
432
+ svs.push(useSharedValue(Number(seedArr?.[i]) || 0));
433
+ }
434
+ arraySvs[k] = svs;
435
+ }
436
+ if (__DEV__) {
437
+ for (const k of arrayKeys) {
438
+ const v = statics[k];
439
+ if (Array.isArray(v) && engaged[k] && v.length !== arrayLengths[k]) {
440
+ throw new Error(
441
+ `[inertia-svg] ${k} length changed after mount (${arrayLengths[k]} \u2192 ${v.length}). Array props are locked at first render \u2014 remount with key={...} to change the length.`
442
+ );
443
+ }
444
+ }
445
+ }
446
+ for (const k of numericKeys) {
447
+ const target = anim?.[k];
448
+ useEffect(() => {
449
+ if (target === void 0) return;
450
+ if (!engaged[k]) {
451
+ if (__DEV__) warnNotEngaged(k);
452
+ return;
453
+ }
454
+ const cfg = reduce ? NO_ANIMATION2 : pickTransition2(transition, k, registry);
455
+ numericSvs[k].value = resolveTransition(cfg, target);
456
+ }, [target, reduce, transition]);
457
+ }
458
+ for (const k of colorKeys) {
459
+ const target = anim?.[k];
460
+ useEffect(() => {
461
+ if (target === void 0) return;
462
+ if (!engaged[k]) {
463
+ if (__DEV__) warnNotEngaged(k);
464
+ return;
465
+ }
466
+ const cfg = reduce ? NO_ANIMATION2 : pickTransition2(transition, k, registry);
467
+ colorSvs[k].value = resolveTransition(cfg, target);
468
+ }, [target, reduce, transition]);
469
+ }
470
+ for (const k of arrayKeys) {
471
+ const target = anim?.[k];
472
+ const sig = target ? target.join(",") : void 0;
473
+ useEffect(() => {
474
+ if (target === void 0) return;
475
+ if (!engaged[k]) {
476
+ if (__DEV__) warnNotEngaged(k);
477
+ return;
478
+ }
479
+ const len = arrayLengths[k];
480
+ if (target.length !== len) {
481
+ if (__DEV__) {
482
+ throw new Error(
483
+ `[inertia-svg] animate.${k} length mismatch (${len} \u2192 ${target.length}). Array props are locked at first render \u2014 remount with key={...} to change the length.`
484
+ );
485
+ }
486
+ return;
487
+ }
488
+ const cfg = reduce ? NO_ANIMATION2 : pickTransition2(transition, k, registry);
489
+ const svs = arraySvs[k];
490
+ for (let i = 0; i < len; i++) {
491
+ svs[i].value = resolveTransition(cfg, target[i] ?? 0);
492
+ }
493
+ }, [sig, reduce, transition]);
494
+ }
495
+ const animatedProps = useAnimatedProps(() => {
496
+ "worklet";
497
+ const out = {};
498
+ for (let i = 0; i < engagedNumeric.length; i++) {
499
+ const k = engagedNumeric[i];
500
+ out[k] = numericSvs[k].value;
501
+ }
502
+ for (let i = 0; i < engagedColor.length; i++) {
503
+ const k = engagedColor[i];
504
+ out[k] = colorSvs[k].value;
505
+ }
506
+ for (let i = 0; i < engagedArray.length; i++) {
507
+ const k = engagedArray[i];
508
+ const svs = arraySvs[k];
509
+ const arr = new Array(svs.length);
510
+ for (let j = 0; j < svs.length; j++) arr[j] = svs[j].value;
511
+ out[k] = arr;
512
+ }
513
+ return out;
514
+ });
515
+ return (
516
+ // `animatedProps` overrides every engaged key each frame; the static
517
+ // props in `rest` are the first-render seeds so the element renders
518
+ // before the first effect tick. The cast sheds Reanimated's strict-prop
519
+ // constraint that the worklet's return type can't express — the runtime
520
+ // shape is the same.
521
+ /* @__PURE__ */ jsx(AnimatedComponent, { animatedProps, ...statics })
522
+ );
523
+ }
524
+ const base = Component.displayName ?? Component.name ?? "SvgComponent";
525
+ MotionSvgComponent.displayName = `Motion${base}`;
526
+ return MotionSvgComponent;
527
+ }
528
+ function warnNotEngaged(key) {
529
+ console.warn(
530
+ `[inertia-svg] animate.${key} was introduced after mount \u2014 the key wasn't present at mount (static prop, initial, or animate), so its animated value can't render. Include ${key} at mount or remount with key={...}.`
531
+ );
532
+ }
533
+ var MotionCircle = createMotionSvgComponent(Circle, {
534
+ animatableProps: [
535
+ "cx",
536
+ "cy",
537
+ "r",
538
+ "strokeWidth",
539
+ "strokeOpacity",
540
+ "fillOpacity",
541
+ "opacity",
542
+ "strokeDashoffset"
543
+ ],
544
+ colorProps: ["fill", "stroke"],
545
+ arrayProps: ["strokeDasharray"]
546
+ });
547
+ var MotionRect = createMotionSvgComponent(Rect, {
548
+ animatableProps: [
549
+ "x",
550
+ "y",
551
+ "width",
552
+ "height",
553
+ "rx",
554
+ "ry",
555
+ "strokeWidth",
556
+ "strokeOpacity",
557
+ "fillOpacity",
558
+ "opacity",
559
+ "strokeDashoffset"
560
+ ],
561
+ colorProps: ["fill", "stroke"],
562
+ arrayProps: ["strokeDasharray"]
563
+ });
564
+ var MotionLine = createMotionSvgComponent(Line, {
565
+ animatableProps: [
566
+ "x1",
567
+ "y1",
568
+ "x2",
569
+ "y2",
570
+ "strokeWidth",
571
+ "strokeOpacity",
572
+ "opacity",
573
+ "strokeDashoffset"
574
+ ],
575
+ colorProps: ["stroke"],
576
+ arrayProps: ["strokeDasharray"]
577
+ });
350
578
 
351
579
  // src/index.ts
352
580
  var MotionSvg = {
353
- Path: MotionPath
581
+ Path: MotionPath,
582
+ Circle: MotionCircle,
583
+ Rect: MotionRect,
584
+ Line: MotionLine
354
585
  };
355
586
 
356
- export { MotionPath, MotionSvg, diffTemplate, flattenParams, parsePathD, serializePath, templateOf };
357
- //# sourceMappingURL=index.mjs.map
358
- //# sourceMappingURL=index.mjs.map
587
+ export { MotionCircle, MotionLine, MotionPath, MotionRect, MotionSvg, createMotionSvgComponent, diffTemplate, flattenParams, parsePathD, serializePath, templateOf };
package/llms.txt CHANGED
@@ -10,7 +10,7 @@
10
10
 
11
11
  `@rootnative/inertia-svg` adds animatable SVG primitives built on [`react-native-svg`](https://github.com/software-mansion/react-native-svg). It is an **optional** sibling package — install it only when you need to morph paths or animate `fill` / `stroke`. The core library has no required `react-native-svg` dependency.
12
12
 
13
- `MotionPath` wraps `<Path>` and accepts the same `initial` / `animate` / `transition` shape as the core `Motion.*` primitives, with animatable keys for the path data (`d`) plus color and numeric paint properties.
13
+ `MotionPath` wraps `<Path>` and accepts the same `initial` / `animate` / `transition` shape as the core `Motion.*` primitives, with animatable keys for the path data (`d`) plus color and numeric paint properties. Prebuilt [`MotionCircle` / `MotionRect` / `MotionLine`](#shapes) cover the other common shapes, and the [`createMotionSvgComponent`](#factory) factory behind them wraps any `react-native-svg` element.
14
14
 
15
15
  ## Install
16
16
 
@@ -52,7 +52,7 @@ import { MotionSvg } from '@rootnative/inertia-svg'
52
52
  ;<MotionSvg.Path d={SOURCE_D} animate={{ d: TARGET_D }} />
53
53
  ```
54
54
 
55
- Additional shape primitives (`Circle`, `Rect`, `Line`, `Ellipse`) will join the namespace in a follow-up release once the path morphing API is validated.
55
+ The namespace also carries the prebuilt shapes: `MotionSvg.Circle`, `MotionSvg.Rect`, and `MotionSvg.Line` see [Shapes](#shapes) below.
56
56
 
57
57
  The static `d` prop is required. Its **command sequence is locked at first render** — every target `d` you pass via `animate` or `initial` must produce the same command letters in the same order after implicit-repeat expansion. Element-wise scalar interpolation is the entire morphing model.
58
58
 
@@ -133,10 +133,78 @@ If `initial.d` is provided, it must be template-compatible with the static `d`
133
133
 
134
134
  `MotionPath` 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`.
135
135
 
136
- ## What this primitive doesn't do (v0.2)
136
+ ## Shapes MotionCircle, MotionRect, MotionLine {#shapes}
137
+
138
+ Prebuilt animatable wrappers for the common non-path shapes, built with the [factory](#factory) below. Each accepts the same `initial` / `animate` / `transition` shape as `MotionPath`, and `transition` also accepts a [named transition](./motion-config.md#named-transitions) registered on the nearest `<MotionConfig transitions>` — top-level and per-property.
139
+
140
+ | Component | Numeric props | Color props | Array props |
141
+ | -------------- | --------------------------------------------------------------------------------------------------------------------- | ---------------- | ----------------- |
142
+ | `MotionCircle` | `cx`, `cy`, `r`, `strokeWidth`, `strokeOpacity`, `fillOpacity`, `opacity`, `strokeDashoffset` | `fill`, `stroke` | `strokeDasharray` |
143
+ | `MotionRect` | `x`, `y`, `width`, `height`, `rx`, `ry`, `strokeWidth`, `strokeOpacity`, `fillOpacity`, `opacity`, `strokeDashoffset` | `fill`, `stroke` | `strokeDasharray` |
144
+ | `MotionLine` | `x1`, `y1`, `x2`, `y2`, `strokeWidth`, `strokeOpacity`, `opacity`, `strokeDashoffset` | `stroke` | `strokeDasharray` |
145
+
146
+ The canonical `MotionCircle` consumer is a circular progress ring — a static `strokeDasharray` of the circumference with an animated `strokeDashoffset`:
147
+
148
+ ```tsx
149
+ import Svg from 'react-native-svg'
150
+ import { MotionCircle } from '@rootnative/inertia-svg'
151
+
152
+ const CIRCUMFERENCE = 2 * Math.PI * 45
153
+
154
+ function ProgressRing({ progress }: { progress: number }) {
155
+ return (
156
+ <Svg viewBox="0 0 100 100" width={64} height={64}>
157
+ <MotionCircle
158
+ cx={50}
159
+ cy={50}
160
+ r={45}
161
+ stroke="#0ea5e9"
162
+ strokeWidth={8}
163
+ fill="none"
164
+ strokeDasharray={[CIRCUMFERENCE]}
165
+ strokeDashoffset={CIRCUMFERENCE}
166
+ animate={{ strokeDashoffset: CIRCUMFERENCE * (1 - progress) }}
167
+ transition={{ type: 'timing', duration: 300 }}
168
+ />
169
+ </Svg>
170
+ )
171
+ }
172
+ ```
173
+
174
+ Shape-specific rules (they mirror `MotionPath`'s command-sequence lock):
175
+
176
+ - **Array props lock their length at first render.** `strokeDasharray` interpolates element-wise, so a target with a different length throws in dev and is ignored in production. Remount with a new `key` to change the length.
177
+ - **A key only animates when it's present at mount** — in the static props, `initial`, or `animate`. Keys introduced into `animate` after mount warn in dev and are ignored. Keys never mentioned pass through as ordinary static props, so un-animated attributes keep their element defaults.
178
+ - Numeric keys engaged only via `animate` seed from `0`; colors seed from `'transparent'`. Give the key a static prop or an `initial` value when the mount animation should start elsewhere.
179
+
180
+ ## createMotionSvgComponent {#factory}
181
+
182
+ The factory behind the prebuilt shapes. Point it at any `react-native-svg` element and declare which props animate and how each interpolates:
183
+
184
+ ```tsx
185
+ import { Ellipse } from 'react-native-svg'
186
+ import { createMotionSvgComponent } from '@rootnative/inertia-svg'
187
+
188
+ const MotionEllipse = createMotionSvgComponent(Ellipse, {
189
+ animatableProps: ['cx', 'cy', 'rx', 'ry', 'opacity'], // numeric
190
+ colorProps: ['fill', 'stroke'], // color strings
191
+ arrayProps: ['strokeDasharray'], // numeric arrays, length locked at mount
192
+ })
193
+
194
+ <MotionEllipse
195
+ cx={50} cy={50} rx={10} ry={20}
196
+ animate={{ rx: 30, fill: '#7c3aed' }}
197
+ transition={{ type: 'spring', tension: 180, friction: 14 }}
198
+ />
199
+ ```
200
+
201
+ Prop keys are inferred from the wrapped component, so a typo in `animatableProps` is a compile error, and the generated component's `animate` / `initial` only accept the declared keys. Everything documented for the shapes above — named transitions, per-property transitions, `initial` semantics, reduced motion, the mount locks — comes from the factory and applies to any component it builds.
202
+
203
+ `MotionPath` remains hand-rolled: path morphing needs the `d` template machinery the generic factory doesn't model.
204
+
205
+ ## What this package doesn't do
137
206
 
138
207
  - **Path resampling** between structurally different shapes. Same-template morphs only — the rest is your `key` to remount.
139
- - **Other SVG shapes** (`Circle`, `Rect`, `Line`, `Ellipse`). They land in a follow-up release; pencil them in if you need them.
140
208
  - **Gradient fills inside the SVG**. For gradient fills use `<Defs>` + `<LinearGradient>` from `react-native-svg` and animate the stops via [`MotionLinearGradient`](./gradients.mdx)'s patterns; the path itself just references the gradient by `url(#id)`.
141
209
  - **Path command interpolation** (e.g. morphing an `L` into a `C`). Element-wise scalar interpolation is intentional — it's predictable, cheap, and matches what designers reach for 95% of the time.
142
210
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rootnative/inertia-svg",
3
- "version": "0.0.0-alpha.1",
3
+ "version": "0.0.0-alpha.2",
4
4
  "description": "Animatable SVG primitives (path morphing, fill/stroke) for @rootnative/inertia, built on react-native-svg.",
5
5
  "license": "MIT",
6
6
  "author": "RootNative",
@@ -46,10 +46,11 @@
46
46
  "LICENSE",
47
47
  "CHANGELOG.md",
48
48
  "!**/__tests__",
49
+ "!**/__type-tests__",
49
50
  "!**/*.test.*"
50
51
  ],
51
52
  "peerDependencies": {
52
- "@rootnative/inertia": ">=0.0.0-alpha.1",
53
+ "@rootnative/inertia": ">=0.0.0-alpha.2",
53
54
  "react": ">=19.0.0",
54
55
  "react-native": ">=0.81.0",
55
56
  "react-native-reanimated": ">=4.0.0",
@@ -68,7 +69,7 @@
68
69
  "react-test-renderer": "19.1.0",
69
70
  "tsup": "^8.3.5",
70
71
  "typescript": "^5.7.3",
71
- "@rootnative/inertia": "0.0.0-alpha.1"
72
+ "@rootnative/inertia": "0.0.0-alpha.2"
72
73
  },
73
74
  "publishConfig": {
74
75
  "access": "public"