morphicons 1.7.1 → 2.0.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/dist/dom.js CHANGED
@@ -1,5 +1,7 @@
1
- import { a as buildPlan, c as interpPolar, n as Spring, o as allocOutputs, r as resampleIcon, t as SPRING_PRESETS } from "./spring-CFHloqPP.js";
1
+ import { a as interpPolar, n as Spring, r as allocOutputs, t as SPRING_PRESETS } from "./spring-B_P92nf6.js";
2
2
  import { i as serialize, n as iconToCubics, r as cubicsToPathD } from "./normalize-CYnN3Npw.js";
3
+ import { a as buildPlan, d as reversePts, n as resamplePath, o as centroid, t as resampleIcon } from "./resample-CXlaZ5ZQ.js";
4
+ import { a as paintOf, c as solid, o as painted, r as inside, s as signedArea } from "./region-BjECuWZR.js";
3
5
  //#region src/dom/index.ts
4
6
  const tickers = /* @__PURE__ */ new Set();
5
7
  let rafId = 0;
@@ -32,11 +34,33 @@ function removeTicker(tick) {
32
34
  const samples = /* @__PURE__ */ new WeakMap();
33
35
  const canon = /* @__PURE__ */ new WeakMap();
34
36
  const plans = /* @__PURE__ */ new WeakMap();
37
+ function cubicsOf(icon) {
38
+ if (typeof icon === "string" || paintOf(icon) === "stroke") return iconToCubics(icon);
39
+ return solid(icon).flatMap((el) => {
40
+ const ps = iconToCubics([el]);
41
+ const odd = String(el[1]["fill-rule"]).trim() === "evenodd";
42
+ let sum = 0;
43
+ for (const p of ps) sum += signedArea(p.pts);
44
+ for (const p of ps) {
45
+ let flip = sum < 0;
46
+ if (odd) {
47
+ let pos = true;
48
+ for (const o of ps) if (o !== p && inside(p.pts[0], p.pts[1], o.pts)) pos = !pos;
49
+ flip = signedArea(p.pts) > 0 !== pos;
50
+ }
51
+ if (flip) p.pts = reversePts(p.pts);
52
+ }
53
+ return ps;
54
+ });
55
+ }
35
56
  function sampledOf(icon) {
36
57
  if (typeof icon === "string") return resampleIcon(icon);
37
58
  let s = samples.get(icon);
38
59
  if (!s) {
39
- s = resampleIcon(icon);
60
+ s = cubicsOf(icon).map((p) => ({
61
+ pts: resamplePath(p, 64),
62
+ closed: p.closed
63
+ }));
40
64
  samples.set(icon, s);
41
65
  }
42
66
  return s;
@@ -44,12 +68,13 @@ function sampledOf(icon) {
44
68
  /** Canonical `d` of an icon: the input string verbatim, or the real cubics
45
69
  * quantized to 4 decimals (the at-rest snap; engine-stable bytes so SSR
46
70
  * hydration matches, see fmtCanon in core/serialize). Exported because it
47
- * is what a binding renders at SSR/rest before any runtime exists. */
71
+ * is what a binding renders at SSR/rest before any runtime exists. A fill
72
+ * icon's translucent duotone layer is left out (one path can't paint it). */
48
73
  function canonicalD(icon) {
49
74
  if (typeof icon === "string") return icon;
50
75
  let d = canon.get(icon);
51
76
  if (!d) {
52
- d = cubicsToPathD(iconToCubics(icon));
77
+ d = cubicsToPathD(cubicsOf(icon));
53
78
  canon.set(icon, d);
54
79
  }
55
80
  return d;
@@ -68,6 +93,138 @@ function planBetween(src, dst) {
68
93
  }
69
94
  return p;
70
95
  }
96
+ let regionMod = null;
97
+ let regionLoading = null;
98
+ let regionSince = 0;
99
+ let regionFailed = false;
100
+ let regionUrl;
101
+ let regionGen = 0;
102
+ let regionLoader = () => import("./region.js");
103
+ /** Replaces how the region normalizer is loaded (default: dynamic import of
104
+ * the bundled chunk). E.g. `setRegionLoader(async () => region)` after a
105
+ * static `import * as region from "morphicons/region"` to never touch the
106
+ * network mid-interaction. The next need loads through the new loader. */
107
+ function setRegionLoader(loader) {
108
+ regionLoader = loader;
109
+ regionGen++;
110
+ regionLoading = null;
111
+ regionMod = null;
112
+ regionFailed = false;
113
+ regionUrl = void 0;
114
+ }
115
+ const reload = (url) => new Promise((ok, ko) => {
116
+ const s = document.createElement("script");
117
+ s.type = "module";
118
+ s.onload = s.onerror = () => {
119
+ s.remove();
120
+ const m = globalThis[Symbol.for("morphicons.region")];
121
+ m ? ok(m) : ko(new Error(url));
122
+ };
123
+ s.src = `${url}#${Date.now()}`;
124
+ document.head.append(s);
125
+ });
126
+ const fromSource = () => {
127
+ const spec = /import\(\s*["'`]([^"'`]+)/.exec(String(regionLoader))?.[1];
128
+ const here = /\w+:\/\/[^\s)]+?(?=:\d+:\d+)/.exec(String((/* @__PURE__ */ new Error()).stack))?.[0];
129
+ try {
130
+ return spec && new URL(spec, here).href;
131
+ } catch {}
132
+ };
133
+ /** Starts (or joins) loading the region normalizer. Call it early — on
134
+ * idle, on hover — to keep the first stroke↔fill morph off the network. */
135
+ function preloadRegion() {
136
+ if (regionMod) return Promise.resolve(regionMod);
137
+ if (!regionLoading) {
138
+ regionSince = Date.now();
139
+ const url = regionUrl;
140
+ const gen = regionGen;
141
+ regionLoading = regionLoader().catch((e) => url ? reload(url) : Promise.reject(e)).then((m) => {
142
+ if (gen === regionGen) regionMod = m;
143
+ return m;
144
+ }, (e) => {
145
+ if (gen !== regionGen) throw e;
146
+ regionLoading = null;
147
+ regionFailed = true;
148
+ regionUrl ??= /dynamically imported module:?\s*(\S+)/i.exec(String(e?.message))?.[1] ?? fromSource();
149
+ throw e;
150
+ });
151
+ }
152
+ return regionLoading;
153
+ }
154
+ const quietPreload = () => new Promise((ok, ko) => {
155
+ if (typeof navigator === "undefined" ? false : navigator.connection?.saveData) return;
156
+ const go = () => {
157
+ regionFailed ? ko() : preloadRegion().then(ok, ko);
158
+ };
159
+ typeof requestIdleCallback === "function" ? requestIdleCallback(go, { timeout: 2e3 }) : setTimeout(go, 200);
160
+ });
161
+ /** How long a morph waits for the region chunk before flying the fallback
162
+ * (RAIL's 100 ms response budget). A slow, failed or hanging load never
163
+ * leaves the icon stuck on the old shape; the load goes on in the
164
+ * background and the next morph flies. A load already older than the
165
+ * budget isn't waited for again. */
166
+ const REGION_WAIT_MS = 100;
167
+ const paintAttrs = (p) => p === "stroke" ? {
168
+ fill: "none",
169
+ stroke: "inherit"
170
+ } : {
171
+ fill: "currentColor",
172
+ stroke: p === "fill" ? "none" : "inherit",
173
+ "fill-rule": "nonzero"
174
+ };
175
+ const looks = /* @__PURE__ */ new WeakMap();
176
+ const paintedEls = /* @__PURE__ */ new WeakSet();
177
+ function restLook(icon) {
178
+ if (typeof icon === "string" || paintOf(icon) === "stroke") return "stroke";
179
+ let look = looks.get(icon);
180
+ if (look) return look;
181
+ let filled = 0;
182
+ let enclosed = -1;
183
+ look = "fill";
184
+ for (const el of solid(icon)) {
185
+ const s = painted(el[1].stroke);
186
+ const f = painted(el[1].fill) || el[1].fill === void 0 && !s;
187
+ if (s) look = "both";
188
+ let a = 0;
189
+ for (const p of iconToCubics([el])) a += Math.abs(signedArea(p.pts));
190
+ if (f) filled += a;
191
+ else if (s) enclosed = Math.max(enclosed, 0) + a;
192
+ }
193
+ if (enclosed > 1.5 * filled) look = "stroke";
194
+ looks.set(icon, look);
195
+ return look;
196
+ }
197
+ /** The paint attributes a binding renders on the `<path>` with its first
198
+ * icon (SSR included), so a fill icon is filled before any script runs.
199
+ * None for a stroke icon: it inherits the `<svg>`'s stroke as always. */
200
+ function restAttrs(icon) {
201
+ if (icon === void 0 || paintOf(icon) === "stroke") return void 0;
202
+ return paintAttrs(restLook(icon));
203
+ }
204
+ function popPlan(from, to) {
205
+ const n = 64;
206
+ const item = ({ pts, closed }, half) => {
207
+ if (pts.length > 128) pts = pts.filter((_, i) => i % 4 < 2);
208
+ const c = centroid(pts);
209
+ const p = pts.map((v, i) => v - c[i & 1]);
210
+ const z = /* @__PURE__ */ new Float64Array(128);
211
+ return {
212
+ aC: half ? z : p,
213
+ bT: half ? p : z,
214
+ ca: c,
215
+ cb: c,
216
+ theta: 0,
217
+ lnSigma: 0,
218
+ closed,
219
+ half,
220
+ block: null
221
+ };
222
+ };
223
+ return {
224
+ n,
225
+ items: [...from.map((s) => item(s, 0)), ...to.map((s) => item(s, 1))]
226
+ };
227
+ }
71
228
  function resolveSpring(s) {
72
229
  if (typeof s === "string") return SPRING_PRESETS[s];
73
230
  const d = SPRING_PRESETS.snappy;
@@ -80,6 +237,7 @@ function resolveSpring(s) {
80
237
  function createMorph(el, icon, options) {
81
238
  const spring = new Spring();
82
239
  let reducedMotion = options?.reducedMotion ?? "never";
240
+ let strokeWidth = options?.strokeWidth ?? 2;
83
241
  let target = icon;
84
242
  let rest = true;
85
243
  let plan = null;
@@ -88,7 +246,35 @@ function createMorph(el, icon, options) {
88
246
  let t = 1;
89
247
  let flying = false;
90
248
  let dead = false;
91
- el.setAttribute("d", canonicalD(icon));
249
+ let regionFlight = false;
250
+ let drawn = false;
251
+ let pending = null;
252
+ let paint = "stroke";
253
+ let known = !paintedEls.has(el);
254
+ let split = 0;
255
+ let popFrom = "stroke";
256
+ let popBase = null;
257
+ const paintAs = (p) => {
258
+ if (p === paint && known) return;
259
+ for (const [k, v] of Object.entries(paintAttrs(p))) el.setAttribute(k, v);
260
+ paint = p;
261
+ known = true;
262
+ paintedEls.add(el);
263
+ };
264
+ const paintRest = (ic) => {
265
+ const r = regionMod && paintOf(ic) === "fill" ? regionMod.regionOf(ic, strokeWidth) : ic;
266
+ el.setAttribute("d", canonicalD(r));
267
+ paintAs(restLook(r));
268
+ };
269
+ const upgrade = (load) => {
270
+ const icon = target;
271
+ load.then(() => {
272
+ if (!dead && rest && target === icon && pending === null) paintRest(icon);
273
+ }, () => {});
274
+ };
275
+ paintRest(icon);
276
+ if (paintOf(icon) === "fill" && !regionMod) upgrade(quietPreload());
277
+ const shown = (tt) => !split ? [0, plan?.items.length ?? 0] : tt < .5 ? [0, split] : [split, plan?.items.length ?? 0];
92
278
  const render = (tt) => {
93
279
  const p = plan;
94
280
  const o = out;
@@ -96,7 +282,10 @@ function createMorph(el, icon, options) {
96
282
  if (!p || !o || !cl) return;
97
283
  t = tt;
98
284
  interpPolar(p, tt, o);
99
- el.setAttribute("d", serialize(o, cl));
285
+ drawn = true;
286
+ const [lo, hi] = shown(tt);
287
+ if (split) paintAs(tt < .5 ? popFrom : restLook(target));
288
+ el.setAttribute("d", serialize(o.slice(lo, hi), cl.slice(lo, hi)));
100
289
  };
101
290
  const stop = () => {
102
291
  if (!flying) return;
@@ -119,7 +308,10 @@ function createMorph(el, icon, options) {
119
308
  t = 1;
120
309
  spring.x = 1;
121
310
  spring.v = 0;
122
- el.setAttribute("d", canonicalD(target));
311
+ regionFlight = false;
312
+ split = 0;
313
+ paintRest(target);
314
+ if (paintOf(target) === "fill" && !regionMod && (regionLoading || !regionFailed)) upgrade(preloadRegion());
123
315
  };
124
316
  /** The current shape as plan source: the at-rest icon, or the rendered
125
317
  * buffers (already N points per subpath). */
@@ -127,20 +319,73 @@ function createMorph(el, icon, options) {
127
319
  const p = plan;
128
320
  const o = out;
129
321
  if (rest || !p || !o) return sampledOf(target);
130
- return o.map((buf, k) => ({
322
+ const [lo, hi] = shown(t);
323
+ return o.slice(lo, hi).map((buf, k) => ({
131
324
  pts: Float64Array.from(buf),
132
- closed: p.items[k].closed
325
+ closed: p.items[lo + k].closed,
326
+ hole: p.items[lo + k].hole
133
327
  }));
134
328
  };
135
- const retarget = (icon) => {
136
- plan = rest ? planBetween(target, icon) : buildPlan(snapshot(), sampledOf(icon));
329
+ /** A morph from the current shape to `icon` needs regions when either
330
+ * side is filled, or when the shape in flight already is a region. */
331
+ const needsRegion = (icon) => !rest && regionFlight || paintOf(icon) === "fill" || paintOf(target) === "fill";
332
+ const retarget = (icon, pop) => {
333
+ if (!rest && plan && out && !drawn) {
334
+ t = 0;
335
+ interpPolar(plan, 0, out);
336
+ }
337
+ if (pop || !rest && split) {
338
+ const from = snapshot();
339
+ popBase = rest ? target : null;
340
+ popFrom = paint;
341
+ plan = popPlan(from, sampledOf(icon));
342
+ split = from.length;
343
+ regionFlight = false;
344
+ } else if (needsRegion(icon)) {
345
+ const mod = regionMod;
346
+ plan = rest || !out ? mod.regionPlan(target, icon, strokeWidth) : mod.regionPlanFrom(regionFlight ? snapshot() : serialize(out, closed ?? void 0), icon, strokeWidth);
347
+ regionFlight = true;
348
+ split = 0;
349
+ paintAs("fill");
350
+ } else {
351
+ plan = rest ? planBetween(target, icon) : buildPlan(snapshot(), sampledOf(icon));
352
+ regionFlight = false;
353
+ }
137
354
  out = allocOutputs(plan);
355
+ drawn = false;
138
356
  closed = plan.items.map((it) => it.closed);
139
357
  target = icon;
140
358
  rest = false;
141
359
  };
360
+ const whenRegion = (icon, run, fallback) => {
361
+ if (regionMod || !rest && split || !needsRegion(icon)) return false;
362
+ const req = {};
363
+ pending = req;
364
+ const give = () => {
365
+ if (dead || pending !== req) return;
366
+ pending = null;
367
+ fallback();
368
+ };
369
+ const load = preloadRegion();
370
+ if (Date.now() - regionSince > REGION_WAIT_MS) {
371
+ give();
372
+ return true;
373
+ }
374
+ const timer = setTimeout(give, REGION_WAIT_MS - (Date.now() - regionSince));
375
+ load.then(() => {
376
+ clearTimeout(timer);
377
+ if (dead || pending !== req) return;
378
+ pending = null;
379
+ run();
380
+ }, () => {
381
+ clearTimeout(timer);
382
+ give();
383
+ });
384
+ return true;
385
+ };
142
386
  const setNow = (icon) => {
143
387
  stop();
388
+ pending = null;
144
389
  target = icon;
145
390
  settle();
146
391
  };
@@ -151,31 +396,44 @@ function createMorph(el, icon, options) {
151
396
  if (typeof matchMedia === "undefined") return false;
152
397
  return matchMedia?.("(prefers-reduced-motion: reduce)").matches ?? false;
153
398
  };
154
- const seek = (icon, tt) => {
399
+ const seek = (icon, tt, pop = false) => {
155
400
  if (dead) return;
156
- const reuse = !rest && plan !== null && icon === target;
401
+ if (!pop && whenRegion(icon, () => seek(icon, tt), () => seek(icon, tt, true))) return;
402
+ pending = null;
403
+ let reuse = !rest && plan !== null && icon === target;
404
+ if (reuse && split && regionMod && popBase !== null) {
405
+ target = popBase;
406
+ rest = true;
407
+ reuse = false;
408
+ }
157
409
  stop();
158
410
  spring.v = 0;
159
- if (!reuse) retarget(icon);
411
+ if (!reuse) retarget(icon, pop);
160
412
  render(tt);
161
413
  };
414
+ const fly = (icon, sp, pop) => {
415
+ if (dead) return;
416
+ if (icon === target && (rest || flying)) {
417
+ pending = null;
418
+ return;
419
+ }
420
+ if (motionOff()) {
421
+ setNow(icon);
422
+ return;
423
+ }
424
+ if (!pop && whenRegion(icon, () => fly(icon, sp, false), () => fly(icon, sp, true))) return;
425
+ pending = null;
426
+ const { k, c } = resolveSpring(sp);
427
+ spring.config(k, c);
428
+ retarget(icon, pop);
429
+ spring.start();
430
+ if (!flying) {
431
+ flying = true;
432
+ addTicker(tick);
433
+ }
434
+ };
162
435
  return {
163
- morphTo(icon, sp) {
164
- if (dead) return;
165
- if (icon === target && (rest || flying)) return;
166
- if (motionOff()) {
167
- setNow(icon);
168
- return;
169
- }
170
- const { k, c } = resolveSpring(sp);
171
- spring.config(k, c);
172
- retarget(icon);
173
- spring.start();
174
- if (!flying) {
175
- flying = true;
176
- addTicker(tick);
177
- }
178
- },
436
+ morphTo: (icon, sp) => fly(icon, sp, false),
179
437
  set(icon) {
180
438
  if (dead) return;
181
439
  setNow(icon);
@@ -193,9 +451,16 @@ function createMorph(el, icon, options) {
193
451
  set reducedMotion(v) {
194
452
  reducedMotion = v;
195
453
  },
454
+ get strokeWidth() {
455
+ return strokeWidth;
456
+ },
457
+ set strokeWidth(v) {
458
+ strokeWidth = v;
459
+ },
196
460
  destroy() {
197
461
  stop();
198
462
  dead = true;
463
+ pending = null;
199
464
  plan = null;
200
465
  out = null;
201
466
  closed = null;
@@ -203,4 +468,4 @@ function createMorph(el, icon, options) {
203
468
  };
204
469
  }
205
470
  //#endregion
206
- export { canonicalD, createMorph };
471
+ export { canonicalD, createMorph, preloadRegion, restAttrs, setRegionLoader };
package/dist/element.d.ts CHANGED
@@ -1,9 +1,10 @@
1
- import { a as Sampled, c as SpringPreset, n as IconInput, r as IconNode } from "./types-JAF3s76Y.js";
2
- import { a as ReducedMotionMode, i as PathEl, n as Morph, r as MorphOptions } from "./index-DXp5b6BU.js";
3
- import { n as computeInitialD, t as MorphHandle } from "./controller-Dkruvf3Y.js";
1
+ import { a as IconInput, c as Sampled, o as IconNode } from "./plan-DhaDhEVq.js";
2
+ import { r as SpringPreset } from "./spring-BgZHrI2W.js";
3
+ import { a as ReducedMotionMode, i as PathEl, n as Morph, r as MorphOptions } from "./index-DpvKR75N.js";
4
+ import { n as computeInitialD, r as computeInitialPaint, t as MorphHandle } from "./controller-DKm8s2ni.js";
4
5
  //#region src/element/index.d.ts
5
6
  declare const Base: typeof HTMLElement;
6
- declare class MorphIconElement extends Base implements MorphHandle {
7
+ export declare class MorphIconElement extends Base implements MorphHandle {
7
8
  #private;
8
9
  static observedAttributes: string[];
9
10
  get icon(): IconInput | undefined;
@@ -26,11 +27,11 @@ declare class MorphIconElement extends Base implements MorphHandle {
26
27
  }
27
28
  /** Defines `<morph-icon>` (or a custom tag). Idempotent per tag; a no-op
28
29
  * without a DOM (safe to call from code that also runs during SSR). */
29
- declare function defineMorphIcon(tag?: string): void;
30
+ export declare function defineMorphIcon(tag?: string): void;
30
31
  declare global {
31
32
  interface HTMLElementTagNameMap {
32
33
  "morph-icon": MorphIconElement;
33
34
  }
34
35
  }
35
36
  //#endregion
36
- export { type IconInput, type IconNode, type Morph, type MorphHandle, MorphIconElement, type MorphOptions, type PathEl, type ReducedMotionMode, type Sampled, type SpringPreset, computeInitialD, defineMorphIcon };
37
+ export { type IconInput, type IconNode, type Morph, type MorphHandle, type MorphOptions, type PathEl, type ReducedMotionMode, type Sampled, type SpringPreset, computeInitialD, computeInitialPaint };
package/dist/element.js CHANGED
@@ -1,4 +1,4 @@
1
- import { n as createController, t as computeInitialD } from "./controller-CXZuwJ_M.js";
1
+ import { n as computeInitialPaint, r as createController, t as computeInitialD } from "./controller--7SSBggk.js";
2
2
  //#region src/element/index.ts
3
3
  const SVG_NS = "http://www.w3.org/2000/svg";
4
4
  const REACTIVE = [
@@ -34,6 +34,7 @@ var MorphIconElement = class extends Base {
34
34
  #props = {};
35
35
  #ctrl = null;
36
36
  #path = null;
37
+ #target = null;
37
38
  #adopted = false;
38
39
  #preConnectDirty = false;
39
40
  #size = "24";
@@ -107,18 +108,20 @@ var MorphIconElement = class extends Base {
107
108
  const path = this.#path;
108
109
  const gate = this.#adopted && !this.#preConnectDirty;
109
110
  this.#adopted = false;
111
+ this.#props.strokeWidth = this.#sw();
110
112
  this.#ctrl = createController(this.#props);
111
113
  if (gate) {
112
114
  let armed = false;
113
115
  const gated = { setAttribute(name, value) {
114
116
  if (armed) path.setAttribute(name, value);
115
117
  } };
118
+ this.#target = gated;
116
119
  try {
117
120
  this.#ctrl.mount(gated, this.#props);
118
121
  } finally {
119
122
  armed = true;
120
123
  }
121
- } else this.#ctrl.mount(path, this.#props);
124
+ } else this.#ctrl.mount(this.#target ?? path, this.#props);
122
125
  }
123
126
  }
124
127
  disconnectedCallback() {
@@ -165,6 +168,8 @@ var MorphIconElement = class extends Base {
165
168
  if (PRESENTATION.has(name)) {
166
169
  const svg = this.querySelector("svg");
167
170
  if (svg) this.#applyPresentation(svg);
171
+ this.#props.strokeWidth = this.#sw();
172
+ this.#ctrl?.watch(this.#props);
168
173
  return;
169
174
  }
170
175
  this.#watch();
@@ -208,7 +213,12 @@ var MorphIconElement = class extends Base {
208
213
  const p = this.#props;
209
214
  if (p.icon === void 0 && p.from === void 0 && p.to === void 0) {
210
215
  const d = existing.getAttribute("d");
211
- if (d) p.icon = d;
216
+ const fill = existing.getAttribute("fill");
217
+ if (d) p.icon = fill && fill !== "none" ? [["path", {
218
+ d,
219
+ fill,
220
+ "fill-rule": existing.getAttribute("fill-rule") ?? "nonzero"
221
+ }]] : d;
212
222
  }
213
223
  return;
214
224
  }
@@ -222,16 +232,21 @@ var MorphIconElement = class extends Base {
222
232
  this.#applyPresentation(svg);
223
233
  const path = doc.createElementNS(SVG_NS, "path");
224
234
  path.setAttribute("d", computeInitialD(this.#props));
235
+ for (const [k, v] of Object.entries(computeInitialPaint(this.#props) ?? {})) path.setAttribute(k, v);
225
236
  svg.appendChild(path);
226
237
  this.appendChild(svg);
227
238
  this.#path = path;
228
239
  }
240
+ /** The stroke width on the 24 grid, absolute-stroke-width resolved. */
241
+ #sw() {
242
+ return this.#absoluteStrokeWidth ? String(Number(this.#strokeWidth) * 24 / Number(this.#size)) : this.#strokeWidth;
243
+ }
229
244
  #applyPresentation(svg) {
230
245
  svg.setAttribute("width", this.#size);
231
246
  svg.setAttribute("height", this.#size);
232
247
  svg.setAttribute("stroke", this.#color);
233
- const sw = this.#absoluteStrokeWidth ? String(Number(this.#strokeWidth) * 24 / Number(this.#size)) : this.#strokeWidth;
234
- svg.setAttribute("stroke-width", sw);
248
+ svg.setAttribute("color", this.#color);
249
+ svg.setAttribute("stroke-width", this.#sw());
235
250
  const label = this.#label;
236
251
  let title = svg.querySelector("title");
237
252
  if (label) {
@@ -258,4 +273,4 @@ function defineMorphIcon(tag = "morph-icon") {
258
273
  registered = true;
259
274
  }
260
275
  //#endregion
261
- export { MorphIconElement, computeInitialD, defineMorphIcon };
276
+ export { MorphIconElement, computeInitialD, computeInitialPaint, defineMorphIcon };
@@ -0,0 +1,55 @@
1
+ import { a as IconInput, c as Sampled, o as IconNode, t as MorphPlan } from "./plan-DhaDhEVq.js";
2
+ //#endregion
3
+ //#region src/region/plan.d.ts
4
+ /** Samples a normalized region: one Sampled per contour, every one closed,
5
+ * `hole` read from the winding (outers positive, holes negative). N per
6
+ * contour: 64, or 128 for every contour of an icon that 64 can't follow
7
+ * (snowflake tips, spiral turns: the landing would snap). */
8
+ declare function resampleRegion(icon: IconInput, N?: number): Sampled[];
9
+ /** Region plan: outers ↔ outers (holed contours never duplicated), holes ↔
10
+ * overlapping holes of the paired outer, direction locked; whatever has no
11
+ * partner collapses/grows without ever breaking the nesting (rules above).
12
+ * Every item flies closed. */
13
+ declare function buildRegionPlan(srcSubs: readonly Sampled[], dstSubs: readonly Sampled[]): MorphPlan;
14
+ //#endregion
15
+ //#region src/core/region.d.ts
16
+ /** How an icon is painted: centerlines with a stroke, or filled surface. */
17
+ type Paint = "stroke" | "fill";
18
+ /** Paint of an icon. Raw `d` strings and IconNodes that never declare a
19
+ * painted fill are stroke icons — the historical contract, so every
20
+ * existing input keeps its meaning. One element with a painted `fill` makes
21
+ * the whole icon a fill icon (mixed stroke+fill icons need the region path
22
+ * too: their silhouette is not a set of centerlines). */
23
+ declare function paintOf(icon: IconInput): Paint;
24
+ /** True when the icon is already a normalized region (no work left). */
25
+ declare function isRegion(icon: IconInput): boolean;
26
+ declare namespace index_d_exports {
27
+ export { Paint, RegionOptions, buildRegionPlan, isRegion, paintOf, regionContours, regionOf, regionPlan, regionPlanFrom, resampleRegion, toRegion };
28
+ }
29
+ interface RegionOptions {
30
+ /** Width for stroked elements that don't declare `stroke-width` (Lucide
31
+ * data carries none: the binding draws it). Default 2. */
32
+ strokeWidth?: number;
33
+ /** Grid step of the distance field, in icon units (24-unit grid).
34
+ * Default ⅛: corners are rebuilt exactly, curves deviate < 0.01. */
35
+ cell?: number;
36
+ }
37
+ /** Contours of the visible surface: closed, non-overlapping, outers
38
+ * positive / holes negative (nesting depth decides which is which). */
39
+ declare function regionContours(input: IconInput, options?: RegionOptions): Float64Array[];
40
+ /** Any icon → its canonical region: one `<path>` (nonzero) whose contours
41
+ * are the visible surface. Idempotent on regions. Stroke icons outline at
42
+ * `strokeWidth` (round caps and joins — Lucide, Tabler, Feather,
43
+ * Heroicons outline); fill icons honor each element's fill rule. */
44
+ declare function toRegion(input: IconInput, options?: RegionOptions): IconNode;
45
+ /** The region the driver draws for an icon (cached): a stroke icon
46
+ * outlined at the element's stroke width, a fill icon as is. */
47
+ declare function regionOf(icon: IconInput, strokeWidth: number): IconNode;
48
+ /** Region plan from `src` at rest to `dst` (cached by reference). */
49
+ declare function regionPlan(src: IconInput, dst: IconInput, strokeWidth: number): MorphPlan;
50
+ /** Region plan from the shape in flight to `dst` (never cached): region
51
+ * samples as they are, or a centerline `d` (a stroke morph interrupted
52
+ * toward a fill icon), outlined as it is. */
53
+ declare function regionPlanFrom(shape: Sampled[] | string, dst: IconInput, strokeWidth: number): MorphPlan;
54
+ //#endregion
55
+ export { regionPlan as a, Paint as c, buildRegionPlan as d, resampleRegion as f, regionOf as i, isRegion as l, index_d_exports as n, regionPlanFrom as o, regionContours as r, toRegion as s, RegionOptions as t, paintOf as u };
@@ -1,4 +1,6 @@
1
- import { c as SpringPreset, n as IconInput } from "./types-JAF3s76Y.js";
1
+ import { n as index_d_exports } from "./index-DDNCPQ4Z.js";
2
+ import { a as IconInput } from "./plan-DhaDhEVq.js";
3
+ import { r as SpringPreset } from "./spring-BgZHrI2W.js";
2
4
  //#region src/dom/index.d.ts
3
5
  /** Target element, structurally typed: any object with setAttribute works
4
6
  * (a real SVGPathElement, or a fake in tests). */
@@ -18,6 +20,11 @@ type ReducedMotionMode = "never" | "user" | "always";
18
20
  interface CreateMorphOptions {
19
21
  /** Reduced-motion policy; also live via the `reducedMotion` property. */
20
22
  reducedMotion?: ReducedMotionMode;
23
+ /** Stroke width the element draws with (24-unit grid). Only used to
24
+ * outline stroke icons when they morph with a fill icon, so the region
25
+ * in flight matches the stroke at rest. Default 2; live via the
26
+ * `strokeWidth` property. */
27
+ strokeWidth?: number;
21
28
  }
22
29
  interface Morph {
23
30
  /** Animates toward the icon with spring physics. Interruptible: mid-flight
@@ -34,15 +41,31 @@ interface Morph {
34
41
  progress: number;
35
42
  /** Reduced-motion policy, live: assigning it applies to the next morphTo. */
36
43
  reducedMotion: ReducedMotionMode;
44
+ /** Stroke width used to outline stroke icons in stroke↔fill morphs. */
45
+ strokeWidth: number;
37
46
  /** Unregisters the instance; later calls are no-ops. */
38
47
  destroy(): void;
39
48
  }
40
49
  /** Canonical `d` of an icon: the input string verbatim, or the real cubics
41
50
  * quantized to 4 decimals (the at-rest snap; engine-stable bytes so SSR
42
51
  * hydration matches, see fmtCanon in core/serialize). Exported because it
43
- * is what a binding renders at SSR/rest before any runtime exists. */
52
+ * is what a binding renders at SSR/rest before any runtime exists. A fill
53
+ * icon's translucent duotone layer is left out (one path can't paint it). */
44
54
  declare function canonicalD(icon: IconInput): string;
55
+ type RegionModule = typeof index_d_exports;
56
+ /** Replaces how the region normalizer is loaded (default: dynamic import of
57
+ * the bundled chunk). E.g. `setRegionLoader(async () => region)` after a
58
+ * static `import * as region from "morphicons/region"` to never touch the
59
+ * network mid-interaction. The next need loads through the new loader. */
60
+ declare function setRegionLoader(loader: () => Promise<RegionModule>): void;
61
+ /** Starts (or joins) loading the region normalizer. Call it early — on
62
+ * idle, on hover — to keep the first stroke↔fill morph off the network. */
63
+ declare function preloadRegion(): Promise<RegionModule>;
64
+ /** The paint attributes a binding renders on the `<path>` with its first
65
+ * icon (SSR included), so a fill icon is filled before any script runs.
66
+ * None for a stroke icon: it inherits the `<svg>`'s stroke as always. */
67
+ declare function restAttrs(icon?: IconInput): Record<string, string> | undefined;
45
68
  /** Creates the morph instance over a `<path>` and paints the initial icon. */
46
69
  declare function createMorph(el: PathEl, icon: IconInput, options?: CreateMorphOptions): Morph;
47
70
  //#endregion
48
- export { ReducedMotionMode as a, PathEl as i, Morph as n, canonicalD as o, MorphOptions as r, createMorph as s, CreateMorphOptions as t };
71
+ export { ReducedMotionMode as a, preloadRegion as c, PathEl as i, restAttrs as l, Morph as n, canonicalD as o, MorphOptions as r, createMorph as s, CreateMorphOptions as t, setRegionLoader as u };