@tremolo-ui/dom 0.7.0 → 0.9.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.
Files changed (53) hide show
  1. package/dist/index.cjs +343 -595
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +230 -284
  4. package/dist/index.d.cts.map +1 -1
  5. package/dist/index.d.ts +230 -284
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +325 -572
  8. package/dist/index.js.map +1 -1
  9. package/dist/internal.cjs +530 -0
  10. package/dist/internal.cjs.map +1 -0
  11. package/dist/internal.d.cts +365 -0
  12. package/dist/internal.d.cts.map +1 -0
  13. package/dist/internal.d.ts +365 -0
  14. package/dist/internal.d.ts.map +1 -0
  15. package/dist/internal.js +500 -0
  16. package/dist/internal.js.map +1 -0
  17. package/dist/marks-DK7xJmGM.d.cts +305 -0
  18. package/dist/marks-DK7xJmGM.d.cts.map +1 -0
  19. package/dist/marks-DK7xJmGM.d.ts +305 -0
  20. package/dist/marks-DK7xJmGM.d.ts.map +1 -0
  21. package/dist/xy-5Oc6JeJr.cjs +952 -0
  22. package/dist/xy-5Oc6JeJr.cjs.map +1 -0
  23. package/dist/xy-lNFl1CTO.js +827 -0
  24. package/dist/xy-lNFl1CTO.js.map +1 -0
  25. package/package.json +12 -2
  26. package/src/canvas/animation.ts +8 -0
  27. package/src/canvas/context.ts +0 -5
  28. package/src/file/accept.ts +17 -0
  29. package/src/file/drop-zone.ts +8 -10
  30. package/src/index.ts +44 -32
  31. package/src/input/change-gesture.ts +104 -0
  32. package/src/input/check-steps.ts +144 -0
  33. package/src/input/defaults.ts +32 -0
  34. package/src/input/direction.ts +107 -0
  35. package/src/internal.ts +51 -0
  36. package/src/knob/geometry.ts +100 -0
  37. package/src/midi/access.ts +16 -15
  38. package/src/midi/input.ts +2 -9
  39. package/src/number-input/stepper-drag.ts +147 -0
  40. package/src/number-input/text.ts +100 -0
  41. package/src/number-input/value.ts +129 -0
  42. package/src/options/replace.ts +22 -0
  43. package/src/piano/index.ts +128 -2
  44. package/src/piano/layout.ts +19 -0
  45. package/src/piano/shortcuts.ts +86 -0
  46. package/src/pointer/drag-value.ts +3 -1
  47. package/src/pointer/long-press.ts +103 -0
  48. package/src/pointer/wheel.ts +10 -7
  49. package/src/points-editor/index.ts +367 -0
  50. package/src/position.ts +20 -0
  51. package/src/slider/decimal-digits.ts +23 -0
  52. package/src/slider/marks.ts +56 -0
  53. package/src/style.ts +41 -0
package/dist/index.cjs CHANGED
@@ -1,4 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_xy = require("./xy-5Oc6JeJr.cjs");
2
3
  let _tremolo_ui_functions = require("@tremolo-ui/functions");
3
4
  //#region src/canvas/context.ts
4
5
  /**
@@ -37,9 +38,6 @@ const drawingState = [
37
38
  "imageSmoothingEnabled",
38
39
  "imageSmoothingQuality"
39
40
  ];
40
- function isDrawingState(value) {
41
- return typeof value === "string" && drawingState.includes(value);
42
- }
43
41
  /** Copy the drawing state off a context, to put back after a resize. */
44
42
  function readDrawingState(context) {
45
43
  const state = {};
@@ -236,39 +234,6 @@ function createAnimationCanvas(canvas, options) {
236
234
  };
237
235
  }
238
236
  //#endregion
239
- //#region src/file/accept.ts
240
- /**
241
- * Does a file satisfy an `accept` attribute?
242
- *
243
- * `accept` is written the way the HTML attribute is: a comma separated list of
244
- * extensions (`.wav`), MIME types (`audio/wav`) and type groups (`audio/*`).
245
- * Anything that matches one entry is accepted, and an empty or missing
246
- * `accept` takes everything.
247
- *
248
- * **The browser's own `accept` is only a hint to the file picker.** A person
249
- * can switch it to "All Files", drag a file in, or pick one the picker was
250
- * never asked about, so what arrives still has to be checked.
251
- *
252
- * A rule that cannot be decided is not treated as a rejection: with no `name`,
253
- * an extension rule says nothing either way, and `accept=".wav"` reports a
254
- * match rather than refusing a file it has not seen the name of. The name is
255
- * there by the time the file is dropped, which is when the answer counts.
256
- */
257
- function matchesAccept(candidate, accept) {
258
- const rules = (accept ?? "").split(",").map((rule) => rule.trim().toLowerCase()).filter(Boolean);
259
- if (rules.length === 0) return true;
260
- const name = candidate.name?.toLowerCase();
261
- const type = candidate.type.toLowerCase();
262
- let undecided = false;
263
- for (const rule of rules) if (rule.startsWith(".")) {
264
- if (name === void 0) undecided = true;
265
- else if (name.endsWith(rule)) return true;
266
- } else if (rule.endsWith("/*")) {
267
- if (type.startsWith(rule.slice(0, -1))) return true;
268
- } else if (type === rule) return true;
269
- return undecided;
270
- }
271
- //#endregion
272
237
  //#region src/file/drop-zone.ts
273
238
  /**
274
239
  * Take files dropped onto an element.
@@ -307,7 +272,7 @@ function createDropZone(element, options = {}) {
307
272
  function anyAcceptable(transfer) {
308
273
  const items = Array.from(transfer?.items ?? []).filter((item) => item.kind === "file");
309
274
  if (items.length === 0) return true;
310
- return items.some((item) => matchesAccept({ type: item.type }, opts.accept));
275
+ return items.some((item) => require_xy.matchesAccept({ type: item.type }, opts.accept));
311
276
  }
312
277
  function onDragEnter(event) {
313
278
  if (!carriesFiles(event.dataTransfer)) return;
@@ -347,14 +312,10 @@ function createDropZone(element, options = {}) {
347
312
  invalid: false
348
313
  });
349
314
  if (opts.disabled) return;
350
- const dropped = Array.from(event.dataTransfer?.files ?? []);
351
- const accepted = [];
352
- const rejected = [];
353
- for (const file of dropped) if (matchesAccept(file, opts.accept)) accepted.push(file);
354
- else rejected.push(file);
355
- if (rejected.length > 0) opts.onReject?.(rejected, event);
315
+ const { accepted, rejected } = require_xy.partitionByAccept(event.dataTransfer?.files ?? [], opts.accept);
356
316
  const taken = opts.multiple ? accepted : accepted.slice(0, 1);
357
317
  if (taken.length > 0) opts.onDrop?.(taken, event);
318
+ if (rejected.length > 0) opts.onReject?.(rejected, event);
358
319
  }
359
320
  /**
360
321
  * A drag that ends anywhere else — dropped on another element, cancelled
@@ -396,118 +357,122 @@ function createDropZone(element, options = {}) {
396
357
  };
397
358
  }
398
359
  //#endregion
399
- //#region src/input/modifiers.ts
360
+ //#region src/input/change-gesture.ts
400
361
  /**
401
- * Checked in this order, and the first one that is both held and configured
402
- * wins. Fixing an order is what keeps two modifiers held at once from
403
- * behaving differently between browsers.
362
+ * Track when a change of value starts and ends, whatever made it: a drag, the
363
+ * wheel, the keys or a reset. A host that records automation needs both ends
364
+ * to know when the control is touched.
404
365
  */
