@tremolo-ui/dom 0.4.0 → 0.5.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.js CHANGED
@@ -1,3 +1,219 @@
1
+ import { clamp, linearScale, normalizeValue, noteAt, stepValue } from "@tremolo-ui/functions";
2
+ //#region src/canvas/context.ts
3
+ /**
4
+ * The properties of a 2D context that survive `save()` / `restore()`, and so
5
+ * are what has to be carried across a resize by hand: setting `canvas.width`
6
+ * resets the context to its defaults.
7
+ *
8
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/save
9
+ */
10
+ const drawingState = [
11
+ "strokeStyle",
12
+ "fillStyle",
13
+ "globalAlpha",
14
+ "lineWidth",
15
+ "lineCap",
16
+ "lineJoin",
17
+ "miterLimit",
18
+ "lineDashOffset",
19
+ "shadowOffsetX",
20
+ "shadowOffsetY",
21
+ "shadowBlur",
22
+ "shadowColor",
23
+ "globalCompositeOperation",
24
+ "font",
25
+ "textAlign",
26
+ "textBaseline",
27
+ "direction",
28
+ "imageSmoothingEnabled"
29
+ ];
30
+ function isDrawingState(value) {
31
+ return typeof value == "string" && drawingState.includes(value);
32
+ }
33
+ /** Copy the drawing state off a context, to put back after a resize. */
34
+ function readDrawingState(context) {
35
+ const state = {};
36
+ for (const property of drawingState) state[property] = context[property];
37
+ return state;
38
+ }
39
+ /** Put a state read by {@link readDrawingState} back onto a context. */
40
+ function writeDrawingState(context, state) {
41
+ for (const property of drawingState) context[property] = state[property];
42
+ }
43
+ /**
44
+ * Give the canvas a backing store of `width * dpr` by `height * dpr` device
45
+ * pixels while keeping it `width` by `height` in CSS pixels, and scale the
46
+ * context so that drawing code can work in CSS pixels throughout.
47
+ *
48
+ * Assigning to `canvas.width` clears the canvas and resets the context, so
49
+ * this both resizes and re-establishes the transform.
50
+ */
51
+ function applyDevicePixelRatio(canvas, context, width, height, dpr) {
52
+ canvas.width = width * dpr;
53
+ canvas.height = height * dpr;
54
+ context.setTransform(1, 0, 0, 1, 0, 0);
55
+ context.scale(dpr, dpr);
56
+ canvas.style.width = `${width}px`;
57
+ canvas.style.height = `${height}px`;
58
+ }
59
+ //#endregion
60
+ //#region src/canvas/animation.ts
61
+ /**
62
+ * Drive a canvas from `requestAnimationFrame`, keeping its backing store in
63
+ * step with the device pixel ratio and, optionally, with the size of its
64
+ * parent.
65
+ *
66
+ * Drawing code works in CSS pixels: the context is scaled by the device pixel
67
+ * ratio, and `width` / `height` of each {@link AnimationFrame} are CSS pixels.
68
+ */
69
+ function createAnimationCanvas(canvas, options) {
70
+ let opts = options;
71
+ const context2d = canvas.getContext("2d", opts.contextAttributes);
72
+ if (!context2d) throw new Error("createAnimationCanvas: cannot get a 2d context");
73
+ const context = context2d;
74
+ const relativeSize = opts.relativeSize ?? false;
75
+ /** Off-document canvas holding the drawing while the real one is resized. */
76
+ let memo = null;
77
+ let memoContext = null;
78
+ let width = 0;
79
+ let height = 0;
80
+ /** Whether a size has been applied, so a frame can be drawn. */
81
+ let sized = false;
82
+ let initialized = false;
83
+ let frameId = null;
84
+ let count = -1;
85
+ const startTime = performance.now();
86
+ let previousTime = startTime;
87
+ function devicePixelRatio() {
88
+ return globalThis.devicePixelRatio || 1;
89
+ }
90
+ /**
91
+ * Copy the canvas onto the memo canvas at its full device resolution, 1:1.
92
+ *
93
+ * Assigning to `memo.width` resets the memo's transform to the identity, so
94
+ * the copy neither scales nor resamples.
95
+ */
96
+ function takeSnapshot() {
97
+ if (!(opts.reduceFlickering ?? true)) return null;
98
+ if (!memo) {
99
+ memo = globalThis.document?.createElement("canvas") ?? null;
100
+ memoContext = memo?.getContext("2d", opts.contextAttributes) ?? null;
101
+ }
102
+ if (!memo || !memoContext) return null;
103
+ const state = readDrawingState(context);
104
+ memo.width = canvas.width;
105
+ memo.height = canvas.height;
106
+ if (canvas.width > 0 && canvas.height > 0) memoContext.drawImage(canvas, 0, 0);
107
+ return state;
108
+ }
109
+ /**
110
+ * Put the snapshot back, given the size in CSS pixels it was taken at.
111
+ *
112
+ * The context is scaled to CSS pixels, so drawing the snapshot at its old
113
+ * CSS size leaves it exactly where it was. While the device pixel ratio
114
+ * stays put that is a 1:1 copy of device pixels, and when it changes the
115
+ * snapshot is rescaled once, from its full resolution.
116
+ */
117
+ function restoreSnapshot(state, previousWidth, previousHeight) {
118
+ if (!state || !memo || !memoContext) return;
119
+ if (memo.width <= 0 || memo.height <= 0) return;
120
+ writeDrawingState(context, state);
121
+ context.drawImage(memo, 0, 0, previousWidth, previousHeight);
122
+ }
123
+ function applySize(w, h) {
124
+ const dpr = devicePixelRatio();
125
+ const previousWidth = width;
126
+ const previousHeight = height;
127
+ const state = takeSnapshot();
128
+ applyDevicePixelRatio(canvas, context, w, h, dpr);
129
+ width = w;
130
+ height = h;
131
+ sized = true;
132
+ restoreSnapshot(state, previousWidth, previousHeight);
133
+ }
134
+ function drawFrame() {
135
+ if (!sized) return;
136
+ if (!initialized) {
137
+ initialized = true;
138
+ opts.init?.(context, {
139
+ width,
140
+ height
141
+ });
142
+ }
143
+ const now = performance.now();
144
+ const deltaTime = now - previousTime;
145
+ previousTime = now;
146
+ count += 1;
147
+ opts.draw(context, {
148
+ width,
149
+ height,
150
+ count,
151
+ deltaTime,
152
+ elapsedTime: now - startTime,
153
+ fps: 1e3 / deltaTime
154
+ });
155
+ }
156
+ function tick() {
157
+ frameId = requestAnimationFrame(tick);
158
+ drawFrame();
159
+ }
160
+ function startLoop() {
161
+ if (frameId != null) return;
162
+ previousTime = performance.now();
163
+ frameId = requestAnimationFrame(tick);
164
+ }
165
+ function stopLoop() {
166
+ if (frameId == null) return;
167
+ cancelAnimationFrame(frameId);
168
+ frameId = null;
169
+ }
170
+ let observer = null;
171
+ if (relativeSize) {
172
+ const parent = canvas.parentElement;
173
+ if (!parent) throw new Error("createAnimationCanvas: relativeSize needs the canvas to have a parent element");
174
+ observer = new ResizeObserver((entries) => {
175
+ for (const entry of entries) {
176
+ const { width: w, height: h } = entry.contentRect;
177
+ applySize(w, h);
178
+ if (!(opts.animate ?? true)) drawFrame();
179
+ }
180
+ });
181
+ observer.observe(parent);
182
+ } else {
183
+ const { width: w = 100, height: h = 100 } = opts.size ?? {};
184
+ applySize(w, h);
185
+ }
186
+ if (opts.animate ?? true) startLoop();
187
+ else if (sized) drawFrame();
188
+ return {
189
+ update: (next) => {
190
+ const wasAnimating = opts.animate ?? true;
191
+ opts = {
192
+ ...opts,
193
+ ...next,
194
+ relativeSize,
195
+ contextAttributes: opts.contextAttributes
196
+ };
197
+ if (!relativeSize) {
198
+ const { width: w = 100, height: h = 100 } = opts.size ?? {};
199
+ if (w != width || h != height) applySize(w, h);
200
+ }
201
+ const animating = opts.animate ?? true;
202
+ if (animating && !wasAnimating) startLoop();
203
+ if (!animating && wasAnimating) stopLoop();
204
+ if (!animating) drawFrame();
205
+ },
206
+ redraw: drawFrame,
207
+ destroy: () => {
208
+ stopLoop();
209
+ observer?.disconnect();
210
+ observer = null;
211
+ memo = null;
212
+ memoContext = null;
213
+ }
214
+ };
215
+ }
216
+ //#endregion
1
217
  //#region src/midi/access.ts
