@tremolo-ui/dom 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1,14 +1,266 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let _tremolo_ui_functions = require("@tremolo-ui/functions");
3
+ //#region src/canvas/context.ts
4
+ /**
5
+ * The assignable parts of a 2D context's drawing state. They have to be
6
+ * carried across a resize by hand because setting `canvas.width` resets the
7
+ * context to its defaults. The transform and line dash are handled separately
8
+ * by {@link DrawingContext} because they are exposed through methods.
9
+ *
10
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/save
11
+ */
12
+ const drawingState = [
13
+ "strokeStyle",
14
+ "fillStyle",
15
+ "globalAlpha",
16
+ "lineWidth",
17
+ "lineCap",
18
+ "lineJoin",
19
+ "miterLimit",
20
+ "lineDashOffset",
21
+ "shadowOffsetX",
22
+ "shadowOffsetY",
23
+ "shadowBlur",
24
+ "shadowColor",
25
+ "globalCompositeOperation",
26
+ "filter",
27
+ "font",
28
+ "fontKerning",
29
+ "fontStretch",
30
+ "fontVariantCaps",
31
+ "textAlign",
32
+ "textBaseline",
33
+ "direction",
34
+ "letterSpacing",
35
+ "textRendering",
36
+ "wordSpacing",
37
+ "imageSmoothingEnabled",
38
+ "imageSmoothingQuality"
39
+ ];
40
+ function isDrawingState(value) {
41
+ return typeof value === "string" && drawingState.includes(value);
42
+ }
43
+ /** Copy the drawing state off a context, to put back after a resize. */
44
+ function readDrawingState(context) {
45
+ const state = {};
46
+ for (const property of drawingState) state[property] = context[property];
47
+ state.lineDash = context.getLineDash();
48
+ state.transform = context.getTransform();
49
+ return state;
50
+ }
51
+ /** Put a state read by {@link readDrawingState} back onto a context. */
52
+ function writeDrawingState(context, state, transformScale = 1) {
53
+ for (const property of drawingState) context[property] = state[property];
54
+ context.setLineDash(state.lineDash);
55
+ const { a, b, c, d, e, f } = state.transform;
56
+ context.setTransform(a * transformScale, b * transformScale, c * transformScale, d * transformScale, e * transformScale, f * transformScale);
57
+ }
58
+ /**
59
+ * Give the canvas a backing store of `width * dpr` by `height * dpr` device
60
+ * pixels while keeping it `width` by `height` in CSS pixels, and scale the
61
+ * context so that drawing code can work in CSS pixels throughout.
62
+ *
63
+ * Assigning to `canvas.width` clears the canvas and resets the context, so
64
+ * this both resizes and re-establishes the transform.
65
+ */
66
+ function applyDevicePixelRatio(canvas, context, width, height, dpr) {
67
+ canvas.width = width * dpr;
68
+ canvas.height = height * dpr;
69
+ context.setTransform(1, 0, 0, 1, 0, 0);
70
+ context.scale(dpr, dpr);
71
+ canvas.style.width = `${width}px`;
72
+ canvas.style.height = `${height}px`;
73
+ }
74
+ //#endregion
75
+ //#region src/canvas/animation.ts
76
+ /**
77
+ * Drive a canvas from `requestAnimationFrame`, keeping its backing store in
78
+ * step with the device pixel ratio and, optionally, with the size of its
79
+ * parent.
80
+ *
81
+ * Drawing code works in CSS pixels: the context is scaled by the device pixel
82
+ * ratio, and `width` / `height` of each {@link AnimationFrame} are CSS pixels.
83
+ */
84
+ function createAnimationCanvas(canvas, options) {
85
+ let opts = options;
86
+ const context2d = canvas.getContext("2d", opts.contextAttributes);
87
+ if (!context2d) throw new Error("createAnimationCanvas: cannot get a 2d context");
88
+ const context = context2d;
89
+ const resizable = opts.resizable ?? false;
90
+ /** Off-document canvas holding the drawing while the real one is resized. */
91
+ let memo = null;
92
+ let memoContext = null;
93
+ let width = 0;
94
+ let height = 0;
95
+ let appliedDpr = 0;
96
+ /** Whether a size has been applied, so a frame can be drawn. */
97
+ let sized = false;
98
+ let initialized = false;
99
+ let frameId = null;
100
+ let count = -1;
101
+ const startTime = performance.now();
102
+ let previousTime = startTime;
103
+ function devicePixelRatio() {
104
+ return globalThis.devicePixelRatio || 1;
105
+ }
106
+ /**
107
+ * Copy the canvas onto the memo canvas at its full device resolution, 1:1.
108
+ *
109
+ * Assigning to `memo.width` resets the memo's transform to the identity, so
110
+ * the copy neither scales nor resamples.
111
+ */
112
+ function takeSnapshot() {
113
+ if (!(opts.reduceFlickering ?? true)) return false;
114
+ if (!memo) {
115
+ memo = globalThis.document?.createElement("canvas") ?? null;
116
+ memoContext = memo?.getContext("2d", opts.contextAttributes) ?? null;
117
+ }
118
+ if (!memo || !memoContext) return false;
119
+ memo.width = canvas.width;
120
+ memo.height = canvas.height;
121
+ if (canvas.width > 0 && canvas.height > 0) memoContext.drawImage(canvas, 0, 0);
122
+ return true;
123
+ }
124
+ /**
125
+ * Put the snapshot back, given the size in CSS pixels it was taken at.
126
+ *
127
+ * The context is scaled to CSS pixels, so drawing the snapshot at its old
128
+ * CSS size leaves it exactly where it was. While the device pixel ratio
129
+ * stays put that is a 1:1 copy of device pixels, and when it changes the
130
+ * snapshot is rescaled once, from its full resolution.
131
+ */
132
+ function restoreSnapshot(hasSnapshot, previousWidth, previousHeight) {
133
+ if (!hasSnapshot || !memo) return;
134
+ if (memo.width <= 0 || memo.height <= 0) return;
135
+ context.drawImage(memo, 0, 0, previousWidth, previousHeight);
136
+ }
137
+ function applySize(w, h) {
138
+ const dpr = devicePixelRatio();
139
+ const previousWidth = width;
140
+ const previousHeight = height;
141
+ const previousDpr = appliedDpr;
142
+ const state = sized ? readDrawingState(context) : null;
143
+ const hasSnapshot = sized && takeSnapshot();
144
+ applyDevicePixelRatio(canvas, context, w, h, dpr);
145
+ width = w;
146
+ height = h;
147
+ appliedDpr = dpr;
148
+ sized = true;
149
+ restoreSnapshot(hasSnapshot, previousWidth, previousHeight);
150
+ if (state) writeDrawingState(context, state, dpr / previousDpr);
151
+ }
152
+ function applyDevicePixelRatioIfNeeded() {
153
+ if (sized && devicePixelRatio() !== appliedDpr) applySize(width, height);
154
+ }
155
+ function drawFrame(timestamp = performance.now()) {
156
+ if (!sized) return;
157
+ applyDevicePixelRatioIfNeeded();
158
+ if (!initialized) {
159
+ initialized = true;
160
+ opts.init?.(context, {
161
+ width,
162
+ height
163
+ });
164
+ }
165
+ const deltaTime = timestamp - previousTime;
166
+ previousTime = timestamp;
167
+ count += 1;
168
+ opts.draw(context, {
169
+ width,
170
+ height,
171
+ count,
172
+ deltaTime,
173
+ elapsedTime: timestamp - startTime,
174
+ fps: deltaTime > 0 ? 1e3 / deltaTime : 0
175
+ });
176
+ }
177
+ function tick(timestamp) {
178
+ frameId = requestAnimationFrame(tick);
179
+ drawFrame(timestamp);
180
+ }
181
+ function startLoop() {
182
+ if (frameId !== null) return;
183
+ previousTime = performance.now();
184
+ frameId = requestAnimationFrame(tick);
185
+ }
186
+ function stopLoop() {
187
+ if (frameId === null) return;
188
+ cancelAnimationFrame(frameId);
189
+ frameId = null;
190
+ }
191
+ let observer = null;
192
+ if (resizable) {
193
+ const parent = canvas.parentElement;
194
+ if (!parent) throw new Error("createAnimationCanvas: resizable needs the canvas to have a parent element");
195
+ observer = new ResizeObserver((entries) => {
196
+ for (const entry of entries) {
197
+ const { width: w, height: h } = entry.contentRect;
198
+ if (w !== width || h !== height || devicePixelRatio() !== appliedDpr) applySize(w, h);
199
+ if (!(opts.animate ?? true)) drawFrame();
200
+ }
201
+ });
202
+ observer.observe(parent);
203
+ } else {
204
+ const { width: w = 100, height: h = 100 } = opts.size ?? {};
205
+ applySize(w, h);
206
+ }
207
+ if (opts.animate ?? true) startLoop();
208
+ else if (sized) drawFrame();
209
+ return {
210
+ update: (next) => {
211
+ const wasAnimating = opts.animate ?? true;
212
+ opts = {
213
+ ...opts,
214
+ ...next,
215
+ resizable,
216
+ contextAttributes: opts.contextAttributes
217
+ };
218
+ if (!resizable) {
219
+ const { width: w = 100, height: h = 100 } = opts.size ?? {};
220
+ if (w !== width || h !== height) applySize(w, h);
221
+ }
222
+ applyDevicePixelRatioIfNeeded();
223
+ const animating = opts.animate ?? true;
224
+ if (animating && !wasAnimating) startLoop();
225
+ if (!animating && wasAnimating) stopLoop();
226
+ if (!animating) drawFrame();
227
+ },
228
+ redraw: drawFrame,
229
+ destroy: () => {
230
+ stopLoop();
231
+ observer?.disconnect();
232
+ observer = null;
233
+ memo = null;
234
+ memoContext = null;
235
+ }
236
+ };
237
+ }
238
+ //#endregion
2
239
  //#region src/midi/access.ts
