domotion-svg 0.10.1 → 0.11.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/dist/animation/animator.d.ts +9 -1
- package/dist/animation/animator.js +2 -2
- package/dist/animation/cursor-glyphs.d.ts +52 -0
- package/dist/animation/cursor-glyphs.js +138 -0
- package/dist/animation/cursor-overlay.d.ts +47 -2
- package/dist/animation/cursor-overlay.js +148 -22
- package/dist/animation/index.d.ts +2 -1
- package/dist/animation/index.js +2 -1
- package/dist/capture/script/emoji-detect.js +42 -1
- package/dist/capture/script/index.js +5 -0
- package/dist/capture/script/utils.d.ts +15 -0
- package/dist/capture/script/utils.js +51 -0
- package/dist/capture/script/walker/pseudo-inject.js +16 -1
- package/dist/capture/script.generated.js +1 -1
- package/dist/capture/types.d.ts +9 -0
- package/dist/cli/animate.js +10 -2
- package/dist/render/element-tree-to-svg.js +75 -7
- package/dist/render/glyph-helper.js +23 -1
- package/dist/render/text-to-path.d.ts +1 -0
- package/dist/render/text-to-path.js +176 -14
- package/dist/scrubber/client.bundle.generated.js +1 -1
- package/dist/scrubber/client.js +189 -6
- package/dist/scrubber/crop.d.ts +51 -0
- package/dist/scrubber/crop.js +144 -0
- package/dist/scrubber/server.js +60 -12
- package/package.json +1 -1
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Takes captured SVG frame content and composes them into a single
|
|
5
5
|
* animated SVG with CSS keyframe transitions.
|
|
6
6
|
*/
|
|
7
|
-
import { type CursorOverlay, type SelectorResolver } from "./cursor-overlay.js";
|
|
7
|
+
import { type CursorAtResolver, type CursorOverlay, type SelectorResolver } from "./cursor-overlay.js";
|
|
8
8
|
import type { MagicMove } from "./magic-move.js";
|
|
9
9
|
export interface AnimationFrame {
|
|
10
10
|
/** SVG content for this frame (from dom-to-svg) */
|
|
@@ -244,6 +244,14 @@ export interface AnimationConfig {
|
|
|
244
244
|
* uses `selector`; otherwise pass undefined / null.
|
|
245
245
|
*/
|
|
246
246
|
resolveSelector?: SelectorResolver;
|
|
247
|
+
/**
|
|
248
|
+
* DM-1106: auto cursor-TYPE hit-tester — given a viewport point and frame
|
|
249
|
+
* index, returns the cursor keyword under it (the caller builds this from the
|
|
250
|
+
* per-frame captured trees via `cursorAtPoint`). When provided, the overlay
|
|
251
|
+
* paints the matching glyph per element and switches at boundary crossings;
|
|
252
|
+
* when omitted, the overlay paints the single arrow.
|
|
253
|
+
*/
|
|
254
|
+
resolveCursorAt?: CursorAtResolver;
|
|
247
255
|
/**
|
|
248
256
|
* Canvas background color painted behind every frame (a full-viewport
|
|
249
257
|
* `<rect>`). Mirrors the single-frame path's `transparentRootBgRect`
|
|
@@ -305,8 +305,8 @@ export function generateAnimatedSvg(config) {
|
|
|
305
305
|
frameStarts.push(acc);
|
|
306
306
|
acc += frameAdvanceMs(f);
|
|
307
307
|
}
|
|
308
|
-
const resolved = resolveCursorScript(config.cursorOverlay, totalDuration, frameStarts, config.resolveSelector ?? null);
|
|
309
|
-
overlayMarkup = "\n" + cursorOverlayMarkup(resolved.positions, resolved.clicks, resolved.style, totalDuration);
|
|
308
|
+
const resolved = resolveCursorScript(config.cursorOverlay, totalDuration, frameStarts, config.resolveSelector ?? null, config.resolveCursorAt ?? null);
|
|
309
|
+
overlayMarkup = "\n" + cursorOverlayMarkup(resolved.positions, resolved.clicks, resolved.style, totalDuration, resolved.cursorTimeline);
|
|
310
310
|
}
|
|
311
311
|
// Canvas background rect — only when a non-transparent background is given.
|
|
312
312
|
// Default (none / transparent) emits nothing so the SVG composites over the
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cursor glyph catalog (DM-1106).
|
|
3
|
+
*
|
|
4
|
+
* Maps every CSS `cursor` keyword to a small SVG glyph drawn from Lucide icons
|
|
5
|
+
* (MIT-licensed, https://lucide.dev) — scaled, rotated, and occasionally
|
|
6
|
+
* composited (an arrow + a badge, the way real OS drag-drop cursors look) to
|
|
7
|
+
* approximate what a browser paints. Lucide's line-art look is intentionally
|
|
8
|
+
* OS-agnostic: it doesn't pixel-match macOS / Windows / X11, but it reads as the
|
|
9
|
+
* right cursor at a glance and renders crisply at any scale (the whole point of
|
|
10
|
+
* the SVG pipeline).
|
|
11
|
+
*
|
|
12
|
+
* Each glyph is authored in Lucide's native 24×24 coordinate box. The renderer
|
|
13
|
+
* paints a white halo under a dark stroke so the cursor stays legible on any
|
|
14
|
+
* background (mirroring the white-outlined arrow the overlay already used). The
|
|
15
|
+
* `hotspot` is the point IN THAT 24×24 BOX that aligns to the cursor's (x, y) —
|
|
16
|
+
* e.g. the arrow tip, the I-beam center — so callers translate the glyph by
|
|
17
|
+
* `(x − hotspotX·scale, y − hotspotY·scale)`.
|
|
18
|
+
*
|
|
19
|
+
* This module is pure markup generation (no DOM); the overlay (DM-1106 phase 2)
|
|
20
|
+
* picks a glyph per captured `cursor` value and animates its position.
|
|
21
|
+
*/
|
|
22
|
+
/** A glyph authored in the Lucide 24×24 box. */
|
|
23
|
+
export interface CursorGlyph {
|
|
24
|
+
/** Inner SVG markup (paths/lines/circles) in the 24×24 Lucide coordinate box. */
|
|
25
|
+
body: string;
|
|
26
|
+
/** Filled silhouette (white fill + dark outline, like a classic arrow) vs.
|
|
27
|
+
* stroked line-art (dark stroke + white halo, the Lucide default look). */
|
|
28
|
+
fill?: boolean;
|
|
29
|
+
/** The point in the 24×24 box that lands on the cursor coordinate. */
|
|
30
|
+
hotspot: [number, number];
|
|
31
|
+
/** Optional rotation (degrees, about the box center 12,12) — e.g. vertical-text. */
|
|
32
|
+
rotate?: number;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Every CSS `cursor` keyword → its glyph. `auto`, `default`, `inherit`,
|
|
36
|
+
* `initial`, and an unresolved `url(...)` fallback all resolve to the arrow
|
|
37
|
+
* here; the overlay resolves `auto` to `text` / `default` per Chrome BEFORE
|
|
38
|
+
* looking up this table (DM-1106 phase 2). `none` is an empty glyph.
|
|
39
|
+
*/
|
|
40
|
+
export declare const CURSOR_GLYPHS: Record<string, CursorGlyph>;
|
|
41
|
+
/** Canonical display order, grouped by the MDN cursor categories. */
|
|
42
|
+
export declare const CURSOR_CATEGORIES: {
|
|
43
|
+
title: string;
|
|
44
|
+
values: string[];
|
|
45
|
+
}[];
|
|
46
|
+
/**
|
|
47
|
+
* Render a cursor glyph as an SVG `<g>`, positioned so its hotspot sits at
|
|
48
|
+
* `(x, y)` and the 24-box is scaled to `size` px. Paints a white halo under the
|
|
49
|
+
* dark glyph (filled glyphs use a white fill + dark outline). Returns "" for an
|
|
50
|
+
* empty glyph (`none`).
|
|
51
|
+
*/
|
|
52
|
+
export declare function cursorGlyphSvg(value: string, x: number, y: number, size?: number, color?: string): string;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cursor glyph catalog (DM-1106).
|
|
3
|
+
*
|
|
4
|
+
* Maps every CSS `cursor` keyword to a small SVG glyph drawn from Lucide icons
|
|
5
|
+
* (MIT-licensed, https://lucide.dev) — scaled, rotated, and occasionally
|
|
6
|
+
* composited (an arrow + a badge, the way real OS drag-drop cursors look) to
|
|
7
|
+
* approximate what a browser paints. Lucide's line-art look is intentionally
|
|
8
|
+
* OS-agnostic: it doesn't pixel-match macOS / Windows / X11, but it reads as the
|
|
9
|
+
* right cursor at a glance and renders crisply at any scale (the whole point of
|
|
10
|
+
* the SVG pipeline).
|
|
11
|
+
*
|
|
12
|
+
* Each glyph is authored in Lucide's native 24×24 coordinate box. The renderer
|
|
13
|
+
* paints a white halo under a dark stroke so the cursor stays legible on any
|
|
14
|
+
* background (mirroring the white-outlined arrow the overlay already used). The
|
|
15
|
+
* `hotspot` is the point IN THAT 24×24 BOX that aligns to the cursor's (x, y) —
|
|
16
|
+
* e.g. the arrow tip, the I-beam center — so callers translate the glyph by
|
|
17
|
+
* `(x − hotspotX·scale, y − hotspotY·scale)`.
|
|
18
|
+
*
|
|
19
|
+
* This module is pure markup generation (no DOM); the overlay (DM-1106 phase 2)
|
|
20
|
+
* picks a glyph per captured `cursor` value and animates its position.
|
|
21
|
+
*/
|
|
22
|
+
// ── Lucide icon bodies (verbatim path data from lucide-icons/lucide) ──────────
|
|
23
|
+
const L = {
|
|
24
|
+
arrow: `<path d="M4.037 4.688a.495.495 0 0 1 .651-.651l16 6.5a.5.5 0 0 1-.063.947l-6.124 1.58a2 2 0 0 0-1.438 1.435l-1.579 6.126a.5.5 0 0 1-.947.063z"/>`,
|
|
25
|
+
pointer: `<path d="M22 14a8 8 0 0 1-8 8"/><path d="M18 11v-1a2 2 0 0 0-2-2a2 2 0 0 0-2 2"/><path d="M14 10V9a2 2 0 0 0-2-2a2 2 0 0 0-2 2v1"/><path d="M10 9.5V4a2 2 0 0 0-2-2a2 2 0 0 0-2 2v10"/><path d="M18 11a2 2 0 1 1 4 0v3a8 8 0 0 1-8 8h-2c-2.8 0-4.5-.86-5.99-2.34l-3.6-3.6a2 2 0 0 1 2.83-2.82L7 15"/>`,
|
|
26
|
+
ibeam: `<path d="M17 22h-1a4 4 0 0 1-4-4V6a4 4 0 0 1 4-4h1"/><path d="M7 22h1a4 4 0 0 0 4-4"/><path d="M7 2h1a4 4 0 0 1 4 4"/>`,
|
|
27
|
+
menu: `<path d="M4 5h16"/><path d="M4 12h16"/><path d="M4 19h16"/>`,
|
|
28
|
+
spinner: `<path d="M21 12a9 9 0 1 1-6.219-8.56"/>`,
|
|
29
|
+
plus: `<path d="M5 12h14"/><path d="M12 5v14"/>`,
|
|
30
|
+
shortcut: `<path d="m15 14 5-5-5-5"/><path d="M4 20v-7a4 4 0 0 1 4-4h12"/>`,
|
|
31
|
+
ban: `<circle cx="12" cy="12" r="10"/><path d="M4.929 4.929 19.07 19.071"/>`,
|
|
32
|
+
move: `<path d="M12 2v20"/><path d="m15 19-3 3-3-3"/><path d="m19 9 3 3-3 3"/><path d="M2 12h20"/><path d="m5 9-3 3 3 3"/><path d="m9 5 3-3 3 3"/>`,
|
|
33
|
+
hand: `<path d="M18 11V6a2 2 0 0 0-2-2a2 2 0 0 0-2 2"/><path d="M14 10V4a2 2 0 0 0-2-2a2 2 0 0 0-2 2v2"/><path d="M10 10.5V6a2 2 0 0 0-2-2a2 2 0 0 0-2 2v8"/><path d="M18 8a2 2 0 1 1 4 0v6a8 8 0 0 1-8 8h-2c-2.8 0-4.5-.86-5.99-2.34l-3.6-3.6a2 2 0 0 1 2.83-2.82L7 15"/>`,
|
|
34
|
+
handGrab: `<path d="M18 11.5V9a2 2 0 0 0-2-2a2 2 0 0 0-2 2v1.4"/><path d="M14 10V8a2 2 0 0 0-2-2a2 2 0 0 0-2 2v2"/><path d="M10 9.9V9a2 2 0 0 0-2-2a2 2 0 0 0-2 2v5"/><path d="M6 14a2 2 0 0 0-2-2a2 2 0 0 0-2 2"/><path d="M18 11a2 2 0 1 1 4 0v3a8 8 0 0 1-8 8h-4a8 8 0 0 1-8-8 2 2 0 1 1 4 0"/>`,
|
|
35
|
+
moveH: `<path d="m18 8 4 4-4 4"/><path d="M2 12h20"/><path d="m6 8-4 4 4 4"/>`,
|
|
36
|
+
moveV: `<path d="M12 2v20"/><path d="m8 18 4 4 4-4"/><path d="m8 6 4-4 4 4"/>`,
|
|
37
|
+
diag: `<path d="M5 5 19 19"/><path d="M5 10 5 5 10 5"/><path d="M19 14 19 19 14 19"/>`, // ↖↘ (nwse): TL–BR line, arrowheads pointing NW + SE
|
|
38
|
+
diag2: `<path d="M19 5 5 19"/><path d="M14 5 19 5 19 10"/><path d="M10 19 5 19 5 14"/>`, // ↗↙ (nesw): TR–BL line, arrowheads pointing NE + SW
|
|
39
|
+
zoomIn: `<circle cx="11" cy="11" r="8"/><line x1="21" x2="16.65" y1="21" y2="16.65"/><line x1="11" x2="11" y1="8" y2="14"/><line x1="8" x2="14" y1="11" y2="11"/>`,
|
|
40
|
+
zoomOut: `<circle cx="11" cy="11" r="8"/><line x1="21" x2="16.65" y1="21" y2="16.65"/><line x1="8" x2="14" y1="11" y2="11"/>`,
|
|
41
|
+
crosshair: `<line x1="22" x2="2" y1="12" y2="12"/><line x1="12" x2="12" y1="22" y2="2"/>`,
|
|
42
|
+
cell: `<rect x="4" y="4" width="16" height="16" rx="1"/><line x1="12" x2="12" y1="2" y2="22"/><line x1="2" x2="22" y1="12" y2="12"/>`,
|
|
43
|
+
// Hand-drawn "?" (lucide circle-help was unavailable on main); used as a badge.
|
|
44
|
+
question: `<path d="M9 9a3 3 0 1 1 4.2 2.75c-.9.5-1.2 1-1.2 1.75"/><path d="M12 17h.01"/>`,
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Compose the macOS-style arrow with a small badge tucked at its lower-right —
|
|
48
|
+
* the shape real OS cursors use for copy (+), alias (shortcut), no-drop (∅),
|
|
49
|
+
* context-menu (☰), and progress (spinner). The badge sits in a white rounded
|
|
50
|
+
* chip for contrast against the arrow and the page.
|
|
51
|
+
*/
|
|
52
|
+
function arrowBadge(badgeBody) {
|
|
53
|
+
// Badge: a 10×10 white chip at (12.5, 12.5)–(23.5, 23.5) with the icon scaled
|
|
54
|
+
// into it. The icon is drawn in its own 24-box then mapped into the chip.
|
|
55
|
+
return `${L.arrow}<g transform="translate(12.5 12.5)"><rect x="0" y="0" width="11" height="11" rx="2.5" fill="#fff" stroke="#1a1a1a" stroke-width="1"/><g transform="translate(1.4 1.4) scale(0.342)" fill="none" stroke="#1a1a1a" stroke-width="3.2" stroke-linecap="round" stroke-linejoin="round">${badgeBody}</g></g>`;
|
|
56
|
+
}
|
|
57
|
+
const ARROW = { body: L.arrow, fill: true, hotspot: [4.5, 4.5] };
|
|
58
|
+
const ARROW_TIP = [4.5, 4.5];
|
|
59
|
+
/**
|
|
60
|
+
* Every CSS `cursor` keyword → its glyph. `auto`, `default`, `inherit`,
|
|
61
|
+
* `initial`, and an unresolved `url(...)` fallback all resolve to the arrow
|
|
62
|
+
* here; the overlay resolves `auto` to `text` / `default` per Chrome BEFORE
|
|
63
|
+
* looking up this table (DM-1106 phase 2). `none` is an empty glyph.
|
|
64
|
+
*/
|
|
65
|
+
export const CURSOR_GLYPHS = {
|
|
66
|
+
// General
|
|
67
|
+
default: ARROW,
|
|
68
|
+
auto: ARROW,
|
|
69
|
+
none: { body: "", hotspot: [12, 12] },
|
|
70
|
+
// Links & status
|
|
71
|
+
"context-menu": { body: arrowBadge(L.menu), fill: true, hotspot: ARROW_TIP },
|
|
72
|
+
help: { body: arrowBadge(L.question), fill: true, hotspot: ARROW_TIP },
|
|
73
|
+
pointer: { body: L.pointer, hotspot: [8, 2] },
|
|
74
|
+
progress: { body: arrowBadge(L.spinner), fill: true, hotspot: ARROW_TIP },
|
|
75
|
+
wait: { body: L.spinner, hotspot: [12, 12] },
|
|
76
|
+
// Selection
|
|
77
|
+
cell: { body: L.cell, hotspot: [12, 12] },
|
|
78
|
+
crosshair: { body: L.crosshair, hotspot: [12, 12] },
|
|
79
|
+
text: { body: L.ibeam, hotspot: [12, 12] },
|
|
80
|
+
"vertical-text": { body: L.ibeam, hotspot: [12, 12], rotate: 90 },
|
|
81
|
+
// Drag & drop
|
|
82
|
+
alias: { body: arrowBadge(L.shortcut), fill: true, hotspot: ARROW_TIP },
|
|
83
|
+
copy: { body: arrowBadge(L.plus), fill: true, hotspot: ARROW_TIP },
|
|
84
|
+
move: { body: L.move, hotspot: [12, 12] },
|
|
85
|
+
"no-drop": { body: arrowBadge(L.ban), fill: true, hotspot: ARROW_TIP },
|
|
86
|
+
"not-allowed": { body: L.ban, hotspot: [12, 12] },
|
|
87
|
+
grab: { body: L.hand, hotspot: [12, 12] },
|
|
88
|
+
grabbing: { body: L.handGrab, hotspot: [12, 12] },
|
|
89
|
+
// Resizing & scrolling
|
|
90
|
+
"all-scroll": { body: `${L.move}<circle cx="12" cy="12" r="1.6" fill="#1a1a1a" stroke="none"/>`, hotspot: [12, 12] },
|
|
91
|
+
"col-resize": { body: `${L.moveH}<line x1="12" y1="4" x2="12" y2="20"/>`, hotspot: [12, 12] },
|
|
92
|
+
"row-resize": { body: `${L.moveV}<line x1="4" y1="12" x2="20" y2="12"/>`, hotspot: [12, 12] },
|
|
93
|
+
"e-resize": { body: L.moveH, hotspot: [12, 12] },
|
|
94
|
+
"w-resize": { body: L.moveH, hotspot: [12, 12] },
|
|
95
|
+
"ew-resize": { body: L.moveH, hotspot: [12, 12] },
|
|
96
|
+
"n-resize": { body: L.moveV, hotspot: [12, 12] },
|
|
97
|
+
"s-resize": { body: L.moveV, hotspot: [12, 12] },
|
|
98
|
+
"ns-resize": { body: L.moveV, hotspot: [12, 12] },
|
|
99
|
+
"ne-resize": { body: L.diag2, hotspot: [12, 12] },
|
|
100
|
+
"sw-resize": { body: L.diag2, hotspot: [12, 12] },
|
|
101
|
+
"nesw-resize": { body: L.diag2, hotspot: [12, 12] },
|
|
102
|
+
"nw-resize": { body: L.diag, hotspot: [12, 12] },
|
|
103
|
+
"se-resize": { body: L.diag, hotspot: [12, 12] },
|
|
104
|
+
"nwse-resize": { body: L.diag, hotspot: [12, 12] },
|
|
105
|
+
// Zoom
|
|
106
|
+
"zoom-in": { body: L.zoomIn, hotspot: [11, 11] },
|
|
107
|
+
"zoom-out": { body: L.zoomOut, hotspot: [11, 11] },
|
|
108
|
+
};
|
|
109
|
+
/** Canonical display order, grouped by the MDN cursor categories. */
|
|
110
|
+
export const CURSOR_CATEGORIES = [
|
|
111
|
+
{ title: "General", values: ["auto", "default", "none"] },
|
|
112
|
+
{ title: "Links & status", values: ["context-menu", "help", "pointer", "progress", "wait"] },
|
|
113
|
+
{ title: "Selection", values: ["cell", "crosshair", "text", "vertical-text"] },
|
|
114
|
+
{ title: "Drag & drop", values: ["alias", "copy", "move", "no-drop", "not-allowed", "grab", "grabbing"] },
|
|
115
|
+
{ title: "Resizing & scrolling", values: ["all-scroll", "col-resize", "row-resize", "n-resize", "e-resize", "s-resize", "w-resize", "ew-resize", "ns-resize", "ne-resize", "nw-resize", "se-resize", "sw-resize", "nesw-resize", "nwse-resize"] },
|
|
116
|
+
{ title: "Zooming", values: ["zoom-in", "zoom-out"] },
|
|
117
|
+
];
|
|
118
|
+
/**
|
|
119
|
+
* Render a cursor glyph as an SVG `<g>`, positioned so its hotspot sits at
|
|
120
|
+
* `(x, y)` and the 24-box is scaled to `size` px. Paints a white halo under the
|
|
121
|
+
* dark glyph (filled glyphs use a white fill + dark outline). Returns "" for an
|
|
122
|
+
* empty glyph (`none`).
|
|
123
|
+
*/
|
|
124
|
+
export function cursorGlyphSvg(value, x, y, size = 22, color = "#1a1a1a") {
|
|
125
|
+
const g = CURSOR_GLYPHS[value] ?? CURSOR_GLYPHS.default;
|
|
126
|
+
if (g.body === "")
|
|
127
|
+
return "";
|
|
128
|
+
const s = size / 24;
|
|
129
|
+
const tx = x - g.hotspot[0] * s;
|
|
130
|
+
const ty = y - g.hotspot[1] * s;
|
|
131
|
+
const rot = g.rotate ? ` rotate(${g.rotate} 12 12)` : "";
|
|
132
|
+
const inner = g.fill
|
|
133
|
+
// Filled silhouette: white fill + dark outline (classic arrow look).
|
|
134
|
+
? `<g fill="#fff" stroke="${color}" stroke-width="1.4" stroke-linejoin="round">${g.body}</g>`
|
|
135
|
+
// Line-art: white halo stroke under the dark stroke.
|
|
136
|
+
: `<g fill="none" stroke-linecap="round" stroke-linejoin="round"><g stroke="#fff" stroke-width="3.4">${g.body}</g><g stroke="${color}" stroke-width="1.7">${g.body}</g></g>`;
|
|
137
|
+
return `<g transform="translate(${tx.toFixed(2)} ${ty.toFixed(2)}) scale(${s.toFixed(4)})"><g transform="${rot.trim() || "translate(0 0)"}">${inner}</g></g>`;
|
|
138
|
+
}
|
|
@@ -14,8 +14,19 @@
|
|
|
14
14
|
* the caller supplies. The frame index is computed from the event's `t`
|
|
15
15
|
* (each frame's start/end time is derived from the animation timing).
|
|
16
16
|
*
|
|
17
|
+
* Cursor TYPE (DM-1106): by default the overlay paints the right cursor for
|
|
18
|
+
* whatever is under it — arrow over body, hand over a link, I-beam over text,
|
|
19
|
+
* resize arrows over a resizer, etc. — switching exactly at element boundaries
|
|
20
|
+
* as the pointer moves, matching what the browser showed. The keyword comes
|
|
21
|
+
* from the captured `cursor` field (resolved per Blink, including `auto`); the
|
|
22
|
+
* caller supplies a `resolveCursorAt(x, y, frameIndex)` hit-tester (built from
|
|
23
|
+
* the per-frame trees) and the glyphs come from `cursor-glyphs.ts`. A per-event
|
|
24
|
+
* `cursor` override forces a specific glyph. Without a resolver the overlay
|
|
25
|
+
* falls back to the single arrow (back-compat).
|
|
26
|
+
*
|
|
17
27
|
* See docs/13-cursor-overlay.md for the design.
|
|
18
28
|
*/
|
|
29
|
+
import type { CapturedElement } from "../capture/types.js";
|
|
19
30
|
export interface CursorStyle {
|
|
20
31
|
/** Pointer variant. v1: only `mouse` is rendered (touch falls through to the same arrow glyph). */
|
|
21
32
|
pointer: "mouse" | "touch";
|
|
@@ -58,6 +69,8 @@ export interface CursorMoveEvent {
|
|
|
58
69
|
dx: number;
|
|
59
70
|
dy: number;
|
|
60
71
|
};
|
|
72
|
+
/** DM-1106: force a specific cursor keyword for this move (skip auto hit-test). */
|
|
73
|
+
cursor?: string;
|
|
61
74
|
}
|
|
62
75
|
export interface CursorClickEvent {
|
|
63
76
|
type: "click";
|
|
@@ -72,6 +85,8 @@ export interface CursorShowEvent {
|
|
|
72
85
|
t: number;
|
|
73
86
|
x: number;
|
|
74
87
|
y: number;
|
|
88
|
+
/** DM-1106: force a specific cursor keyword from this point (skip auto hit-test). */
|
|
89
|
+
cursor?: string;
|
|
75
90
|
}
|
|
76
91
|
export interface CursorHideEvent {
|
|
77
92
|
type: "hide";
|
|
@@ -90,11 +105,14 @@ export type SelectorResolver = (sel: string, frameIndex: number) => {
|
|
|
90
105
|
w: number;
|
|
91
106
|
h: number;
|
|
92
107
|
} | null;
|
|
108
|
+
/** DM-1106: hit-tester for the cursor TYPE at a viewport point in a given frame. */
|
|
109
|
+
export type CursorAtResolver = (x: number, y: number, frameIndex: number) => string;
|
|
93
110
|
interface KeyframePoint {
|
|
94
111
|
t: number;
|
|
95
112
|
x: number;
|
|
96
113
|
y: number;
|
|
97
114
|
visible: boolean;
|
|
115
|
+
cursor?: string;
|
|
98
116
|
}
|
|
99
117
|
interface ResolvedClick {
|
|
100
118
|
t: number;
|
|
@@ -103,21 +121,48 @@ interface ResolvedClick {
|
|
|
103
121
|
button: "primary" | "secondary" | "middle";
|
|
104
122
|
style: CursorStyle;
|
|
105
123
|
}
|
|
124
|
+
/** A cursor-keyword timeline entry: from time `t` the active glyph is `cursor`
|
|
125
|
+
* (or null = cursor hidden). DM-1106. */
|
|
126
|
+
export interface CursorTimelineEntry {
|
|
127
|
+
t: number;
|
|
128
|
+
cursor: string | null;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* DM-1106: the effective cursor keyword at viewport point (x, y) for a captured
|
|
132
|
+
* tree — the LAST element in paint order (DFS pre-order) whose box contains the
|
|
133
|
+
* point. `cursor` inherits and was resolved per element at capture, so the
|
|
134
|
+
* topmost element's stored value (or `default` when omitted) is what Chrome
|
|
135
|
+
* painted. A z-index-agnostic approximation: good for the nested-element and
|
|
136
|
+
* later-sibling-on-top cases a cursor overlay cares about.
|
|
137
|
+
*/
|
|
138
|
+
export declare function cursorAtPoint(roots: CapturedElement[], x: number, y: number): string;
|
|
106
139
|
/**
|
|
107
140
|
* Resolve a script into absolute-coord position keyframes + click pulses.
|
|
108
141
|
* Caller passes a `resolveSelector(sel, frameIndex)` if the script uses
|
|
109
142
|
* selectors; otherwise pass `null` and selector events become no-ops with
|
|
110
143
|
* a console warning.
|
|
111
144
|
*/
|
|
112
|
-
export declare function resolveCursorScript(overlay: CursorOverlay, totalDurationMs: number, frameStartTimes: number[], resolveSelector: SelectorResolver | null
|
|
145
|
+
export declare function resolveCursorScript(overlay: CursorOverlay, totalDurationMs: number, frameStartTimes: number[], resolveSelector: SelectorResolver | null,
|
|
146
|
+
/** DM-1106: auto cursor-type hit-tester. When provided, the result includes a
|
|
147
|
+
* `cursorTimeline` driving per-glyph switching; when null, the overlay paints
|
|
148
|
+
* the single arrow (back-compat). */
|
|
149
|
+
resolveCursorAt?: CursorAtResolver | null): {
|
|
113
150
|
positions: KeyframePoint[];
|
|
114
151
|
clicks: ResolvedClick[];
|
|
115
152
|
style: CursorStyle;
|
|
153
|
+
cursorTimeline: CursorTimelineEntry[] | null;
|
|
116
154
|
};
|
|
117
155
|
/**
|
|
118
156
|
* Emit the `<g class="cursor-overlay">` markup for an already-resolved
|
|
119
157
|
* timeline. Returns "" when the timeline has no positions or every keyframe
|
|
120
158
|
* is invisible.
|
|
159
|
+
*
|
|
160
|
+
* When `cursorTimeline` is provided (DM-1106), the pointer's GLYPH switches over
|
|
161
|
+
* time to match what was under it: each distinct keyword gets a glyph drawn
|
|
162
|
+
* hotspot-at-origin inside the shared position-animated group, with a discrete
|
|
163
|
+
* opacity track that turns it on only during its windows (and visibility folds
|
|
164
|
+
* in as the all-glyphs-off `null` state). Without a timeline, the single white
|
|
165
|
+
* arrow paints (back-compat).
|
|
121
166
|
*/
|
|
122
|
-
export declare function cursorOverlayMarkup(positions: KeyframePoint[], clicks: ResolvedClick[], style: CursorStyle, totalDurationMs: number): string;
|
|
167
|
+
export declare function cursorOverlayMarkup(positions: KeyframePoint[], clicks: ResolvedClick[], style: CursorStyle, totalDurationMs: number, cursorTimeline?: CursorTimelineEntry[] | null): string;
|
|
123
168
|
export {};
|
|
@@ -14,8 +14,41 @@
|
|
|
14
14
|
* the caller supplies. The frame index is computed from the event's `t`
|
|
15
15
|
* (each frame's start/end time is derived from the animation timing).
|
|
16
16
|
*
|
|
17
|
+
* Cursor TYPE (DM-1106): by default the overlay paints the right cursor for
|
|
18
|
+
* whatever is under it — arrow over body, hand over a link, I-beam over text,
|
|
19
|
+
* resize arrows over a resizer, etc. — switching exactly at element boundaries
|
|
20
|
+
* as the pointer moves, matching what the browser showed. The keyword comes
|
|
21
|
+
* from the captured `cursor` field (resolved per Blink, including `auto`); the
|
|
22
|
+
* caller supplies a `resolveCursorAt(x, y, frameIndex)` hit-tester (built from
|
|
23
|
+
* the per-frame trees) and the glyphs come from `cursor-glyphs.ts`. A per-event
|
|
24
|
+
* `cursor` override forces a specific glyph. Without a resolver the overlay
|
|
25
|
+
* falls back to the single arrow (back-compat).
|
|
26
|
+
*
|
|
17
27
|
* See docs/13-cursor-overlay.md for the design.
|
|
18
28
|
*/
|
|
29
|
+
import { cursorGlyphSvg } from "./cursor-glyphs.js";
|
|
30
|
+
/**
|
|
31
|
+
* DM-1106: the effective cursor keyword at viewport point (x, y) for a captured
|
|
32
|
+
* tree — the LAST element in paint order (DFS pre-order) whose box contains the
|
|
33
|
+
* point. `cursor` inherits and was resolved per element at capture, so the
|
|
34
|
+
* topmost element's stored value (or `default` when omitted) is what Chrome
|
|
35
|
+
* painted. A z-index-agnostic approximation: good for the nested-element and
|
|
36
|
+
* later-sibling-on-top cases a cursor overlay cares about.
|
|
37
|
+
*/
|
|
38
|
+
export function cursorAtPoint(roots, x, y) {
|
|
39
|
+
let best = null;
|
|
40
|
+
const visit = (n) => {
|
|
41
|
+
if (n.width > 0 && n.height > 0 && x >= n.x && x < n.x + n.width && y >= n.y && y < n.y + n.height)
|
|
42
|
+
best = n;
|
|
43
|
+
const kids = n.children;
|
|
44
|
+
if (kids != null)
|
|
45
|
+
for (const c of kids)
|
|
46
|
+
visit(c);
|
|
47
|
+
};
|
|
48
|
+
for (const r of roots)
|
|
49
|
+
visit(r);
|
|
50
|
+
return best?.cursor ?? "default";
|
|
51
|
+
}
|
|
19
52
|
const DEFAULT_STYLE = {
|
|
20
53
|
pointer: "mouse",
|
|
21
54
|
cursorFill: "rgb(255, 255, 255)",
|
|
@@ -32,7 +65,11 @@ const DEFAULT_STYLE = {
|
|
|
32
65
|
* selectors; otherwise pass `null` and selector events become no-ops with
|
|
33
66
|
* a console warning.
|
|
34
67
|
*/
|
|
35
|
-
export function resolveCursorScript(overlay, totalDurationMs, frameStartTimes, resolveSelector
|
|
68
|
+
export function resolveCursorScript(overlay, totalDurationMs, frameStartTimes, resolveSelector,
|
|
69
|
+
/** DM-1106: auto cursor-type hit-tester. When provided, the result includes a
|
|
70
|
+
* `cursorTimeline` driving per-glyph switching; when null, the overlay paints
|
|
71
|
+
* the single arrow (back-compat). */
|
|
72
|
+
resolveCursorAt = null) {
|
|
36
73
|
const baseStyle = { ...DEFAULT_STYLE, ...overlay.style };
|
|
37
74
|
const events = [...overlay.events].sort((a, b) => a.t - b.t);
|
|
38
75
|
const positions = [];
|
|
@@ -40,8 +77,8 @@ export function resolveCursorScript(overlay, totalDurationMs, frameStartTimes, r
|
|
|
40
77
|
let curX = 0;
|
|
41
78
|
let curY = 0;
|
|
42
79
|
let visible = false;
|
|
43
|
-
const pushKey = (t, x, y, vis) => {
|
|
44
|
-
positions.push({ t, x, y, visible: vis });
|
|
80
|
+
const pushKey = (t, x, y, vis, cursor) => {
|
|
81
|
+
positions.push({ t, x, y, visible: vis, cursor });
|
|
45
82
|
curX = x;
|
|
46
83
|
curY = y;
|
|
47
84
|
visible = vis;
|
|
@@ -58,7 +95,7 @@ export function resolveCursorScript(overlay, totalDurationMs, frameStartTimes, r
|
|
|
58
95
|
};
|
|
59
96
|
for (const ev of events) {
|
|
60
97
|
if (ev.type === "show") {
|
|
61
|
-
pushKey(ev.t, ev.x, ev.y, true);
|
|
98
|
+
pushKey(ev.t, ev.x, ev.y, true, ev.cursor);
|
|
62
99
|
}
|
|
63
100
|
else if (ev.type === "hide") {
|
|
64
101
|
pushKey(ev.t, curX, curY, false);
|
|
@@ -73,11 +110,12 @@ export function resolveCursorScript(overlay, totalDurationMs, frameStartTimes, r
|
|
|
73
110
|
// interpolate to the target over `dur`, so it slides rather than popping
|
|
74
111
|
// in mid-frame. (Both the first-positioning and subsequent-move cases
|
|
75
112
|
// emit the same start keyframe — DM-1073 collapsed a no-op branch here.)
|
|
76
|
-
|
|
77
|
-
pushKey(ev.t
|
|
113
|
+
// The `cursor` override (if any) rides the whole slide.
|
|
114
|
+
pushKey(ev.t, curX, curY, true, ev.cursor);
|
|
115
|
+
pushKey(ev.t + dur, target.x, target.y, true, ev.cursor);
|
|
78
116
|
}
|
|
79
117
|
else {
|
|
80
|
-
pushKey(ev.t, target.x, target.y, true);
|
|
118
|
+
pushKey(ev.t, target.x, target.y, true, ev.cursor);
|
|
81
119
|
}
|
|
82
120
|
}
|
|
83
121
|
else if (ev.type === "click") {
|
|
@@ -96,9 +134,69 @@ export function resolveCursorScript(overlay, totalDurationMs, frameStartTimes, r
|
|
|
96
134
|
// through the whole loop.
|
|
97
135
|
if (positions[positions.length - 1].t < totalDurationMs) {
|
|
98
136
|
const last = positions[positions.length - 1];
|
|
99
|
-
positions.push({ t: totalDurationMs, x: last.x, y: last.y, visible: last.visible });
|
|
137
|
+
positions.push({ t: totalDurationMs, x: last.x, y: last.y, visible: last.visible, cursor: last.cursor });
|
|
138
|
+
}
|
|
139
|
+
const cursorTimeline = resolveCursorAt != null
|
|
140
|
+
? buildCursorTimeline(positions, totalDurationMs, frameForT, resolveCursorAt)
|
|
141
|
+
: null;
|
|
142
|
+
return { positions, clicks, style: baseStyle, cursorTimeline };
|
|
143
|
+
}
|
|
144
|
+
/** Interpolated cursor state at time `t` from the position keyframes. Position
|
|
145
|
+
* lerps within a keyframe interval; visibility + the per-segment cursor
|
|
146
|
+
* override are step-held from the interval's start keyframe. */
|
|
147
|
+
function stateAtTime(positions, t) {
|
|
148
|
+
if (t <= positions[0].t)
|
|
149
|
+
return positions[0];
|
|
150
|
+
const n = positions.length;
|
|
151
|
+
for (let i = 0; i < n - 1; i++) {
|
|
152
|
+
const a = positions[i], b = positions[i + 1];
|
|
153
|
+
if (t >= a.t && t <= b.t) {
|
|
154
|
+
const span = b.t - a.t;
|
|
155
|
+
const f = span > 0 ? (t - a.t) / span : 0;
|
|
156
|
+
return { x: a.x + (b.x - a.x) * f, y: a.y + (b.y - a.y) * f, visible: a.visible, cursor: a.cursor };
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return positions[n - 1];
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Build the cursor-keyword timeline (DM-1106). Samples the position timeline,
|
|
163
|
+
* resolving the cursor at each sample (an override wins; otherwise hit-test the
|
|
164
|
+
* frame under the point); emits an entry whenever the keyword changes, with the
|
|
165
|
+
* change time bisection-refined so the glyph switches AT the element boundary
|
|
166
|
+
* the pointer crosses (not at the next coarse sample). `null` = cursor hidden.
|
|
167
|
+
*/
|
|
168
|
+
function buildCursorTimeline(positions, totalDurationMs, frameForT, resolveCursorAt) {
|
|
169
|
+
const cursorAtTime = (t) => {
|
|
170
|
+
const s = stateAtTime(positions, t);
|
|
171
|
+
if (!s.visible)
|
|
172
|
+
return null;
|
|
173
|
+
return s.cursor ?? resolveCursorAt(s.x, s.y, frameForT(t));
|
|
174
|
+
};
|
|
175
|
+
const step = Math.max(8, Math.min(40, totalDurationMs / 400));
|
|
176
|
+
const timeline = [];
|
|
177
|
+
let prev = cursorAtTime(0);
|
|
178
|
+
timeline.push({ t: 0, cursor: prev });
|
|
179
|
+
for (let t = step; t <= totalDurationMs; t += step) {
|
|
180
|
+
const c = cursorAtTime(t);
|
|
181
|
+
if (c !== prev) {
|
|
182
|
+
// Refine the crossing time in (t-step, t] so the switch lands on the
|
|
183
|
+
// boundary, not the sample grid.
|
|
184
|
+
let lo = t - step, hi = t;
|
|
185
|
+
for (let k = 0; k < 14; k++) {
|
|
186
|
+
const mid = (lo + hi) / 2;
|
|
187
|
+
if (cursorAtTime(mid) === prev)
|
|
188
|
+
lo = mid;
|
|
189
|
+
else
|
|
190
|
+
hi = mid;
|
|
191
|
+
}
|
|
192
|
+
timeline.push({ t: Math.min(totalDurationMs, hi), cursor: c });
|
|
193
|
+
prev = c;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (timeline[timeline.length - 1].t < totalDurationMs) {
|
|
197
|
+
timeline.push({ t: totalDurationMs, cursor: prev });
|
|
100
198
|
}
|
|
101
|
-
return
|
|
199
|
+
return timeline;
|
|
102
200
|
}
|
|
103
201
|
function resolveMoveTarget(ev, curX, curY, frameIndex, resolveSelector) {
|
|
104
202
|
if (ev.to != null)
|
|
@@ -125,8 +223,15 @@ function resolveMoveTarget(ev, curX, curY, frameIndex, resolveSelector) {
|
|
|
125
223
|
* Emit the `<g class="cursor-overlay">` markup for an already-resolved
|
|
126
224
|
* timeline. Returns "" when the timeline has no positions or every keyframe
|
|
127
225
|
* is invisible.
|
|
226
|
+
*
|
|
227
|
+
* When `cursorTimeline` is provided (DM-1106), the pointer's GLYPH switches over
|
|
228
|
+
* time to match what was under it: each distinct keyword gets a glyph drawn
|
|
229
|
+
* hotspot-at-origin inside the shared position-animated group, with a discrete
|
|
230
|
+
* opacity track that turns it on only during its windows (and visibility folds
|
|
231
|
+
* in as the all-glyphs-off `null` state). Without a timeline, the single white
|
|
232
|
+
* arrow paints (back-compat).
|
|
128
233
|
*/
|
|
129
|
-
export function cursorOverlayMarkup(positions, clicks, style, totalDurationMs) {
|
|
234
|
+
export function cursorOverlayMarkup(positions, clicks, style, totalDurationMs, cursorTimeline = null) {
|
|
130
235
|
if (positions.length === 0 || totalDurationMs <= 0)
|
|
131
236
|
return "";
|
|
132
237
|
const totalSec = totalDurationMs / 1000;
|
|
@@ -139,21 +244,42 @@ export function cursorOverlayMarkup(positions, clicks, style, totalDurationMs) {
|
|
|
139
244
|
}
|
|
140
245
|
// SMIL animateTransform requires keyTimes to start at 0 and end at 1; the
|
|
141
246
|
// resolveCursorScript anchor at t=0 and t=totalDurationMs guarantees this.
|
|
142
|
-
|
|
143
|
-
// `visible` differs from the previous one. Use `discrete` calc-mode for
|
|
144
|
-
// step-wise transitions (no interpolation between 0 and 1).
|
|
145
|
-
const visValues = [];
|
|
146
|
-
for (const p of positions)
|
|
147
|
-
visValues.push(p.visible ? "1" : "0");
|
|
148
|
-
const cursorPath = macosCursorPath(style.cursorScale);
|
|
247
|
+
const posAnim = `<animateTransform attributeName="transform" type="translate" values="${valueStrs.join("; ")}" keyTimes="${keyTimes.join("; ")}" dur="${totalSec}s" repeatCount="indefinite" fill="freeze" />`;
|
|
149
248
|
// Pulse SVG fragments — one per click, with timing keyed off `t`.
|
|
150
249
|
const pulseMarkup = clicks.map((c, i) => buildPulseFragment(c, i, totalDurationMs)).join("\n");
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
250
|
+
let pointerGroup;
|
|
251
|
+
if (cursorTimeline != null && cursorTimeline.length > 0) {
|
|
252
|
+
// DM-1106: one glyph per distinct keyword, each toggled by a discrete
|
|
253
|
+
// opacity track derived from the keyword timeline. The shared parent group
|
|
254
|
+
// carries the position animation; glyphs are hotspot-at-origin so each lands
|
|
255
|
+
// correctly regardless of its own hotspot.
|
|
256
|
+
const size = 22 * (style.cursorScale || 1);
|
|
257
|
+
const tKeyTimes = cursorTimeline.map((e) => (e.t / totalDurationMs).toFixed(4));
|
|
258
|
+
const kinds = Array.from(new Set(cursorTimeline.map((e) => e.cursor).filter((c) => c != null)));
|
|
259
|
+
const glyphLayers = kinds.map((kind) => {
|
|
260
|
+
const glyph = cursorGlyphSvg(kind, 0, 0, size, style.cursorStroke);
|
|
261
|
+
const opVals = cursorTimeline.map((e) => (e.cursor === kind ? "1" : "0"));
|
|
262
|
+
return ` <g opacity="0">
|
|
263
|
+
<animate attributeName="opacity" values="${opVals.join(";")}" keyTimes="${tKeyTimes.join(";")}" dur="${totalSec}s" repeatCount="indefinite" calcMode="discrete" fill="freeze" />
|
|
264
|
+
${glyph}
|
|
265
|
+
</g>`;
|
|
266
|
+
}).join("\n");
|
|
267
|
+
pointerGroup = ` <g class="cursor-pointer">
|
|
268
|
+
${posAnim}
|
|
269
|
+
${glyphLayers}
|
|
270
|
+
</g>`;
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
// Legacy single-arrow path (no auto cursor-type resolver supplied).
|
|
274
|
+
const visValues = positions.map((p) => (p.visible ? "1" : "0"));
|
|
275
|
+
pointerGroup = ` <g class="cursor-arrow" opacity="0">
|
|
276
|
+
${posAnim}
|
|
154
277
|
<animate attributeName="opacity" values="${visValues.join(";")}" keyTimes="${keyTimes.join(";")}" dur="${totalSec}s" repeatCount="indefinite" calcMode="discrete" fill="freeze" />
|
|
155
|
-
${
|
|
156
|
-
</g
|
|
278
|
+
${macosCursorPath(style.cursorScale)}
|
|
279
|
+
</g>`;
|
|
280
|
+
}
|
|
281
|
+
return ` <g class="cursor-overlay" pointer-events="none">
|
|
282
|
+
${pointerGroup}
|
|
157
283
|
${pulseMarkup}
|
|
158
284
|
</g>`;
|
|
159
285
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { buildMagicMove, type MagicMove, type MagicMoveSlide, } from "./magic-move.js";
|
|
2
2
|
export { generateAnimatedSvg, type AnimationConfig, type AnimationFrame, type AnimationOverlay, type TypingOverlay, type TapOverlay, type SvgOverlay, type IntraFrameAnimation, } from "./animator.js";
|
|
3
|
-
export { cursorOverlayMarkup, resolveCursorScript, type CursorOverlay, type CursorEvent, type CursorMoveEvent, type CursorClickEvent, type CursorShowEvent, type CursorHideEvent, type CursorStyle, type SelectorResolver, } from "./cursor-overlay.js";
|
|
3
|
+
export { cursorOverlayMarkup, resolveCursorScript, cursorAtPoint, type CursorOverlay, type CursorEvent, type CursorMoveEvent, type CursorClickEvent, type CursorShowEvent, type CursorHideEvent, type CursorStyle, type CursorTimelineEntry, type SelectorResolver, type CursorAtResolver, } from "./cursor-overlay.js";
|
|
4
|
+
export { CURSOR_GLYPHS, CURSOR_CATEGORIES, cursorGlyphSvg, type CursorGlyph } from "./cursor-glyphs.js";
|
package/dist/animation/index.js
CHANGED
|
@@ -5,4 +5,5 @@
|
|
|
5
5
|
// typing / tap / SVG / cursor overlays.
|
|
6
6
|
export { buildMagicMove, } from "./magic-move.js";
|
|
7
7
|
export { generateAnimatedSvg, } from "./animator.js";
|
|
8
|
-
export { cursorOverlayMarkup, resolveCursorScript, } from "./cursor-overlay.js";
|
|
8
|
+
export { cursorOverlayMarkup, resolveCursorScript, cursorAtPoint, } from "./cursor-overlay.js";
|
|
9
|
+
export { CURSOR_GLYPHS, CURSOR_CATEGORIES, cursorGlyphSvg } from "./cursor-glyphs.js";
|