2
218
  /** @private */
3
219
  const PERMISSION_DENIED = "PERMISSION_DENIED";
@@ -118,9 +334,12 @@ const MANAGED_STYLES = [
118
334
  * element gets `touch-action: none` so that touch dragging does not scroll the
119
335
  * page, plus `user-select: none` so that a long press does not start a text
120
336
  * selection instead.
337
+ *
338
+ * One pointer at a time by default; see {@link DragOptions.multiPointer}.
121
339
  */
122
- function createDrag(element, { threshold: _threshold = 1, cursor, onDragStart, onDrag, onDragEnd } = {}) {
123
- const threshold = Math.max(_threshold, 1);
340
+ function createDrag(element, options = {}) {
341
+ let opts = options;
342
+ const multiPointer = options.multiPointer ?? false;
124
343
  const capture = element;
125
344
  const style = element.style;
126
345
  const previousStyles = /* @__PURE__ */ new Map();
@@ -128,22 +347,23 @@ function createDrag(element, { threshold: _threshold = 1, cursor, onDragStart, o
128
347
  previousStyles.set(property, style.getPropertyValue(property));
129
348
  style.setProperty(property, value);
130
349
  }
131
- let pointerId = null;
132
- /** Where the listeners for the current drag live. */
133
- let moveTarget = null;
134
- let startX = 0;
135
- let startY = 0;
136
- let lastX = 0;
137
- let lastY = 0;
350
+ const pointers = /* @__PURE__ */ new Map();
351
+ /**
352
+ * How many pointers each target carries. Adding the same listener twice is a
353
+ * no-op and removing it once removes it for good, so a target is subscribed
354
+ * to while at least one pointer is on it and no longer.
355
+ */
356
+ const targets = /* @__PURE__ */ new Map();
138
357
  let previousCursor;
139
- function state(event, deltaX, deltaY) {
358
+ function state(event, pointer, deltaX, deltaY) {
140
359
  return {
141
- x: event.screenX - startX,
142
- y: event.screenY - startY,
360
+ x: event.screenX - pointer.startX,
361
+ y: event.screenY - pointer.startY,
143
362
  deltaX,
144
363
  deltaY,
145
364
  clientX: event.clientX,
146
365
  clientY: event.clientY,
366
+ pointerId: event.pointerId,
147
367
  event
148
368
  };
149
369
  }
@@ -155,65 +375,307 @@ function createDrag(element, { threshold: _threshold = 1, cursor, onDragStart, o
155
375
  function preventSelectStart(event) {
156
376
  event.preventDefault();
157
377
  }
378
+ function retainTarget(target) {
379
+ const count = targets.get(target) ?? 0;
380
+ if (count === 0) {
381
+ target.addEventListener("pointermove", handlePointerMove);
382
+ target.addEventListener("pointerup", handlePointerUp);
383
+ target.addEventListener("pointercancel", handlePointerUp);
384
+ }
385
+ targets.set(target, count + 1);
386
+ }
387
+ function releaseTarget(target) {
388
+ const count = targets.get(target) ?? 0;
389
+ if (count > 1) {
390
+ targets.set(target, count - 1);
391
+ return;
392
+ }
393
+ target.removeEventListener("pointermove", handlePointerMove);
394
+ target.removeEventListener("pointerup", handlePointerUp);
395
+ target.removeEventListener("pointercancel", handlePointerUp);
396
+ targets.delete(target);
397
+ }
158
398
  function handlePointerDown(event) {
159
399
  const pointerEvent = event;
160
- if (pointerId !== null) return;
161
- pointerId = pointerEvent.pointerId;
162
- startX = lastX = pointerEvent.screenX;
163
- startY = lastY = pointerEvent.screenY;
164
- if (cursor && style) {
400
+ const pointerId = pointerEvent.pointerId;
401
+ if (pointers.has(pointerId)) return;
402
+ if (!multiPointer && pointers.size > 0) return;
403
+ const isFirst = pointers.size === 0;
404
+ if (isFirst && opts.cursor && style) {
165
405
  previousCursor = style.cursor;
166
- style.cursor = cursor;
406
+ style.cursor = opts.cursor;
167
407
  }
168
408
  capture.setPointerCapture?.(pointerId);
169
- moveTarget = capture.hasPointerCapture?.(pointerId) === true ? element : globalThis.window ?? element;
170
- moveTarget.addEventListener("pointermove", handlePointerMove);
171
- moveTarget.addEventListener("pointerup", handlePointerUp);
172
- moveTarget.addEventListener("pointercancel", handlePointerUp);
173
- globalThis.document?.addEventListener("selectstart", preventSelectStart);
174
- onDragStart?.(state(pointerEvent, 0, 0));
409
+ const moveTarget = capture.hasPointerCapture?.(pointerId) === true ? element : globalThis.window ?? element;
410
+ const pointer = {
411
+ moveTarget,
412
+ startX: pointerEvent.screenX,
413
+ startY: pointerEvent.screenY,
414
+ lastX: pointerEvent.screenX,
415
+ lastY: pointerEvent.screenY
416
+ };
417
+ pointers.set(pointerId, pointer);
418
+ retainTarget(moveTarget);
419
+ if (isFirst) globalThis.document?.addEventListener("selectstart", preventSelectStart);
420
+ opts.onDragStart?.(state(pointerEvent, pointer, 0, 0));
175
421
  }
176
422
  function handlePointerMove(event) {
177
423
  const pointerEvent = event;
178
- if (pointerEvent.pointerId !== pointerId) return;
179
- const deltaX = pointerEvent.screenX - lastX;
180
- const deltaY = pointerEvent.screenY - lastY;
181
- lastX = pointerEvent.screenX;
182
- lastY = pointerEvent.screenY;
424
+ const pointer = pointers.get(pointerEvent.pointerId);
425
+ if (!pointer) return;
426
+ const deltaX = pointerEvent.screenX - pointer.lastX;
427
+ const deltaY = pointerEvent.screenY - pointer.lastY;
428
+ const threshold = opts.threshold ?? 0;
183
429
  if (Math.abs(deltaX) < threshold && Math.abs(deltaY) < threshold) return;
184
- onDrag?.(state(pointerEvent, deltaX, deltaY));
430
+ pointer.lastX = pointerEvent.screenX;
431
+ pointer.lastY = pointerEvent.screenY;
432
+ opts.onDrag?.(state(pointerEvent, pointer, deltaX, deltaY));
185
433
  }
186
434
  function handlePointerUp(event) {
187
435
  const pointerEvent = event;
188
- if (pointerEvent.pointerId !== pointerId) return;
189
- const finalState = state(pointerEvent, pointerEvent.screenX - lastX, pointerEvent.screenY - lastY);
190
- stopTracking();
191
- onDragEnd?.(finalState);
436
+ const pointer = pointers.get(pointerEvent.pointerId);
437
+ if (!pointer) return;
438
+ const finalState = state(pointerEvent, pointer, pointerEvent.screenX - pointer.lastX, pointerEvent.screenY - pointer.lastY);
439
+ stopTracking(pointerEvent.pointerId);
440
+ opts.onDragEnd?.(finalState);
192
441
  }
193
- function stopTracking() {
194
- if (pointerId === null) return;
442
+ function stopTracking(pointerId) {
443
+ const pointer = pointers.get(pointerId);
444
+ if (!pointer) return;
195
445
  capture.releasePointerCapture?.(pointerId);
196
- moveTarget?.removeEventListener("pointermove", handlePointerMove);
197
- moveTarget?.removeEventListener("pointerup", handlePointerUp);
198
- moveTarget?.removeEventListener("pointercancel", handlePointerUp);
446
+ releaseTarget(pointer.moveTarget);
447
+ pointers.delete(pointerId);
448
+ if (pointers.size > 0) return;
199
449
  globalThis.document?.removeEventListener("selectstart", preventSelectStart);
200
- if (cursor && style) {
201
- style.cursor = previousCursor ?? "";
450
+ if (previousCursor !== void 0 && style) {
451
+ style.cursor = previousCursor;
202
452
  previousCursor = void 0;
203
453
  }
204
- moveTarget = null;
205
- pointerId = null;
206
454
  }
207
455
  element.addEventListener("pointerdown", handlePointerDown);
208
- return { destroy: () => {
209
- stopTracking();
210
- element.removeEventListener("pointerdown", handlePointerDown);
211
- if (style) for (const [property] of MANAGED_STYLES) {
212
- const previous = previousStyles.get(property);
213
- if (previous) style.setProperty(property, previous);
214
- else style.removeProperty(property);
456
+ return {
457
+ update: (next) => {
458
+ opts = {
459
+ ...opts,
460
+ ...next,
461
+ multiPointer
462
+ };
463
+ },
464
+ destroy: () => {
465
+ for (const pointerId of [...pointers.keys()]) stopTracking(pointerId);
466
+ element.removeEventListener("pointerdown", handlePointerDown);
467
+ if (style) for (const [property] of MANAGED_STYLES) {
468
+ const previous = previousStyles.get(property);
469
+ if (previous) style.setProperty(property, previous);
470
+ else style.removeProperty(property);
471
+ }
472
+ }
473
+ };
474
+ }
475
+ //#endregion
476
+ //#region src/piano/input.ts
477
+ const DEFAULT_SOURCE = "api";
478
+ /**
479
+ * Drive a piano keyboard with pointers, and own what is sounding.
480
+ *
481
+ * Every way of playing a note goes through the one instance — pointers here,
482
+ * keyboard shortcuts and MIDI through {@link PianoInputInstance.noteOn} — so
483
+ * there is a single answer to what is currently held, and drawing the keys is
484
+ * left entirely to the wrapper.
485
+ *
486
+ * Tracks every pointer at once, so a chord can be played with several fingers.
487
+ */
488
+ function createPianoInput(element, options) {
489
+ let opts = options;
490
+ /** Which sources are holding each sounding note. */
491
+ const held = /* @__PURE__ */ new Map();
492
+ /** The note each pointer is currently on. */
493
+ const pointerNotes = /* @__PURE__ */ new Map();
494
+ function activeNotes() {
495
+ return [...held.keys()].sort((a, b) => a - b);
496
+ }
497
+ function noteOn(note, { source = DEFAULT_SOURCE, velocity } = {}) {
498
+ if (note > (opts.midiMax ?? 127)) return;
499
+ const sources = held.get(note);
500
+ if (sources) {
501
+ sources.add(source);
502
+ return;
503
+ }
504
+ held.set(note, new Set([source]));
505
+ opts.onPlayNote?.(note, velocity);
506
+ opts.onActiveNotesChange?.(activeNotes());
507
+ }
508
+ function noteOff(note, { source = DEFAULT_SOURCE } = {}) {
509
+ const sources = held.get(note);
510
+ if (!sources) return;
511
+ sources.delete(source);
512
+ if (sources.size > 0) return;
513
+ held.delete(note);
514
+ opts.onStopNote?.(note);
515
+ opts.onActiveNotesChange?.(activeNotes());
516
+ }
517
+ /** The note under a pointer, or null where the pointer is off the keys. */
518
+ function noteUnder(clientX, clientY) {
519
+ const { left, top, height } = element.getBoundingClientRect();
520
+ return noteAt(clientX - left, clientY - top, height, opts.layout);
521
+ }
522
+ /** Move a pointer onto a note, releasing whatever it held before. */
523
+ function movePointer(pointerId, note) {
524
+ const source = `pointer:${pointerId}`;
525
+ const previous = pointerNotes.get(pointerId);
526
+ if (previous === note) return;
527
+ if (previous !== void 0) noteOff(previous, { source });
528
+ if (note === null) pointerNotes.delete(pointerId);
529
+ else {
530
+ pointerNotes.set(pointerId, note);
531
+ noteOn(note, { source });
532
+ }
533
+ }
534
+ const drag = createDrag(element, {
535
+ multiPointer: true,
536
+ onDragStart: (state) => movePointer(state.pointerId, noteUnder(state.clientX, state.clientY)),
537
+ onDrag: (state) => {
538
+ if (opts.glissando === false) return;
539
+ movePointer(state.pointerId, noteUnder(state.clientX, state.clientY));
540
+ },
541
+ onDragEnd: (state) => movePointer(state.pointerId, null)
542
+ });
543
+ return {
544
+ update: (next) => {
545
+ opts = {
546
+ ...opts,
547
+ ...next
548
+ };
549
+ },
550
+ noteOn,
551
+ noteOff,
552
+ activeNotes,
553
+ destroy: () => {
554
+ drag.destroy();
555
+ for (const note of activeNotes()) {
556
+ held.delete(note);
557
+ opts.onStopNote?.(note);
558
+ }
559
+ pointerNotes.clear();
560
+ opts.onActiveNotesChange?.([]);
215
561
  }
562
+ };
563
+ }
564
+ //#endregion
565
+ //#region src/xy.ts
566
+ function isPair(value) {
567
+ return Array.isArray(value);
568
+ }
569
+ /**
570
+ * Spread a setting that may have been given as a single value.
571
+ *
572
+ * The parameter is written out rather than taken as `XYInput<T>`: a
573
+ * conditional type cannot be narrowed, so the constraint stays where it is
574
+ * declared and this takes both forms.
575
+ *
576
+ * The pair is copied rather than passed along, so that the result is a tuple
577
+ * the caller owns even when a readonly one was given.
578
+ */
579
+ function toXY(value) {
580
+ return isPair(value) ? [value[0], value[1]] : [value, value];
581
+ }
582
+ //#endregion
583
+ //#region src/pointer/dragValue.ts
584
+ /**
585
+ * Map the pointer onto the bounding rect of an element: the value *is* the
586
+ * position pointed at, so the middle of the element is 0.5.
587
+ *
588
+ * The element is read on every event, so it may be mounted after the drag is
589
+ * set up and may change size while a drag is in progress.
590
+ */
591
+ function elementMapping(getElement) {
592
+ function positionIn(state) {
593
+ const element = getElement();
594
+ if (!element) return null;
595
+ const { left, top, right, bottom } = element.getBoundingClientRect();
596
+ return [right > left ? normalizeValue(state.clientX, left, right) : 0, bottom > top ? normalizeValue(state.clientY, top, bottom) : 0];
597
+ }
598
+ return {
599
+ start: positionIn,
600
+ move: positionIn
601
+ };
602
+ }
603
+ /**
604
+ * Move the value away from where it stood when the drag started, by the
605
+ * distance dragged. The pointer position itself carries no meaning, so the
606
+ * value can be adjusted from anywhere on the screen.
607
+ */
608
+ function relativeMapping({ pixelRange = 100 } = {}) {
609
+ const [rangeX, rangeY] = toXY(pixelRange);
610
+ let origin = [0, 0];
611
+ return {
612
+ start: (_state, context) => {
613
+ origin = context.position();
614
+ return origin;
615
+ },
616
+ move: (state) => [origin[0] + state.x / rangeX, origin[1] + state.y / rangeY]
617
+ };
618
+ }
619
+ /**
620
+ * Drive a value with a pointer drag.
621
+ *
622
+ * Combines {@link createDrag} with the scaling of `@tremolo-ui/functions`: the
623
+ * mapping decides where the pointer sits on the 0-1 travel of each axis, and
624
+ * the axis options turn that into a value.
625
+ */
626
+ function createDragValue(element, options) {
627
+ let opts = options;
628
+ let lastValue = [0, 0];
629
+ const axes = () => toXY(opts.axis);
630
+ function valueOf(position) {
631
+ return axes().map((axis, i) => {
632
+ const p = axis.reverse ? 1 - position[i] : position[i];
633
+ const value = (axis.scale ?? linearScale).denormalize(p, axis.min, axis.max);
634
+ return clamp(axis.step ? stepValue(value, axis.step) : value, axis.min, axis.max);
635
+ });
636
+ }
637
+ const context = { position: () => {
638
+ const getValue = opts.getValue;
639
+ if (!getValue) throw new Error("createDragValue: getValue is required by the given mapping");
640
+ const value = getValue();
641
+ return axes().map((axis, i) => {
642
+ const n = (axis.scale ?? linearScale).normalize(value[i], axis.min, axis.max);
643
+ return axis.reverse ? 1 - n : n;
644
+ });
216
645
  } };
646
+ const drag = createDrag(element, {
647
+ threshold: opts.threshold,
648
+ cursor: opts.cursor,
649
+ onDragStart: (state) => {
650
+ const position = opts.mapping.start(state, context);
651
+ if (position) {
652
+ lastValue = valueOf(position);
653
+ if (opts.updateOnPointerDown) opts.onChange?.(lastValue, state);
654
+ }
655
+ opts.onDragStart?.(lastValue, state);
656
+ },
657
+ onDrag: (state) => {
658
+ const position = opts.mapping.move(state, context);
659
+ if (!position) return;
660
+ lastValue = valueOf(position);
661
+ opts.onChange?.(lastValue, state);
662
+ },
663
+ onDragEnd: (state) => opts.onDragEnd?.(lastValue, state)
664
+ });
665
+ return {
666
+ update: (next) => {
667
+ opts = {
668
+ ...opts,
669
+ ...next,
670
+ mapping: opts.mapping
671
+ };
672
+ drag.update({
673
+ threshold: opts.threshold,
674
+ cursor: opts.cursor
675
+ });
676
+ },
677
+ destroy: () => drag.destroy()
678
+ };
217
679
  }
218
680
  //#endregion
219
681
  //#region src/pointer/wheel.ts
@@ -223,14 +685,30 @@ function createDrag(element, { threshold: _threshold = 1, cursor, onDragStart, o
223
685
  * The listener is registered with `passive: false` so that the handler can call
224
686
  * `preventDefault()` to stop the page from scrolling.
225
687
  */
226
- function createWheel(element, onWheel) {
227
- const handler = (event) => onWheel(event);
688
+ function createWheel(element, onWheel, options = {}) {
689
+ let opts = options;
690
+ function hasFocus() {
691
+ const active = element.ownerDocument?.activeElement;
692
+ return !!active && element.contains(active);
693
+ }
694
+ const handler = (event) => {
695
+ if (opts.requireFocus && !hasFocus()) return;
696
+ onWheel(event);
697
+ };
228
698
  element.addEventListener("wheel", handler, { passive: false });
229
- return { destroy: () => {
230
- element.removeEventListener("wheel", handler);
231
- } };
699
+ return {
700
+ update: (next) => {
701
+ opts = {
702
+ ...opts,
703
+ ...next
704
+ };
705
+ },
706
+ destroy: () => {
707
+ element.removeEventListener("wheel", handler);
708
+ }
709
+ };
232
710
  }
233
711
  //#endregion
234
- export { NOT_SUPPORTED, PERMISSION_DENIED, createDrag, createMIDIAccess, createMIDIInput, createMIDIMessage, createWheel };
712
+ export { NOT_SUPPORTED, PERMISSION_DENIED, createAnimationCanvas, createDrag, createDragValue, createMIDIAccess, createMIDIInput, createMIDIMessage, createPianoInput, createWheel, drawingState, elementMapping, isDrawingState, relativeMapping, toXY };
235
713
 
236
714
  //# sourceMappingURL=index.js.map