@weasel-js/cursor 1.4.0-pre.1 → 1.4.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/index.d.ts CHANGED
@@ -8,6 +8,13 @@ interface CursorGlyph {
8
8
  readonly box: number;
9
9
  /** Hotspot in glyph units, scaled to integer CSS px at bake time. */
10
10
  readonly hotspot: readonly [number, number];
11
+ /**
12
+ * The glyph-unit radius a `worldRadius` spec means, for glyphs that measure
13
+ * something — a brush ring reads as a lie about the brush size unless the
14
+ * painter knows which circle in the drawing is the one being sized. Defaults
15
+ * to the box's inscribed radius.
16
+ */
17
+ readonly radius?: number;
11
18
  readonly paths: readonly CursorPath[];
12
19
  }
13
20
  type CursorPath =
@@ -42,6 +49,12 @@ type CursorPath =
42
49
  declare const CURSOR_INK = "#141418";
43
50
  declare const CURSOR_HALO = "#ffffff";
44
51
  declare const CURSOR_HALO_WIDTH = 2.6;
52
+ /**
53
+ * Rotation steps a baked angle is quantized to. Below 16 the cursor visibly
54
+ * snaps against a smoothly rotating selection; above it the cache grows for no
55
+ * perceptible gain.
56
+ */
57
+ declare const CURSOR_ANGLE_STEPS = 16;
45
58
  /**
46
59
  * Chrome silently drops a cursor image above this size and falls back to the
47
60
  * keyword after the comma, with no error anywhere. Measured on Chrome 152 /
@@ -55,13 +68,35 @@ declare const CURSOR_MAX_CSS_PX = 128;
55
68
  * rather than discovering it on a dark background.
56
69
  */
57
70
  declare function haloFitsInBox(glyph: CursorGlyph): boolean;
71
+ /**
72
+ * True when the glyph still fits its viewBox at every rotation — i.e. every
73
+ * authored point is inside the box's inscribed circle, halo included.
74
+ *
75
+ * Bake rotates about the box centre in a viewBox that does not grow, so a
76
+ * glyph that merely satisfies {@link haloFitsInBox} can have its corners
77
+ * sheared off partway round. The loss is a few pixels at cursor size and
78
+ * invisible until someone looks at exactly the wrong angle, which is why it
79
+ * is worth failing at authoring time.
80
+ */
81
+ declare function rotationFitsInBox(glyph: CursorGlyph): boolean;
58
82
 
59
83
  interface BakeOptions {
60
84
  /** Rendered size in CSS px. Default 24. */
61
85
  readonly size?: number;
86
+ /** Clockwise rotation in radians about the box centre, quantized to
87
+ * {@link CURSOR_ANGLE_STEPS} steps. Default 0. */
88
+ readonly angle?: number;
62
89
  /** Keyword drawn if the browser rejects the image. Default 'default'. */
63
90
  readonly fallback?: string;
64
91
  }
92
+ /**
93
+ * Index of the quantization step an angle falls in, in `[0, CURSOR_ANGLE_STEPS)`.
94
+ *
95
+ * Exported because the bake cache keys on this rather than on the raw angle:
96
+ * the hover pump feeds a continuously varying selection rotation, and two
97
+ * angles in the same step must be one cache entry, not two.
98
+ */
99
+ declare function quantizeCursorAngle(angle: number): number;
65
100
  /**
66
101
  * Render a glyph to a CSS cursor value.
67
102
  *
@@ -107,6 +142,160 @@ declare const GLYPHS: {
107
142
  readonly d: "M 18.2 2.6 A 3.2 3.2 0 1 0 18.2 9 A 3.2 3.2 0 1 0 18.2 2.6 Z";
108
143
  }];
109
144
  };
145
+ readonly brush: {
146
+ readonly box: 24;
147
+ readonly hotspot: readonly [12, 12];
148
+ readonly radius: 9.9;
149
+ readonly paths: readonly [{
150
+ readonly role: "stroke";
151
+ readonly d: "M 12 2.1 A 9.9 9.9 0 1 0 12 21.9 A 9.9 9.9 0 1 0 12 2.1 Z";
152
+ readonly width: 1.6;
153
+ }];
154
+ };
155
+ readonly crosshairRect: {
156
+ readonly box: 24;
157
+ readonly hotspot: readonly [9, 9];
158
+ readonly paths: readonly [{
159
+ readonly role: "stroke";
160
+ readonly d: "M 1.6 9 L 6.4 9";
161
+ readonly width: 1.6;
162
+ }, {
163
+ readonly role: "stroke";
164
+ readonly d: "M 11.6 9 L 16.4 9";
165
+ readonly width: 1.6;
166
+ }, {
167
+ readonly role: "stroke";
168
+ readonly d: "M 9 1.6 L 9 6.4";
169
+ readonly width: 1.6;
170
+ }, {
171
+ readonly role: "stroke";
172
+ readonly d: "M 9 11.6 L 9 16.4";
173
+ readonly width: 1.6;
174
+ }, {
175
+ readonly role: "ink";
176
+ readonly d: "M 14.2 14.2 L 21 14.2 L 21 21 L 14.2 21 Z";
177
+ }];
178
+ };
179
+ readonly crosshairEllipse: {
180
+ readonly box: 24;
181
+ readonly hotspot: readonly [9, 9];
182
+ readonly paths: readonly [{
183
+ readonly role: "stroke";
184
+ readonly d: "M 1.6 9 L 6.4 9";
185
+ readonly width: 1.6;
186
+ }, {
187
+ readonly role: "stroke";
188
+ readonly d: "M 11.6 9 L 16.4 9";
189
+ readonly width: 1.6;
190
+ }, {
191
+ readonly role: "stroke";
192
+ readonly d: "M 9 1.6 L 9 6.4";
193
+ readonly width: 1.6;
194
+ }, {
195
+ readonly role: "stroke";
196
+ readonly d: "M 9 11.6 L 9 16.4";
197
+ readonly width: 1.6;
198
+ }, {
199
+ readonly role: "ink";
200
+ readonly d: "M 17.6 13.9 A 3.7 3.7 0 1 0 17.6 21.3 A 3.7 3.7 0 1 0 17.6 13.9 Z";
201
+ }];
202
+ };
203
+ readonly crosshairLine: {
204
+ readonly box: 24;
205
+ readonly hotspot: readonly [9, 9];
206
+ readonly paths: readonly [{
207
+ readonly role: "stroke";
208
+ readonly d: "M 1.6 9 L 6.4 9";
209
+ readonly width: 1.6;
210
+ }, {
211
+ readonly role: "stroke";
212
+ readonly d: "M 11.6 9 L 16.4 9";
213
+ readonly width: 1.6;
214
+ }, {
215
+ readonly role: "stroke";
216
+ readonly d: "M 9 1.6 L 9 6.4";
217
+ readonly width: 1.6;
218
+ }, {
219
+ readonly role: "stroke";
220
+ readonly d: "M 9 11.6 L 9 16.4";
221
+ readonly width: 1.6;
222
+ }, {
223
+ readonly role: "stroke";
224
+ readonly d: "M 14.2 21 L 21 14.2";
225
+ readonly width: 2.2;
226
+ }];
227
+ };
228
+ readonly crosshairStar: {
229
+ readonly box: 24;
230
+ readonly hotspot: readonly [9, 9];
231
+ readonly paths: readonly [{
232
+ readonly role: "stroke";
233
+ readonly d: "M 1.6 9 L 6.4 9";
234
+ readonly width: 1.6;
235
+ }, {
236
+ readonly role: "stroke";
237
+ readonly d: "M 11.6 9 L 16.4 9";
238
+ readonly width: 1.6;
239
+ }, {
240
+ readonly role: "stroke";
241
+ readonly d: "M 9 1.6 L 9 6.4";
242
+ readonly width: 1.6;
243
+ }, {
244
+ readonly role: "stroke";
245
+ readonly d: "M 9 11.6 L 9 16.4";
246
+ readonly width: 1.6;
247
+ }, {
248
+ readonly role: "ink";
249
+ readonly d: "M 17.6 13.4 L 18.72 16.06 L 21.59 16.3 L 19.41 18.19 L 20.07 21 L 17.6 19.5 L 15.13 21 L 15.79 18.19 L 13.61 16.3 L 16.48 16.06 Z";
250
+ }];
251
+ };
252
+ readonly crosshairPolygon: {
253
+ readonly box: 24;
254
+ readonly hotspot: readonly [9, 9];
255
+ readonly paths: readonly [{
256
+ readonly role: "stroke";
257
+ readonly d: "M 1.6 9 L 6.4 9";
258
+ readonly width: 1.6;
259
+ }, {
260
+ readonly role: "stroke";
261
+ readonly d: "M 11.6 9 L 16.4 9";
262
+ readonly width: 1.6;
263
+ }, {
264
+ readonly role: "stroke";
265
+ readonly d: "M 9 1.6 L 9 6.4";
266
+ readonly width: 1.6;
267
+ }, {
268
+ readonly role: "stroke";
269
+ readonly d: "M 9 11.6 L 9 16.4";
270
+ readonly width: 1.6;
271
+ }, {
272
+ readonly role: "ink";
273
+ readonly d: "M 17.6 13.5 L 21.5 16.33 L 20.01 20.92 L 15.19 20.92 L 13.7 16.33 Z";
274
+ }];
275
+ };
276
+ readonly resize: {
277
+ readonly box: 24;
278
+ readonly hotspot: readonly [12, 12];
279
+ readonly paths: readonly [{
280
+ readonly role: "ink";
281
+ readonly d: "M 2.5 12 L 7 8.8 L 7 10.7 L 17 10.7 L 17 8.8 L 21.5 12 L 17 15.2 L 17 13.3 L 7 13.3 L 7 15.2 Z";
282
+ }];
283
+ };
284
+ readonly rotate: {
285
+ readonly box: 24;
286
+ readonly hotspot: readonly [12, 12];
287
+ readonly paths: readonly [{
288
+ readonly role: "stroke";
289
+ readonly d: "M 5.8 9.74 A 6.6 6.6 0 0 1 18.2 9.74";
290
+ readonly width: 2.8;
291
+ }, {
292
+ readonly role: "ink";
293
+ readonly d: "M 4.22 14.07 L 2.98 8.72 L 8.62 10.77 Z";
294
+ }, {
295
+ readonly role: "ink";
296
+ readonly d: "M 19.78 14.07 L 15.38 10.77 L 21.02 8.72 Z";
297
+ }];
298
+ };
110
299
  };
111
300
  /** Every glyph name in the set. */
