morphicons 1.5.0 → 1.6.0

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
@@ -230,6 +230,44 @@ It *is* `createMorph` — only the write target differs — so springs, reduced
230
230
 
231
231
  **Cost.** A live inline `<path>` gets the cheapest possible write; a mask element additionally makes the browser re-rasterize the mask and repaint the masked box every frame (main thread, no compositor-only path). Great for toggles and short lists (menu↔close, chevrons, play↔pause); for icon-heavy views, the inline-SVG bindings remain the leanest option — same styling, one less indirection.
232
232
 
233
+ ### `canvasTarget` — 2D canvas out
234
+
235
+ For surfaces where the DOM never enters: **the icon stops being a document node and becomes pixels you own.** The platform does the heavy lifting — `Path2D` parses `d` strings natively — so every frame is one `stroke(new Path2D(d))` on whatever you hand it: an `HTMLCanvasElement`, an `OffscreenCanvas`, or a bare 2D context (what a worker, or a host that already owns the context, has in hand).
236
+
237
+ ```ts
238
+ import { canvasTarget } from "morphicons/adapters";
239
+ import { createMorph } from "morphicons/dom";
240
+
241
+ const m = createMorph(canvasTarget(canvas), MENU);
242
+ m.morphTo(X, "snappy");
243
+ // the canvas is a live texture: gl.texImage2D(gl.TEXTURE_2D, ..., canvas)
244
+ ```
245
+
246
+ Once the icon is pixels, everything the SVG model can't reach opens up:
247
+
248
+ - **Canvas-first apps** with no DOM to put an icon in: Konva, Fabric.js, PixiJS, games, chart draws, tldraw-style editors.
249
+ - **`OffscreenCanvas` + workers**: the whole morph runs off the main thread.
250
+ - **Native export**: `toBlob()` for PNG sprites, `captureStream()` + MediaRecorder for the morph as a video asset.
251
+ - **An animated favicon**: draw, `toDataURL()`, swap the `<link>`.
252
+ - **HUDs over video or fullscreen surfaces**: same buffer as the player or the game, no DOM layer on top.
253
+ - **WebGL textures**: the canvas is a ready `texImage2D` source — shader effects over live icons are one branch of this list, not its headline.
254
+ - Down the road, the same gesture (`PathEl` → `Path2D`) is `PathEl` → `SkPath`: a react-native-skia / CanvasKit target would consume the identical contract.
255
+
256
+ It *is* `createMorph` — springs, mid-flight interruption and reduced motion behave identically. The target maps the icon grid onto whatever backing store it finds, per write: min-side scale, centered (the canvas equivalent of `preserveAspectRatio="xMidYMid meet"`), so non-square canvases letterbox and devicePixelRatio is simply whatever you sized the backing store to. Color cascades: the `color` option wins; without it, a CSS-styled canvas contributes its computed `color` (the moral equivalent of `currentColor`, which canvas lacks — read lazily, once); where CSS can't reach (OffscreenCanvas, workers) the context's current `strokeStyle` stands, which is the idiomatic canvas contract. `strokeWidth` and `viewBox` mirror the other adapters; `clear: false` skips the per-frame clear for manual compositing and trails.
257
+
258
+ **Hosts with their own render loop.** Don't hand a Konva/Chart.js/game context to a morph — the driver writes on its own rAF and the two clocks fight over the frame. Give the icon a small dedicated canvas and composite it into the scene (`drawImage`, `Konva.Image`, a texture upload), with `onWrite` as the dirty signal:
259
+
260
+ ```ts
261
+ const icon = new OffscreenCanvas(64, 64);
262
+ const target = canvasTarget(icon, {
263
+ color: "#e8eaed", // CSS can't reach an OffscreenCanvas
264
+ onWrite: () => { textureDirty = true } // re-upload / blit on the host's next frame
265
+ });
266
+ const m = createMorph(target, PLAY);
267
+ ```
268
+
269
+ **Cost.** The cheapest target of the three: no layout, no DOM paint — one native path parse and one stroke rasterization per frame, on a canvas that is typically small. 0.48 KB gzip standalone, the lightest adapter yet.
270
+
233
271
  ## Icon library compatibility
234
272
 
235
273
  morphicons has no per-library adapters — any icon set that meets these requirements works out of the box:
@@ -407,9 +445,10 @@ CI budget (size-limit, gzip) as an anti-regression tripwire — gates carry ~10%
407
445
  |---|---|---|
408
446
  | `morphicons` (core) | 6.60 KB | 7 KB |
409
447
  | `morphicons/dom` (core + driver) | 7.12 KB | 7.5 KB |
410
- | `morphicons/adapters` (aggregate — grows by design) | 4.03 KB | 4.4 KB |
448
+ | `morphicons/adapters` (aggregate — grows by design) | 4.32 KB | 4.8 KB |
411
449
  | `adapters` › `{ svgToIcon }` alone | 3.34 KB | 3.7 KB |
412
450
  | `adapters` › `{ maskTarget }` alone | 0.70 KB | 0.8 KB |
451
+ | `adapters` › `{ canvasTarget }` alone | 0.48 KB | 0.55 KB |
413
452
  | `morphicons/react` (all, react external) | 8.01 KB | 8.5 KB |
414
453
  | `morphicons/react-native` (all, react/rn/rnsvg external) | 8.37 KB | 9 KB |
415
454
  | `morphicons/vue` (all, vue external) | 8.04 KB | 8.5 KB |
@@ -1,5 +1,49 @@
1
1
  import { n as IconInput } from "./types-JAF3s76Y.js";
2
2
  import { i as PathEl } from "./index-DXp5b6BU.js";
3
+ //#region src/adapters/canvas.d.ts
4
+ /** The 2D context surface the target drives — structural, so a real
5
+ * CanvasRenderingContext2D, an OffscreenCanvasRenderingContext2D or a test
6
+ * fake all satisfy it. Only what canvasTarget actually touches. */
7
+ interface Canvas2DContext {
8
+ canvas: {
9
+ width: number;
10
+ height: number;
11
+ };
12
+ lineWidth: number;
13
+ lineCap: string;
14
+ lineJoin: string;
15
+ strokeStyle: string | object;
16
+ setTransform(a: number, b: number, c: number, d: number, e: number, f: number): void;
17
+ clearRect(x: number, y: number, w: number, h: number): void;
18
+ stroke(path: object): void;
19
+ }
20
+ /** Anything that can hand out a 2D context (an HTMLCanvasElement, an
21
+ * OffscreenCanvas, or a fake in tests). */
22
+ interface CanvasSurface {
23
+ getContext(contextId: "2d"): Canvas2DContext | null;
24
+ }
25
+ interface CanvasTargetOptions {
26
+ /** Grid the icon's coordinates live on (Lucide/Iconify default: "0 0 24 24").
27
+ * Used to map the geometry onto the backing store. */
28
+ viewBox?: string;
29
+ /** Stroke width in grid units (Lucide's default is 2). */
30
+ strokeWidth?: number;
31
+ /** Stroke color. Defaults to the canvas' computed CSS color when it is a
32
+ * styled element; otherwise the context's current `strokeStyle` stands. */
33
+ color?: string;
34
+ /** Clear the canvas before each frame. Turn off for manual compositing or
35
+ * trails — every frame then draws on top of the last. Default true. */
36
+ clear?: boolean;
37
+ /** Called after every drawn frame — the texture consumer's dirty signal
38
+ * (e.g. re-upload the canvas with `texImage2D`, or blit it into a scene). */
39
+ onWrite?: () => void;
40
+ }
41
+ /** Adapts a canvas (or its 2D context) into a `PathEl` whose writes stroke
42
+ * the morph's geometry onto the canvas, so
43
+ * `createMorph(canvasTarget(canvas), icon)` animates pixels instead of an
44
+ * inline `<svg><path>`. */
45
+ declare function canvasTarget(surface: CanvasSurface | Canvas2DContext, opts?: CanvasTargetOptions): PathEl;
46
+ //#endregion
3
47
  //#region src/adapters/mask.d.ts
4
48
  /** The subset of an element's inline style the mask target writes — structural
5
49
  * so a real HTMLElement's `.style` satisfies it (and a test fake supplies
@@ -52,4 +96,4 @@ declare function maskTarget(el: MaskEl, opts?: MaskTargetOptions): MaskPathEl;
52
96
  * an IconNode. */
