morphicons 1.4.0 → 1.4.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/README.md CHANGED
@@ -13,7 +13,14 @@
13
13
  <a href="https://www.morphicons.com"><strong>morphicons.com</strong></a> — live playground
14
14
  </p>
15
15
 
16
- Universal morphing for stroke-based icons (Lucide, Tabler, Heroicons, Iconoir, or your own paths): **any icon morphs into any other** with spring physics. Rotations are never declared by hand — they emerge from the math (2D Procrustes + polar interpolation). Zero runtime dependencies.
16
+ <p align="center">
17
+ <a href="https://github.com/sponsors/guillermolg00">
18
+ <img src="https://img.shields.io/badge/%E2%98%95%20Buy%20me%20a%20coffee-EA4AAA" alt="Buy me a coffee — sponsor morphicons on GitHub Sponsors">
19
+ </a>
20
+ </p>
21
+
22
+ Universal morphing for stroke-based icons (Lucide, Tab
23
+ ler, Heroicons, Iconoir, or your own paths): **any icon morphs into any other** with spring physics. Rotations are never declared by hand — they emerge from the math (2D Procrustes + polar interpolation). Zero runtime dependencies.
17
24
 
18
25
  ```tsx
19
26
  import { MorphIcon } from "morphicons/react";
@@ -58,7 +65,7 @@ ref.current?.morphTo(Check); // animates
58
65
  ref.current?.set(X); // jumps without animating
59
66
  ```
60
67
 
