morphicons 1.0.0 → 1.1.1
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 +28 -9
- package/dist/dom.js +1 -1
- package/dist/index.d.ts +19 -1
- package/dist/index.js +2 -2
- package/dist/react.js +1 -1
- package/dist/{spring-Dbj7NCw2.js → spring-BTcHiHR5.js} +91 -33
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
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,
|
|
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.
|
|
17
17
|
|
|
18
18
|
```tsx
|
|
19
19
|
import { MorphIcon } from "morphicons/react";
|
|
@@ -86,7 +86,7 @@ interpPolar(plan, 0.5, out); // t ∈ [0, 1]
|
|
|
86
86
|
const d = serialize(out, plan.items.map(it => it.closed)); // `d` attribute
|
|
87
87
|
```
|
|
88
88
|
|
|
89
|
-
Accepts `IconNode` (Lucide's data format, structurally typed — Lucide is neither a dependency nor a peer) or a raw `d` attribute.
|
|
89
|
+
Accepts `IconNode` (Lucide's data format, structurally typed — Lucide is neither a dependency nor a peer) or a raw `d` attribute. Tabler, Heroicons, Iconoir and custom paths work the same.
|
|
90
90
|
|
|
91
91
|
## Icon library compatibility
|
|
92
92
|
|
|
@@ -94,10 +94,21 @@ morphicons has no per-library adapters — any icon set that meets these require
|
|
|
94
94
|
|
|
95
95
|
1. **Stroke-drawn icons.** The geometry must be the stroked centerline (`fill="none"`, color via `stroke`). The whole pipeline — resampling, correspondence, in-flight polylines — assumes strokes; filled or outlined-fill glyphs (Material Symbols, Bootstrap Icons, Remix Icon, Phosphor, Heroicons *solid*) parse fine but won't read correctly in transit.
|
|
96
96
|
2. **Geometry available as data.** Either a raw `d` attribute per icon, or a `[tag, attrs][]` node list (Lucide's IconNode shape) using only `path`, `line`, `circle`, `ellipse`, `rect`, `polyline`, `polygon`. No `<g>` wrappers and no `transform` attributes — coordinates must be literal. Any other tag throws a clear error.
|
|
97
|
-
3. **A shared coordinate space per pair.** Both endpoints of a morph must live on the same grid. Lucide,
|
|
97
|
+
3. **A shared coordinate space per pair.** Both endpoints of a morph must live on the same grid. Lucide, Tabler, Heroicons and Iconoir all draw on 24×24 — that's why cross-library morphs just work. For a pack on another canvas (Heroicons *solid* on 20, Carbon on 32, Teenyicons on 15), re-grid it once with `fitIcon`:
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
import { fitIcon } from "morphicons";
|
|
101
|
+
|
|
102
|
+
const search = fitIcon(carbonSearch, 32); // 32×32 → the 24 grid
|
|
103
|
+
const bell = fitIcon(teenySmallBell, 15); // also "0 0 15 15" or [0, 0, 15, 15]
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
It returns a plain `d`, so it goes anywhere an icon is accepted. Call it at module scope, not per render. Skipping it doesn't throw — Procrustes is similarity-invariant, so the grid mismatch never reads as false rotation; it lands in σ as an unwanted zoom, and the target draws outside the canvas. The React binding defaults to `viewBox="0 0 24 24"`, overridable via props.
|
|
98
107
|
4. **Uniform stroke.** Cosmetic rather than structural: a consistent stroke width and round caps/joins make in-flight shapes look native. These live on the `<svg>`, not in morphicons.
|
|
99
108
|
|
|
100
|
-
Beyond the
|
|
109
|
+
Beyond the libraries in the playground, sets known to qualify on the 24×24 grid — no fitting needed: **Heroicons** (outline style), **Iconoir** (1.5px stroke), **Akar Icons**, **Untitled UI** and **Hugeicons** (stroke style). Off-grid packs work through `fitIcon`: **Teenyicons** (15×15), **Carbon** (32×32), **Heroicons solid** (20×20).
|
|
110
|
+
|
|
111
|
+
This also covers the [shadcn registry](https://www.shadcn.io/icons), which re-publishes 200+ icon libraries as React components with an inline `<path d>` — pass that `d` straight in, adding `fitIcon` when the pack's `viewBox` isn't 24.
|
|
101
112
|
|
|
102
113
|
## Architecture
|
|
103
114
|
|
|
@@ -197,7 +208,7 @@ Let `aᶜᵢ = aᵢ − c_A` and `b̃ᵢ = R(−θ*)·(bᵢ − c_B)/σ*` (B bro
|
|
|
197
208
|
```
|
|
198
209
|
P(t) = c(t) + σ*ᵗ · R(t·θ*) · [(1−t)·aᶜᵢ + t·b̃ᵢ]
|
|
199
210
|
|
|
200
|
-
c(t) = lerp(c_A, c_B, t) linear translation
|
|
211
|
+
c(t) = lerp(c_A, c_B, t) linear translation (block transport under the global hybrid — below)
|
|
201
212
|
t·θ* = linear angle (θ* is already in (−π, π], short path)
|
|
202
213
|
σ*ᵗ = exp(t·ln σ*) log-linear scale (geodesic in ℝ⁺)
|
|
203
214
|
```
|
|
@@ -209,6 +220,14 @@ Exact at both endpoints (max error ~3.6e-15 in tests). Properties:
|
|
|
209
220
|
- Mixed cases do both simultaneously and continuously.
|
|
210
221
|
- With spring overshoot (t > 1) the formula **extrapolates** naturally: rotation and scale overshoot slightly and come back — free juice.
|
|
211
222
|
|
|
223
|
+
**Block transport (global hybrid).** When the whole icon is congruent (§4), lerping each subpath centroid reintroduces the chord disease one level up: every part spins with the shared θ*, but an off-center part travels the chord — inside the arc — and drifts toward the center mid-flight. An arrow's head sagged ~1 px toward its shaft at t = 0.5 (`r·(1 − cos(θ/2))` with r = 3.5, θ = 90°) while both parts rotated perfectly. Under the hybrid, each centroid rides the shared similarity around the global centroid `g_A` instead:
|
|
224
|
+
|
|
225
|
+
```
|
|
226
|
+
c_k(t) = c_{A,k} + t·d_k + (σ*ᵗ·R(t·θ*) − I)·(c_{A,k} − g_A)
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
`d_k` is fixed once so that `c_k(1) = c_{B,k}` exactly (it absorbs the tiny global residual). Every point of every subpath then moves under ONE similarity per frame: congruent icons are rigid through the whole flight, not only at the endpoints. With θ* = 0, or centroids sitting on `g_A`, or no hybrid at all, the formula degrades continuously to the plain lerp — no thresholds, no special cases.
|
|
230
|
+
|
|
212
231
|
### 6. Serialization and canonical snap
|
|
213
232
|
|
|
214
233
|
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.
|
|
@@ -244,9 +263,9 @@ CI budget (size-limit, gzip) as an anti-regression tripwire — gates carry ~10%
|
|
|
244
263
|
|
|
245
264
|
| entry | measured | gate |
|
|
246
265
|
|---|---|---|
|
|
247
|
-
| `morphicons` (core) | 6.
|
|
248
|
-
| `morphicons/dom` (core + driver) |
|
|
249
|
-
| `morphicons/react` (all, react external) | 7.
|
|
266
|
+
| `morphicons` (core) | 6.60 KB | 7 KB |
|
|
267
|
+
| `morphicons/dom` (core + driver) | 7.04 KB | 7.5 KB |
|
|
268
|
+
| `morphicons/react` (all, react external) | 7.77 KB | 8.5 KB |
|
|
250
269
|
|
|
251
270
|
## Playground
|
|
252
271
|
|
|
@@ -259,7 +278,7 @@ bun run play # → http://localhost:3000
|
|
|
259
278
|
## Development
|
|
260
279
|
|
|
261
280
|
```bash
|
|
262
|
-
bun test #
|
|
281
|
+
bun test # 83 tests / ~13,400 asserts
|
|
263
282
|
bun run typecheck # strict ×3: core+dom without lib DOM, playground, react
|
|
264
283
|
bun run format # biome
|
|
265
284
|
bun run build # tsdown → dist/ (entries index, dom, react)
|
package/dist/dom.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as
|
|
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";
|
|
2
2
|
//#region src/dom/index.ts
|
|
3
3
|
const tickers = /* @__PURE__ */ new Set();
|
|
4
4
|
let rafId = 0;
|
package/dist/index.d.ts
CHANGED
|
@@ -18,6 +18,13 @@ interface PlanItem {
|
|
|
18
18
|
/** true if both endpoints are closed loops: the subpath flies with Z.
|
|
19
19
|
* Closed → open flies open: the loop opens at the chosen cut. */
|
|
20
20
|
closed: boolean;
|
|
21
|
+
/** Block transport (set by the global hybrid, else null): mid-flight the
|
|
22
|
+
* centroid rides the shared similarity around the global centroid instead
|
|
23
|
+
* of lerping — off = c_A − g_A; drift closes c(1) = c_B exactly. */
|
|
24
|
+
block: {
|
|
25
|
+
off: readonly [number, number];
|
|
26
|
+
drift: readonly [number, number];
|
|
27
|
+
} | null;
|
|
21
28
|
}
|
|
22
29
|
interface MorphPlan {
|
|
23
30
|
items: PlanItem[];
|
|
@@ -38,6 +45,17 @@ declare function interpLinear(plan: MorphPlan, t: number, out: Float64Array[]):
|
|
|
38
45
|
//#region src/core/normalize.d.ts
|
|
39
46
|
/** Icon (IconNode or `d` string) → list of cubic subpaths. */
|
|
40
47
|
declare function iconToCubics(input: IconInput): CubicPath[];
|
|
48
|
+
/** A source viewBox: `24`, `"0 0 20 20"` or `[minX, minY, w, h]`. */
|
|
49
|
+
type ViewBox = number | string | readonly [number, number, number, number];
|
|
50
|
+
/** Re-grids an icon drawn on `viewBox` onto the shared `grid` (24 by default),
|
|
51
|
+
* centred and preserving aspect ratio — the SVG `xMidYMid meet` rule.
|
|
52
|
+
*
|
|
53
|
+
* Both endpoints of a morph must live on the same coordinate space. Lucide and
|
|
54
|
+
* Tabler already draw on 24×24; packs on 20 (Heroicons solid) or 32 (Carbon)
|
|
55
|
+
* do not, and mixing them unfitted makes Procrustes read the scale/offset gap
|
|
56
|
+
* as rotation. Apply once at module scope (not per render) and pass the
|
|
57
|
+
* resulting `d` anywhere an icon is accepted. */
|
|
58
|
+
declare function fitIcon(input: IconInput, viewBox: ViewBox, grid?: number): string;
|
|
41
59
|
//#endregion
|
|
42
60
|
//#region src/core/resample.d.ts
|
|
43
61
|
/** Samples a cubic subpath at N points equidistant by arc length, anchoring
|
|
@@ -57,4 +75,4 @@ declare function serialize(subs: readonly Float64Array[], closed?: readonly bool
|
|
|
57
75
|
/** Cubic subpaths → canonical `d` at full precision (round-trip). */
|
|
58
76
|
declare function cubicsToPathD(paths: readonly CubicPath[]): string;
|
|
59
77
|
//#endregion
|
|
60
|
-
export { type CubicPath, type IconInput, type IconNode, type IconNodeAttrs, type MorphPlan, type PlanItem, SPRING_PRESETS, type Sampled, Spring, type SpringPreset, allocOutputs, buildPlan, cubicsToPathD, iconToCubics, interpLinear, interpPolar, resampleIcon, resamplePath, serialize };
|
|
78
|
+
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
|
|
2
|
-
export { SPRING_PRESETS, Spring, allocOutputs, buildPlan, cubicsToPathD, iconToCubics, interpLinear, interpPolar, resampleIcon, resamplePath, serialize };
|
|
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";
|
|
2
|
+
export { SPRING_PRESETS, Spring, allocOutputs, buildPlan, cubicsToPathD, fitIcon, iconToCubics, interpLinear, interpPolar, resampleIcon, resamplePath, serialize };
|
package/dist/react.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as
|
|
1
|
+
import { a as buildPlan, f as interpPolar, l as serialize, r as resampleIcon, u as allocOutputs } from "./spring-BTcHiHR5.js";
|
|
2
2
|
import { canonicalD, createMorph } from "./dom.js";
|
|
3
3
|
import { forwardRef, useEffect, useImperativeHandle, useLayoutEffect, useRef, useState } from "react";
|
|
4
4
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -8,12 +8,21 @@ function interpPolar(plan, t, out) {
|
|
|
8
8
|
const it = plan.items[k];
|
|
9
9
|
const o = out[k];
|
|
10
10
|
const n = plan.n;
|
|
11
|
-
const cx = it.ca[0] + (it.cb[0] - it.ca[0]) * t;
|
|
12
|
-
const cy = it.ca[1] + (it.cb[1] - it.ca[1]) * t;
|
|
13
11
|
const s = Math.exp(it.lnSigma * t);
|
|
14
12
|
const ang = it.theta * t;
|
|
15
13
|
const cos = Math.cos(ang) * s;
|
|
16
14
|
const sin = Math.sin(ang) * s;
|
|
15
|
+
let cx;
|
|
16
|
+
let cy;
|
|
17
|
+
if (it.block) {
|
|
18
|
+
const [ox, oy] = it.block.off;
|
|
19
|
+
const [dx, dy] = it.block.drift;
|
|
20
|
+
cx = it.ca[0] + dx * t + (ox * cos - oy * sin - ox);
|
|
21
|
+
cy = it.ca[1] + dy * t + (ox * sin + oy * cos - oy);
|
|
22
|
+
} else {
|
|
23
|
+
cx = it.ca[0] + (it.cb[0] - it.ca[0]) * t;
|
|
24
|
+
cy = it.ca[1] + (it.cb[1] - it.ca[1]) * t;
|
|
25
|
+
}
|
|
17
26
|
for (let i = 0; i < n; i++) {
|
|
18
27
|
const px = it.aC[2 * i] + (it.bT[2 * i] - it.aC[2 * i]) * t;
|
|
19
28
|
const py = it.aC[2 * i + 1] + (it.bT[2 * i + 1] - it.aC[2 * i + 1]) * t;
|
|
@@ -279,6 +288,34 @@ function parsePath(d) {
|
|
|
279
288
|
return subs.filter((s) => s.segs.length > 0);
|
|
280
289
|
}
|
|
281
290
|
//#endregion
|
|
291
|
+
//#region src/core/serialize.ts
|
|
292
|
+
function fmt(v) {
|
|
293
|
+
return String(Math.round(v * 100) / 100);
|
|
294
|
+
}
|
|
295
|
+
/** Sampled subpaths → polyline `d` attribute. `closed[k]` appends Z to
|
|
296
|
+
* subpath k (closed loops in flight); without flags everything is open. */
|
|
297
|
+
function serialize(subs, closed) {
|
|
298
|
+
let d = "";
|
|
299
|
+
for (let k = 0; k < subs.length; k++) {
|
|
300
|
+
const o = subs[k];
|
|
301
|
+
const n = o.length / 2;
|
|
302
|
+
d += `M${fmt(o[0])} ${fmt(o[1])}`;
|
|
303
|
+
for (let i = 1; i < n; i++) d += `L${fmt(o[2 * i])} ${fmt(o[2 * i + 1])}`;
|
|
304
|
+
if (closed?.[k]) d += "Z";
|
|
305
|
+
}
|
|
306
|
+
return d;
|
|
307
|
+
}
|
|
308
|
+
/** Cubic subpaths → canonical `d` at full precision (round-trip). */
|
|
309
|
+
function cubicsToPathD(paths) {
|
|
310
|
+
let d = "";
|
|
311
|
+
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]}`;
|
|
314
|
+
if (closed) d += "Z";
|
|
315
|
+
}
|
|
316
|
+
return d;
|
|
317
|
+
}
|
|
318
|
+
//#endregion
|
|
282
319
|
//#region src/core/normalize.ts
|
|
283
320
|
/** Control-point offset for a quarter circle: (4/3)·tan(π/8) ≈ 0.5523. */
|
|
284
321
|
const KAPPA = 4 / 3 * Math.tan(Math.PI / 8);
|
|
@@ -505,6 +542,42 @@ function iconToCubics(input) {
|
|
|
505
542
|
}
|
|
506
543
|
return out;
|
|
507
544
|
}
|
|
545
|
+
function parseViewBox(vb) {
|
|
546
|
+
const v = typeof vb === "number" ? [
|
|
547
|
+
0,
|
|
548
|
+
0,
|
|
549
|
+
vb,
|
|
550
|
+
vb
|
|
551
|
+
] : typeof vb === "string" ? vb.trim().split(/[\s,]+/).map(Number) : vb;
|
|
552
|
+
const [minX, minY, w, h] = v;
|
|
553
|
+
if (v.length !== 4 || !(w > 0) || !(h > 0) || !Number.isFinite(minX) || !Number.isFinite(minY)) throw new Error(`morphicons: invalid viewBox "${String(vb)}"`);
|
|
554
|
+
return [
|
|
555
|
+
minX,
|
|
556
|
+
minY,
|
|
557
|
+
w,
|
|
558
|
+
h
|
|
559
|
+
];
|
|
560
|
+
}
|
|
561
|
+
/** Re-grids an icon drawn on `viewBox` onto the shared `grid` (24 by default),
|
|
562
|
+
* centred and preserving aspect ratio — the SVG `xMidYMid meet` rule.
|
|
563
|
+
*
|
|
564
|
+
* Both endpoints of a morph must live on the same coordinate space. Lucide and
|
|
565
|
+
* Tabler already draw on 24×24; packs on 20 (Heroicons solid) or 32 (Carbon)
|
|
566
|
+
* do not, and mixing them unfitted makes Procrustes read the scale/offset gap
|
|
567
|
+
* as rotation. Apply once at module scope (not per render) and pass the
|
|
568
|
+
* resulting `d` anywhere an icon is accepted. */
|
|
569
|
+
function fitIcon(input, viewBox, grid = 24) {
|
|
570
|
+
const [minX, minY, w, h] = parseViewBox(viewBox);
|
|
571
|
+
const s = Math.min(grid / w, grid / h);
|
|
572
|
+
const tx = (grid - w * s) / 2 - minX * s;
|
|
573
|
+
const ty = (grid - h * s) / 2 - minY * s;
|
|
574
|
+
const paths = iconToCubics(input);
|
|
575
|
+
for (const { pts } of paths) for (let i = 0; i < pts.length; i += 2) {
|
|
576
|
+
pts[i] = pts[i] * s + tx;
|
|
577
|
+
pts[i + 1] = pts[i + 1] * s + ty;
|
|
578
|
+
}
|
|
579
|
+
return cubicsToPathD(paths);
|
|
580
|
+
}
|
|
508
581
|
//#endregion
|
|
509
582
|
//#region src/core/plan.ts
|
|
510
583
|
/** Weight of |ΔL| in the subpath pairing cost. */
|
|
@@ -742,7 +815,8 @@ function applyGlobal(items, n) {
|
|
|
742
815
|
ga.set(it.a, 2 * n * k);
|
|
743
816
|
gb.set(it.bO, 2 * n * k);
|
|
744
817
|
});
|
|
745
|
-
const
|
|
818
|
+
const gca = centroid(ga);
|
|
819
|
+
const g = procrustes(ga, gb, gca, centroid(gb));
|
|
746
820
|
if (g.res >= GLOBAL_EPS) return;
|
|
747
821
|
const cos = Math.cos(-g.theta);
|
|
748
822
|
const sin = Math.sin(-g.theta);
|
|
@@ -764,6 +838,17 @@ function applyGlobal(items, n) {
|
|
|
764
838
|
it.theta = g.theta;
|
|
765
839
|
it.lnSigma = Math.log(g.sigma);
|
|
766
840
|
it.res = nb > 1e-12 ? Math.sqrt(e2 / nb) : 0;
|
|
841
|
+
const s1 = Math.exp(it.lnSigma);
|
|
842
|
+
const c1 = Math.cos(it.theta) * s1;
|
|
843
|
+
const n1 = Math.sin(it.theta) * s1;
|
|
844
|
+
const ox = it.ca[0] - gca[0];
|
|
845
|
+
const oy = it.ca[1] - gca[1];
|
|
846
|
+
const rx = ox * c1 - oy * n1 - ox;
|
|
847
|
+
const ry = ox * n1 + oy * c1 - oy;
|
|
848
|
+
it.block = {
|
|
849
|
+
off: [ox, oy],
|
|
850
|
+
drift: [it.cb[0] - it.ca[0] - rx, it.cb[1] - it.ca[1] - ry]
|
|
851
|
+
};
|
|
767
852
|
}
|
|
768
853
|
}
|
|
769
854
|
/** Builds the morph plan between two lists of sampled subpaths. The plan is
|
|
@@ -815,7 +900,8 @@ function buildPlan(srcSubs, dstSubs) {
|
|
|
815
900
|
theta: al.theta,
|
|
816
901
|
lnSigma: Math.log(al.sigma),
|
|
817
902
|
res: al.res,
|
|
818
|
-
closed: srcSubs[si].closed && dstSubs[di].closed
|
|
903
|
+
closed: srcSubs[si].closed && dstSubs[di].closed,
|
|
904
|
+
block: null
|
|
819
905
|
};
|
|
820
906
|
});
|
|
821
907
|
if (items.length > 1) applyGlobal(items, n);
|
|
@@ -1021,34 +1107,6 @@ function resampleIcon(input, N = 64) {
|
|
|
1021
1107
|
}));
|
|
1022
1108
|
}
|
|
1023
1109
|
//#endregion
|
|
1024
|
-
//#region src/core/serialize.ts
|
|
1025
|
-
function fmt(v) {
|
|
1026
|
-
return String(Math.round(v * 100) / 100);
|
|
1027
|
-
}
|
|
1028
|
-
/** Sampled subpaths → polyline `d` attribute. `closed[k]` appends Z to
|
|
1029
|
-
* subpath k (closed loops in flight); without flags everything is open. */
|
|
1030
|
-
function serialize(subs, closed) {
|
|
1031
|
-
let d = "";
|
|
1032
|
-
for (let k = 0; k < subs.length; k++) {
|
|
1033
|
-
const o = subs[k];
|
|
1034
|
-
const n = o.length / 2;
|
|
1035
|
-
d += `M${fmt(o[0])} ${fmt(o[1])}`;
|
|
1036
|
-
for (let i = 1; i < n; i++) d += `L${fmt(o[2 * i])} ${fmt(o[2 * i + 1])}`;
|
|
1037
|
-
if (closed?.[k]) d += "Z";
|
|
1038
|
-
}
|
|
1039
|
-
return d;
|
|
1040
|
-
}
|
|
1041
|
-
/** Cubic subpaths → canonical `d` at full precision (round-trip). */
|
|
1042
|
-
function cubicsToPathD(paths) {
|
|
1043
|
-
let d = "";
|
|
1044
|
-
for (const { pts, closed } of paths) {
|
|
1045
|
-
d += `M${pts[0]} ${pts[1]}`;
|
|
1046
|
-
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]}`;
|
|
1047
|
-
if (closed) d += "Z";
|
|
1048
|
-
}
|
|
1049
|
-
return d;
|
|
1050
|
-
}
|
|
1051
|
-
//#endregion
|
|
1052
1110
|
//#region src/core/spring.ts
|
|
1053
1111
|
var Spring = class {
|
|
1054
1112
|
x = 1;
|
|
@@ -1096,4 +1154,4 @@ const SPRING_PRESETS = {
|
|
|
1096
1154
|
}
|
|
1097
1155
|
};
|
|
1098
1156
|
//#endregion
|
|
1099
|
-
export {
|
|
1157
|
+
export { buildPlan as a, cubicsToPathD as c, interpLinear as d, interpPolar as f, resamplePath as i, serialize as l, Spring as n, fitIcon as o, resampleIcon as r, iconToCubics as s, SPRING_PRESETS as t, allocOutputs as u };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "morphicons",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
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)",
|
|
@@ -23,8 +23,11 @@
|
|
|
23
23
|
"spring",
|
|
24
24
|
"lucide",
|
|
25
25
|
"lucide-react",
|
|
26
|
-
"feather",
|
|
27
26
|
"tabler",
|
|
27
|
+
"heroicons",
|
|
28
|
+
"iconoir",
|
|
29
|
+
"shadcn",
|
|
30
|
+
"feather",
|
|
28
31
|
"react",
|
|
29
32
|
"icon-animation"
|
|
30
33
|
],
|