53
97
  declare function svgToIcon(markup: string): IconInput;
54
98
  //#endregion
55
- export { type MaskEl, type MaskPathEl, type MaskStyle, type MaskTargetOptions, maskTarget, svgToIcon };
99
+ export { type Canvas2DContext, type CanvasSurface, type CanvasTargetOptions, type MaskEl, type MaskPathEl, type MaskStyle, type MaskTargetOptions, canvasTarget, maskTarget, svgToIcon };
package/dist/adapters.js CHANGED
@@ -1,4 +1,46 @@
1
1
  import { t as fitIcon } from "./normalize-CYnN3Npw.js";
2
+ //#region src/adapters/canvas.ts
3
+ /** Adapts a canvas (or its 2D context) into a `PathEl` whose writes stroke
4
+ * the morph's geometry onto the canvas, so
5
+ * `createMorph(canvasTarget(canvas), icon)` animates pixels instead of an
6
+ * inline `<svg><path>`. */
7
+ function canvasTarget(surface, opts = {}) {
8
+ if (typeof Path2D === "undefined") throw new Error("morphicons: canvasTarget needs Path2D (browser or worker)");
9
+ const ctx = "getContext" in surface ? surface.getContext("2d") : surface;
10
+ if (!ctx) throw new Error("morphicons: canvasTarget needs a 2D context");
11
+ const vb = (opts.viewBox ?? "0 0 24 24").trim().split(/[\s,]+/).map(Number);
12
+ const [mx, my, vw, vh] = [
13
+ vb[0] || 0,
14
+ vb[1] || 0,
15
+ vb[2] || 24,
16
+ vb[3] || vb[2] || 24
17
+ ];
18
+ const stroke = opts.strokeWidth ?? 2;
19
+ const clear = opts.clear !== false;
20
+ let color = opts.color;
21
+ let cssPending = color === void 0 && "style" in ctx.canvas;
22
+ return { setAttribute(name, value) {
23
+ if (name !== "d") return;
24
+ if (cssPending) {
25
+ cssPending = false;
26
+ color = getComputedStyle?.(ctx.canvas).color || void 0;
27
+ }
28
+ const c = ctx.canvas;
29
+ const s = Math.min(c.width / vw, c.height / vh);
30
+ if (clear) {
31
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
32
+ ctx.clearRect(0, 0, c.width, c.height);
33
+ }
34
+ ctx.setTransform(s, 0, 0, s, (c.width - vw * s) / 2 - mx * s, (c.height - vh * s) / 2 - my * s);
35
+ ctx.lineWidth = stroke;
36
+ ctx.lineCap = "round";
37
+ ctx.lineJoin = "round";
38
+ if (color !== void 0) ctx.strokeStyle = color;
39
+ ctx.stroke(new Path2D(value));
40
+ opts.onWrite?.();
41
+ } };
42
+ }
43
+ //#endregion
2
44
  //#region src/adapters/mask.ts
3
45
  const SVG_NS = "http://www.w3.org/2000/svg";
4
46
  let seq = 0;
@@ -136,4 +178,4 @@ function svgToIcon(markup) {
136
178
  return vb ? fitIcon(node, vb[1]) : node;
137
179
  }
138
180
  //#endregion
139
- export { maskTarget, svgToIcon };
181
+ export { canvasTarget, maskTarget, svgToIcon };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "morphicons",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
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)",
@@ -129,7 +129,7 @@
129
129
  "name": "adapters (aggregate — grows by design, see per-export gates)",
130
130
  "path": "dist/adapters.js",
131
131
  "import": "*",
132
- "limit": "4.4 KB",
132
+ "limit": "4.8 KB",
133
133
  "gzip": true
134
134
  },
135
135
  {
@@ -146,6 +146,13 @@
146
146
  "limit": "0.8 KB",
147
147
  "gzip": true
148
148
  },
149
+ {
150
+ "name": "adapters › canvasTarget (per-export shakeability gate)",
151
+ "path": "dist/adapters.js",
152
+ "import": "{ canvasTarget }",
153
+ "limit": "0.55 KB",
154
+ "gzip": true
155
+ },
149
156
  {
150
157
  "name": "core + dom + react (MorphIcon, react external)",
151
158
  "path": "dist/react.js",