61
- Drop-in replacement for lucide-react: `size`, `strokeWidth`, `absoluteStrokeWidth`, `color`, `className` and the rest of the `<svg>` props pass straight through. Correct accessibility by default: `aria-hidden` unless you pass `label` (→ `role="img"` + `<title>`). Clean SSR: the server emits the exact static SVG (zero flash, zero layout shift); the runtime is born on hydration. `prefers-reduced-motion` degrades to an instant swap.
68
+ Drop-in replacement for lucide-react: `size`, `strokeWidth`, `absoluteStrokeWidth`, `color`, `className` and the rest of the `<svg>` props pass straight through. Correct accessibility by default: `aria-hidden` unless you pass `label` (→ `role="img"` + `<title>`). Clean SSR: the server emits the exact static SVG (zero flash, zero layout shift); the runtime is born on hydration. Morphs play regardless of the OS reduce-motion setting by default; opt into honoring it with `reducedMotion="user"` (see [Reduced motion](#reduced-motion-all-four-bindings)).
62
69
 
63
70
  ### Vue — same three modes
64
71
 
@@ -132,7 +139,7 @@ ref.current?.morphTo(Check); // animates
132
139
  ref.current?.set(X); // jumps without animating
133
140
  ```
134
141
 
135
- The DOM-free core beyond the browser: the dom driver is reused verbatim as the engine (React Native has a global `requestAnimationFrame`, and `PathEl` is structural), so the whole platform difference is a shim that forwards the per-frame `d` write to `Path.setNativeProps` of react-native-svg — outside the React render, exactly like the web mutation. Same surface as the React binding (`size`, `strokeWidth`, `absoluteStrokeWidth`, `color`, plus the native `Svg` props: `testID`, touch handlers…), same accessibility defaults (`aria-hidden` unless you pass `label` → `role="img"` + `aria-label`). Reduced motion comes from `AccessibilityInfo` (best-effort: the query is async; a `reduceMotionChanged` subscription keeps it exact from then on). Requires Metro with package `exports` resolution — default since React Native 0.79; on older versions enable `unstable_enablePackageExports`.
142
+ The DOM-free core beyond the browser: the dom driver is reused verbatim as the engine (React Native has a global `requestAnimationFrame`, and `PathEl` is structural), so the whole platform difference is a shim that forwards the per-frame `d` write to `Path.setNativeProps` of react-native-svg — outside the React render, exactly like the web mutation. Same surface as the React binding (`size`, `strokeWidth`, `absoluteStrokeWidth`, `color`, plus the native `Svg` props: `testID`, touch handlers…), same accessibility defaults (`aria-hidden` unless you pass `label` → `role="img"` + `aria-label`). With `reducedMotion="user"`, the OS setting comes from `AccessibilityInfo` (best-effort: the query is async; a `reduceMotionChanged` subscription keeps it exact from then on). Requires Metro with package `exports` resolution — default since React Native 0.79; on older versions enable `unstable_enablePackageExports`.
136
143
 
137
144
  ### Lifecycle contract (all four bindings)
138
145
 
@@ -142,6 +149,20 @@ The four components share one contract, pinned by mirrored client-mount tests:
142
149
  - **Controlled wins.** While `from` AND `to` are both present, the pair owns the path: `icon` changes are ignored, no spring fires. Drop the pair and the current `icon` takes over (animated). Mixing the modes is not an error — the precedence is just explicit.
143
150
  - **Clean re-entry.** Any exit from controlled mode (an imperative call or an icon takeover) invalidates the frozen pair, so returning to the same `from`/`to` re-bases on `from` and renders exactly like a clean mount at that `progress`.
144
151
 
152
+ ### Reduced motion (all four bindings)
153
+
154
+ Icon morphs are small, short, communicative micro-transitions: the kind of motion the reduce-motion guidance considers generally acceptable, unlike parallax or full-screen movement. Auto-degrading them made the library look broken to every user with the OS setting on, so since 1.4.2 they play by default and the policy is an explicit prop (the same default Motion, formerly Framer Motion, ships):
155
+
156
+ ```tsx
157
+ <MorphIcon icon={open ? X : Menu} reducedMotion="user" />
158
+ ```
159
+
160
+ - `"never"` (default): morphs always animate; the OS setting is ignored.
161
+ - `"user"`: while the OS reduce-motion setting is on, every `morphTo` degrades to an instant swap (the pre-1.4.2 behavior). The web bindings read `prefers-reduced-motion`; React Native reads `AccessibilityInfo`, and only touches the accessibility bridge when some instance opts in.
162
+ - `"always"`: every `morphTo` jumps without animating (tests, screenshots).
163
+
164
+ The prop is live: changing it governs the next morph. The vanilla driver takes the same policy as an option, `createMorph(el, icon, { reducedMotion: "user" })`, and exposes it as a mutable `morph.reducedMotion`. Apps that must strictly follow the user's system preference should pass `reducedMotion="user"` across the board.
165
+
145
166
  ### Vanilla (no React)
146
167
 
147
168
  ```ts
@@ -152,6 +173,7 @@ m.morphTo(X, "snappy"); // preset or { stiffness, damping }
152
173
  m.set(Check); // jump without animating (canonical d)
153
174
  m.seek(X, 0.4); // morph frozen at t — scrubbing, gestures
154
175
  m.progress = 0.4; // sugar: seek on the active target
176
+ m.reducedMotion = "user"; // live policy: "never" (default) | "user" | "always"
155
177
  m.destroy();
156
178
  ```
157
179
 
@@ -312,7 +334,7 @@ c_k(t) = c_{A,k} + t·d_k + (σ*ᵗ·R(t·θ*) − I)·(c_{A,k} − g_A)
312
334
 
313
335
  ### 6. Serialization and canonical snap
314
336
 
315
- In flight, each subpath is emitted as `M x y L x y …` (+ `Z` if the pair is closed↔closed; closed→open flies open, the loop opening at the cut chosen by the circular correspondence) with 2 decimals — invisible at 24px, and the frame's only allocation. On settle, the driver **snaps to the canonical `d`** of the target icon: exact fidelity at rest (real curves, not polylines), subpath count reset after duplications, and the DOM ends up identical to a static icon's. The jump is < 0.02px — imperceptible.
337
+ In flight, each subpath is emitted as `M x y L x y …` (+ `Z` if the pair is closed↔closed; closed→open flies open, the loop opening at the cut chosen by the circular correspondence) with 2 decimals — invisible at 24px, and the frame's only allocation. On settle, the driver **snaps to the canonical `d`** of the target icon: exact fidelity at rest (real curves, not polylines), subpath count reset after duplications, and the DOM ends up identical to a static icon's. The jump is < 0.02px — imperceptible. The canonical `d` itself is emitted with 4 decimals, and that is a correctness choice, not a size one: arc→cubic conversion goes through trig whose last ulp differs across JS engines, and SSR hydration compares the server's bytes against the browser's — quantized emission keeps them identical, where full precision leaked each engine's ulp into the markup (a Next hydration mismatch on icons with non-cardinal arcs).
316
338
 
317
339
  ### 7. The spring
318
340
 
@@ -25,6 +25,7 @@ let {
25
25
  to,
26
26
  progress,
27
27
  spring,
28
+ reducedMotion,
28
29
  size = 24,
29
30
  color = "currentColor",
30
31
  strokeWidth = 2,
@@ -40,17 +41,17 @@ const initialD = computeInitialD({ icon, from, to, progress });
40
41
  let pathEl: SVGPathElement | undefined = $state();
41
42
  // svelte-ignore state_referenced_locally — init-time values on purpose: they
42
43
  // seed the controller's watch baselines so the first $effect run is a no-op.
43
- const ctrl = createController({ icon, from, to, progress });
44
+ const ctrl = createController({ icon, from, to, progress, reducedMotion });
44
45
 
45
46
  onMount(() => {
46
- if (pathEl) ctrl.mount(pathEl, { icon, from, to, progress });
47
+ if (pathEl) ctrl.mount(pathEl, { icon, from, to, progress, reducedMotion });
47
48
  return () => ctrl.destroy();
48
49
  });
49
50
 
50
51
  // Prop changes: one watcher owns the mode logic (controlled wins — change
51
52
  // detection and precedence live in shared.ts).
52
53
  $effect(() => {
53
- const next = { icon, from, to, progress, spring };
54
+ const next = { icon, from, to, progress, spring, reducedMotion };
54
55
  untrack(() => ctrl.watch(next));
55
56
  });
56
57
 
package/dist/dom.d.ts CHANGED
@@ -10,6 +10,15 @@ interface MorphOptions {
10
10
  stiffness?: number;
11
11
  damping?: number;
12
12
  }
13
+ /** Reduced-motion policy. `"never"` (default): morphs always animate,
14
+ * ignoring the OS setting. `"user"`: while the OS reduce-motion setting is
15
+ * on, `morphTo` degrades to an instant `set`. `"always"`: every `morphTo`
16
+ * jumps (tests, screenshots). */
17
+ type ReducedMotionMode = "never" | "user" | "always";
18
+ interface CreateMorphOptions {
19
+ /** Reduced-motion policy; also live via the `reducedMotion` property. */
20
+ reducedMotion?: ReducedMotionMode;
21
+ }
13
22
  interface Morph {
14
23
  /** Animates toward the icon with spring physics. Interruptible: mid-flight
15
24
  * it re-plans from the intermediate shape while preserving velocity. */
@@ -23,14 +32,17 @@ interface Morph {
23
32
  /** Current progress (t of the last frame; 1 at rest). Assigning it is
24
33
  * equivalent to seek(target, t) on the active target. */
25
34
  progress: number;
35
+ /** Reduced-motion policy, live: assigning it applies to the next morphTo. */
36
+ reducedMotion: ReducedMotionMode;
26
37
  /** Unregisters the instance; later calls are no-ops. */
27
38
  destroy(): void;
28
39
  }
29
40
  /** Canonical `d` of an icon: the input string verbatim, or the real cubics
30
- * at full precision (the at-rest snap). Exported because it is what a
31
- * binding renders at SSR/rest before any runtime exists. */
41
+ * quantized to 4 decimals (the at-rest snap; engine-stable bytes so SSR
42
+ * hydration matches, see fmtCanon in core/serialize). Exported because it
43
+ * is what a binding renders at SSR/rest before any runtime exists. */
32
44
  declare function canonicalD(icon: IconInput): string;
33
45
  /** Creates the morph instance over a `<path>` and paints the initial icon. */
34
- declare function createMorph(el: PathEl, icon: IconInput): Morph;
46
+ declare function createMorph(el: PathEl, icon: IconInput, options?: CreateMorphOptions): Morph;
35
47
  //#endregion
36
- export { Morph, MorphOptions, PathEl, canonicalD, createMorph };
48
+ export { CreateMorphOptions, Morph, MorphOptions, PathEl, ReducedMotionMode, canonicalD, createMorph };
package/dist/dom.js CHANGED
@@ -1,4 +1,4 @@
1
- import { a as buildPlan, c as cubicsToPathD, f as interpPolar, l as serialize, n as Spring, r as resampleIcon, s as iconToCubics, t as SPRING_PRESETS, u as allocOutputs } from "./spring-BTcHiHR5.js";
1
+ import { a as buildPlan, c as cubicsToPathD, f as interpPolar, l as serialize, n as Spring, r as resampleIcon, s as iconToCubics, t as SPRING_PRESETS, u as allocOutputs } from "./spring-CBynweFS.js";
2
2
  //#region src/dom/index.ts
3
3
  const tickers = /* @__PURE__ */ new Set();
4
4
  let rafId = 0;
@@ -41,8 +41,9 @@ function sampledOf(icon) {
41
41
  return s;
42
42
  }
43
43
  /** Canonical `d` of an icon: the input string verbatim, or the real cubics
44
- * at full precision (the at-rest snap). Exported because it is what a
45
- * binding renders at SSR/rest before any runtime exists. */
44
+ * quantized to 4 decimals (the at-rest snap; engine-stable bytes so SSR
45
+ * hydration matches, see fmtCanon in core/serialize). Exported because it
46
+ * is what a binding renders at SSR/rest before any runtime exists. */
46
47
  function canonicalD(icon) {
47
48
  if (typeof icon === "string") return icon;
48
49
  let d = canon.get(icon);
@@ -75,8 +76,9 @@ function resolveSpring(s) {
75
76
  };
76
77
  }
77
78
  /** Creates the morph instance over a `<path>` and paints the initial icon. */
78
- function createMorph(el, icon) {
79
+ function createMorph(el, icon, options) {
79
80
  const spring = new Spring();
81
+ let reducedMotion = options?.reducedMotion ?? "never";
80
82
  let target = icon;
81
83
  let rest = true;
82
84
  let plan = null;
@@ -141,6 +143,13 @@ function createMorph(el, icon) {
141
143
  target = icon;
142
144
  settle();
143
145
  };
146
+ /** True when the policy says this morphTo must jump instead of flying. */
147
+ const motionOff = () => {
148
+ if (reducedMotion === "always") return true;
149
+ if (reducedMotion !== "user") return false;
150
+ if (typeof matchMedia === "undefined") return false;
151
+ return matchMedia?.("(prefers-reduced-motion: reduce)").matches ?? false;
152
+ };
144
153
  const seek = (icon, tt) => {
145
154
  if (dead) return;
146
155
  const reuse = !rest && plan !== null && icon === target;
@@ -153,11 +162,9 @@ function createMorph(el, icon) {
153
162
  morphTo(icon, sp) {
154
163
  if (dead) return;
155
164
  if (icon === target && (rest || flying)) return;
156
- if (typeof matchMedia !== "undefined") {
157
- if (matchMedia?.("(prefers-reduced-motion: reduce)").matches) {
158
- setNow(icon);
159
- return;
160
- }
165
+ if (motionOff()) {
166
+ setNow(icon);
167
+ return;
161
168
  }
162
169
  const { k, c } = resolveSpring(sp);
163
170
  spring.config(k, c);
@@ -179,6 +186,12 @@ function createMorph(el, icon) {
179
186
  set progress(v) {
180
187
  if (!dead) seek(target, v);
181
188
  },
189
+ get reducedMotion() {
190
+ return reducedMotion;
191
+ },
192
+ set reducedMotion(v) {
193
+ reducedMotion = v;
194
+ },
182
195
  destroy() {
183
196
  stop();
184
197
  dead = true;
package/dist/index.d.ts CHANGED
@@ -72,7 +72,8 @@ declare function resampleIcon(input: IconInput, N?: number): Sampled[];
72
72
  /** Sampled subpaths → polyline `d` attribute. `closed[k]` appends Z to
73
73
  * subpath k (closed loops in flight); without flags everything is open. */
74
74
  declare function serialize(subs: readonly Float64Array[], closed?: readonly boolean[]): string;
75
- /** Cubic subpaths → canonical `d` at full precision (round-trip). */
75
+ /** Cubic subpaths → canonical `d`, quantized to 4 decimals (engine-stable
76
+ * bytes; see fmtCanon). */
76
77
  declare function cubicsToPathD(paths: readonly CubicPath[]): string;
77
78
  //#endregion
78
79
  export { type CubicPath, type IconInput, type IconNode, type IconNodeAttrs, type MorphPlan, type PlanItem, SPRING_PRESETS, type Sampled, Spring, type SpringPreset, type ViewBox, allocOutputs, buildPlan, cubicsToPathD, fitIcon, iconToCubics, interpLinear, interpPolar, resampleIcon, resamplePath, serialize };
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- import { a as buildPlan, c as cubicsToPathD, d as interpLinear, f as interpPolar, i as resamplePath, l as serialize, n as Spring, o as fitIcon, r as resampleIcon, s as iconToCubics, t as SPRING_PRESETS, u as allocOutputs } from "./spring-BTcHiHR5.js";
1
+ import { a as buildPlan, c as cubicsToPathD, d as interpLinear, f as interpPolar, i as resamplePath, l as serialize, n as Spring, o as fitIcon, r as resampleIcon, s as iconToCubics, t as SPRING_PRESETS, u as allocOutputs } from "./spring-CBynweFS.js";
2
2
  export { SPRING_PRESETS, Spring, allocOutputs, buildPlan, cubicsToPathD, fitIcon, iconToCubics, interpLinear, interpPolar, resampleIcon, resamplePath, serialize };
@@ -1,5 +1,5 @@
1
1
  import { a as Sampled, c as SpringPreset, n as IconInput, r as IconNode } from "./types-JAF3s76Y.js";
2
- import { Morph, MorphOptions, PathEl } from "./dom.js";
2
+ import { Morph, MorphOptions, PathEl, ReducedMotionMode } from "./dom.js";
3
3
  import { SvgProps } from "react-native-svg";
4
4
  //#region src/react-native/index.d.ts
5
5
  /** Imperative surface exposed via ref. */
@@ -18,6 +18,11 @@ interface MorphIconProps extends Omit<SvgProps, "from" | "to"> {
18
18
  progress?: number;
19
19
  /** Physics for uncontrolled/imperative mode: preset or custom spring. */
20
20
  spring?: SpringPreset | MorphOptions;
21
+ /** Reduced-motion policy: "never" (default) animates regardless of the OS
22
+ * setting, "user" honors AccessibilityInfo's reduce motion (morphs degrade
23
+ * to an instant swap while it is on; best-effort, the query is async),
24
+ * "always" always jumps. */
25
+ reducedMotion?: ReducedMotionMode;
21
26
  size?: number | string;
22
27
  color?: string;
23
28
  strokeWidth?: number | string;
@@ -28,4 +33,4 @@ interface MorphIconProps extends Omit<SvgProps, "from" | "to"> {
28
33
  }
29
34
  declare const MorphIcon: import("react").ForwardRefExoticComponent<MorphIconProps & import("react").RefAttributes<MorphHandle>>;
30
35
  //#endregion
31
- export { type IconInput, type IconNode, type Morph, MorphHandle, MorphIcon, MorphIconProps, type MorphOptions, type PathEl, type Sampled, type SpringPreset };
36
+ export { type IconInput, type IconNode, type Morph, MorphHandle, MorphIcon, MorphIconProps, type MorphOptions, type PathEl, type ReducedMotionMode, type Sampled, type SpringPreset };
@@ -1,4 +1,4 @@
1
- import { a as buildPlan, f as interpPolar, l as serialize, r as resampleIcon, u as allocOutputs } from "./spring-BTcHiHR5.js";
1
+ import { a as buildPlan, f as interpPolar, l as serialize, r as resampleIcon, u as allocOutputs } from "./spring-CBynweFS.js";
2
2
  import { canonicalD, createMorph } from "./dom.js";
3
3
  import { forwardRef, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useRef, useState } from "react";
4
4
  import { jsx } from "react/jsx-runtime";
@@ -31,7 +31,7 @@ function frozenD(from, to, t) {
31
31
  return serialize(out, plan.items.map((it) => it.closed));
32
32
  }
33
33
  const MorphIcon = forwardRef(function MorphIcon(props, ref) {
34
- const { icon, from, to, progress, spring, size = 24, color = "currentColor", strokeWidth = 2, absoluteStrokeWidth, label, ...rest } = props;
34
+ const { icon, from, to, progress, spring, reducedMotion, size = 24, color = "currentColor", strokeWidth = 2, absoluteStrokeWidth, label, ...rest } = props;
35
35
  const controlled = from !== void 0 && to !== void 0;
36
36
  const initialIcon = icon ?? from ?? to;
37
37
  const [initialD] = useState(() => {
@@ -42,6 +42,8 @@ const MorphIcon = forwardRef(function MorphIcon(props, ref) {
42
42
  const morphRef = useRef(null);
43
43
  const springRef = useRef(spring);
44
44
  springRef.current = spring;
45
+ const rmRef = useRef(reducedMotion);
46
+ rmRef.current = reducedMotion;
45
47
  const prevIcon = useRef(icon);
46
48
  const prevControlled = useRef(controlled);
47
49
  const dead = useRef(false);
@@ -52,9 +54,12 @@ const MorphIcon = forwardRef(function MorphIcon(props, ref) {
52
54
  const el = useRef({ setAttribute: (name, value) => {
53
55
  if (name === "d") pathRef.current?.setNativeProps({ d: value });
54
56
  } });
55
- /** morphTo that honors the OS reduce-motion setting (best-effort). */
57
+ /** morphTo that applies the reduced-motion policy ("user" reads the OS
58
+ * setting via AccessibilityInfo, best-effort). */
56
59
  const fly = (m, i, s) => {
57
- if (reduced) m.set(i);
60
+ const mode = rmRef.current ?? "never";
61
+ if (mode === "user") initReducedMotion();
62
+ if (mode === "always" || mode === "user" && reduced) m.set(i);
58
63
  else m.morphTo(i, s);
59
64
  };
60
65
  const flyRef = useRef(fly);
@@ -71,7 +76,6 @@ const MorphIcon = forwardRef(function MorphIcon(props, ref) {
71
76
  }, []);
72
77
  useIsoLayoutEffect(() => {
73
78
  dead.current = false;
74
- initReducedMotion();
75
79
  if (pathRef.current && initialIcon !== void 0) {
76
80
  const m = createMorph(el.current, controlled ? from : initialIcon);
77
81
  morphRef.current = m;
@@ -94,6 +98,9 @@ const MorphIcon = forwardRef(function MorphIcon(props, ref) {
94
98
  pair.current = null;
95
99
  };
96
100
  }, []);
101
+ useEffect(() => {
102
+ if (reducedMotion === "user") initReducedMotion();
103
+ }, [reducedMotion]);
97
104
  useEffect(() => {
98
105
  const left = prevControlled.current && !controlled;
99
106
  prevControlled.current = controlled;
package/dist/react.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { a as Sampled, c as SpringPreset, n as IconInput, r as IconNode } from "./types-JAF3s76Y.js";
2
- import { Morph, MorphOptions, PathEl } from "./dom.js";
2
+ import { Morph, MorphOptions, PathEl, ReducedMotionMode } from "./dom.js";
3
3
  import { SVGProps } from "react";
4
4
  //#region src/react/index.d.ts
5
5
  /** Imperative surface exposed via ref. */
@@ -18,6 +18,10 @@ interface MorphIconProps extends Omit<SVGProps<SVGSVGElement>, "from" | "to" | "
18
18
  progress?: number;
19
19
  /** Physics for uncontrolled/imperative mode: preset or custom spring. */
20
20
  spring?: SpringPreset | MorphOptions;
21
+ /** Reduced-motion policy: "never" (default) animates regardless of the OS
22
+ * setting, "user" honors prefers-reduced-motion (morphs degrade to an
23
+ * instant swap while it is on), "always" always jumps. */
24
+ reducedMotion?: ReducedMotionMode;
21
25
  size?: number | string;
22
26
  color?: string;
23
27
  strokeWidth?: number | string;
@@ -28,4 +32,4 @@ interface MorphIconProps extends Omit<SVGProps<SVGSVGElement>, "from" | "to" | "
28
32
  }
29
33
  declare const MorphIcon: import("react").ForwardRefExoticComponent<MorphIconProps & import("react").RefAttributes<MorphHandle>>;
30
34
  //#endregion
31
- export { type IconInput, type IconNode, type Morph, MorphHandle, MorphIcon, MorphIconProps, type MorphOptions, type PathEl, type Sampled, type SpringPreset };
35
+ export { type IconInput, type IconNode, type Morph, MorphHandle, MorphIcon, MorphIconProps, type MorphOptions, type PathEl, type ReducedMotionMode, type Sampled, type SpringPreset };
package/dist/react.js CHANGED
@@ -1,4 +1,4 @@
1
- import { a as buildPlan, f as interpPolar, l as serialize, r as resampleIcon, u as allocOutputs } from "./spring-BTcHiHR5.js";
1
+ import { a as buildPlan, f as interpPolar, l as serialize, r as resampleIcon, u as allocOutputs } from "./spring-CBynweFS.js";
2
2
  import { canonicalD, createMorph } from "./dom.js";
3
3
  import { forwardRef, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useRef, useState } from "react";
4
4
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -15,7 +15,7 @@ function frozenD(from, to, t) {
15
15
  return serialize(out, plan.items.map((it) => it.closed));
16
16
  }
17
17
  const MorphIcon = forwardRef(function MorphIcon(props, ref) {
18
- const { icon, from, to, progress, spring, size = 24, color = "currentColor", strokeWidth = 2, absoluteStrokeWidth, label, ...rest } = props;
18
+ const { icon, from, to, progress, spring, reducedMotion, size = 24, color = "currentColor", strokeWidth = 2, absoluteStrokeWidth, label, ...rest } = props;
19
19
  const controlled = from !== void 0 && to !== void 0;
20
20
  const initialIcon = icon ?? from ?? to;
21
21
  const [initialD] = useState(() => {
@@ -26,6 +26,8 @@ const MorphIcon = forwardRef(function MorphIcon(props, ref) {
26
26
  const morphRef = useRef(null);
27
27
  const springRef = useRef(spring);
28
28
  springRef.current = spring;
29
+ const rmRef = useRef(reducedMotion);
30
+ rmRef.current = reducedMotion;
29
31
  const prevIcon = useRef(icon);
30
32
  const prevControlled = useRef(controlled);
31
33
  const dead = useRef(false);
@@ -39,14 +41,14 @@ const MorphIcon = forwardRef(function MorphIcon(props, ref) {
39
41
  if (morphRef.current) return morphRef.current;
40
42
  const el = pathRef.current;
41
43
  if (dead.current || !el) return null;
42
- morphRef.current = createMorph(el, birth);
44
+ morphRef.current = createMorph(el, birth, { reducedMotion: rmRef.current });
43
45
  return morphRef.current;
44
46
  }, []);
45
47
  useIsoLayoutEffect(() => {
46
48
  dead.current = false;
47
49
  const el = pathRef.current;
48
50
  if (el && initialIcon !== void 0) {
49
- const m = createMorph(el, controlled ? from : initialIcon);
51
+ const m = createMorph(el, controlled ? from : initialIcon, { reducedMotion: rmRef.current });
50
52
  morphRef.current = m;
51
53
  if (controlled) {
52
54
  pair.current = [from, to];
@@ -67,6 +69,10 @@ const MorphIcon = forwardRef(function MorphIcon(props, ref) {
67
69
  pair.current = null;
68
70
  };
69
71
  }, []);
72
+ useEffect(() => {
73
+ const m = morphRef.current;
74
+ if (m) m.reducedMotion = reducedMotion ?? "never";
75
+ }, [reducedMotion]);
70
76
  useEffect(() => {
71
77
  const left = prevControlled.current && !controlled;
72
78
  prevControlled.current = controlled;
@@ -305,12 +305,16 @@ function serialize(subs, closed) {
305
305
  }
306
306
  return d;
307
307
  }
308
- /** Cubic subpaths → canonical `d` at full precision (round-trip). */
308
+ function fmtCanon(v) {
309
+ return String(Math.round(v * 1e4) / 1e4);
310
+ }
311
+ /** Cubic subpaths → canonical `d`, quantized to 4 decimals (engine-stable
312
+ * bytes; see fmtCanon). */
309
313
  function cubicsToPathD(paths) {
310
314
  let d = "";
311
315
  for (const { pts, closed } of paths) {
312
- d += `M${pts[0]} ${pts[1]}`;
313
- for (let i = 2; i < pts.length; i += 6) d += `C${pts[i]} ${pts[i + 1]} ${pts[i + 2]} ${pts[i + 3]} ${pts[i + 4]} ${pts[i + 5]}`;
316
+ d += `M${fmtCanon(pts[0])} ${fmtCanon(pts[1])}`;
317
+ for (let i = 2; i < pts.length; i += 6) d += `C${fmtCanon(pts[i])} ${fmtCanon(pts[i + 1])} ${fmtCanon(pts[i + 2])} ${fmtCanon(pts[i + 3])} ${fmtCanon(pts[i + 4])} ${fmtCanon(pts[i + 5])}`;
314
318
  if (closed) d += "Z";
315
319
  }
316
320
  return d;
@@ -1,5 +1,5 @@
1
1
  import { a as Sampled, c as SpringPreset, n as IconInput, r as IconNode } from "./types-JAF3s76Y.js";
2
- import { Morph, MorphOptions, PathEl } from "./dom.js";
2
+ import { Morph, MorphOptions, PathEl, ReducedMotionMode } from "./dom.js";
3
3
  import { SVGAttributes } from "svelte/elements";
4
4
  //#region src/svelte/shared.d.ts
5
5
  /** Imperative surface exposed on the component instance (`bind:this`). */
@@ -22,6 +22,10 @@ interface MorphIconProps extends Omit<SVGAttributes<SVGSVGElement>, "from" | "to
22
22
  progress?: number;
23
23
  /** Physics for uncontrolled/imperative mode: preset or custom spring. */
24
24
  spring?: SpringPreset | MorphOptions;
25
+ /** Reduced-motion policy: "never" (default) animates regardless of the OS
26
+ * setting, "user" honors prefers-reduced-motion (morphs degrade to an
27
+ * instant swap while it is on), "always" always jumps. */
28
+ reducedMotion?: ReducedMotionMode;
25
29
  size?: number | string;
26
30
  color?: string;
27
31
  strokeWidth?: number | string;
@@ -31,7 +35,8 @@ interface MorphIconProps extends Omit<SVGAttributes<SVGSVGElement>, "from" | "to
31
35
  label?: string;
32
36
  }
33
37
  type ModeProps = Pick<MorphIconProps, "icon" | "from" | "to" | "progress">;
34
- type WatchProps = ModeProps & {
38
+ type CtrlProps = ModeProps & Pick<MorphIconProps, "reducedMotion">;
39
+ type WatchProps = CtrlProps & {
35
40
  spring?: SpringPreset | MorphOptions;
36
41
  };
37
42
  /** The initial d is constant for Svelte: computed once from the mount-time
@@ -54,15 +59,15 @@ declare function computeInitialD({ icon, from, to, progress }: ModeProps): strin
54
59
  * path back to `icon`.
55
60
  * - Every exit from controlled mode (imperative call or icon takeover)
56
61
  * invalidates the frozen pair, so returning to it re-bases on `from`. */
57
- declare function createController({ icon, from, to, progress }: ModeProps): {
58
- mount(mountEl: PathEl, { icon, from, to, progress }: ModeProps): void;
62
+ declare function createController({ icon, from, to, progress, reducedMotion }: CtrlProps): {
63
+ mount(mountEl: PathEl, { icon, from, to, progress, reducedMotion }: CtrlProps): void;
59
64
  destroy(): void;
60
65
  /** Prop watcher: ONE owner decides per run — the controlled pair while it
61
66
  * is fully present, `icon` otherwise (mount doesn't fire thanks to the
62
67
  * init-time baselines). */
63
- watch({ icon, from, to, progress, spring }: WatchProps): void;
68
+ watch({ icon, from, to, progress, spring, reducedMotion }: WatchProps): void;
64
69
  morphTo(icon: IconInput, spring?: SpringPreset | MorphOptions): void;
65
70
  set(icon: IconInput): void;
66
71
  };
67
72
  //#endregion
68
- export { type IconInput, type IconNode, type Morph, MorphHandle, MorphIconProps, type MorphOptions, type PathEl, type Sampled, type SpringPreset, computeInitialD, createController };
73
+ export { type IconInput, type IconNode, type Morph, MorphHandle, MorphIconProps, type MorphOptions, type PathEl, type ReducedMotionMode, type Sampled, type SpringPreset, computeInitialD, createController };
@@ -1,4 +1,4 @@
1
- import { a as buildPlan, f as interpPolar, l as serialize, r as resampleIcon, u as allocOutputs } from "./spring-BTcHiHR5.js";
1
+ import { a as buildPlan, f as interpPolar, l as serialize, r as resampleIcon, u as allocOutputs } from "./spring-CBynweFS.js";
2
2
  import { canonicalD, createMorph } from "./dom.js";
3
3
  //#region src/svelte/shared.ts
4
4
  /** Frozen shape of the from→to pair at t, using the pure core (SSR-safe).
@@ -35,10 +35,11 @@ function computeInitialD({ icon, from, to, progress }) {
35
35
  * path back to `icon`.
36
36
  * - Every exit from controlled mode (imperative call or icon takeover)
37
37
  * invalidates the frozen pair, so returning to it re-bases on `from`. */
38
- function createController({ icon, from, to, progress }) {
38
+ function createController({ icon, from, to, progress, reducedMotion }) {
39
39
  let el = null;
40
40
  let dead = false;
41
41
  let morph = null;
42
+ let rm = reducedMotion ?? "never";
42
43
  let based = false;
43
44
  let pair = null;
44
45
  let prevIcon = icon;
@@ -50,7 +51,7 @@ function createController({ icon, from, to, progress }) {
50
51
  const ensure = (birth) => {
51
52
  if (morph) return morph;
52
53
  if (dead || !el) return null;
53
- morph = createMorph(el, birth);
54
+ morph = createMorph(el, birth, { reducedMotion: rm });
54
55
  return morph;
55
56
  };
56
57
  /** Controlled mode: freeze the pair at `progress` via seek (no spring). */
@@ -77,12 +78,13 @@ function createController({ icon, from, to, progress }) {
77
78
  }
78
79
  };
79
80
  return {
80
- mount(mountEl, { icon, from, to, progress }) {
81
+ mount(mountEl, { icon, from, to, progress, reducedMotion }) {
81
82
  el = mountEl;
83
+ rm = reducedMotion ?? rm;
82
84
  const controlled = from !== void 0 && to !== void 0;
83
85
  const initialIcon = icon ?? from ?? to;
84
86
  if (initialIcon === void 0) return;
85
- const m = createMorph(el, controlled ? from : initialIcon);
87
+ const m = createMorph(el, controlled ? from : initialIcon, { reducedMotion: rm });
86
88
  morph = m;
87
89
  if (controlled) {
88
90
  pair = [from, to];
@@ -106,7 +108,9 @@ function createController({ icon, from, to, progress }) {
106
108
  /** Prop watcher: ONE owner decides per run — the controlled pair while it
107
109
  * is fully present, `icon` otherwise (mount doesn't fire thanks to the
108
110
  * init-time baselines). */
109
- watch({ icon, from, to, progress, spring }) {
111
+ watch({ icon, from, to, progress, spring, reducedMotion }) {
112
+ rm = reducedMotion ?? "never";
113
+ if (morph) morph.reducedMotion = rm;
110
114
  const controlled = from !== void 0 && to !== void 0;
111
115
  const left = prevControlled && !controlled;
112
116
  const iconChanged = icon !== prevIcon;
package/dist/vue.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { a as Sampled, c as SpringPreset, n as IconInput, r as IconNode } from "./types-JAF3s76Y.js";
2
- import { Morph, MorphOptions, PathEl } from "./dom.js";
2
+ import { Morph, MorphOptions, PathEl, ReducedMotionMode } from "./dom.js";
3
3
  import { PropType } from "vue";
4
4
  //#region src/vue/index.d.ts
5
5
  /** Imperative surface exposed on the component instance (template ref). */
@@ -33,6 +33,13 @@ declare const MorphIcon: import("vue").DefineComponent<import("vue").ExtractProp
33
33
  type: PropType<SpringPreset | MorphOptions>;
34
34
  default: undefined;
35
35
  };
36
+ /** Reduced-motion policy: "never" (default) animates regardless of the OS
37
+ * setting, "user" honors prefers-reduced-motion (morphs degrade to an
38
+ * instant swap while it is on), "always" always jumps. */
39
+ reducedMotion: {
40
+ type: PropType<ReducedMotionMode>;
41
+ default: string;
42
+ };
36
43
  size: {
37
44
  type: (NumberConstructor | StringConstructor)[];
38
45
  default: number;
@@ -83,6 +90,13 @@ declare const MorphIcon: import("vue").DefineComponent<import("vue").ExtractProp
83
90
  type: PropType<SpringPreset | MorphOptions>;
84
91
  default: undefined;
85
92
  };
93
+ /** Reduced-motion policy: "never" (default) animates regardless of the OS
94
+ * setting, "user" honors prefers-reduced-motion (morphs degrade to an
95
+ * instant swap while it is on), "always" always jumps. */
96
+ reducedMotion: {
97
+ type: PropType<ReducedMotionMode>;
98
+ default: string;
99
+ };
86
100
  size: {
87
101
  type: (NumberConstructor | StringConstructor)[];
88
102
  default: number;
@@ -111,6 +125,7 @@ declare const MorphIcon: import("vue").DefineComponent<import("vue").ExtractProp
111
125
  to: IconInput;
112
126
  progress: number;
113
127
  spring: "bouncy" | "smooth" | "snappy" | MorphOptions;
128
+ reducedMotion: ReducedMotionMode;
114
129
  size: string | number;
115
130
  color: string;
116
131
  strokeWidth: string | number;
@@ -118,4 +133,4 @@ declare const MorphIcon: import("vue").DefineComponent<import("vue").ExtractProp
118
133
  label: string;
119
134
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
120
135
  //#endregion
121
- export { type IconInput, type IconNode, type Morph, MorphHandle, MorphIcon, type MorphOptions, type PathEl, type Sampled, type SpringPreset };
136
+ export { type IconInput, type IconNode, type Morph, MorphHandle, MorphIcon, type MorphOptions, type PathEl, type ReducedMotionMode, type Sampled, type SpringPreset };
package/dist/vue.js CHANGED
@@ -1,4 +1,4 @@
1
- import { a as buildPlan, f as interpPolar, l as serialize, r as resampleIcon, u as allocOutputs } from "./spring-BTcHiHR5.js";
1
+ import { a as buildPlan, f as interpPolar, l as serialize, r as resampleIcon, u as allocOutputs } from "./spring-CBynweFS.js";
2
2
  import { canonicalD, createMorph } from "./dom.js";
3
3
  import { defineComponent, h, onBeforeUnmount, onMounted, shallowRef, watch } from "vue";
4
4
  //#region src/vue/index.ts
@@ -40,6 +40,13 @@ const MorphIcon = defineComponent({
40
40
  type: [String, Object],
41
41
  default: void 0
42
42
  },
43
+ /** Reduced-motion policy: "never" (default) animates regardless of the OS
44
+ * setting, "user" honors prefers-reduced-motion (morphs degrade to an
45
+ * instant swap while it is on), "always" always jumps. */
46
+ reducedMotion: {
47
+ type: String,
48
+ default: "never"
49
+ },
43
50
  size: {
44
51
  type: [Number, String],
45
52
  default: 24
@@ -84,7 +91,7 @@ const MorphIcon = defineComponent({
84
91
  if (morph) return morph;
85
92
  const el = pathEl.value;
86
93
  if (dead || !el) return null;
87
- morph = createMorph(el, birth);
94
+ morph = createMorph(el, birth, { reducedMotion: props.reducedMotion });
88
95
  return morph;
89
96
  };
90
97
  onMounted(() => {
@@ -93,7 +100,7 @@ const MorphIcon = defineComponent({
93
100
  const controlled = from !== void 0 && to !== void 0;
94
101
  const initialIcon = icon ?? from ?? to;
95
102
  if (!el || initialIcon === void 0) return;
96
- const m = createMorph(el, controlled ? from : initialIcon);
103
+ const m = createMorph(el, controlled ? from : initialIcon, { reducedMotion: props.reducedMotion });
97
104
  morph = m;
98
105
  if (controlled) {
99
106
  pair = [from, to];
@@ -113,6 +120,9 @@ const MorphIcon = defineComponent({
113
120
  based = false;
114
121
  pair = null;
115
122
  });
123
+ watch(() => props.reducedMotion, (v) => {
124
+ if (morph) morph.reducedMotion = v;
125
+ });
116
126
  watch(() => [
117
127
  props.icon,
118
128
  props.from,
@@ -128,8 +138,10 @@ const MorphIcon = defineComponent({
128
138
  if (icon === void 0 || !changed && !left) return;
129
139
  pair = null;
130
140
  based = false;
131
- if (morph) morph.morphTo(icon, props.spring);
132
- else ensure(icon);
141
+ if (morph) {
142
+ morph.reducedMotion = props.reducedMotion;
143
+ morph.morphTo(icon, props.spring);
144
+ } else ensure(icon);
133
145
  });
134
146
  watch(() => [
135
147
  props.from,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "morphicons",
3
- "version": "1.4.0",
3
+ "version": "1.4.2",
4
4
  "description": "Universal morphing for stroke-based icons with spring physics — zero runtime dependencies",
5
5
  "license": "MIT",
6
6
  "author": "Guillermo (https://guillermolg.com)",