112
301
  type CursorGlyphName = keyof typeof GLYPHS;
@@ -114,9 +303,146 @@ type CursorGlyphName = keyof typeof GLYPHS;
114
303
  /**
115
304
  * The baked cursor string for a named glyph, memoized.
116
305
  *
117
- * The key space is bounded by the glyph set times the handful of sizes and
118
- * fallbacks in use, so the cache needs no eviction.
306
+ * The key space is bounded by the glyph set times 16 angles times the handful
307
+ * of sizes and fallbacks in use, so the cache needs no eviction.
119
308
  */
120
309
  declare function cursorFor(name: CursorGlyphName, opts?: BakeOptions): string;
121
310
 
122
- export { type BakeOptions, CURSOR_HALO, CURSOR_HALO_WIDTH, CURSOR_INK, CURSOR_MAX_CSS_PX, type CursorGlyph, type CursorGlyphName, type CursorPath, GLYPHS, bakeCursor, cursorFor, haloFitsInBox };
311
+ /**
312
+ * What a tool, action or affordance declares as its cursor.
313
+ *
314
+ * A bare string is a CSS cursor value and passes through untouched, so every
315
+ * declaration written before this type existed keeps working.
316
+ */
317
+ type CursorSpec = string | CursorGlyphSpec;
318
+ /** The glyph form of a {@link CursorSpec}. */
319
+ interface CursorGlyphSpec {
320
+ readonly glyph: CursorGlyphName;
321
+ /** Rendered size in CSS px. Default 24. */
322
+ readonly size?: number;
323
+ /**
324
+ * Size in world units instead of `size`, so the glyph tracks zoom — a brush
325
+ * radius ring. Forces the painted tier at every zoom level, because a baked
326
+ * image would have to be rebuilt on every wheel tick.
327
+ */
328
+ readonly worldRadius?: number;
329
+ /** Clockwise rotation in radians, quantized to 22.5° at bake. */
330
+ readonly angle?: number;
331
+ /** Keyword drawn if the browser rejects the image. Default 'default'. */
332
+ readonly fallback?: string;
333
+ }
334
+ /**
335
+ * Which tier a spec reaches the screen through.
336
+ *
337
+ * Tools never choose this — they declare what they want and the resolver
338
+ * picks, so a brush cursor stays a brush cursor across the radius where it
339
+ * stops being expressible as a CSS cursor at all.
340
+ */
341
+ type ResolvedCursor = {
342
+ readonly kind: 'css';
343
+ readonly css: string;
344
+ } | {
345
+ readonly kind: 'painted';
346
+ readonly glyph: CursorGlyphName;
347
+ readonly angle: number;
348
+ /** CSS px, when the spec fixed one. */
349
+ readonly size?: number;
350
+ /** World units; the painter scales this by the live view. */
351
+ readonly worldRadius?: number;
352
+ };
353
+ /**
354
+ * Resolve a spec to the tier that can actually draw it.
355
+ *
356
+ * Escalates to painted when the glyph is sized in world units, or when its
357
+ * fixed size is past {@link CURSOR_MAX_CSS_PX} — above which the browser drops
358
+ * the image and silently falls back to the keyword.
359
+ */
360
+ declare function resolveCursorTier(spec: CursorSpec): ResolvedCursor;
361
+ /**
362
+ * A {@link CursorSpec} as a CSS cursor value.
363
+ *
364
+ * Every consumer of a widened cursor field calls this, so a declaration never
365
+ * has to know which form it holds. A painted cursor answers `'none'` — the
366
+ * native cursor gets out of the way and the painted-tier layer draws it. A
367
+ * caller that writes that string without the layer installed leaves the user
368
+ * with no cursor, so reach for {@link resolveCursorTier} if you need to know.
369
+ */
370
+ declare function resolveCursor(spec: CursorSpec | undefined): string | undefined;
371
+
372
+ /**
373
+ * One path of a glyph, with the paint the register gives it.
374
+ *
375
+ * Deliberately not a renderer's own type: `@weasel-js/cursor` decides *what*
376
+ * to paint and in what order, and leaves *how* to whoever is drawing — the
377
+ * baker turns each op into an SVG element, the painted-tier layer turns it
378
+ * into a draw command. Both get the same list, which is what makes the two
379
+ * tiers show the same glyph rather than two drawings that resemble each other.
380
+ *
381
+ * Stroked ops are round-capped and round-joined; nothing in the register
382
+ * wants a mitre.
383
+ */
384
+ interface CursorPaintOp {
385
+ readonly d: string;
386
+ readonly fill?: string;
387
+ readonly stroke?: {
388
+ readonly color: string;
389
+ readonly width: number;
390
+ };
391
+ }
392
+ interface PaintOptions {
393
+ /**
394
+ * Multiplies every stroke width, leaving geometry alone. Default 1.
395
+ *
396
+ * A world-sized glyph passes the reciprocal of its scale here so line weight
397
+ * stays chrome-weight in CSS px: a brush ring at a 400px radius otherwise
398
+ * carries a 40px-thick stroke and a halo to match.
399
+ */
400
+ readonly lineWidthScale?: number;
401
+ }
402
+ /** Every paint a glyph needs, in draw order: all halos, then all ink. */
403
+ declare function cursorPaintOps(glyph: CursorGlyph, opts?: PaintOptions): CursorPaintOp[];
404
+ /** An affine `[a, b, c, d, e, f]`: `x' = ax + cy + e`, `y' = bx + dy + f`. */
405
+ type CursorMatrix = readonly [number, number, number, number, number, number];
406
+ interface PaintPlacement {
407
+ /** Rendered size in CSS px, as `bakeCursor` means it. */
408
+ readonly size: number;
409
+ /** Clockwise rotation in radians. Default 0. */
410
+ readonly angle?: number;
411
+ /** Where the hotspot goes — the pointer, in the layer's own coords. */
412
+ readonly at: {
413
+ readonly x: number;
414
+ readonly y: number;
415
+ };
416
+ }
417
+ /**
418
+ * Where a glyph's own units land, to place it under the pointer at a size and
419
+ * an angle.
420
+ *
421
+ * Rotation is about the hotspot, not the box centre. That is the same
422
+ * placement the baker produces — it turns the glyph about the centre and then
423
+ * carries the hotspot around with it, and the centre cancels — but it is worth
424
+ * stating, because a painter that rotates about the centre and forgets to move
425
+ * the hotspot swings the glyph around a point the user never sees.
426
+ *
427
+ * Unquantized on purpose: the 22.5° step exists to bound the bake cache, and
428
+ * the painter has no cache to bound. A painted cursor turns smoothly.
429
+ */
430
+ declare function cursorPaintMatrix(glyph: CursorGlyph, placement: PaintPlacement): CursorMatrix;
431
+ /**
432
+ * The CSS-px size a world-sized glyph is drawn at, so the circle its `radius`
433
+ * names measures `worldRadius` world units on screen.
434
+ *
435
+ * A glyph with no `radius` is measured by its inscribed circle. Getting this
436
+ * wrong scales every brush cursor by a constant factor, which reads as a
437
+ * slightly-off cursor rather than as a bug.
438
+ */
439
+ declare function cursorWorldSize(glyph: CursorGlyph, worldRadius: number, scale: number): number;
440
+ /**
441
+ * The `lineWidthScale` that keeps a glyph's line weight at chrome weight while
442
+ * its geometry scales — what a world-sized glyph wants, and what a fixed-size
443
+ * one must not have (there, weight scaling is what makes the painted tier
444
+ * match the baked one).
445
+ */
446
+ declare function chromeLineWidthScale(glyph: CursorGlyph, size: number): number;
447
+
448
+ export { type BakeOptions, CURSOR_ANGLE_STEPS, CURSOR_HALO, CURSOR_HALO_WIDTH, CURSOR_INK, CURSOR_MAX_CSS_PX, type CursorGlyph, type CursorGlyphName, type CursorGlyphSpec, type CursorMatrix, type CursorPaintOp, type CursorPath, type CursorSpec, GLYPHS, type PaintOptions, type PaintPlacement, type ResolvedCursor, bakeCursor, chromeLineWidthScale, cursorFor, cursorPaintMatrix, cursorPaintOps, cursorWorldSize, haloFitsInBox, quantizeCursorAngle, resolveCursor, resolveCursorTier, rotationFitsInBox };
package/dist/index.js CHANGED
@@ -2,16 +2,11 @@
2
2
  var CURSOR_INK = "#141418";
3
3
  var CURSOR_HALO = "#ffffff";
4
4
  var CURSOR_HALO_WIDTH = 2.6;
5
+ var CURSOR_ANGLE_STEPS = 16;
5
6
  var CURSOR_MAX_CSS_PX = 128;