3
240
  /** @private */
4
241
  const PERMISSION_DENIED = "PERMISSION_DENIED";
5
242
  /** @private */
6
243
  const NOT_SUPPORTED = "NOT_SUPPORTED";
244
+ /** @private */
245
+ const UNAVAILABLE = "UNAVAILABLE";
7
246
  const INITIAL_STATE = {
8
247
  midiAccess: null,
9
- error: null
248
+ error: null,
249
+ inputs: []
10
250
  };
11
251
  /**
252
+ * Which of our errors a rejected `requestMIDIAccess()` amounts to.
253
+ *
254
+ * The spec names the reasons, and they need telling apart: a user who said no
255
+ * can say yes on a second ask, while a browser without the API never will.
256
+ */
257
+ function toError(reason) {
258
+ const name = typeof reason === "object" && reason !== null && "name" in reason ? String(reason.name) : "";
259
+ if (name === "SecurityError" || name === "NotAllowedError") return PERMISSION_DENIED;
260
+ if (name === "NotSupportedError" || name === "TypeError") return NOT_SUPPORTED;
261
+ return UNAVAILABLE;
262
+ }
263
+ /**
12
264
  * Request MIDI access in the browser.
13
265
  *
14
266
  * The returned instance holds the state and notifies subscribers when it changes,
@@ -17,13 +269,31 @@ const INITIAL_STATE = {
17
269
  function createMIDIAccess() {
18
270
  let state = INITIAL_STATE;
19
271
  let destroyed = false;
272
+ let access = null;
273
+ let requestGeneration = 0;
20
274
  const listeners = /* @__PURE__ */ new Set();
21
275
  function setState(next) {
22
276
  state = next;
23
277
  for (const listener of listeners) listener();
24
278
  }