405
- const MODIFIER_ORDER = [
406
- "meta",
407
- "ctrl",
408
- "alt",
409
- "shift"
410
- ];
411
- const MODIFIER_FLAG = {
412
- meta: "metaKey",
413
- ctrl: "ctrlKey",
414
- alt: "altKey",
415
- shift: "shiftKey"
416
- };
417
- /**
418
- * A map is the only form with a `default` key, which is what tells it apart
419
- * from a bare setting. Tuples are arrays, so they never match.
420
- */
421
- function isModifierMap(value) {
422
- return typeof value === "object" && value !== null && !Array.isArray(value) && "default" in value;
366
+ function createChangeGesture(options = {}) {
367
+ let opts = options;
368
+ let current = null;
369
+ let timer = null;
370
+ function clearTimer() {
371
+ if (timer !== null) clearTimeout(timer);
372
+ timer = null;
373
+ }
374
+ function end() {
375
+ clearTimer();
376
+ if (!current) return;
377
+ const { source } = current;
378
+ current = null;
379
+ opts.onEnd?.(source);
380
+ }
381
+ function start(source, held) {
382
+ current = {
383
+ source,
384
+ held
385
+ };
386
+ opts.onStart?.(source);
387
+ }
388
+ return {
389
+ hold: (source) => {
390
+ if (current?.held && current.source === source) return;
391
+ end();
392
+ start(source, true);
393
+ },
394
+ pulse: (source) => {
395
+ if (current?.held) return;
396
+ if (!current || current.source !== source) {
397
+ end();
398
+ start(source, false);
399
+ }
400
+ clearTimer();
401
+ timer = setTimeout(end, opts.endDelay ?? 500);
402
+ },
403
+ instant: (source, change) => {
404
+ end();
405
+ start(source, false);
406
+ change();
407
+ end();
408
+ },
409
+ end,
410
+ active: () => current !== null,
411
+ update: (next) => {
412
+ opts = {
413
+ ...opts,
414
+ ...next
415
+ };
416
+ },
417
+ destroy: end
418
+ };
423
419
  }
420
+ //#endregion
421
+ //#region src/knob/geometry.ts
424
422
  /**
425
- * Pick the setting that applies, given the modifier keys being held.
426
- *
427
- * @example
428
- * selectModifier({ default: 1, shift: 0.1 }, event)
423
+ * Width and height of the viewBox a knob is drawn in. The arcs and the thumb
424
+ * are laid out in these units, and the SVG scales them to the knob's size.
429
425
  */
430
- function selectModifier(options, modifiers) {
431
- if (!isModifierMap(options)) return {
432
- value: options,
433
- modifier: null
434
- };
435
- if (modifiers) for (const modifier of MODIFIER_ORDER) {
436
- const value = options[modifier];
437
- if (value !== void 0 && modifiers[MODIFIER_FLAG[modifier]]) return {
438
- value,
439
- modifier
440
- };
441
- }
426
+ const KNOB_VIEWBOX_SIZE = 100;
427
+ const center = 50;
428
+ function knobAngles({ value, min, max, scale, startValue, angleRange }) {
429
+ const p = scale.normalize(value, min, max);
430
+ const s = scale.normalize(startValue, min, max);
431
+ const r1 = -angleRange / 2;
442
432
  return {
443
- value: options.default,
444
- modifier: null
433
+ p,
434
+ r1,
435
+ r2: r1 + Math.min(p, s) * angleRange,
436
+ r3: r1 + Math.max(p, s) * angleRange,
437
+ r4: angleRange / 2
445
438
  };
446
439
  }
447
440
  /**
448
- * Turn every entry of a setting into another kind of setting, keeping which
449
- * modifier each belongs to.
450
- *
451
- * A drag sensitivity is a number and a keyboard amount is a tuple, but the two
452
- * describe the same thing from the caller's side. This carries one over to the
453
- * other so that a component can hand a sensitivity to {@link applyDelta}
454
- * without unpicking the modifier map itself — which matters, since naming a
455
- * modifier is also what takes `step` out of the pipeline.
441
+ * The point at `angle` on a circle of `radius` around the centre of the
442
+ * viewBox.
456
443
  *
457
- * @example
458
- * mapModifier({ default: 1, shift: 0.1 }, (f) => ['raw', step * f])
459
- * // { default: ['raw', 1], shift: ['raw', 0.1] }
444
+ * The radius depends on the stroke of the line being drawn, which each arc
445
+ * has its own of, so the point is found per arc rather than once for the knob.
460
446
  */
461
- function mapModifier(options, fn) {
462
- if (!isModifierMap(options)) return fn(options);
463
- const mapped = { default: fn(options.default) };
464
- for (const modifier of MODIFIER_ORDER) {
465
- const value = options[modifier];
466
- if (value !== void 0) mapped[modifier] = fn(value);
467
- }
468
- return mapped;
447
+ function knobArcPoint(angle, radius) {
448
+ return {
449
+ x: center + radius * Math.cos((0, _tremolo_ui_functions.radian)(angle - 90)),
450
+ y: center + radius * Math.sin((0, _tremolo_ui_functions.radian)(angle - 90))
451
+ };
469
452
  }
470
- //#endregion
471
- //#region src/input/apply-delta.ts
472
453
  /**
473
- * Move a value by an amount of input, as reported by a wheel or an arrow key.
474
- *
475
- * The pipeline matches {@link createDragValue}: scale, then step, then clamp.
476
- * Which key or which sign of `deltaY` counts as which direction is left to the
477
- * caller, since it differs per component.
478
- *
479
- * @param direction which way, and how many times, to apply the option. The
480
- * size of one step is `option[1]`, so this is normally `1` or `-1`.
481
- *
482
- * @param modifiers the event, for `options` that name a modifier key. See
483
- * {@link selectModifier}.
484
- *
485
- * @example
486
- * // ArrowDown on a slider whose keyboard option is ['raw', 1]
487
- * applyDelta(value, -1, keyboard, { min, max, step, scale })
488
- *
489
- * @example
490
- * // Shift+ArrowDown, where `keyboard` is { default: …, shift: ['raw', 0.1] }
491
- * applyDelta(value, -1, keyboard, range, event)
454
+ * The radius that keeps a stroke of `strokeWidth` inside the viewBox: half of
455
+ * the stroke falls outside the path it is drawn along.
492
456
  */
493
- function applyDelta(value, direction, options, { min, max, step, scale = _tremolo_ui_functions.linearScale }, modifiers) {
494
- if (min >= max) throw new RangeError("requirements: min < max");
495
- if (step !== void 0 && (!Number.isFinite(step) || step <= 0)) throw new RangeError("applyDelta step: requirements: finite and greater than 0");
496
- const { value: [mode, amount], modifier } = selectModifier(options, modifiers);
497
- const x = direction * amount;
498
- const next = mode === "normalized" ? scale.denormalize(scale.normalize(value, min, max) + x, min, max) : value + x;
499
- const quantum = modifier === null ? step : void 0;
500
- const stepped = quantum !== void 0 ? (0, _tremolo_ui_functions.stepValue)(next, quantum) : next;
501
- return (0, _tremolo_ui_functions.clamp)((0, _tremolo_ui_functions.toPrecision)(stepped), min, max);
457
+ function knobArcRadius(strokeWidth) {
458
+ const width = typeof strokeWidth === "number" ? strokeWidth : Number.parseFloat(String(strokeWidth));
459
+ return Number.isFinite(width) ? center - width / 2 : center;
460
+ }
461
+ /** Build an SVG path for an arc, splitting full turns into drawable segments. */
462
+ function knobArcPath(startAngle, endAngle, radius) {
463
+ const start = knobArcPoint(startAngle, radius);
464
+ const sweep = endAngle - startAngle;
465
+ const segmentCount = Math.max(1, Math.ceil(Math.abs(sweep) / 180));
466
+ const segmentSweep = sweep / segmentCount;
467
+ let path = `M ${start.x} ${start.y}`;
468
+ for (let i = 1; i <= segmentCount; i += 1) {
469
+ const end = knobArcPoint(startAngle + segmentSweep * i, radius);
470
+ path += ` A ${radius} ${radius} 0 0 ${segmentSweep >= 0 ? 1 : 0} ${end.x} ${end.y}`;
471
+ }
472
+ return path;
502
473
  }
503
474
  //#endregion
504
475
  //#region src/midi/access.ts
505
- /** @private */
506
- const PERMISSION_DENIED = "PERMISSION_DENIED";
507
- /** @private */
508
- const NOT_SUPPORTED = "NOT_SUPPORTED";
509
- /** @private */
510
- const UNAVAILABLE = "UNAVAILABLE";
511
476
  const INITIAL_STATE = {
512
477
  midiAccess: null,
513
478
  error: null,
@@ -521,9 +486,9 @@ const INITIAL_STATE = {
521
486
  */
522
487
  function toError(reason) {
523
488
  const name = typeof reason === "object" && reason !== null && "name" in reason ? String(reason.name) : "";
524
- if (name === "SecurityError" || name === "NotAllowedError") return PERMISSION_DENIED;
525
- if (name === "NotSupportedError" || name === "TypeError") return NOT_SUPPORTED;
526
- return UNAVAILABLE;
489
+ if (name === "SecurityError" || name === "NotAllowedError") return "PERMISSION_DENIED";
490
+ if (name === "NotSupportedError" || name === "TypeError") return "NOT_SUPPORTED";
491
+ return "UNAVAILABLE";
527
492
  }
528
493
  /**
529
494
  * Request MIDI access in the browser.
@@ -562,7 +527,7 @@ function createMIDIAccess() {
562
527
  if (typeof navigator === "undefined" || !navigator.requestMIDIAccess) {
563
528
  setState({
564
529
  ...state,
565
- error: NOT_SUPPORTED
530
+ error: "NOT_SUPPORTED"
566
531
  });
567
532
  return;
568
533
  }
@@ -660,13 +625,6 @@ const STATUS = {
660
625
  PITCH_BEND: 224
661
626
  };
662
627
  /**
663
- * Centre of the 14-bit pitch bend range: no bend.
664
- *
665
- * The range is not symmetric — 0 is 8192 below centre and 16383 is 8191 above
666
- * — so a wheel at rest reports exactly this rather than half of the maximum.
667
- */
668
- const PITCH_BEND_CENTER = 8192;
669
- /**
670
628
  * Handle the channel voice messages of every connected input. To be used with
671
629
  * {@link createMIDIAccess}. Internally uses {@link createMIDIMessage}, so
672
630
  * devices plugged in later are picked up.
@@ -714,347 +672,65 @@ function createMIDIInput(midiAccess, handlers) {
714
672
  };
715
673
  }
716
674
  //#endregion
717
- //#region src/piano/layout.ts
718
- /**
719
- * `[noteRange.first, noteRange.first + 1, ..., noteRange.last]`
720
- */
721
- function getNoteRangeArray(noteRange) {
722
- return Array.from({ length: noteRange.last - noteRange.first + 1 }, (_, i) => i + noteRange.first);
723
- }
724
- const DEFAULT_KEY_GAP = 1;
725
- const DEFAULT_BLACK_KEY_WIDTH_RATIO = .65;
726
- const DEFAULT_BLACK_KEY_HEIGHT_RATIO = .6;
675
+ //#region src/piano/shortcuts.ts
727
676
  /**
728
- * How many white keys sit at or before each pitch class, counting from C.
729
- *
730
- * A black key shares the number of the white key to its left plus one, which
731
- * puts it on the boundary between the two; {@link notePosition} then shifts it
732
- * back by half its width to centre it there.
677
+ * Ready-made keyboard layouts. Both assume `noteRange.first` is a C.
733
678
  */
734
- const whiteKeysBefore = {
735
- C: 0,
736
- "C#": 1,
737
- D: 1,
738
- "D#": 2,
739
- E: 2,
740
- F: 3,
741
- "F#": 4,
742
- G: 4,
743
- "G#": 5,
744
- A: 5,
745
- "A#": 6,
746
- B: 6
679
+ const SHORTCUTS = {
680
+ /** Every semitone from C, over the two rows of a QWERTY keyboard. */
681
+ HOME_ROW: { keys: [
682
+ "a",
683
+ "w",
684
+ "s",
685
+ "e",
686
+ "d",
687
+ "f",
688
+ "t",
689
+ "g",
690
+ "y",
691
+ "h",
692
+ "u",
693
+ "j",
694
+ "k",
695
+ "o",
696
+ "l",
697
+ "p",
698
+ ";"
699
+ ] },
700
+ /** The white keys only, on the home row. Black keys have no shortcut. */
701
+ HOME_ROW_NATURAL: { keys: [
702
+ "a",
703
+ "",
704
+ "s",
705
+ "",
706
+ "d",
707
+ "f",
708
+ "",
709
+ "g",
710
+ "",
711
+ "h",
712
+ "",
713
+ "j",
714
+ "k",
715
+ "",
716
+ "l",
717
+ "",
718
+ ";"
719
+ ] }
747
720
  };
748
- /** Width of a black key in pixels. */
749
- function blackKeyWidth(layout) {
750
- return layout.whiteKeyWidth * (layout.blackKeyWidthRatio ?? DEFAULT_BLACK_KEY_WIDTH_RATIO);
751
- }
752
- function rawNotePosition(note, layout) {
753
- const slot = layout.whiteKeyWidth + (layout.keyGap ?? DEFAULT_KEY_GAP);
754
- const target = (0, _tremolo_ui_functions.noteKey)(note);
755
- const first = (0, _tremolo_ui_functions.noteKey)(layout.noteRange.first);
756
- const octave = Math.floor((note - layout.noteRange.first) / 12);
757
- const octaveOffset = _tremolo_ui_functions.noteKeys.indexOf(first) > _tremolo_ui_functions.noteKeys.indexOf(target) ? 1 : 0;
758
- const whiteKeysIn = whiteKeysBefore[target] - whiteKeysBefore[first] + (octave + octaveOffset) * 7;
759
- return (0, _tremolo_ui_functions.isBlackKey)(note) ? whiteKeysIn * slot - blackKeyWidth(layout) / 2 : whiteKeysIn * slot;
760
- }
761
- function pianoBounds(layout) {
762
- const notes = getNoteRangeArray(layout.noteRange);
763
- if (notes.length === 0) return {
764
- left: 0,
765
- right: 0
766
- };
767
- let left = Infinity;
768
- let right = -Infinity;
769
- const whiteWidth = layout.whiteKeyWidth + (layout.keyGap ?? DEFAULT_KEY_GAP);
770
- for (const note of notes) {
771
- const noteLeft = rawNotePosition(note, layout);
772
- const width = (0, _tremolo_ui_functions.isBlackKey)(note) ? blackKeyWidth(layout) : whiteWidth;
773
- left = Math.min(left, noteLeft);
774
- right = Math.max(right, noteLeft + width);
775
- }
776
- return {
777
- left,
778
- right
779
- };
780
- }
781
- /** Width of the whole keyboard in pixels. */
782
- function pianoWidth(layout) {
783
- const { left, right } = pianoBounds(layout);
784
- return right - left;
785
- }
786
- /**
787
- * Offset of the left edge of a key from the left edge of the keyboard, in
788
- * pixels.
789
- *
790
- * Notes outside `noteRange` are placed too, so the value is negative below
791
- * `noteRange.first`.
792
- */
793
- function notePosition(note, layout) {
794
- return rawNotePosition(note, layout) - pianoBounds(layout).left;
795
- }
796
- /**
797
- * The note drawn at a point, or null where there is none.
798
- *
799
- * Black keys are tested first, so they win where they overlap a white one. A
800
- * white key covers its gap as well as its width, so the whole width of the
801
- * keyboard belongs to some key and a click cannot fall between two.
802
- *
803
- * @param x offset from the left edge of the keyboard, in pixels
804
- * @param y offset from its top edge, in pixels
805
- * @param height height of the keyboard, in pixels
806
- */
807
- function noteAt(x, y, height, layout) {
808
- if (x < 0 || x >= pianoWidth(layout) || y < 0 || y >= height) return null;
809
- const notes = getNoteRangeArray(layout.noteRange);
810
- if (y < height * (layout.blackKeyHeightRatio ?? DEFAULT_BLACK_KEY_HEIGHT_RATIO)) for (const note of notes) {
811
- if ((0, _tremolo_ui_functions.isWhiteKey)(note)) continue;
812
- const left = notePosition(note, layout);
813
- if (left <= x && x < left + blackKeyWidth(layout)) return note;
814
- }
815
- const slot = layout.whiteKeyWidth + (layout.keyGap ?? DEFAULT_KEY_GAP);
816
- for (const note of notes) {
817
- if ((0, _tremolo_ui_functions.isBlackKey)(note)) continue;
818
- const left = notePosition(note, layout);
819
- if (left <= x && x < left + slot) return note;
820
- }
821
- return null;
822
- }
823
- //#endregion
824
- //#region src/pointer/drag.ts
825
- /**
826
- * Applied to the element for the lifetime of the instance.
827
- *
828
- * `touch-action` stops touch dragging from scrolling the page. That alone makes
829
- * the browser treat a long press as the start of a text selection, so selection
830
- * and the iOS callout are suppressed here too.
831
- */
832
- const MANAGED_STYLES = [
833
- ["touch-action", "none"],
834
- ["user-select", "none"],
835
- ["-webkit-user-select", "none"],
836
- ["-webkit-touch-callout", "none"]
837
- ];
838
- const managedStyleStates = /* @__PURE__ */ new WeakMap();
839
721
  /**
840
- * Track a pointer drag on an element.
841
- *
842
- * Uses Pointer Events only, and pointer capture so that the drag keeps working
843
- * once the pointer leaves the element. For the lifetime of the instance the
844
- * element gets `touch-action: none` so that touch dragging does not scroll the
845
- * page, plus `user-select: none` so that a long press does not start a text
846
- * selection instead.
847
- *
848
- * One pointer at a time by default; see {@link DragOptions.multiPointer}.
722
+ * Whether a key press was typed into something: shortcuts must not play a
723
+ * note for a letter that is going into a text field.
849
724
  */
850
- function createDrag(element, options = {}) {
851
- let opts = options;
852
- const multiPointer = options.multiPointer ?? false;
853
- const capture = element;
854
- const style = element.style;
855
- let managedStyles = managedStyleStates.get(element);
856
- if (style) {
857
- if (managedStyles) managedStyles.count += 1;
858
- else {
859
- const previous = /* @__PURE__ */ new Map();
860
- for (const [property, value] of MANAGED_STYLES) {
861
- previous.set(property, {
862
- value: style.getPropertyValue(property),
863
- priority: style.getPropertyPriority(property)
864
- });
865
- style.setProperty(property, value);
866
- }
867
- managedStyles = {
868
- count: 1,
869
- previous
870
- };
871
- managedStyleStates.set(element, managedStyles);
872
- }
873
- }
874
- const pointers = /* @__PURE__ */ new Map();
875
- /**
876
- * How many pointers each target carries. Adding the same listener twice is a
877
- * no-op and removing it once removes it for good, so a target is subscribed
878
- * to while at least one pointer is on it and no longer.
879
- */
880
- const targets = /* @__PURE__ */ new Map();
881
- let previousCursor;
882
- /** The pointer that asked for the lock, while it is still down. */
883
- let lockedPointerId = null;
884
- let lockRequest = 0;
885
- let destroyed = false;
886
- function state(event, pointer, deltaX, deltaY) {
887
- return {
888
- x: pointer.lockBaseX !== void 0 ? pointer.lockBaseX + pointer.lockMoveX : event.screenX - pointer.startX,
889
- y: pointer.lockBaseY !== void 0 ? pointer.lockBaseY + pointer.lockMoveY : event.screenY - pointer.startY,
890
- deltaX,
891
- deltaY,
892
- clientX: event.clientX,
893
- clientY: event.clientY,
894
- pointerId: event.pointerId,
895
- event
896
- };
897
- }
898
- /**
899
- * `user-select: none` only makes the element's own text unselectable; the
900
- * browser still starts a selection on long press and grabs whatever text it
901
- * finds nearby. Cancelling the selection outright is what actually stops it.
902
- */
903
- function preventSelectStart(event) {
904
- event.preventDefault();
905
- }
906
- function retainTarget(target) {
907
- const count = targets.get(target) ?? 0;
908
- if (count === 0) {
909
- target.addEventListener("pointermove", handlePointerMove);
910
- target.addEventListener("pointerup", handlePointerUp);
911
- target.addEventListener("pointercancel", handlePointerUp);
912
- if (target === element) target.addEventListener("lostpointercapture", handleLostPointerCapture);
913
- }
914
- targets.set(target, count + 1);
725
+ function isEditableTarget(target) {
726
+ if (!(target instanceof HTMLElement)) return false;
727
+ if (target.matches("input, textarea, select")) return true;
728
+ for (let element = target; element;) {
729
+ const contentEditable = element.getAttribute("contenteditable");
730
+ if (contentEditable !== null) return contentEditable.toLowerCase() !== "false";
731
+ element = element.parentElement;
915
732
  }
916
- function releaseTarget(target) {
917
- const count = targets.get(target) ?? 0;
918
- if (count > 1) {
919
- targets.set(target, count - 1);
920
- return;
921
- }
922
- target.removeEventListener("pointermove", handlePointerMove);
923
- target.removeEventListener("pointerup", handlePointerUp);
924
- target.removeEventListener("pointercancel", handlePointerUp);
925
- if (target === element) target.removeEventListener("lostpointercapture", handleLostPointerCapture);
926
- targets.delete(target);
927
- }
928
- function handlePointerDown(event) {
929
- const pointerEvent = event;
930
- if (pointerEvent.button !== 0) return;
931
- const pointerId = pointerEvent.pointerId;
932
- if (pointers.has(pointerId)) return;
933
- if (!multiPointer && pointers.size > 0) return;
934
- if (opts.shouldStart && !opts.shouldStart(pointerEvent)) return;
935
- const isFirst = pointers.size === 0;
936
- if (isFirst && opts.cursor && style) {
937
- previousCursor = style.cursor;
938
- style.cursor = opts.cursor;
939
- }
940
- capture.setPointerCapture?.(pointerId);
941
- const moveTarget = capture.hasPointerCapture?.(pointerId) === true ? element : globalThis.window ?? element;
942
- const pointer = {
943
- moveTarget,
944
- startX: pointerEvent.screenX,
945
- startY: pointerEvent.screenY,
946
- lastX: pointerEvent.screenX,
947
- lastY: pointerEvent.screenY,
948
- lockMoveX: 0,
949
- lockMoveY: 0,
950
- lastEvent: pointerEvent
951
- };
952
- pointers.set(pointerId, pointer);
953
- retainTarget(moveTarget);
954
- if (isFirst) globalThis.document?.addEventListener("selectstart", preventSelectStart);
955
- if (isFirst && opts.pointerLock) {
956
- const requestId = ++lockRequest;
957
- lockedPointerId = pointerId;
958
- globalThis.document?.addEventListener("pointerlockchange", handleLockChange);
959
- try {
960
- (capture.requestPointerLock?.())?.then?.(() => {
961
- if (requestId === lockRequest && (!pointers.has(pointerId) || destroyed) && globalThis.document?.pointerLockElement === element) globalThis.document.exitPointerLock?.();
962
- }, () => {});
963
- } catch {}
964
- }
965
- opts.onDragStart?.(state(pointerEvent, pointer, 0, 0));
966
- }
967
- function handlePointerMove(event) {
968
- const pointerEvent = event;
969
- const pointer = pointers.get(pointerEvent.pointerId);
970
- if (!pointer) return;
971
- const locked = pointer.lockBaseX !== void 0;
972
- const deltaX = locked ? pointerEvent.movementX ?? 0 : pointerEvent.screenX - pointer.lastX;
973
- const deltaY = locked ? pointerEvent.movementY ?? 0 : pointerEvent.screenY - pointer.lastY;
974
- const threshold = opts.threshold ?? 0;
975
- if (Math.abs(deltaX) < threshold && Math.abs(deltaY) < threshold) return;
976
- pointer.lastX = pointerEvent.screenX;
977
- pointer.lastY = pointerEvent.screenY;
978
- pointer.lastEvent = pointerEvent;
979
- if (locked) {
980
- pointer.lockMoveX += deltaX;
981
- pointer.lockMoveY += deltaY;
982
- }
983
- opts.onDrag?.(state(pointerEvent, pointer, deltaX, deltaY));
984
- }
985
- function handleLockChange() {
986
- if (lockedPointerId === null) return;
987
- const pointer = pointers.get(lockedPointerId);
988
- if (!pointer) return;
989
- if (globalThis.document?.pointerLockElement === element) {
990
- pointer.lockBaseX = pointer.lastX - pointer.startX;
991
- pointer.lockBaseY = pointer.lastY - pointer.startY;
992
- pointer.lockMoveX = 0;
993
- pointer.lockMoveY = 0;
994
- return;
995
- }
996
- if (pointer.lockBaseX === void 0) return;
997
- finishDrag(lockedPointerId);
998
- }
999
- function handlePointerUp(event) {
1000
- const pointerEvent = event;
1001
- finishDrag(pointerEvent.pointerId, pointerEvent);
1002
- }
1003
- function handleLostPointerCapture(event) {
1004
- finishDrag(event.pointerId);
1005
- }
1006
- function finishDrag(pointerId, event) {
1007
- const pointer = pointers.get(pointerId);
1008
- if (!pointer) return;
1009
- const finalState = event ? state(event, pointer, event.screenX - pointer.lastX, event.screenY - pointer.lastY) : state(pointer.lastEvent, pointer, 0, 0);
1010
- stopTracking(pointerId);
1011
- opts.onDragEnd?.(finalState);
1012
- }
1013
- function stopTracking(pointerId) {
1014
- const pointer = pointers.get(pointerId);
1015
- if (!pointer) return;
1016
- pointers.delete(pointerId);
1017
- releaseTarget(pointer.moveTarget);
1018
- try {
1019
- if (capture.hasPointerCapture?.(pointerId) === true) capture.releasePointerCapture?.(pointerId);
1020
- } catch {}
1021
- if (lockedPointerId === pointerId) {
1022
- lockedPointerId = null;
1023
- const document = globalThis.document;
1024
- document?.removeEventListener("pointerlockchange", handleLockChange);
1025
- if (document?.pointerLockElement === element) document.exitPointerLock?.();
1026
- }
1027
- if (pointers.size > 0) return;
1028
- globalThis.document?.removeEventListener("selectstart", preventSelectStart);
1029
- if (previousCursor !== void 0 && style) {
1030
- style.cursor = previousCursor;
1031
- previousCursor = void 0;
1032
- }
1033
- }
1034
- element.addEventListener("pointerdown", handlePointerDown);
1035
- return {
1036
- update: (next) => {
1037
- opts = {
1038
- ...opts,
1039
- ...next,
1040
- multiPointer
1041
- };
1042
- },
1043
- destroy: () => {
1044
- if (destroyed) return;
1045
- destroyed = true;
1046
- element.removeEventListener("pointerdown", handlePointerDown);
1047
- for (const pointerId of [...pointers.keys()]) finishDrag(pointerId);
1048
- if (style && managedStyles && --managedStyles.count === 0) {
1049
- for (const [property] of MANAGED_STYLES) {
1050
- const previous = managedStyles.previous.get(property);
1051
- if (previous?.value) style.setProperty(property, previous.value, previous.priority);
1052
- else style.removeProperty(property);
1053
- }
1054
- managedStyleStates.delete(element);
1055
- }
1056
- }
1057
- };
733
+ return false;
1058
734
  }
1059
735
  //#endregion
1060
736
  //#region src/piano/index.ts
@@ -1075,6 +751,11 @@ function createPianoInput(element, options) {
1075
751
  const held = /* @__PURE__ */ new Map();
1076
752
  /** The note each pointer is currently on. */
1077
753
  const pointerNotes = /* @__PURE__ */ new Map();
754
+ /**
755
+ * The note each held shortcut key is playing, by `code` — the physical key,
756
+ * so that releasing it with a different modifier or layout still matches.
757
+ */
758
+ const shortcutNotes = /* @__PURE__ */ new Map();
1078
759
  function activeNotes() {
1079
760
  return [...held.keys()].sort((a, b) => a - b);
1080
761
  }
@@ -1103,7 +784,7 @@ function createPianoInput(element, options) {
1103
784
  /** The note under a pointer, or null where the pointer is off the keys. */
1104
785
  function noteUnder(clientX, clientY) {
1105
786
  const { left, top, height } = element.getBoundingClientRect();
1106
- return noteAt(clientX - left, clientY - top, height, opts.layout);
787
+ return require_xy.noteAt(clientX - left, clientY - top, height, opts.layout);
1107
788
  }
1108
789
  /** Move a pointer onto a note, releasing whatever it held before. */
1109
790
  function movePointer(pointerId, note) {
@@ -1117,7 +798,81 @@ function createPianoInput(element, options) {
1117
798
  noteOn(note, { source });
1118
799
  }
1119
800
  }
1120
- const drag = createDrag(element, {
801
+ /** The note a shortcut key plays, or null when it has none. */
802
+ function shortcutNote(key) {
803
+ const keys = opts.keyboardShortcuts?.keys;
804
+ if (!keys || key === "") return null;
805
+ const { first, last } = opts.layout.noteRange;
806
+ const index = keys.indexOf(key);
807
+ const note = first + index;
808
+ return index === -1 || note > last ? null : note;
809
+ }
810
+ function onKeyDown(event) {
811
+ const id = event.code || event.key;
812
+ if (event.repeat || shortcutNotes.has(id)) return;
813
+ if (isEditableTarget(event.target)) return;
814
+ const note = shortcutNote(event.key);
815
+ if (note === null) return;
816
+ shortcutNotes.set(id, note);
817
+ noteOn(note, { source: `keyboard:${id}` });
818
+ }
819
+ function onKeyUp(event) {
820
+ const id = event.code || event.key;
821
+ const note = shortcutNotes.get(id);
822
+ if (note === void 0) return;
823
+ shortcutNotes.delete(id);
824
+ noteOff(note, { source: `keyboard:${id}` });
825
+ }
826
+ function releaseShortcuts() {
827
+ for (const [id, note] of shortcutNotes) noteOff(note, { source: `keyboard:${id}` });
828
+ shortcutNotes.clear();
829
+ }
830
+ function onFocusOut(event) {
831
+ const next = event.relatedTarget;
832
+ if (next instanceof Node && element.contains(next)) return;
833
+ releaseShortcuts();
834
+ }
835
+ /** The scope the listeners are bound for, or null when none are. */
836
+ let boundScope = null;
837
+ function bindShortcuts() {
838
+ const scope = opts.keyboardShortcuts ? opts.keyboardShortcutsScope ?? "root" : null;
839
+ if (scope === boundScope) return;
840
+ unbindShortcuts();
841
+ const win = globalThis.window;
842
+ if (!scope || !win) return;
843
+ const target = scope === "window" ? win : element;
844
+ target.addEventListener("keydown", onKeyDown);
845
+ target.addEventListener("keyup", onKeyUp);
846
+ if (scope === "root") element.addEventListener("focusout", onFocusOut);
847
+ win.addEventListener("blur", releaseShortcuts);
848
+ boundScope = scope;
849
+ }
850
+ function unbindShortcuts() {
851
+ const win = globalThis.window;
852
+ if (!boundScope || !win) return;
853
+ const target = boundScope === "window" ? win : element;
854
+ target.removeEventListener("keydown", onKeyDown);
855
+ target.removeEventListener("keyup", onKeyUp);
856
+ element.removeEventListener("focusout", onFocusOut);
857
+ win.removeEventListener("blur", releaseShortcuts);
858
+ boundScope = null;
859
+ }
860
+ /**
861
+ * What the held keys were started against. Compared by value: a caller
862
+ * who writes the keys inline hands over a new array on every update, and
863
+ * releasing on that would stop a note as soon as anything re-renders.
864
+ */
865
+ function shortcutMapping() {
866
+ const { first, last } = opts.layout.noteRange;
867
+ return [
868
+ opts.keyboardShortcuts?.keys.join("\0") ?? "",
869
+ opts.keyboardShortcutsScope ?? "root",
870
+ first,
871
+ last
872
+ ].join("");
873
+ }
874
+ bindShortcuts();
875
+ const drag = require_xy.createDrag(element, {
1121
876
  multiPointer: true,
1122
877
  onDragStart: (state) => movePointer(state.pointerId, noteUnder(state.clientX, state.clientY)),
1123
878
  onDrag: (state) => {
@@ -1128,10 +883,13 @@ function createPianoInput(element, options) {
1128
883
  });
1129
884
  return {
1130
885
  update: (next) => {
886
+ const mapping = shortcutMapping();
1131
887
  opts = {
1132
888
  ...opts,
1133
889
  ...next
1134
890
  };
891
+ if (shortcutMapping() !== mapping) releaseShortcuts();
892
+ bindShortcuts();
1135
893
  const midiMax = opts.midiMax ?? 127;
1136
894
  const stoppedNotes = activeNotes().filter((note) => note > midiMax);
1137
895
  if (stoppedNotes.length === 0) return;
@@ -1147,6 +905,8 @@ function createPianoInput(element, options) {
1147
905
  activeNotes,
1148
906
  destroy: () => {
1149
907
  drag.destroy();
908
+ unbindShortcuts();
909
+ shortcutNotes.clear();
1150
910
  const notes = activeNotes();
1151
911
  for (const note of notes) {
1152
912
  held.delete(note);
@@ -1158,24 +918,6 @@ function createPianoInput(element, options) {
1158
918
  };
1159
919
  }
1160
920
  //#endregion
1161
- //#region src/xy.ts
1162
- function isPair(value) {
1163
- return Array.isArray(value);
1164
- }
1165
- /**
1166
- * Spread a setting that may have been given as a single value.
1167
- *
1168
- * The parameter is written out rather than taken as `XYInput<T>`: a
1169
- * conditional type cannot be narrowed, so the constraint stays where it is
1170
- * declared and this takes both forms.
1171
- *
1172
- * The pair is copied rather than passed along, so that the result is a tuple
1173
- * the caller owns even when a readonly one was given.
1174
- */
1175
- function toXY(value) {
1176
- return isPair(value) ? [value[0], value[1]] : [value, value];
1177
- }
1178
- //#endregion
1179
921
  //#region src/pointer/drag-value.ts
1180
922
  /**
1181
923
  * Map the pointer onto the bounding rect of an element: the value *is* the
@@ -1229,7 +971,7 @@ function elementMapping(getElement, { sensitivity } = {}) {
1229
971
  * value can be adjusted from anywhere on the screen.
1230
972
  */
1231
973
  function relativeMapping({ pixelRange = 100, sensitivity } = {}) {
1232
- const [baseX, baseY] = toXY(pixelRange);
974
+ const [baseX, baseY] = require_xy.toXY(pixelRange);
1233
975
  let origin = [0, 0];
1234
976
  /** Where the current sensitivity took over, in drag coordinates. */
1235
977
  let anchor = [0, 0];
@@ -1268,7 +1010,7 @@ function createDragValue(element, options) {
1268
1010
  let opts = options;
1269
1011
  let lastValue = [0, 0];
1270
1012
  let active = false;
1271
- const axes = () => toXY(opts.axis);
1013
+ const axes = () => require_xy.toXY(opts.axis);
1272
1014
  function valueOf(position) {
1273
1015
  return axes().map((axis, i) => {
1274
1016
  const p = axis.reverse ? 1 - position[i] : position[i];
@@ -1286,7 +1028,7 @@ function createDragValue(element, options) {
1286
1028
  return axis.reverse ? 1 - n : n;
1287
1029
  });
1288
1030
  } };
1289
- const drag = createDrag(element, {
1031
+ const drag = require_xy.createDrag(element, {
1290
1032
  threshold: opts.threshold,
1291
1033
  cursor: opts.cursor,
1292
1034
  pointerLock: opts.pointerLock,
@@ -1296,8 +1038,8 @@ function createDragValue(element, options) {
1296
1038
  if (!position) return;
1297
1039
  active = true;
1298
1040
  lastValue = valueOf(position);
1299
- if (opts.updateOnPointerDown) opts.onChange?.(lastValue, state);
1300
1041
  opts.onDragStart?.(lastValue, state);
1042
+ if (opts.updateOnPointerDown) opts.onChange?.(lastValue, state);
1301
1043
  },
1302
1044
  onDrag: (state) => {
1303
1045
  if (!active) return;
@@ -1336,6 +1078,73 @@ function createDragValue(element, options) {
1336
1078
  };
1337
1079
  }
1338
1080
  //#endregion
1081
+ //#region src/pointer/long-press.ts
1082
+ /**
1083
+ * Repeat an action while a pointer is held down: once on the press, then
1084
+ * again every `interval` after `delay` — the way a stepper button or a
1085
+ * key held on a keyboard behaves.
1086
+ *
1087
+ * The release is listened for on the window, not on the element: the
1088
+ * pointer may be let go anywhere. Only the pointer that started the press
1089
+ * ends it, so a second finger lifting elsewhere does not. The window losing
1090
+ * focus ends it too, since the release would never arrive.
1091
+ */
1092
+ function createLongPress(options) {
1093
+ let opts = options;
1094
+ let pointerId = null;
1095
+ let active = false;
1096
+ let timer = null;
1097
+ function clearTimer() {
1098
+ if (timer !== null) clearTimeout(timer);
1099
+ timer = null;
1100
+ }
1101
+ /** Wait `wait`, fire, and keep going at `interval` from then on. */
1102
+ function schedule(wait) {
1103
+ timer = setTimeout(() => {
1104
+ opts.onPress();
1105
+ if (active) schedule(opts.interval ?? 40);
1106
+ }, wait);
1107
+ }
1108
+ function onPointerEnd(event) {
1109
+ if (pointerId !== null && event.pointerId !== pointerId) return;
1110
+ stop();
1111
+ }
1112
+ function listen(add) {
1113
+ const target = globalThis.window;
1114
+ if (!target) return;
1115
+ const method = add ? "addEventListener" : "removeEventListener";
1116
+ target[method]("pointerup", onPointerEnd);
1117
+ target[method]("pointercancel", onPointerEnd);
1118
+ target[method]("blur", stop);
1119
+ }
1120
+ function stop() {
1121
+ if (!active) return;
1122
+ active = false;
1123
+ pointerId = null;
1124
+ clearTimer();
1125
+ listen(false);
1126
+ }
1127
+ return {
1128
+ start: (event) => {
1129
+ if (active || event && event.button !== 0) return;
1130
+ active = true;
1131
+ pointerId = event?.pointerId ?? null;
1132
+ listen(true);
1133
+ opts.onPress();
1134
+ if (active) schedule(opts.delay ?? 500);
1135
+ },
1136
+ stop,
1137
+ update: (next) => {
1138
+ opts = {
1139
+ ...opts,
1140
+ ...next
1141
+ };
1142
+ },
1143
+ pressed: () => active,
1144
+ destroy: stop
1145
+ };
1146
+ }
1147
+ //#endregion
1339
1148
  //#region src/pointer/wheel.ts
1340
1149
  /**
1341
1150
  * Listen to wheel events on an element.
@@ -1343,18 +1152,15 @@ function createDragValue(element, options) {
1343
1152
  * The listener is registered with `passive: false` so that the handler can call
1344
1153
  * `preventDefault()` to stop the page from scrolling.
1345
1154
  */
1346
- function createWheel(element, onWheel, options = {}) {
1347
- let opts = {
1348
- ...options,
1349
- onWheel
1350
- };
1155
+ function createWheel(element, options) {
1156
+ let opts = options;
1351
1157
  function hasFocus() {
1352
1158
  const active = element.ownerDocument?.activeElement;
1353
1159
  return !!active && element.contains(active);
1354
1160
  }
1355
1161
  const handler = (event) => {
1356
1162
  if (opts.requireFocus && !hasFocus()) return;
1357
- opts.onWheel?.(event);
1163
+ opts.onWheel(event);
1358
1164
  };
1359
1165
  element.addEventListener("wheel", handler, { passive: false });
1360
1166
  return {
@@ -1370,101 +1176,43 @@ function createWheel(element, onWheel, options = {}) {
1370
1176
  };
1371
1177
  }
1372
1178
  //#endregion
1373
- //#region src/selection/box.ts
1374
- function rectOf(from, to) {
1375
- return {
1376
- x: Math.min(from[0], to[0]),
1377
- y: Math.min(from[1], to[1]),
1378
- width: Math.abs(to[0] - from[0]),
1379
- height: Math.abs(to[1] - from[1])
1380
- };
1381
- }
1382
- /** Whether the box covers the point, edges included. */
1383
- function selectionBoxCovers(box, [x, y]) {
1384
- return x >= box.x && x <= box.x + box.width && y >= box.y && y <= box.y + box.height;
1385
- }
1179
+ //#region src/position.ts
1386
1180
  /**
1387
- * Selecting by dragging a box over a set of items.
1181
+ * Where a value sits along a track, as a whole percentage from its start.
1388
1182
  *
1389
- * The drag itself is not here: the caller owns the pointer, and reports where
1390
- * it went in the same 0..1 space the items are given in.
1183
+ * Normalized along the scale, so a thumb, the fill behind it and the marks
1184
+ * all sit on the curve the drag follows. `reversed` measures from the other
1185
+ * end — for a track that grows upwards or leftwards on screen, since CSS
1186
+ * places things from the top and the left.
1391
1187
  */
1392
- function createSelectionBox(options) {
1393
- let opts = options;
1394
- let drag = null;
1395
- let box = null;
1396
- function setBox(next) {
1397
- box = next;
1398
- opts.onBoxChange?.(next);
1399
- }
1400
- function apply(rect, base) {
1401
- const inside = [];
1402
- for (const [id, at] of opts.items()) if (selectionBoxCovers(rect, at) && !base.includes(id)) inside.push(id);
1403
- opts.onSelectionChange?.([...base, ...inside]);
1404
- }
1405
- return {
1406
- update: (next) => {
1407
- opts = {
1408
- ...opts,
1409
- ...next
1410
- };
1411
- },
1412
- begin: (at, { additive = false, selection = [] } = {}) => {
1413
- drag = {
1414
- from: at,
1415
- base: additive ? [...selection] : []
1416
- };
1417
- setBox(rectOf(at, at));
1418
- if (!additive) opts.onSelectionChange?.([]);
1419
- },
1420
- move: (to) => {
1421
- if (!drag) return;
1422
- const rect = rectOf(drag.from, to);
1423
- setBox(rect);
1424
- apply(rect, drag.base);
1425
- },
1426
- end: () => {
1427
- const running = drag !== null;
1428
- drag = null;
1429
- if (box !== null) setBox(null);
1430
- return running;
1431
- },
1432
- box: () => box,
1433
- destroy: () => {
1434
- drag = null;
1435
- box = null;
1436
- }
1437
- };
1188
+ function valuePercent(value, { min, max, scale = _tremolo_ui_functions.linearScale }, reversed = false) {
1189
+ const normalized = scale.normalize(value, min, max);
1190
+ return (0, _tremolo_ui_functions.toFixed)((reversed ? 1 - normalized : normalized) * 100);
1438
1191
  }
1439
1192
  //#endregion
1440
- exports.NOT_SUPPORTED = NOT_SUPPORTED;
1441
- exports.PERMISSION_DENIED = PERMISSION_DENIED;
1442
- exports.PITCH_BEND_CENTER = PITCH_BEND_CENTER;
1443
- exports.UNAVAILABLE = UNAVAILABLE;
1444
- exports.applyDelta = applyDelta;
1445
- exports.blackKeyWidth = blackKeyWidth;
1193
+ exports.DEFAULT_DRAG_SENSITIVITY = require_xy.DEFAULT_DRAG_SENSITIVITY;
1194
+ exports.DEFAULT_KEYBOARD_OPTIONS = require_xy.DEFAULT_KEYBOARD_OPTIONS;
1195
+ exports.DEFAULT_WHEEL_OPTIONS = require_xy.DEFAULT_WHEEL_OPTIONS;
1196
+ exports.KNOB_VIEWBOX_SIZE = KNOB_VIEWBOX_SIZE;
1197
+ exports.SHORTCUTS = SHORTCUTS;
1446
1198
  exports.createAnimationCanvas = createAnimationCanvas;
1447
- exports.createDrag = createDrag;
1199
+ exports.createChangeGesture = createChangeGesture;
1200
+ exports.createDrag = require_xy.createDrag;
1448
1201
  exports.createDragValue = createDragValue;
1449
1202
  exports.createDropZone = createDropZone;
1203
+ exports.createLongPress = createLongPress;
1450
1204
  exports.createMIDIAccess = createMIDIAccess;
1451
1205
  exports.createMIDIInput = createMIDIInput;
1452
1206
  exports.createMIDIMessage = createMIDIMessage;
1453
1207
  exports.createPianoInput = createPianoInput;
1454
- exports.createSelectionBox = createSelectionBox;
1208
+ exports.createPointsEditor = require_xy.createPointsEditor;
1455
1209
  exports.createWheel = createWheel;
1456
- exports.drawingState = drawingState;
1457
1210
  exports.elementMapping = elementMapping;
1458
- exports.getNoteRangeArray = getNoteRangeArray;
1459
- exports.isDrawingState = isDrawingState;
1460
- exports.mapModifier = mapModifier;
1461
- exports.matchesAccept = matchesAccept;
1462
- exports.noteAt = noteAt;
1463
- exports.notePosition = notePosition;
1464
- exports.pianoWidth = pianoWidth;
1211
+ exports.knobAngles = knobAngles;
1212
+ exports.knobArcPath = knobArcPath;
1213
+ exports.knobArcRadius = knobArcRadius;
1214
+ exports.notePosition = require_xy.notePosition;
1465
1215
  exports.relativeMapping = relativeMapping;
1466
- exports.selectModifier = selectModifier;
1467
- exports.selectionBoxCovers = selectionBoxCovers;
1468
- exports.toXY = toXY;
1216
+ exports.valuePercent = valuePercent;
1469
1217
 
1470
1218
  //# sourceMappingURL=index.cjs.map