6
- function extent(d) {
7
- let min = Number.POSITIVE_INFINITY;
8
- let max = Number.NEGATIVE_INFINITY;
7
+ function walk(d, see) {
9
8
  let cx = 0;
10
9
  let cy = 0;
11
- const see = (v) => {
12
- if (v < min) min = v;
13
- if (v > max) max = v;
14
- };
15
10
  for (const [, cmd, args] of d.matchAll(/([A-Za-z])([^A-Za-z]*)/g)) {
16
11
  if (cmd !== cmd.toUpperCase()) {
17
12
  throw new Error(`cursor glyph path uses a relative command '${cmd}': ${d}`);
@@ -31,10 +26,10 @@ function extent(d) {
31
26
  const uy = half > 0 ? dx / (half * 2) : 0;
32
27
  const ox = (cx + x) / 2 + sign * off * ux;
33
28
  const oy = (cy + y) / 2 + sign * off * uy;
34
- see(ox - r);
35
- see(ox + r);
36
- see(oy - r);
37
- see(oy + r);
29
+ see(ox - r, oy - r);
30
+ see(ox + r, oy - r);
31
+ see(ox - r, oy + r);
32
+ see(ox + r, oy + r);
38
33
  cx = x;
39
34
  cy = y;
40
35
  }
@@ -43,44 +38,94 @@ function extent(d) {
43
38
  for (let i = 0; i + 2 <= nums.length; i += 2) {
44
39
  cx = nums[i];
45
40
  cy = nums[i + 1];
46
- see(cx);
47
- see(cy);
41
+ see(cx, cy);
48
42
  }
49
43
  }
50
- return { min, max };
51
44
  }
52
45
  function haloFitsInBox(glyph) {
53
46
  const margin = CURSOR_HALO_WIDTH / 2;
54
- return glyph.paths.every((p) => {
55
- const { min, max } = extent(p.d);
56
- return min >= margin && max <= glyph.box - margin;
57
- });
47
+ let ok = true;
48
+ for (const p of glyph.paths) {
49
+ walk(p.d, (x, y) => {
50
+ for (const v of [x, y]) {
51
+ if (v < margin || v > glyph.box - margin) ok = false;
52
+ }
53
+ });
54
+ }
55
+ return ok;
56
+ }
57
+ function rotationFitsInBox(glyph) {
58
+ const c = glyph.box / 2;
59
+ const limit = c - CURSOR_HALO_WIDTH / 2;
60
+ let ok = true;
61
+ for (const p of glyph.paths) {
62
+ walk(p.d, (x, y) => {
63
+ if (Math.hypot(x - c, y - c) > limit) ok = false;
64
+ });
65
+ }
66
+ return ok;
58
67
  }
59
68
 
60
- // src/bake.ts
61
- function renderHalo(p) {
69
+ // src/paint.ts
70
+ function halo(p, k) {
62
71
  switch (p.role) {
63
72
  case "ink":
64
73
  case "accent":
65
- return `<path d="${p.d}" fill="${CURSOR_HALO}" stroke="${CURSOR_HALO}" stroke-width="${CURSOR_HALO_WIDTH}" stroke-linejoin="round"/>`;
74
+ return { d: p.d, fill: CURSOR_HALO, stroke: { color: CURSOR_HALO, width: CURSOR_HALO_WIDTH * k } };
66
75
  case "stroke":
67
- return `<path d="${p.d}" fill="none" stroke="${CURSOR_HALO}" stroke-width="${p.width + CURSOR_HALO_WIDTH}" stroke-linecap="round" stroke-linejoin="round"/>`;
76
+ return { d: p.d, stroke: { color: CURSOR_HALO, width: (p.width + CURSOR_HALO_WIDTH) * k } };
68
77
  // A detail IS halo-coloured and sits on top of the ink; it has no halo.
69
78
  case "detail":
70
- return "";
79
+ return null;
71
80
  }
72
81
  }
73
- function renderInk(p) {
82
+ function ink(p, k) {
74
83
  switch (p.role) {
75
84
  case "ink":
76
- return `<path d="${p.d}" fill="${CURSOR_INK}"/>`;
85
+ return { d: p.d, fill: CURSOR_INK };
77
86
  case "stroke":
78
- return `<path d="${p.d}" fill="none" stroke="${CURSOR_INK}" stroke-width="${p.width}" stroke-linecap="round" stroke-linejoin="round"/>`;
87
+ return { d: p.d, stroke: { color: CURSOR_INK, width: p.width * k } };
79
88
  case "detail":
80
- return `<path d="${p.d}" fill="none" stroke="${CURSOR_HALO}" stroke-width="${p.width}" stroke-linecap="round"/>`;
89
+ return { d: p.d, stroke: { color: CURSOR_HALO, width: p.width * k } };
81
90
  case "accent":
82
- return `<path d="${p.d}" fill="${p.fill}"/>`;
91
+ return { d: p.d, fill: p.fill };
92
+ }
93
+ }
94
+ function cursorPaintOps(glyph, opts = {}) {
95
+ const k = opts.lineWidthScale ?? 1;
96
+ const out = [];
97
+ for (const p of glyph.paths) {
98
+ const h = halo(p, k);
99
+ if (h) out.push(h);
83
100
  }
101
+ for (const p of glyph.paths) out.push(ink(p, k));
102
+ return out;
103
+ }
104
+ function cursorPaintMatrix(glyph, placement) {
105
+ const s = placement.size / glyph.box;
106
+ const theta = placement.angle ?? 0;
107
+ const a = s * Math.cos(theta);
108
+ const b = s * Math.sin(theta);
109
+ const [hx, hy] = glyph.hotspot;
110
+ return [a, b, -b, a, placement.at.x - (a * hx - b * hy), placement.at.y - (b * hx + a * hy)];
111
+ }
112
+ function cursorWorldSize(glyph, worldRadius, scale) {
113
+ const r = glyph.radius ?? glyph.box / 2;
114
+ return worldRadius * scale * glyph.box / r;
115
+ }
116
+ function chromeLineWidthScale(glyph, size) {
117
+ return glyph.box / size;
118
+ }
119
+
120
+ // src/bake.ts
121
+ function quantizeCursorAngle(angle) {
122
+ const i = Math.round(angle / (Math.PI * 2) * CURSOR_ANGLE_STEPS);
123
+ return (i % CURSOR_ANGLE_STEPS + CURSOR_ANGLE_STEPS) % CURSOR_ANGLE_STEPS;
124
+ }
125
+ function renderOp(op) {
126
+ const fill = op.fill === void 0 ? "none" : op.fill;
127
+ const stroke = op.stroke ? ` stroke="${op.stroke.color}" stroke-width="${op.stroke.width}" stroke-linecap="round" stroke-linejoin="round"` : "";
128
+ return `<path d="${op.d}" fill="${fill}"${stroke}/>`;
84
129
  }
85
130
  function bakeCursor(glyph, opts = {}) {
86
131
  const size = opts.size ?? 24;
@@ -89,10 +134,18 @@ function bakeCursor(glyph, opts = {}) {
89
134
  `cursor size ${size} exceeds the ${CURSOR_MAX_CSS_PX}px cap: the browser would drop the image and silently fall back. Use the painted tier.`
90
135
  );
91
136
  }
92
- const body = glyph.paths.map(renderHalo).join("") + glyph.paths.map(renderInk).join("");
137
+ const paths = cursorPaintOps(glyph).map(renderOp).join("");
138
+ const c = glyph.box / 2;
139
+ const step = quantizeCursorAngle(opts.angle ?? 0);
140
+ const theta = step / CURSOR_ANGLE_STEPS * Math.PI * 2;
141
+ const body = step === 0 ? paths : `<g transform="rotate(${step * 360 / CURSOR_ANGLE_STEPS} ${c} ${c})">${paths}</g>`;
93
142
  const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 ${glyph.box} ${glyph.box}">${body}</svg>`;
94
- const hx = Math.round(glyph.hotspot[0] / glyph.box * size);
95
- const hy = Math.round(glyph.hotspot[1] / glyph.box * size);
143
+ const dx = glyph.hotspot[0] - c;
144
+ const dy = glyph.hotspot[1] - c;
145
+ const cos = Math.cos(theta);
146
+ const sin = Math.sin(theta);
147
+ const hx = Math.round((c + cos * dx - sin * dy) / glyph.box * size);
148
+ const hy = Math.round((c + sin * dx + cos * dy) / glyph.box * size);
96
149
  const uri = `data:image/svg+xml,${encodeURIComponent(svg)}`;
97
150
  return `url("${uri}") ${hx} ${hy}, ${opts.fallback ?? "default"}`;
98
151
  }
@@ -151,6 +204,222 @@ var GLYPHS = {
151
204
  "d": "M 18.2 2.6 A 3.2 3.2 0 1 0 18.2 9 A 3.2 3.2 0 1 0 18.2 2.6 Z"
152
205
  }
153
206
  ]
207
+ },
208
+ "brush": {
209
+ "box": 24,
210
+ "hotspot": [
211
+ 12,
212
+ 12
213
+ ],
214
+ "radius": 9.9,
215
+ "paths": [
216
+ {
217
+ "role": "stroke",
218
+ "d": "M 12 2.1 A 9.9 9.9 0 1 0 12 21.9 A 9.9 9.9 0 1 0 12 2.1 Z",
219
+ "width": 1.6
220
+ }
221
+ ]
222
+ },
223
+ "crosshairRect": {
224
+ "box": 24,
225
+ "hotspot": [
226
+ 9,
227
+ 9
228
+ ],
229
+ "paths": [
230
+ {
231
+ "role": "stroke",
232
+ "d": "M 1.6 9 L 6.4 9",
233
+ "width": 1.6
234
+ },
235
+ {
236
+ "role": "stroke",
237
+ "d": "M 11.6 9 L 16.4 9",
238
+ "width": 1.6
239
+ },
240
+ {
241
+ "role": "stroke",
242
+ "d": "M 9 1.6 L 9 6.4",
243
+ "width": 1.6
244
+ },
245
+ {
246
+ "role": "stroke",
247
+ "d": "M 9 11.6 L 9 16.4",
248
+ "width": 1.6
249
+ },
250
+ {
251
+ "role": "ink",
252
+ "d": "M 14.2 14.2 L 21 14.2 L 21 21 L 14.2 21 Z"
253
+ }
254
+ ]
255
+ },
256
+ "crosshairEllipse": {
257
+ "box": 24,
258
+ "hotspot": [
259
+ 9,
260
+ 9
261
+ ],
262
+ "paths": [
263
+ {
264
+ "role": "stroke",
265
+ "d": "M 1.6 9 L 6.4 9",
266
+ "width": 1.6
267
+ },
268
+ {
269
+ "role": "stroke",
270
+ "d": "M 11.6 9 L 16.4 9",
271
+ "width": 1.6
272
+ },
273
+ {
274
+ "role": "stroke",
275
+ "d": "M 9 1.6 L 9 6.4",
276
+ "width": 1.6
277
+ },
278
+ {
279
+ "role": "stroke",
280
+ "d": "M 9 11.6 L 9 16.4",
281
+ "width": 1.6
282
+ },
283
+ {
284
+ "role": "ink",
285
+ "d": "M 17.6 13.9 A 3.7 3.7 0 1 0 17.6 21.3 A 3.7 3.7 0 1 0 17.6 13.9 Z"
286
+ }
287
+ ]
288
+ },
289
+ "crosshairLine": {
290
+ "box": 24,
291
+ "hotspot": [
292
+ 9,
293
+ 9
294
+ ],
295
+ "paths": [
296
+ {
297
+ "role": "stroke",
298
+ "d": "M 1.6 9 L 6.4 9",
299
+ "width": 1.6
300
+ },
301
+ {
302
+ "role": "stroke",
303
+ "d": "M 11.6 9 L 16.4 9",
304
+ "width": 1.6
305
+ },
306
+ {
307
+ "role": "stroke",
308
+ "d": "M 9 1.6 L 9 6.4",
309
+ "width": 1.6
310
+ },
311
+ {
312
+ "role": "stroke",
313
+ "d": "M 9 11.6 L 9 16.4",
314
+ "width": 1.6
315
+ },
316
+ {
317
+ "role": "stroke",
318
+ "d": "M 14.2 21 L 21 14.2",
319
+ "width": 2.2
320
+ }
321
+ ]
322
+ },
323
+ "crosshairStar": {
324
+ "box": 24,
325
+ "hotspot": [
326
+ 9,
327
+ 9
328
+ ],
329
+ "paths": [
330
+ {
331
+ "role": "stroke",
332
+ "d": "M 1.6 9 L 6.4 9",
333
+ "width": 1.6
334
+ },
335
+ {
336
+ "role": "stroke",
337
+ "d": "M 11.6 9 L 16.4 9",
338
+ "width": 1.6
339
+ },
340
+ {
341
+ "role": "stroke",
342
+ "d": "M 9 1.6 L 9 6.4",
343
+ "width": 1.6
344
+ },
345
+ {
346
+ "role": "stroke",
347
+ "d": "M 9 11.6 L 9 16.4",
348
+ "width": 1.6
349
+ },
350
+ {
351
+ "role": "ink",
352
+ "d": "M 17.6 13.4 L 18.72 16.06 L 21.59 16.3 L 19.41 18.19 L 20.07 21 L 17.6 19.5 L 15.13 21 L 15.79 18.19 L 13.61 16.3 L 16.48 16.06 Z"
353
+ }
354
+ ]
355
+ },
356
+ "crosshairPolygon": {
357
+ "box": 24,
358
+ "hotspot": [
359
+ 9,
360
+ 9
361
+ ],
362
+ "paths": [
363
+ {
364
+ "role": "stroke",
365
+ "d": "M 1.6 9 L 6.4 9",
366
+ "width": 1.6
367
+ },
368
+ {
369
+ "role": "stroke",
370
+ "d": "M 11.6 9 L 16.4 9",
371
+ "width": 1.6
372
+ },
373
+ {
374
+ "role": "stroke",
375
+ "d": "M 9 1.6 L 9 6.4",
376
+ "width": 1.6
377
+ },
378
+ {
379
+ "role": "stroke",
380
+ "d": "M 9 11.6 L 9 16.4",
381
+ "width": 1.6
382
+ },
383
+ {
384
+ "role": "ink",
385
+ "d": "M 17.6 13.5 L 21.5 16.33 L 20.01 20.92 L 15.19 20.92 L 13.7 16.33 Z"
386
+ }
387
+ ]
388
+ },
389
+ "resize": {
390
+ "box": 24,
391
+ "hotspot": [
392
+ 12,
393
+ 12
394
+ ],
395
+ "paths": [
396
+ {
397
+ "role": "ink",
398
+ "d": "M 2.5 12 L 7 8.8 L 7 10.7 L 17 10.7 L 17 8.8 L 21.5 12 L 17 15.2 L 17 13.3 L 7 13.3 L 7 15.2 Z"
399
+ }
400
+ ]
401
+ },
402
+ "rotate": {
403
+ "box": 24,
404
+ "hotspot": [
405
+ 12,
406
+ 12
407
+ ],
408
+ "paths": [
409
+ {
410
+ "role": "stroke",
411
+ "d": "M 5.8 9.74 A 6.6 6.6 0 0 1 18.2 9.74",
412
+ "width": 2.8
413
+ },
414
+ {
415
+ "role": "ink",
416
+ "d": "M 4.22 14.07 L 2.98 8.72 L 8.62 10.77 Z"
417
+ },
418
+ {
419
+ "role": "ink",
420
+ "d": "M 19.78 14.07 L 15.38 10.77 L 21.02 8.72 Z"
421
+ }
422
+ ]
154
423
  }
155
424
  };
156
425
 
@@ -159,18 +428,44 @@ var cache = /* @__PURE__ */ new Map();
159
428
  function cursorFor(name, opts = {}) {
160
429
  const size = opts.size ?? 24;
161
430
  const fallback = opts.fallback ?? "default";
162
- const key = `${name}|${size}|${fallback}`;
431
+ const step = quantizeCursorAngle(opts.angle ?? 0);
432
+ const key = `${name}|${size}|${step}|${fallback}`;
163
433
  const hit = cache.get(key);
164
434
  if (hit !== void 0) return hit;
165
435
  const glyph = GLYPHS[name];
166
436
  if (glyph === void 0) {
167
437
  throw new Error(`unknown cursor glyph: ${String(name)}`);
168
438
  }
169
- const css = bakeCursor(glyph, { size, fallback });
439
+ const css = bakeCursor(glyph, { size, fallback, angle: opts.angle ?? 0 });
170
440
  cache.set(key, css);
171
441
  return css;
172
442
  }
173
443
 
174
- export { CURSOR_HALO, CURSOR_HALO_WIDTH, CURSOR_INK, CURSOR_MAX_CSS_PX, GLYPHS, bakeCursor, cursorFor, haloFitsInBox };
444
+ // src/resolve.ts
445
+ function resolveCursorTier(spec) {
446
+ if (typeof spec === "string") return { kind: "css", css: spec };
447
+ const { glyph, size, worldRadius, angle = 0, fallback } = spec;
448
+ if (worldRadius !== void 0) {
449
+ return { kind: "painted", glyph, angle, worldRadius };
450
+ }
451
+ if (size !== void 0 && size > CURSOR_MAX_CSS_PX) {
452
+ return { kind: "painted", glyph, angle, size };
453
+ }
454
+ return {
455
+ kind: "css",
456
+ css: cursorFor(glyph, {
457
+ ...size !== void 0 ? { size } : {},
458
+ ...fallback !== void 0 ? { fallback } : {},
459
+ angle
460
+ })
461
+ };
462
+ }
463
+ function resolveCursor(spec) {
464
+ if (spec === void 0) return void 0;
465
+ const r = resolveCursorTier(spec);
466
+ return r.kind === "css" ? r.css : "none";
467
+ }
468
+
469
+ export { CURSOR_ANGLE_STEPS, CURSOR_HALO, CURSOR_HALO_WIDTH, CURSOR_INK, CURSOR_MAX_CSS_PX, GLYPHS, bakeCursor, chromeLineWidthScale, cursorFor, cursorPaintMatrix, cursorPaintOps, cursorWorldSize, haloFitsInBox, quantizeCursorAngle, resolveCursor, resolveCursorTier, rotationFitsInBox };
175
470
  //# sourceMappingURL=index.js.map
176
471
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/types.ts","../src/bake.ts","../src/glyphs.ts","../src/registry.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * A cursor glyph: SVG path `d` strings tagged with a paint role, plus the\n * hotspot in glyph units. The same record feeds both the data-URI baker and\n * the Path2D painter, so `d` is the one geometry form neither has to translate.\n */\nexport interface CursorGlyph {\n /** Side of the square viewBox the paths are authored in. */\n readonly box: number;\n /** Hotspot in glyph units, scaled to integer CSS px at bake time. */\n readonly hotspot: readonly [number, number];\n readonly paths: readonly CursorPath[];\n}\n\nexport type CursorPath =\n /** A filled part of the silhouette. */\n | { readonly role: 'ink'; readonly d: string }\n /** An unfilled part of the silhouette — a handle, a wire, an arc. */\n | { readonly role: 'stroke'; readonly d: string; readonly width: number }\n /** A division inside the silhouette, drawn in the halo color. */\n | { readonly role: 'detail'; readonly d: string; readonly width: number }\n /** A literal color, for glyphs that carry a swatch. */\n | { readonly role: 'accent'; readonly d: string; readonly fill: string };\n\n/**\n * Ink and halo are constants of the register rather than parameters: a\n * self-contrasting glyph reads on white paper, dark chrome and mid-tone\n * artwork alike precisely because it does not track the theme.\n */\nexport const CURSOR_INK = '#141418';\nexport const CURSOR_HALO = '#ffffff';\nexport const CURSOR_HALO_WIDTH = 2.6;\n\n/**\n * Chrome silently drops a cursor image above this size and falls back to the\n * keyword after the comma, with no error anywhere. Measured on Chrome 152 /\n * macOS 26.5; see the spec's \"Measured browser behavior\".\n */\nexport const CURSOR_MAX_CSS_PX = 128;\n\n/**\n * Axis-aligned bounds of an authored `d` string.\n *\n * Command-aware on purpose. Scraping every number out of the string instead\n * reads an arc's radii and its three flags as coordinates, which reports a\n * bogus `0` for every `A` ever written.\n *\n * An arc is bounded by the circle it lies on: its centre is recovered from the\n * endpoints and radius, then the whole circle is admitted. Expanding the arc's\n * *endpoint* by the radii instead over-reports whenever an endpoint sits at an\n * extreme of the circle, which is exactly where the half-circle idiom these\n * glyphs use puts it.\n *\n * The authored dialect is absolute `M`/`L`/`A`/`Z` with circular arcs. A\n * relative command would be measured as if absolute and silently under-report,\n * so it throws.\n */\nfunction extent(d: string): { min: number; max: number } {\n let min = Number.POSITIVE_INFINITY;\n let max = Number.NEGATIVE_INFINITY;\n let cx = 0;\n let cy = 0;\n const see = (v: number) => {\n if (v < min) min = v;\n if (v > max) max = v;\n };\n for (const [, cmd, args] of d.matchAll(/([A-Za-z])([^A-Za-z]*)/g)) {\n if (cmd !== cmd.toUpperCase()) {\n throw new Error(`cursor glyph path uses a relative command '${cmd}': ${d}`);\n }\n const nums = (args.match(/-?\\d+(?:\\.\\d+)?/g) ?? []).map(Number);\n if (cmd === 'Z') continue;\n if (cmd === 'A') {\n for (let i = 0; i + 7 <= nums.length; i += 7) {\n const [rx, ry, , largeArc, sweep, x, y] = nums.slice(i, i + 7);\n const r = Math.max(rx, ry);\n const dx = x - cx;\n const dy = y - cy;\n const half = Math.hypot(dx, dy) / 2;\n // Centre sits off the chord midpoint by this much, perpendicular to it.\n const off = Math.sqrt(Math.max(0, r * r - half * half));\n const sign = largeArc === sweep ? -1 : 1;\n const ux = half > 0 ? -dy / (half * 2) : 0;\n const uy = half > 0 ? dx / (half * 2) : 0;\n const ox = (cx + x) / 2 + sign * off * ux;\n const oy = (cy + y) / 2 + sign * off * uy;\n see(ox - r);\n see(ox + r);\n see(oy - r);\n see(oy + r);\n cx = x;\n cy = y;\n }\n continue;\n }\n // M and L: plain coordinate pairs.\n for (let i = 0; i + 2 <= nums.length; i += 2) {\n cx = nums[i];\n cy = nums[i + 1];\n see(cx);\n see(cy);\n }\n }\n return { min, max };\n}\n\n/**\n * True when every authored path sits at least half a halo stroke inside the\n * viewBox. A clipped halo is invisible at proof size and flattens the glyph's\n * outline at cursor size, so it is worth failing loudly at authoring time\n * rather than discovering it on a dark background.\n */\nexport function haloFitsInBox(glyph: CursorGlyph): boolean {\n const margin = CURSOR_HALO_WIDTH / 2;\n return glyph.paths.every((p) => {\n const { min, max } = extent(p.d);\n return min >= margin && max <= glyph.box - margin;\n });\n}\n","import {\n CURSOR_HALO,\n CURSOR_HALO_WIDTH,\n CURSOR_INK,\n CURSOR_MAX_CSS_PX,\n} from './types';\nimport type { CursorGlyph, CursorPath } from './types';\n\nexport interface BakeOptions {\n /** Rendered size in CSS px. Default 24. */\n readonly size?: number;\n /** Keyword drawn if the browser rejects the image. Default 'default'. */\n readonly fallback?: string;\n}\n\n/**\n * The halo pass. Every silhouette member is drawn once, wide, in halo colour\n * before any ink lands.\n *\n * It has to be a separate pass rather than a per-path `paint-order`: with each\n * path stroking its own halo, a later path's halo cuts a white trench through\n * an earlier path's fill wherever the two overlap. One pass under everything\n * gives the glyph a single continuous outline instead.\n */\nfunction renderHalo(p: CursorPath): string {\n switch (p.role) {\n case 'ink':\n case 'accent':\n return (\n `<path d=\"${p.d}\" fill=\"${CURSOR_HALO}\" stroke=\"${CURSOR_HALO}\"` +\n ` stroke-width=\"${CURSOR_HALO_WIDTH}\" stroke-linejoin=\"round\"/>`\n );\n case 'stroke':\n return (\n `<path d=\"${p.d}\" fill=\"none\" stroke=\"${CURSOR_HALO}\"` +\n ` stroke-width=\"${p.width + CURSOR_HALO_WIDTH}\"` +\n ` stroke-linecap=\"round\" stroke-linejoin=\"round\"/>`\n );\n // A detail IS halo-coloured and sits on top of the ink; it has no halo.\n case 'detail':\n return '';\n }\n}\n\nfunction renderInk(p: CursorPath): string {\n switch (p.role) {\n case 'ink':\n return `<path d=\"${p.d}\" fill=\"${CURSOR_INK}\"/>`;\n case 'stroke':\n return (\n `<path d=\"${p.d}\" fill=\"none\" stroke=\"${CURSOR_INK}\"` +\n ` stroke-width=\"${p.width}\" stroke-linecap=\"round\"` +\n ` stroke-linejoin=\"round\"/>`\n );\n case 'detail':\n return (\n `<path d=\"${p.d}\" fill=\"none\" stroke=\"${CURSOR_HALO}\"` +\n ` stroke-width=\"${p.width}\" stroke-linecap=\"round\"/>`\n );\n case 'accent':\n return `<path d=\"${p.d}\" fill=\"${p.fill}\"/>`;\n }\n}\n\n/**\n * Render a glyph to a CSS cursor value.\n *\n * Ships SVG rather than a bitmap because Chrome rasterizes an SVG data-URI\n * cursor at device scale — it is already crisp on a retina display, so there\n * is no PNG pipeline and no `image-set()` here. See the spec.\n */\nexport function bakeCursor(glyph: CursorGlyph, opts: BakeOptions = {}): string {\n const size = opts.size ?? 24;\n if (size > CURSOR_MAX_CSS_PX) {\n throw new RangeError(\n `cursor size ${size} exceeds the ${CURSOR_MAX_CSS_PX}px cap: the browser ` +\n `would drop the image and silently fall back. Use the painted tier.`,\n );\n }\n // Halos first, then ink; within each pass, source order is z-order.\n const body = glyph.paths.map(renderHalo).join('') + glyph.paths.map(renderInk).join('');\n const svg =\n `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${size}\" height=\"${size}\"` +\n ` viewBox=\"0 0 ${glyph.box} ${glyph.box}\">${body}</svg>`;\n const hx = Math.round((glyph.hotspot[0] / glyph.box) * size);\n const hy = Math.round((glyph.hotspot[1] / glyph.box) * size);\n const uri = `data:image/svg+xml,${encodeURIComponent(svg)}`;\n return `url(\"${uri}\") ${hx} ${hy}, ${opts.fallback ?? 'default'}`;\n}\n","// GENERATED by scripts/gen-cursors.mjs — do not edit by hand.\n// Filled silhouettes on a 24-unit box; the halo is applied at bake time.\n\nimport type { CursorGlyph } from './types';\n\nexport const GLYPHS = {\n \"pencil\": {\n \"box\": 24,\n \"hotspot\": [\n 5,\n 19\n ],\n \"paths\": [\n {\n \"role\": \"ink\",\n \"d\": \"M 5 19 L 7.5 16.5 L 16 8 L 19 11 L 10.5 19.5 L 8 19 Z\"\n },\n {\n \"role\": \"detail\",\n \"d\": \"M 14 6 L 19 11\",\n \"width\": 1.2\n }\n ]\n },\n \"pen\": {\n \"box\": 24,\n \"hotspot\": [\n 5,\n 19\n ],\n \"paths\": [\n {\n \"role\": \"ink\",\n \"d\": \"M 5 19 L 8.5 9.5 L 13 5 L 18 10 L 13.5 14.5 Z\"\n },\n {\n \"role\": \"detail\",\n \"d\": \"M 8.4 13.9 L 12.2 10.1\",\n \"width\": 0.9\n }\n ]\n },\n \"eyedropper\": {\n \"box\": 24,\n \"hotspot\": [\n 5,\n 19\n ],\n \"paths\": [\n {\n \"role\": \"ink\",\n \"d\": \"M 5 19 L 6.8 14.8 L 14.4 7.2 L 16.8 9.6 L 9.2 17.2 Z\"\n },\n {\n \"role\": \"ink\",\n \"d\": \"M 18.2 2.6 A 3.2 3.2 0 1 0 18.2 9 A 3.2 3.2 0 1 0 18.2 2.6 Z\"\n }\n ]\n }\n} as const satisfies Record<string, CursorGlyph>;\n\n/** Every glyph name in the set. */\nexport type CursorGlyphName = keyof typeof GLYPHS;\n","import { bakeCursor } from './bake';\nimport type { BakeOptions } from './bake';\nimport { GLYPHS } from './glyphs';\nimport type { CursorGlyphName } from './glyphs';\nimport type { CursorGlyph } from './types';\n\nconst cache = new Map<string, string>();\n\n/**\n * The baked cursor string for a named glyph, memoized.\n *\n * The key space is bounded by the glyph set times the handful of sizes and\n * fallbacks in use, so the cache needs no eviction.\n */\nexport function cursorFor(name: CursorGlyphName, opts: BakeOptions = {}): string {\n const size = opts.size ?? 24;\n const fallback = opts.fallback ?? 'default';\n const key = `${name}|${size}|${fallback}`;\n const hit = cache.get(key);\n if (hit !== undefined) return hit;\n\n // Widened deliberately: the key type says this is always defined, but the\n // guard is what makes a bad name from untyped JS throw instead of baking\n // `undefined` into a cursor string that silently does nothing.\n const glyph = (GLYPHS as Record<string, CursorGlyph | undefined>)[name];\n if (glyph === undefined) {\n throw new Error(`unknown cursor glyph: ${String(name)}`);\n }\n const css = bakeCursor(glyph, { size, fallback });\n cache.set(key, css);\n return css;\n}\n"]}
1
+ {"version":3,"sources":["../src/types.ts","../src/paint.ts","../src/bake.ts","../src/glyphs.ts","../src/registry.ts","../src/resolve.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * A cursor glyph: SVG path `d` strings tagged with a paint role, plus the\n * hotspot in glyph units. The same record feeds both the data-URI baker and\n * the Path2D painter, so `d` is the one geometry form neither has to translate.\n */\nexport interface CursorGlyph {\n /** Side of the square viewBox the paths are authored in. */\n readonly box: number;\n /** Hotspot in glyph units, scaled to integer CSS px at bake time. */\n readonly hotspot: readonly [number, number];\n /**\n * The glyph-unit radius a `worldRadius` spec means, for glyphs that measure\n * something — a brush ring reads as a lie about the brush size unless the\n * painter knows which circle in the drawing is the one being sized. Defaults\n * to the box's inscribed radius.\n */\n readonly radius?: number;\n readonly paths: readonly CursorPath[];\n}\n\nexport type CursorPath =\n /** A filled part of the silhouette. */\n | { readonly role: 'ink'; readonly d: string }\n /** An unfilled part of the silhouette — a handle, a wire, an arc. */\n | { readonly role: 'stroke'; readonly d: string; readonly width: number }\n /** A division inside the silhouette, drawn in the halo color. */\n | { readonly role: 'detail'; readonly d: string; readonly width: number }\n /** A literal color, for glyphs that carry a swatch. */\n | { readonly role: 'accent'; readonly d: string; readonly fill: string };\n\n/**\n * Ink and halo are constants of the register rather than parameters: a\n * self-contrasting glyph reads on white paper, dark chrome and mid-tone\n * artwork alike precisely because it does not track the theme.\n */\nexport const CURSOR_INK = '#141418';\nexport const CURSOR_HALO = '#ffffff';\nexport const CURSOR_HALO_WIDTH = 2.6;\n\n/**\n * Rotation steps a baked angle is quantized to. Below 16 the cursor visibly\n * snaps against a smoothly rotating selection; above it the cache grows for no\n * perceptible gain.\n */\nexport const CURSOR_ANGLE_STEPS = 16;\n\n/**\n * Chrome silently drops a cursor image above this size and falls back to the\n * keyword after the comma, with no error anywhere. Measured on Chrome 152 /\n * macOS 26.5; see the spec's \"Measured browser behavior\".\n */\nexport const CURSOR_MAX_CSS_PX = 128;\n\n/**\n * Visits the points that bound an authored `d` string, for the fit guards below.\n *\n * Command-aware on purpose. Scraping every number out of the string instead\n * reads an arc's radii and its three flags as coordinates, which reports a\n * bogus `0` for every `A` ever written.\n *\n * An arc is bounded by the circle it lies on: its centre is recovered from the\n * endpoints and radius, then the whole circle is admitted. Expanding the arc's\n * *endpoint* by the radii instead over-reports whenever an endpoint sits at an\n * extreme of the circle, which is exactly where the half-circle idiom these\n * glyphs use puts it.\n *\n * The authored dialect is absolute `M`/`L`/`A`/`Z` with circular arcs. A\n * relative command would be measured as if absolute and silently under-report,\n * so it throws.\n */\nfunction walk(d: string, see: (x: number, y: number) => void): void {\n let cx = 0;\n let cy = 0;\n for (const [, cmd, args] of d.matchAll(/([A-Za-z])([^A-Za-z]*)/g)) {\n if (cmd !== cmd.toUpperCase()) {\n throw new Error(`cursor glyph path uses a relative command '${cmd}': ${d}`);\n }\n const nums = (args.match(/-?\\d+(?:\\.\\d+)?/g) ?? []).map(Number);\n if (cmd === 'Z') continue;\n if (cmd === 'A') {\n for (let i = 0; i + 7 <= nums.length; i += 7) {\n const [rx, ry, , largeArc, sweep, x, y] = nums.slice(i, i + 7);\n const r = Math.max(rx, ry);\n const dx = x - cx;\n const dy = y - cy;\n const half = Math.hypot(dx, dy) / 2;\n // Centre sits off the chord midpoint by this much, perpendicular to it.\n const off = Math.sqrt(Math.max(0, r * r - half * half));\n const sign = largeArc === sweep ? -1 : 1;\n const ux = half > 0 ? -dy / (half * 2) : 0;\n const uy = half > 0 ? dx / (half * 2) : 0;\n const ox = (cx + x) / 2 + sign * off * ux;\n const oy = (cy + y) / 2 + sign * off * uy;\n see(ox - r, oy - r);\n see(ox + r, oy - r);\n see(ox - r, oy + r);\n see(ox + r, oy + r);\n cx = x;\n cy = y;\n }\n continue;\n }\n // M and L: plain coordinate pairs.\n for (let i = 0; i + 2 <= nums.length; i += 2) {\n cx = nums[i];\n cy = nums[i + 1];\n see(cx, cy);\n }\n }\n}\n\n/**\n * True when every authored path sits at least half a halo stroke inside the\n * viewBox. A clipped halo is invisible at proof size and flattens the glyph's\n * outline at cursor size, so it is worth failing loudly at authoring time\n * rather than discovering it on a dark background.\n */\nexport function haloFitsInBox(glyph: CursorGlyph): boolean {\n const margin = CURSOR_HALO_WIDTH / 2;\n let ok = true;\n for (const p of glyph.paths) {\n walk(p.d, (x, y) => {\n for (const v of [x, y]) {\n if (v < margin || v > glyph.box - margin) ok = false;\n }\n });\n }\n return ok;\n}\n\n/**\n * True when the glyph still fits its viewBox at every rotation — i.e. every\n * authored point is inside the box's inscribed circle, halo included.\n *\n * Bake rotates about the box centre in a viewBox that does not grow, so a\n * glyph that merely satisfies {@link haloFitsInBox} can have its corners\n * sheared off partway round. The loss is a few pixels at cursor size and\n * invisible until someone looks at exactly the wrong angle, which is why it\n * is worth failing at authoring time.\n */\nexport function rotationFitsInBox(glyph: CursorGlyph): boolean {\n const c = glyph.box / 2;\n const limit = c - CURSOR_HALO_WIDTH / 2;\n let ok = true;\n for (const p of glyph.paths) {\n walk(p.d, (x, y) => {\n if (Math.hypot(x - c, y - c) > limit) ok = false;\n });\n }\n return ok;\n}\n","import { CURSOR_HALO, CURSOR_HALO_WIDTH, CURSOR_INK } from './types';\nimport type { CursorGlyph, CursorPath } from './types';\n\n/**\n * One path of a glyph, with the paint the register gives it.\n *\n * Deliberately not a renderer's own type: `@weasel-js/cursor` decides *what*\n * to paint and in what order, and leaves *how* to whoever is drawing — the\n * baker turns each op into an SVG element, the painted-tier layer turns it\n * into a draw command. Both get the same list, which is what makes the two\n * tiers show the same glyph rather than two drawings that resemble each other.\n *\n * Stroked ops are round-capped and round-joined; nothing in the register\n * wants a mitre.\n */\nexport interface CursorPaintOp {\n readonly d: string;\n readonly fill?: string;\n readonly stroke?: { readonly color: string; readonly width: number };\n}\n\nexport interface PaintOptions {\n /**\n * Multiplies every stroke width, leaving geometry alone. Default 1.\n *\n * A world-sized glyph passes the reciprocal of its scale here so line weight\n * stays chrome-weight in CSS px: a brush ring at a 400px radius otherwise\n * carries a 40px-thick stroke and a halo to match.\n */\n readonly lineWidthScale?: number;\n}\n\n/**\n * The halo pass — every silhouette member drawn once, wide, in halo colour\n * before any ink lands.\n *\n * One pass under everything rather than a per-path `paint-order`: with each\n * path haloing itself, a later path's halo cuts a white trench through an\n * earlier path's fill wherever the two overlap.\n */\nfunction halo(p: CursorPath, k: number): CursorPaintOp | null {\n switch (p.role) {\n case 'ink':\n case 'accent':\n return { d: p.d, fill: CURSOR_HALO, stroke: { color: CURSOR_HALO, width: CURSOR_HALO_WIDTH * k } };\n case 'stroke':\n return { d: p.d, stroke: { color: CURSOR_HALO, width: (p.width + CURSOR_HALO_WIDTH) * k } };\n // A detail IS halo-coloured and sits on top of the ink; it has no halo.\n case 'detail':\n return null;\n }\n}\n\nfunction ink(p: CursorPath, k: number): CursorPaintOp {\n switch (p.role) {\n case 'ink':\n return { d: p.d, fill: CURSOR_INK };\n case 'stroke':\n return { d: p.d, stroke: { color: CURSOR_INK, width: p.width * k } };\n case 'detail':\n return { d: p.d, stroke: { color: CURSOR_HALO, width: p.width * k } };\n case 'accent':\n return { d: p.d, fill: p.fill };\n }\n}\n\n/** Every paint a glyph needs, in draw order: all halos, then all ink. */\nexport function cursorPaintOps(glyph: CursorGlyph, opts: PaintOptions = {}): CursorPaintOp[] {\n const k = opts.lineWidthScale ?? 1;\n const out: CursorPaintOp[] = [];\n for (const p of glyph.paths) {\n const h = halo(p, k);\n if (h) out.push(h);\n }\n for (const p of glyph.paths) out.push(ink(p, k));\n return out;\n}\n\n/** An affine `[a, b, c, d, e, f]`: `x' = ax + cy + e`, `y' = bx + dy + f`. */\nexport type CursorMatrix = readonly [number, number, number, number, number, number];\n\nexport interface PaintPlacement {\n /** Rendered size in CSS px, as `bakeCursor` means it. */\n readonly size: number;\n /** Clockwise rotation in radians. Default 0. */\n readonly angle?: number;\n /** Where the hotspot goes — the pointer, in the layer's own coords. */\n readonly at: { readonly x: number; readonly y: number };\n}\n\n/**\n * Where a glyph's own units land, to place it under the pointer at a size and\n * an angle.\n *\n * Rotation is about the hotspot, not the box centre. That is the same\n * placement the baker produces — it turns the glyph about the centre and then\n * carries the hotspot around with it, and the centre cancels — but it is worth\n * stating, because a painter that rotates about the centre and forgets to move\n * the hotspot swings the glyph around a point the user never sees.\n *\n * Unquantized on purpose: the 22.5° step exists to bound the bake cache, and\n * the painter has no cache to bound. A painted cursor turns smoothly.\n */\nexport function cursorPaintMatrix(glyph: CursorGlyph, placement: PaintPlacement): CursorMatrix {\n const s = placement.size / glyph.box;\n const theta = placement.angle ?? 0;\n // y is down, so a positive angle carries +x toward +y — clockwise on screen.\n const a = s * Math.cos(theta);\n const b = s * Math.sin(theta);\n const [hx, hy] = glyph.hotspot;\n return [a, b, -b, a, placement.at.x - (a * hx - b * hy), placement.at.y - (b * hx + a * hy)];\n}\n\n/**\n * The CSS-px size a world-sized glyph is drawn at, so the circle its `radius`\n * names measures `worldRadius` world units on screen.\n *\n * A glyph with no `radius` is measured by its inscribed circle. Getting this\n * wrong scales every brush cursor by a constant factor, which reads as a\n * slightly-off cursor rather than as a bug.\n */\nexport function cursorWorldSize(\n glyph: CursorGlyph,\n worldRadius: number,\n scale: number,\n): number {\n const r = glyph.radius ?? glyph.box / 2;\n return (worldRadius * scale * glyph.box) / r;\n}\n\n/**\n * The `lineWidthScale` that keeps a glyph's line weight at chrome weight while\n * its geometry scales — what a world-sized glyph wants, and what a fixed-size\n * one must not have (there, weight scaling is what makes the painted tier\n * match the baked one).\n */\nexport function chromeLineWidthScale(glyph: CursorGlyph, size: number): number {\n return glyph.box / size;\n}\n","import { cursorPaintOps } from './paint';\nimport type { CursorPaintOp } from './paint';\nimport { CURSOR_ANGLE_STEPS, CURSOR_MAX_CSS_PX } from './types';\nimport type { CursorGlyph } from './types';\n\nexport interface BakeOptions {\n /** Rendered size in CSS px. Default 24. */\n readonly size?: number;\n /** Clockwise rotation in radians about the box centre, quantized to\n * {@link CURSOR_ANGLE_STEPS} steps. Default 0. */\n readonly angle?: number;\n /** Keyword drawn if the browser rejects the image. Default 'default'. */\n readonly fallback?: string;\n}\n\n/**\n * Index of the quantization step an angle falls in, in `[0, CURSOR_ANGLE_STEPS)`.\n *\n * Exported because the bake cache keys on this rather than on the raw angle:\n * the hover pump feeds a continuously varying selection rotation, and two\n * angles in the same step must be one cache entry, not two.\n */\nexport function quantizeCursorAngle(angle: number): number {\n const i = Math.round((angle / (Math.PI * 2)) * CURSOR_ANGLE_STEPS);\n return ((i % CURSOR_ANGLE_STEPS) + CURSOR_ANGLE_STEPS) % CURSOR_ANGLE_STEPS;\n}\n\n/** One paint op as an SVG element. */\nfunction renderOp(op: CursorPaintOp): string {\n const fill = op.fill === undefined ? 'none' : op.fill;\n const stroke = op.stroke\n ? ` stroke=\"${op.stroke.color}\" stroke-width=\"${op.stroke.width}\"` +\n ` stroke-linecap=\"round\" stroke-linejoin=\"round\"`\n : '';\n return `<path d=\"${op.d}\" fill=\"${fill}\"${stroke}/>`;\n}\n\n/**\n * Render a glyph to a CSS cursor value.\n *\n * Ships SVG rather than a bitmap because Chrome rasterizes an SVG data-URI\n * cursor at device scale — it is already crisp on a retina display, so there\n * is no PNG pipeline and no `image-set()` here. See the spec.\n */\nexport function bakeCursor(glyph: CursorGlyph, opts: BakeOptions = {}): string {\n const size = opts.size ?? 24;\n if (size > CURSOR_MAX_CSS_PX) {\n throw new RangeError(\n `cursor size ${size} exceeds the ${CURSOR_MAX_CSS_PX}px cap: the browser ` +\n `would drop the image and silently fall back. Use the painted tier.`,\n );\n }\n // Halos first, then ink; within each pass, source order is z-order. The\n // pass structure is `paint.ts`'s, so the painted tier draws the same glyph\n // rather than a second drawing that resembles it.\n const paths = cursorPaintOps(glyph).map(renderOp).join('');\n\n const c = glyph.box / 2;\n const step = quantizeCursorAngle(opts.angle ?? 0);\n const theta = (step / CURSOR_ANGLE_STEPS) * Math.PI * 2;\n // A whole-glyph transform rather than rotated path data: the same record\n // has to reach `paint.ts` unrotated, and a `d` string is not re-authorable.\n const body = step === 0\n ? paths\n : `<g transform=\"rotate(${(step * 360) / CURSOR_ANGLE_STEPS} ${c} ${c})\">${paths}</g>`;\n const svg =\n `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${size}\" height=\"${size}\"` +\n ` viewBox=\"0 0 ${glyph.box} ${glyph.box}\">${body}</svg>`;\n // The hotspot travels with the glyph. Left behind, a rotated arrow would\n // point from its tail.\n const dx = glyph.hotspot[0] - c;\n const dy = glyph.hotspot[1] - c;\n const cos = Math.cos(theta);\n const sin = Math.sin(theta);\n const hx = Math.round(((c + cos * dx - sin * dy) / glyph.box) * size);\n const hy = Math.round(((c + sin * dx + cos * dy) / glyph.box) * size);\n const uri = `data:image/svg+xml,${encodeURIComponent(svg)}`;\n return `url(\"${uri}\") ${hx} ${hy}, ${opts.fallback ?? 'default'}`;\n}\n","// GENERATED by scripts/gen-cursors.mjs — do not edit by hand.\n// Filled silhouettes on a 24-unit box; the halo is applied at bake time.\n\nimport type { CursorGlyph } from './types';\n\nexport const GLYPHS = {\n \"pencil\": {\n \"box\": 24,\n \"hotspot\": [\n 5,\n 19\n ],\n \"paths\": [\n {\n \"role\": \"ink\",\n \"d\": \"M 5 19 L 7.5 16.5 L 16 8 L 19 11 L 10.5 19.5 L 8 19 Z\"\n },\n {\n \"role\": \"detail\",\n \"d\": \"M 14 6 L 19 11\",\n \"width\": 1.2\n }\n ]\n },\n \"pen\": {\n \"box\": 24,\n \"hotspot\": [\n 5,\n 19\n ],\n \"paths\": [\n {\n \"role\": \"ink\",\n \"d\": \"M 5 19 L 8.5 9.5 L 13 5 L 18 10 L 13.5 14.5 Z\"\n },\n {\n \"role\": \"detail\",\n \"d\": \"M 8.4 13.9 L 12.2 10.1\",\n \"width\": 0.9\n }\n ]\n },\n \"eyedropper\": {\n \"box\": 24,\n \"hotspot\": [\n 5,\n 19\n ],\n \"paths\": [\n {\n \"role\": \"ink\",\n \"d\": \"M 5 19 L 6.8 14.8 L 14.4 7.2 L 16.8 9.6 L 9.2 17.2 Z\"\n },\n {\n \"role\": \"ink\",\n \"d\": \"M 18.2 2.6 A 3.2 3.2 0 1 0 18.2 9 A 3.2 3.2 0 1 0 18.2 2.6 Z\"\n }\n ]\n },\n \"brush\": {\n \"box\": 24,\n \"hotspot\": [\n 12,\n 12\n ],\n \"radius\": 9.9,\n \"paths\": [\n {\n \"role\": \"stroke\",\n \"d\": \"M 12 2.1 A 9.9 9.9 0 1 0 12 21.9 A 9.9 9.9 0 1 0 12 2.1 Z\",\n \"width\": 1.6\n }\n ]\n },\n \"crosshairRect\": {\n \"box\": 24,\n \"hotspot\": [\n 9,\n 9\n ],\n \"paths\": [\n {\n \"role\": \"stroke\",\n \"d\": \"M 1.6 9 L 6.4 9\",\n \"width\": 1.6\n },\n {\n \"role\": \"stroke\",\n \"d\": \"M 11.6 9 L 16.4 9\",\n \"width\": 1.6\n },\n {\n \"role\": \"stroke\",\n \"d\": \"M 9 1.6 L 9 6.4\",\n \"width\": 1.6\n },\n {\n \"role\": \"stroke\",\n \"d\": \"M 9 11.6 L 9 16.4\",\n \"width\": 1.6\n },\n {\n \"role\": \"ink\",\n \"d\": \"M 14.2 14.2 L 21 14.2 L 21 21 L 14.2 21 Z\"\n }\n ]\n },\n \"crosshairEllipse\": {\n \"box\": 24,\n \"hotspot\": [\n 9,\n 9\n ],\n \"paths\": [\n {\n \"role\": \"stroke\",\n \"d\": \"M 1.6 9 L 6.4 9\",\n \"width\": 1.6\n },\n {\n \"role\": \"stroke\",\n \"d\": \"M 11.6 9 L 16.4 9\",\n \"width\": 1.6\n },\n {\n \"role\": \"stroke\",\n \"d\": \"M 9 1.6 L 9 6.4\",\n \"width\": 1.6\n },\n {\n \"role\": \"stroke\",\n \"d\": \"M 9 11.6 L 9 16.4\",\n \"width\": 1.6\n },\n {\n \"role\": \"ink\",\n \"d\": \"M 17.6 13.9 A 3.7 3.7 0 1 0 17.6 21.3 A 3.7 3.7 0 1 0 17.6 13.9 Z\"\n }\n ]\n },\n \"crosshairLine\": {\n \"box\": 24,\n \"hotspot\": [\n 9,\n 9\n ],\n \"paths\": [\n {\n \"role\": \"stroke\",\n \"d\": \"M 1.6 9 L 6.4 9\",\n \"width\": 1.6\n },\n {\n \"role\": \"stroke\",\n \"d\": \"M 11.6 9 L 16.4 9\",\n \"width\": 1.6\n },\n {\n \"role\": \"stroke\",\n \"d\": \"M 9 1.6 L 9 6.4\",\n \"width\": 1.6\n },\n {\n \"role\": \"stroke\",\n \"d\": \"M 9 11.6 L 9 16.4\",\n \"width\": 1.6\n },\n {\n \"role\": \"stroke\",\n \"d\": \"M 14.2 21 L 21 14.2\",\n \"width\": 2.2\n }\n ]\n },\n \"crosshairStar\": {\n \"box\": 24,\n \"hotspot\": [\n 9,\n 9\n ],\n \"paths\": [\n {\n \"role\": \"stroke\",\n \"d\": \"M 1.6 9 L 6.4 9\",\n \"width\": 1.6\n },\n {\n \"role\": \"stroke\",\n \"d\": \"M 11.6 9 L 16.4 9\",\n \"width\": 1.6\n },\n {\n \"role\": \"stroke\",\n \"d\": \"M 9 1.6 L 9 6.4\",\n \"width\": 1.6\n },\n {\n \"role\": \"stroke\",\n \"d\": \"M 9 11.6 L 9 16.4\",\n \"width\": 1.6\n },\n {\n \"role\": \"ink\",\n \"d\": \"M 17.6 13.4 L 18.72 16.06 L 21.59 16.3 L 19.41 18.19 L 20.07 21 L 17.6 19.5 L 15.13 21 L 15.79 18.19 L 13.61 16.3 L 16.48 16.06 Z\"\n }\n ]\n },\n \"crosshairPolygon\": {\n \"box\": 24,\n \"hotspot\": [\n 9,\n 9\n ],\n \"paths\": [\n {\n \"role\": \"stroke\",\n \"d\": \"M 1.6 9 L 6.4 9\",\n \"width\": 1.6\n },\n {\n \"role\": \"stroke\",\n \"d\": \"M 11.6 9 L 16.4 9\",\n \"width\": 1.6\n },\n {\n \"role\": \"stroke\",\n \"d\": \"M 9 1.6 L 9 6.4\",\n \"width\": 1.6\n },\n {\n \"role\": \"stroke\",\n \"d\": \"M 9 11.6 L 9 16.4\",\n \"width\": 1.6\n },\n {\n \"role\": \"ink\",\n \"d\": \"M 17.6 13.5 L 21.5 16.33 L 20.01 20.92 L 15.19 20.92 L 13.7 16.33 Z\"\n }\n ]\n },\n \"resize\": {\n \"box\": 24,\n \"hotspot\": [\n 12,\n 12\n ],\n \"paths\": [\n {\n \"role\": \"ink\",\n \"d\": \"M 2.5 12 L 7 8.8 L 7 10.7 L 17 10.7 L 17 8.8 L 21.5 12 L 17 15.2 L 17 13.3 L 7 13.3 L 7 15.2 Z\"\n }\n ]\n },\n \"rotate\": {\n \"box\": 24,\n \"hotspot\": [\n 12,\n 12\n ],\n \"paths\": [\n {\n \"role\": \"stroke\",\n \"d\": \"M 5.8 9.74 A 6.6 6.6 0 0 1 18.2 9.74\",\n \"width\": 2.8\n },\n {\n \"role\": \"ink\",\n \"d\": \"M 4.22 14.07 L 2.98 8.72 L 8.62 10.77 Z\"\n },\n {\n \"role\": \"ink\",\n \"d\": \"M 19.78 14.07 L 15.38 10.77 L 21.02 8.72 Z\"\n }\n ]\n }\n} as const satisfies Record<string, CursorGlyph>;\n\n/** Every glyph name in the set. */\nexport type CursorGlyphName = keyof typeof GLYPHS;\n","import { bakeCursor, quantizeCursorAngle } from './bake';\nimport type { BakeOptions } from './bake';\nimport { GLYPHS } from './glyphs';\nimport type { CursorGlyphName } from './glyphs';\nimport type { CursorGlyph } from './types';\n\nconst cache = new Map<string, string>();\n\n/**\n * The baked cursor string for a named glyph, memoized.\n *\n * The key space is bounded by the glyph set times 16 angles times the handful\n * of sizes and fallbacks in use, so the cache needs no eviction.\n */\nexport function cursorFor(name: CursorGlyphName, opts: BakeOptions = {}): string {\n const size = opts.size ?? 24;\n const fallback = opts.fallback ?? 'default';\n // The step index, not the raw angle: the hover pump feeds a continuously\n // varying selection rotation, so a raw-angle key would never hit.\n const step = quantizeCursorAngle(opts.angle ?? 0);\n const key = `${name}|${size}|${step}|${fallback}`;\n const hit = cache.get(key);\n if (hit !== undefined) return hit;\n\n // Widened deliberately: the key type says this is always defined, but the\n // guard is what makes a bad name from untyped JS throw instead of baking\n // `undefined` into a cursor string that silently does nothing.\n const glyph = (GLYPHS as Record<string, CursorGlyph | undefined>)[name];\n if (glyph === undefined) {\n throw new Error(`unknown cursor glyph: ${String(name)}`);\n }\n const css = bakeCursor(glyph, { size, fallback, angle: opts.angle ?? 0 });\n cache.set(key, css);\n return css;\n}\n","import { cursorFor } from './registry';\nimport type { CursorGlyphName } from './glyphs';\nimport { CURSOR_MAX_CSS_PX } from './types';\n\n/**\n * What a tool, action or affordance declares as its cursor.\n *\n * A bare string is a CSS cursor value and passes through untouched, so every\n * declaration written before this type existed keeps working.\n */\nexport type CursorSpec = string | CursorGlyphSpec;\n\n/** The glyph form of a {@link CursorSpec}. */\nexport interface CursorGlyphSpec {\n readonly glyph: CursorGlyphName;\n /** Rendered size in CSS px. Default 24. */\n readonly size?: number;\n /**\n * Size in world units instead of `size`, so the glyph tracks zoom — a brush\n * radius ring. Forces the painted tier at every zoom level, because a baked\n * image would have to be rebuilt on every wheel tick.\n */\n readonly worldRadius?: number;\n /** Clockwise rotation in radians, quantized to 22.5° at bake. */\n readonly angle?: number;\n /** Keyword drawn if the browser rejects the image. Default 'default'. */\n readonly fallback?: string;\n}\n\n/**\n * Which tier a spec reaches the screen through.\n *\n * Tools never choose this — they declare what they want and the resolver\n * picks, so a brush cursor stays a brush cursor across the radius where it\n * stops being expressible as a CSS cursor at all.\n */\nexport type ResolvedCursor =\n | { readonly kind: 'css'; readonly css: string }\n | {\n readonly kind: 'painted';\n readonly glyph: CursorGlyphName;\n readonly angle: number;\n /** CSS px, when the spec fixed one. */\n readonly size?: number;\n /** World units; the painter scales this by the live view. */\n readonly worldRadius?: number;\n };\n\n/**\n * Resolve a spec to the tier that can actually draw it.\n *\n * Escalates to painted when the glyph is sized in world units, or when its\n * fixed size is past {@link CURSOR_MAX_CSS_PX} — above which the browser drops\n * the image and silently falls back to the keyword.\n */\nexport function resolveCursorTier(spec: CursorSpec): ResolvedCursor {\n if (typeof spec === 'string') return { kind: 'css', css: spec };\n const { glyph, size, worldRadius, angle = 0, fallback } = spec;\n if (worldRadius !== undefined) {\n return { kind: 'painted', glyph, angle, worldRadius };\n }\n if (size !== undefined && size > CURSOR_MAX_CSS_PX) {\n return { kind: 'painted', glyph, angle, size };\n }\n return {\n kind: 'css',\n css: cursorFor(glyph, {\n ...(size !== undefined ? { size } : {}),\n ...(fallback !== undefined ? { fallback } : {}),\n angle,\n }),\n };\n}\n\n/**\n * A {@link CursorSpec} as a CSS cursor value.\n *\n * Every consumer of a widened cursor field calls this, so a declaration never\n * has to know which form it holds. A painted cursor answers `'none'` — the\n * native cursor gets out of the way and the painted-tier layer draws it. A\n * caller that writes that string without the layer installed leaves the user\n * with no cursor, so reach for {@link resolveCursorTier} if you need to know.\n */\nexport function resolveCursor(spec: CursorSpec | undefined): string | undefined {\n if (spec === undefined) return undefined;\n const r = resolveCursorTier(spec);\n return r.kind === 'css' ? r.css : 'none';\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@weasel-js/cursor",
3
- "version": "1.4.0-pre.1",
3
+ "version": "1.4.0",
4
4
  "description": "Tool cursors as authored glyphs: bake one to a CSS url() cursor string, or paint it when it is too big to be one.",
5
5
  "license": "MIT",
6
6
  "type": "module",