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