25
- function request() {
279
+ /**
280
+ * A new array every time, so that `useSyncExternalStore` and its equivalents
281
+ * see the change. The identity of the state object is what they compare.
282
+ */
283
+ function readInputs() {
284
+ return access ? [...access.inputs.values()] : [];
285
+ }
286
+ function handleStateChange(event) {
287
+ if (destroyed || !access) return;
288
+ if (event && event.port?.type === "output") return;
289
+ setState({
290
+ ...state,
291
+ inputs: readInputs()
292
+ });
293
+ }
294
+ function request(options = {}) {
26
295
  if (destroyed) return;
296
+ const generation = ++requestGeneration;
27
297
  if (typeof navigator === "undefined" || !navigator.requestMIDIAccess) {
28
298
  setState({
29
299
  ...state,
@@ -31,17 +301,21 @@ function createMIDIAccess() {
31
301
  });
32
302
  return;
33
303
  }
34
- navigator.requestMIDIAccess().then((access) => {
35
- if (destroyed) return;
304
+ navigator.requestMIDIAccess({ sysex: options.sysex ?? false }).then((granted) => {
305
+ if (destroyed || generation !== requestGeneration) return;
306
+ access?.removeEventListener("statechange", handleStateChange);
307
+ access = granted;
308
+ granted.addEventListener("statechange", handleStateChange);
36
309
  setState({
37
- midiAccess: access,
38
- error: null
310
+ midiAccess: granted,
311
+ error: null,
312
+ inputs: readInputs()
39
313
  });
40
- }).catch(() => {
41
- if (destroyed) return;
314
+ }, (reason) => {
315
+ if (destroyed || generation !== requestGeneration) return;
42
316
  setState({
43
317
  ...state,
44
- error: PERMISSION_DENIED
318
+ error: toError(reason)
45
319
  });
46
320
  });
47
321
  }
@@ -57,6 +331,9 @@ function createMIDIAccess() {
57
331
  },
58
332
  destroy: () => {
59
333
  destroyed = true;
334
+ requestGeneration += 1;
335
+ access?.removeEventListener("statechange", handleStateChange);
336
+ access = null;
60
337
  listeners.clear();
61
338
  }
62
339
  };
@@ -66,35 +343,110 @@ function createMIDIAccess() {
66
343
  /**
67
344
  * Listen to raw `midimessage` events on every input of a MIDIAccess.
68
345
  *
346
+ * The set of inputs is followed rather than sampled: MIDIAccess fires
347
+ * `statechange` when a device is plugged in or unplugged, and the listeners
348
+ * move with it. A keyboard connected after access was granted works without
349
+ * the caller having to rebuild anything.
350
+ *
69
351
  * Use this when you need more detail than {@link createMIDIInput} provides.
70
352
  */
71
353
  function createMIDIMessage(midiAccess, onMIDIMessage) {
72
- if (!midiAccess) return { destroy: () => {} };
73
- const inputs = [...midiAccess.inputs.values()];
74
- for (const input of inputs) input.addEventListener("midimessage", onMIDIMessage);
75
- return { destroy: () => {
76
- for (const input of inputs) input.removeEventListener("midimessage", onMIDIMessage);
77
- } };
354
+ let handler = onMIDIMessage;
355
+ /** Inputs this instance currently listens on. */
356
+ const attached = /* @__PURE__ */ new Set();
357
+ const listener = (event) => handler(event);
358
+ function sync() {
359
+ if (!midiAccess) return;
360
+ const connected = new Set(midiAccess.inputs.values());
361
+ for (const input of connected) {
362
+ if (attached.has(input)) continue;
363
+ input.addEventListener("midimessage", listener);
364
+ attached.add(input);
365
+ }
366
+ for (const input of [...attached]) {
367
+ if (connected.has(input)) continue;
368
+ input.removeEventListener("midimessage", listener);
369
+ attached.delete(input);
370
+ }
371
+ }
372
+ sync();
373
+ midiAccess?.addEventListener("statechange", sync);
374
+ return {
375
+ update: (next) => {
376
+ handler = next;
377
+ },
378
+ destroy: () => {
379
+ midiAccess?.removeEventListener("statechange", sync);
380
+ for (const input of attached) input.removeEventListener("midimessage", listener);
381
+ attached.clear();
382
+ }
383
+ };
78
384
  }
79
385
  //#endregion
80
386
  //#region src/midi/input.ts
81
- const MIDI_EVENT_TO_NUMBER = {
82
- NOTE_ON: 144,
387
+ /** Status byte of each channel message, with the channel nibble cleared. */
388
+ const STATUS = {
83
389
  NOTE_OFF: 128,
390
+ NOTE_ON: 144,
391
+ AFTERTOUCH: 160,
392
+ CONTROL_CHANGE: 176,
393
+ PROGRAM_CHANGE: 192,
394
+ CHANNEL_PRESSURE: 208,
84
395
  PITCH_BEND: 224
85
396
  };
86
397
  /**
87
- * Handle note on/off and pitch bend events. To be used with {@link createMIDIAccess}.
88
- * Internally uses {@link createMIDIMessage}.
398
+ * Centre of the 14-bit pitch bend range: no bend.
399
+ *
400
+ * The range is not symmetric — 0 is 8192 below centre and 16383 is 8191 above
401
+ * — so a wheel at rest reports exactly this rather than half of the maximum.
89
402
  */
90
- function createMIDIInput(midiAccess, { onNoteOnEvent, onNoteOffEvent, onPitchBendEvent }) {
91
- return createMIDIMessage(midiAccess, (event) => {
92
- if (!event.data) return;
93
- const kind = event.data[0] & 240;
94
- if (kind == MIDI_EVENT_TO_NUMBER.NOTE_OFF || kind == MIDI_EVENT_TO_NUMBER.NOTE_ON && event.data[2] == 0) onNoteOffEvent?.(event.data[1]);
95
- else if (kind == MIDI_EVENT_TO_NUMBER.NOTE_ON) onNoteOnEvent?.(event.data[1], event.data[2]);
96
- else if (kind == MIDI_EVENT_TO_NUMBER.PITCH_BEND) onPitchBendEvent?.(event.data[1], event.data[2]);
97
- });
403
+ const PITCH_BEND_CENTER = 8192;
404
+ /**
405
+ * Handle the channel voice messages of every connected input. To be used with
406
+ * {@link createMIDIAccess}. Internally uses {@link createMIDIMessage}, so
407
+ * devices plugged in later are picked up.
408
+ *
409
+ * System messages (clock, sysex, and the rest of `0xf0`-`0xff`) are not
410
+ * decoded here; reach for {@link createMIDIMessage} for those.
411
+ */
412
+ function createMIDIInput(midiAccess, handlers) {
413
+ let current = handlers;
414
+ return {
415
+ update: (next) => {
416
+ current = next;
417
+ },
418
+ destroy: createMIDIMessage(midiAccess, (event) => {
419
+ if (!event.data || event.data.length === 0) return;
420
+ const status = event.data[0];
421
+ if (status >= 240) return;
422
+ const kind = status & 240;
423
+ const channel = status & 15;
424
+ const first = event.data[1] ?? 0;
425
+ const second = event.data[2] ?? 0;
426
+ switch (kind) {
427
+ case STATUS.NOTE_OFF:
428
+ current.onNoteOffEvent?.(first, channel);
429
+ break;
430
+ case STATUS.NOTE_ON:
431
+ if (second === 0) current.onNoteOffEvent?.(first, channel);
432
+ else current.onNoteOnEvent?.(first, second, channel);
433
+ break;
434
+ case STATUS.AFTERTOUCH:
435
+ current.onAftertouchEvent?.(first, second, channel);
436
+ break;
437
+ case STATUS.CONTROL_CHANGE:
438
+ current.onControlChangeEvent?.(first, second, channel);
439
+ break;
440
+ case STATUS.PROGRAM_CHANGE:
441
+ current.onProgramChangeEvent?.(first, channel);
442
+ break;
443
+ case STATUS.CHANNEL_PRESSURE:
444
+ current.onChannelPressureEvent?.(first, channel);
445
+ break;
446
+ case STATUS.PITCH_BEND: current.onPitchBendEvent?.(second << 7 | first, channel);
447
+ }
448
+ }).destroy
449
+ };
98
450
  }
99
451
  //#endregion
100
452
  //#region src/pointer/drag.ts
@@ -111,6 +463,7 @@ const MANAGED_STYLES = [
111
463
  ["-webkit-user-select", "none"],
112
464
  ["-webkit-touch-callout", "none"]
113
465
  ];
466
+ const managedStyleStates = /* @__PURE__ */ new WeakMap();
114
467
  /**
115
468
  * Track a pointer drag on an element.
116
469
  *
@@ -119,32 +472,54 @@ const MANAGED_STYLES = [
119
472
  * element gets `touch-action: none` so that touch dragging does not scroll the
120
473
  * page, plus `user-select: none` so that a long press does not start a text
121
474
  * selection instead.
475
+ *
476
+ * One pointer at a time by default; see {@link DragOptions.multiPointer}.
122
477
  */
123
- function createDrag(element, { threshold: _threshold = 1, cursor, onDragStart, onDrag, onDragEnd } = {}) {
124
- const threshold = Math.max(_threshold, 1);
478
+ function createDrag(element, options = {}) {
479
+ let opts = options;
480
+ const multiPointer = options.multiPointer ?? false;
125
481
  const capture = element;
126
482
  const style = element.style;
127
- const previousStyles = /* @__PURE__ */ new Map();
128
- if (style) for (const [property, value] of MANAGED_STYLES) {
129
- previousStyles.set(property, style.getPropertyValue(property));
130
- style.setProperty(property, value);
131
- }
132
- let pointerId = null;
133
- /** Where the listeners for the current drag live. */
134
- let moveTarget = null;
135
- let startX = 0;
136
- let startY = 0;
137
- let lastX = 0;
138
- let lastY = 0;
483
+ let managedStyles = managedStyleStates.get(element);
484
+ if (style) {
485
+ if (managedStyles) managedStyles.count += 1;
486
+ else {
487
+ const previous = /* @__PURE__ */ new Map();
488
+ for (const [property, value] of MANAGED_STYLES) {
489
+ previous.set(property, {
490
+ value: style.getPropertyValue(property),
491
+ priority: style.getPropertyPriority(property)
492
+ });
493
+ style.setProperty(property, value);
494
+ }
495
+ managedStyles = {
496
+ count: 1,
497
+ previous
498
+ };
499
+ managedStyleStates.set(element, managedStyles);
500
+ }
501
+ }
502
+ const pointers = /* @__PURE__ */ new Map();
503
+ /**
504
+ * How many pointers each target carries. Adding the same listener twice is a
505
+ * no-op and removing it once removes it for good, so a target is subscribed
506
+ * to while at least one pointer is on it and no longer.
507
+ */
508
+ const targets = /* @__PURE__ */ new Map();
139
509
  let previousCursor;
140
- function state(event, deltaX, deltaY) {
510
+ /** The pointer that asked for the lock, while it is still down. */
511
+ let lockedPointerId = null;
512
+ let lockRequest = 0;
513
+ let destroyed = false;
514
+ function state(event, pointer, deltaX, deltaY) {
141
515
  return {
142
- x: event.screenX - startX,
143
- y: event.screenY - startY,
516
+ x: pointer.lockBaseX !== void 0 ? pointer.lockBaseX + pointer.lockMoveX : event.screenX - pointer.startX,
517
+ y: pointer.lockBaseY !== void 0 ? pointer.lockBaseY + pointer.lockMoveY : event.screenY - pointer.startY,
144
518
  deltaX,
145
519
  deltaY,
146
520
  clientX: event.clientX,
147
521
  clientY: event.clientY,
522
+ pointerId: event.pointerId,
148
523
  event
149
524
  };
150
525
  }
@@ -156,65 +531,437 @@ function createDrag(element, { threshold: _threshold = 1, cursor, onDragStart, o
156
531
  function preventSelectStart(event) {
157
532
  event.preventDefault();
158
533
  }
534
+ function retainTarget(target) {
535
+ const count = targets.get(target) ?? 0;
536
+ if (count === 0) {
537
+ target.addEventListener("pointermove", handlePointerMove);
538
+ target.addEventListener("pointerup", handlePointerUp);
539
+ target.addEventListener("pointercancel", handlePointerUp);
540
+ if (target === element) target.addEventListener("lostpointercapture", handleLostPointerCapture);
541
+ }
542
+ targets.set(target, count + 1);
543
+ }
544
+ function releaseTarget(target) {
545
+ const count = targets.get(target) ?? 0;
546
+ if (count > 1) {
547
+ targets.set(target, count - 1);
548
+ return;
549
+ }
550
+ target.removeEventListener("pointermove", handlePointerMove);
551
+ target.removeEventListener("pointerup", handlePointerUp);
552
+ target.removeEventListener("pointercancel", handlePointerUp);
553
+ if (target === element) target.removeEventListener("lostpointercapture", handleLostPointerCapture);
554
+ targets.delete(target);
555
+ }
159
556
  function handlePointerDown(event) {
160
557
  const pointerEvent = event;
161
- if (pointerId !== null) return;
162
- pointerId = pointerEvent.pointerId;
163
- startX = lastX = pointerEvent.screenX;
164
- startY = lastY = pointerEvent.screenY;
165
- if (cursor && style) {
558
+ if (pointerEvent.button !== 0) return;
559
+ const pointerId = pointerEvent.pointerId;
560
+ if (pointers.has(pointerId)) return;
561
+ if (!multiPointer && pointers.size > 0) return;
562
+ if (opts.shouldStart && !opts.shouldStart(pointerEvent)) return;
563
+ const isFirst = pointers.size === 0;
564
+ if (isFirst && opts.cursor && style) {
166
565
  previousCursor = style.cursor;
167
- style.cursor = cursor;
566
+ style.cursor = opts.cursor;
168
567
  }
169
568
  capture.setPointerCapture?.(pointerId);
170
- moveTarget = capture.hasPointerCapture?.(pointerId) === true ? element : globalThis.window ?? element;
171
- moveTarget.addEventListener("pointermove", handlePointerMove);
172
- moveTarget.addEventListener("pointerup", handlePointerUp);
173
- moveTarget.addEventListener("pointercancel", handlePointerUp);
174
- globalThis.document?.addEventListener("selectstart", preventSelectStart);
175
- onDragStart?.(state(pointerEvent, 0, 0));
569
+ const moveTarget = capture.hasPointerCapture?.(pointerId) === true ? element : globalThis.window ?? element;
570
+ const pointer = {
571
+ moveTarget,
572
+ startX: pointerEvent.screenX,
573
+ startY: pointerEvent.screenY,
574
+ lastX: pointerEvent.screenX,
575
+ lastY: pointerEvent.screenY,
576
+ lockMoveX: 0,
577
+ lockMoveY: 0,
578
+ lastEvent: pointerEvent
579
+ };
580
+ pointers.set(pointerId, pointer);
581
+ retainTarget(moveTarget);
582
+ if (isFirst) globalThis.document?.addEventListener("selectstart", preventSelectStart);
583
+ if (isFirst && opts.pointerLock) {
584
+ const requestId = ++lockRequest;
585
+ lockedPointerId = pointerId;
586
+ globalThis.document?.addEventListener("pointerlockchange", handleLockChange);
587
+ try {
588
+ (capture.requestPointerLock?.())?.then?.(() => {
589
+ if (requestId === lockRequest && (!pointers.has(pointerId) || destroyed) && globalThis.document?.pointerLockElement === element) globalThis.document.exitPointerLock?.();
590
+ }, () => {});
591
+ } catch {}
592
+ }
593
+ opts.onDragStart?.(state(pointerEvent, pointer, 0, 0));
176
594
  }
177
595
  function handlePointerMove(event) {
178
596
  const pointerEvent = event;
179
- if (pointerEvent.pointerId !== pointerId) return;
180
- const deltaX = pointerEvent.screenX - lastX;
181
- const deltaY = pointerEvent.screenY - lastY;
182
- lastX = pointerEvent.screenX;
183
- lastY = pointerEvent.screenY;
597
+ const pointer = pointers.get(pointerEvent.pointerId);
598
+ if (!pointer) return;
599
+ const locked = pointer.lockBaseX !== void 0;
600
+ const deltaX = locked ? pointerEvent.movementX ?? 0 : pointerEvent.screenX - pointer.lastX;
601
+ const deltaY = locked ? pointerEvent.movementY ?? 0 : pointerEvent.screenY - pointer.lastY;
602
+ const threshold = opts.threshold ?? 0;
184
603
  if (Math.abs(deltaX) < threshold && Math.abs(deltaY) < threshold) return;
185
- onDrag?.(state(pointerEvent, deltaX, deltaY));
604
+ pointer.lastX = pointerEvent.screenX;
605
+ pointer.lastY = pointerEvent.screenY;
606
+ pointer.lastEvent = pointerEvent;
607
+ if (locked) {
608
+ pointer.lockMoveX += deltaX;
609
+ pointer.lockMoveY += deltaY;
610
+ }
611
+ opts.onDrag?.(state(pointerEvent, pointer, deltaX, deltaY));
612
+ }
613
+ function handleLockChange() {
614
+ if (lockedPointerId === null) return;
615
+ const pointer = pointers.get(lockedPointerId);
616
+ if (!pointer) return;
617
+ if (globalThis.document?.pointerLockElement === element) {
618
+ pointer.lockBaseX = pointer.lastX - pointer.startX;
619
+ pointer.lockBaseY = pointer.lastY - pointer.startY;
620
+ pointer.lockMoveX = 0;
621
+ pointer.lockMoveY = 0;
622
+ return;
623
+ }
624
+ if (pointer.lockBaseX === void 0) return;
625
+ finishDrag(lockedPointerId);
186
626
  }
187
627
  function handlePointerUp(event) {
188
628
  const pointerEvent = event;
189
- if (pointerEvent.pointerId !== pointerId) return;
190
- const finalState = state(pointerEvent, pointerEvent.screenX - lastX, pointerEvent.screenY - lastY);
191
- stopTracking();
192
- onDragEnd?.(finalState);
193
- }
194
- function stopTracking() {
195
- if (pointerId === null) return;
196
- capture.releasePointerCapture?.(pointerId);
197
- moveTarget?.removeEventListener("pointermove", handlePointerMove);
198
- moveTarget?.removeEventListener("pointerup", handlePointerUp);
199
- moveTarget?.removeEventListener("pointercancel", handlePointerUp);
629
+ finishDrag(pointerEvent.pointerId, pointerEvent);
630
+ }
631
+ function handleLostPointerCapture(event) {
632
+ finishDrag(event.pointerId);
633
+ }
634
+ function finishDrag(pointerId, event) {
635
+ const pointer = pointers.get(pointerId);
636
+ if (!pointer) return;
637
+ const finalState = event ? state(event, pointer, event.screenX - pointer.lastX, event.screenY - pointer.lastY) : state(pointer.lastEvent, pointer, 0, 0);
638
+ stopTracking(pointerId);
639
+ opts.onDragEnd?.(finalState);
640
+ }
641
+ function stopTracking(pointerId) {
642
+ const pointer = pointers.get(pointerId);
643
+ if (!pointer) return;
644
+ pointers.delete(pointerId);
645
+ releaseTarget(pointer.moveTarget);
646
+ try {
647
+ if (capture.hasPointerCapture?.(pointerId) === true) capture.releasePointerCapture?.(pointerId);
648
+ } catch {}
649
+ if (lockedPointerId === pointerId) {
650
+ lockedPointerId = null;
651
+ const document = globalThis.document;
652
+ document?.removeEventListener("pointerlockchange", handleLockChange);
653
+ if (document?.pointerLockElement === element) document.exitPointerLock?.();
654
+ }
655
+ if (pointers.size > 0) return;
200
656
  globalThis.document?.removeEventListener("selectstart", preventSelectStart);
201
- if (cursor && style) {
202
- style.cursor = previousCursor ?? "";
657
+ if (previousCursor !== void 0 && style) {
658
+ style.cursor = previousCursor;
203
659
  previousCursor = void 0;
204
660
  }
205
- moveTarget = null;
206
- pointerId = null;
207
661
  }
208
662
  element.addEventListener("pointerdown", handlePointerDown);
209
- return { destroy: () => {
210
- stopTracking();
211
- element.removeEventListener("pointerdown", handlePointerDown);
212
- if (style) for (const [property] of MANAGED_STYLES) {
213
- const previous = previousStyles.get(property);
214
- if (previous) style.setProperty(property, previous);
215
- else style.removeProperty(property);
663
+ return {
664
+ update: (next) => {
665
+ opts = {
666
+ ...opts,
667
+ ...next,
668
+ multiPointer
669
+ };
670
+ },
671
+ destroy: () => {
672
+ if (destroyed) return;
673
+ destroyed = true;
674
+ element.removeEventListener("pointerdown", handlePointerDown);
675
+ for (const pointerId of [...pointers.keys()]) finishDrag(pointerId);
676
+ if (style && managedStyles && --managedStyles.count === 0) {
677
+ for (const [property] of MANAGED_STYLES) {
678
+ const previous = managedStyles.previous.get(property);
679
+ if (previous?.value) style.setProperty(property, previous.value, previous.priority);
680
+ else style.removeProperty(property);
681
+ }
682
+ managedStyleStates.delete(element);
683
+ }
684
+ }
685
+ };
686
+ }
687
+ //#endregion
688
+ //#region src/piano/index.ts
689
+ const DEFAULT_SOURCE = "api";
690
+ /**
691
+ * Drive a piano keyboard with pointers, and own what is sounding.
692
+ *
693
+ * Every way of playing a note goes through the one instance — pointers here,
694
+ * keyboard shortcuts and MIDI through {@link PianoInputInstance.noteOn} — so
695
+ * there is a single answer to what is currently held, and drawing the keys is
696
+ * left entirely to the wrapper.
697
+ *
698
+ * Tracks every pointer at once, so a chord can be played with several fingers.
699
+ */
700
+ function createPianoInput(element, options) {
701
+ let opts = options;
702
+ /** Which sources are holding each sounding note. */
703
+ const held = /* @__PURE__ */ new Map();
704
+ /** The note each pointer is currently on. */
705
+ const pointerNotes = /* @__PURE__ */ new Map();
706
+ function activeNotes() {
707
+ return [...held.keys()].sort((a, b) => a - b);
708
+ }
709
+ function noteOn(note, { source = DEFAULT_SOURCE, velocity } = {}) {
710
+ if (!Number.isInteger(note) || note < 0 || note > 127) throw new RangeError("note: requirements: an integer from 0 to 127");
711
+ if (note > (opts.midiMax ?? 127)) return;
712
+ const sources = held.get(note);
713
+ if (sources) {
714
+ sources.add(source);
715
+ return;
716
+ }
717
+ held.set(note, /* @__PURE__ */ new Set([source]));
718
+ opts.onPlayNote?.(note, velocity);
719
+ opts.onActiveNotesChange?.(activeNotes());
720
+ }
721
+ function noteOff(note, { source = DEFAULT_SOURCE } = {}) {
722
+ if (!Number.isInteger(note) || note < 0 || note > 127) throw new RangeError("note: requirements: an integer from 0 to 127");
723
+ const sources = held.get(note);
724
+ if (!sources) return;
725
+ sources.delete(source);
726
+ if (sources.size > 0) return;
727
+ held.delete(note);
728
+ opts.onStopNote?.(note);
729
+ opts.onActiveNotesChange?.(activeNotes());
730
+ }
731
+ /** The note under a pointer, or null where the pointer is off the keys. */
732
+ function noteUnder(clientX, clientY) {
733
+ const { left, top, height } = element.getBoundingClientRect();
734
+ return (0, _tremolo_ui_functions.noteAt)(clientX - left, clientY - top, height, opts.layout);
735
+ }
736
+ /** Move a pointer onto a note, releasing whatever it held before. */
737
+ function movePointer(pointerId, note) {
738
+ const source = `pointer:${pointerId}`;
739
+ const previous = pointerNotes.get(pointerId);
740
+ if (previous === note) return;
741
+ if (previous !== void 0) noteOff(previous, { source });
742
+ if (note === null || note > (opts.midiMax ?? 127)) pointerNotes.delete(pointerId);
743
+ else {
744
+ pointerNotes.set(pointerId, note);
745
+ noteOn(note, { source });
746
+ }
747
+ }
748
+ const drag = createDrag(element, {
749
+ multiPointer: true,
750
+ onDragStart: (state) => movePointer(state.pointerId, noteUnder(state.clientX, state.clientY)),
751
+ onDrag: (state) => {
752
+ if (opts.glissando === false) return;
753
+ movePointer(state.pointerId, noteUnder(state.clientX, state.clientY));
754
+ },
755
+ onDragEnd: (state) => movePointer(state.pointerId, null)
756
+ });
757
+ return {
758
+ update: (next) => {
759
+ opts = {
760
+ ...opts,
761
+ ...next
762
+ };
763
+ const midiMax = opts.midiMax ?? 127;
764
+ const stoppedNotes = activeNotes().filter((note) => note > midiMax);
765
+ if (stoppedNotes.length === 0) return;
766
+ for (const note of stoppedNotes) {
767
+ held.delete(note);
768
+ opts.onStopNote?.(note);
769
+ }
770
+ for (const [pointerId, note] of pointerNotes) if (note > midiMax) pointerNotes.delete(pointerId);
771
+ opts.onActiveNotesChange?.(activeNotes());
772
+ },
773
+ noteOn,
774
+ noteOff,
775
+ activeNotes,
776
+ destroy: () => {
777
+ drag.destroy();
778
+ const notes = activeNotes();
779
+ for (const note of notes) {
780
+ held.delete(note);
781
+ opts.onStopNote?.(note);
782
+ }
783
+ pointerNotes.clear();
784
+ if (notes.length > 0) opts.onActiveNotesChange?.([]);
785
+ }
786
+ };
787
+ }
788
+ //#endregion
789
+ //#region src/xy.ts
790
+ function isPair(value) {
791
+ return Array.isArray(value);
792
+ }
793
+ /**
794
+ * Spread a setting that may have been given as a single value.
795
+ *
796
+ * The parameter is written out rather than taken as `XYInput<T>`: a
797
+ * conditional type cannot be narrowed, so the constraint stays where it is
798
+ * declared and this takes both forms.
799
+ *
800
+ * The pair is copied rather than passed along, so that the result is a tuple
801
+ * the caller owns even when a readonly one was given.
802
+ */
803
+ function toXY(value) {
804
+ return isPair(value) ? [value[0], value[1]] : [value, value];
805
+ }
806
+ //#endregion
807
+ //#region src/pointer/drag-value.ts
808
+ /**
809
+ * Map the pointer onto the bounding rect of an element: the value *is* the
810
+ * position pointed at, so the middle of the element is 0.5.
811
+ *
812
+ * The element is read on every event, so it may be mounted after the drag is
813
+ * set up and may change size while a drag is in progress.
814
+ */
815
+ function elementMapping(getElement, { sensitivity } = {}) {
816
+ /** The reported position when the sensitivity last changed. */
817
+ let origin = [0, 0];
818
+ /** The raw position at that same moment. */
819
+ let anchor = [0, 0];
820
+ /** The previous event, so a sensitivity change can be dated back to it. */
821
+ let previous = [0, 0];
822
+ let factor = 1;
823
+ function positionIn(state) {
824
+ const element = getElement();
825
+ if (!element) return null;
826
+ const { left, top, right, bottom } = element.getBoundingClientRect();
827
+ return [right > left ? (0, _tremolo_ui_functions.normalizeValue)(state.clientX, left, right) : 0, bottom > top ? (0, _tremolo_ui_functions.normalizeValue)(state.clientY, top, bottom) : 0];
828
+ }
829
+ const reported = (raw, at) => [origin[0] + (raw[0] - anchor[0]) * at, origin[1] + (raw[1] - anchor[1]) * at];
830
+ return {
831
+ start: (state) => {
832
+ const raw = positionIn(state);
833
+ if (!raw) return null;
834
+ origin = raw;
835
+ anchor = raw;
836
+ previous = raw;
837
+ factor = sensitivity ? sensitivity(state) : 1;
838
+ return raw;
839
+ },
840
+ move: (state) => {
841
+ const raw = positionIn(state);
842
+ if (!raw) return null;
843
+ const next = sensitivity ? sensitivity(state) : 1;
844
+ if (next !== factor) {
845
+ origin = reported(previous, factor);
846
+ anchor = previous;
847
+ factor = next;
848
+ }
849
+ previous = raw;
850
+ return reported(raw, factor);
851
+ }
852
+ };
853
+ }
854
+ /**
855
+ * Move the value away from where it stood when the drag started, by the
856
+ * distance dragged. The pointer position itself carries no meaning, so the
857
+ * value can be adjusted from anywhere on the screen.
858
+ */
859
+ function relativeMapping({ pixelRange = 100, sensitivity } = {}) {
860
+ const [baseX, baseY] = toXY(pixelRange);
861
+ let origin = [0, 0];
862
+ /** Where the current sensitivity took over, in drag coordinates. */
863
+ let anchor = [0, 0];
864
+ /** The previous event, so a sensitivity change can be dated back to it. */
865
+ let previous = [0, 0];
866
+ let factor = 1;
867
+ const travelled = (to, at) => [baseX === 0 ? origin[0] : origin[0] + (to[0] - anchor[0]) * at / baseX, baseY === 0 ? origin[1] : origin[1] + (to[1] - anchor[1]) * at / baseY];
868
+ return {
869
+ start: (state, context) => {
870
+ origin = context.position();
871
+ anchor = [0, 0];
872
+ previous = [0, 0];
873
+ factor = sensitivity ? sensitivity(state) : 1;
874
+ return origin;
875
+ },
876
+ move: (state) => {
877
+ const next = sensitivity ? sensitivity(state) : 1;
878
+ if (next !== factor) {
879
+ origin = travelled(previous, factor);
880
+ anchor = previous;
881
+ factor = next;
882
+ }
883
+ previous = [state.x, state.y];
884
+ return travelled([state.x, state.y], factor);
216
885
  }
886
+ };
887
+ }
888
+ /**
889
+ * Drive a value with a pointer drag.
890
+ *
891
+ * Combines {@link createDrag} with the scaling of `@tremolo-ui/functions`: the
892
+ * mapping decides where the pointer sits on the 0-1 travel of each axis, and
893
+ * the axis options turn that into a value.
894
+ */
895
+ function createDragValue(element, options) {
896
+ let opts = options;
897
+ let lastValue = [0, 0];
898
+ let active = false;
899
+ const axes = () => toXY(opts.axis);
900
+ function valueOf(position) {
901
+ return axes().map((axis, i) => {
902
+ const p = axis.reverse ? 1 - position[i] : position[i];
903
+ const value = (axis.scale ?? _tremolo_ui_functions.linearScale).denormalize(p, axis.min, axis.max);
904
+ const stepped = axis.step ? (0, _tremolo_ui_functions.stepValue)(value, axis.step) : value;
905
+ return (0, _tremolo_ui_functions.clamp)((0, _tremolo_ui_functions.toPrecision)(stepped), axis.min, axis.max);
906
+ });
907
+ }
908
+ const context = { position: () => {
909
+ const getValue = opts.getValue;
910
+ if (!getValue) throw new Error("createDragValue: getValue is required by the given mapping");
911
+ const value = getValue();
912
+ return axes().map((axis, i) => {
913
+ const n = (axis.scale ?? _tremolo_ui_functions.linearScale).normalize(value[i], axis.min, axis.max);
914
+ return axis.reverse ? 1 - n : n;
915
+ });
217
916
  } };
917
+ const drag = createDrag(element, {
918
+ threshold: opts.threshold,
919
+ cursor: opts.cursor,
920
+ pointerLock: opts.pointerLock,
921
+ shouldStart: (event) => opts.shouldStart?.(event) ?? true,
922
+ onDragStart: (state) => {
923
+ const position = opts.mapping.start(state, context);
924
+ if (!position) return;
925
+ active = true;
926
+ lastValue = valueOf(position);
927
+ if (opts.updateOnPointerDown) opts.onChange?.(lastValue, state);
928
+ opts.onDragStart?.(lastValue, state);
929
+ },
930
+ onDrag: (state) => {
931
+ if (!active) return;
932
+ const position = opts.mapping.move(state, context);
933
+ if (!position) return;
934
+ lastValue = valueOf(position);
935
+ opts.onChange?.(lastValue, state);
936
+ },
937
+ onDragEnd: (state) => {
938
+ if (!active) return;
939
+ if (state.event.type === "pointerup" && (state.deltaX !== 0 || state.deltaY !== 0)) {
940
+ const position = opts.mapping.move(state, context);
941
+ if (position) {
942
+ lastValue = valueOf(position);
943
+ opts.onChange?.(lastValue, state);
944
+ }
945
+ }
946
+ active = false;
947
+ opts.onDragEnd?.(lastValue, state);
948
+ }
949
+ });
950
+ return {
951
+ update: (next) => {
952
+ opts = {
953
+ ...opts,
954
+ ...next,
955
+ mapping: opts.mapping
956
+ };
957
+ drag.update({
958
+ threshold: opts.threshold,
959
+ cursor: opts.cursor,
960
+ pointerLock: opts.pointerLock
961
+ });
962
+ },
963
+ destroy: () => drag.destroy()
964
+ };
218
965
  }
219
966
  //#endregion
220
967
  //#region src/pointer/wheel.ts
@@ -224,20 +971,118 @@ function createDrag(element, { threshold: _threshold = 1, cursor, onDragStart, o
224
971
  * The listener is registered with `passive: false` so that the handler can call
225
972
  * `preventDefault()` to stop the page from scrolling.
226
973
  */
227
- function createWheel(element, onWheel) {
228
- const handler = (event) => onWheel(event);
974
+ function createWheel(element, onWheel, options = {}) {
975
+ let opts = {
976
+ ...options,
977
+ onWheel
978
+ };
979
+ function hasFocus() {
980
+ const active = element.ownerDocument?.activeElement;
981
+ return !!active && element.contains(active);
982
+ }
983
+ const handler = (event) => {
984
+ if (opts.requireFocus && !hasFocus()) return;
985
+ opts.onWheel?.(event);
986
+ };
229
987
  element.addEventListener("wheel", handler, { passive: false });
230
- return { destroy: () => {
231
- element.removeEventListener("wheel", handler);
232
- } };
988
+ return {
989
+ update: (next) => {
990
+ opts = {
991
+ ...opts,
992
+ ...next
993
+ };
994
+ },
995
+ destroy: () => {
996
+ element.removeEventListener("wheel", handler);
997
+ }
998
+ };
999
+ }
1000
+ //#endregion
1001
+ //#region src/selection/box.ts
1002
+ function rectOf(from, to) {
1003
+ return {
1004
+ x: Math.min(from[0], to[0]),
1005
+ y: Math.min(from[1], to[1]),
1006
+ width: Math.abs(to[0] - from[0]),
1007
+ height: Math.abs(to[1] - from[1])
1008
+ };
1009
+ }
1010
+ /** Whether the box covers the point, edges included. */
1011
+ function selectionBoxCovers(box, [x, y]) {
1012
+ return x >= box.x && x <= box.x + box.width && y >= box.y && y <= box.y + box.height;
1013
+ }
1014
+ /**
1015
+ * Selecting by dragging a box over a set of items.
1016
+ *
1017
+ * The drag itself is not here: the caller owns the pointer, and reports where
1018
+ * it went in the same 0..1 space the items are given in.
1019
+ */
1020
+ function createSelectionBox(options) {
1021
+ let opts = options;
1022
+ let drag = null;
1023
+ let box = null;
1024
+ function setBox(next) {
1025
+ box = next;
1026
+ opts.onBoxChange?.(next);
1027
+ }
1028
+ function apply(rect, base) {
1029
+ const inside = [];
1030
+ for (const [id, at] of opts.items()) if (selectionBoxCovers(rect, at) && !base.includes(id)) inside.push(id);
1031
+ opts.onSelectionChange?.([...base, ...inside]);
1032
+ }
1033
+ return {
1034
+ update: (next) => {
1035
+ opts = {
1036
+ ...opts,
1037
+ ...next
1038
+ };
1039
+ },
1040
+ begin: (at, { additive = false, selection = [] } = {}) => {
1041
+ drag = {
1042
+ from: at,
1043
+ base: additive ? [...selection] : []
1044
+ };
1045
+ setBox(rectOf(at, at));
1046
+ if (!additive) opts.onSelectionChange?.([]);
1047
+ },
1048
+ move: (to) => {
1049
+ if (!drag) return;
1050
+ const rect = rectOf(drag.from, to);
1051
+ setBox(rect);
1052
+ apply(rect, drag.base);
1053
+ },
1054
+ end: () => {
1055
+ const running = drag !== null;
1056
+ drag = null;
1057
+ if (box !== null) setBox(null);
1058
+ return running;
1059
+ },
1060
+ box: () => box,
1061
+ destroy: () => {
1062
+ drag = null;
1063
+ box = null;
1064
+ }
1065
+ };
233
1066
  }
234
1067
  //#endregion
235
1068
  exports.NOT_SUPPORTED = NOT_SUPPORTED;
236
1069
  exports.PERMISSION_DENIED = PERMISSION_DENIED;
1070
+ exports.PITCH_BEND_CENTER = PITCH_BEND_CENTER;
1071
+ exports.UNAVAILABLE = UNAVAILABLE;
1072
+ exports.createAnimationCanvas = createAnimationCanvas;
237
1073
  exports.createDrag = createDrag;
1074
+ exports.createDragValue = createDragValue;
238
1075
  exports.createMIDIAccess = createMIDIAccess;
239
1076
  exports.createMIDIInput = createMIDIInput;
240
1077
  exports.createMIDIMessage = createMIDIMessage;
1078
+ exports.createPianoInput = createPianoInput;
1079
+ exports.createSelectionBox = createSelectionBox;
241
1080
  exports.createWheel = createWheel;
1081
+ exports.drawingState = drawingState;
1082
+ exports.elementMapping = elementMapping;
1083
+ exports.isDrawingState = isDrawingState;
1084
+ exports.relativeMapping = relativeMapping;
1085
+ exports.selectionBoxCovers = selectionBoxCovers;
1086
+ exports.toXY = toXY;
242
1087
 
243
1088
  //# sourceMappingURL=index.cjs.map