@tremolo-ui/dom 0.8.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.
- package/dist/index.cjs +76 -1391
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +71 -808
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +71 -808
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +64 -1337
- package/dist/index.js.map +1 -1
- package/dist/internal.cjs +530 -0
- package/dist/internal.cjs.map +1 -0
- package/dist/internal.d.cts +365 -0
- package/dist/internal.d.cts.map +1 -0
- package/dist/internal.d.ts +365 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.js +500 -0
- package/dist/internal.js.map +1 -0
- package/dist/marks-DK7xJmGM.d.cts +305 -0
- package/dist/marks-DK7xJmGM.d.cts.map +1 -0
- package/dist/marks-DK7xJmGM.d.ts +305 -0
- package/dist/marks-DK7xJmGM.d.ts.map +1 -0
- package/dist/xy-5Oc6JeJr.cjs +952 -0
- package/dist/xy-5Oc6JeJr.cjs.map +1 -0
- package/dist/xy-lNFl1CTO.js +827 -0
- package/dist/xy-lNFl1CTO.js.map +1 -0
- package/package.json +12 -2
- package/src/canvas/animation.ts +8 -0
- package/src/canvas/context.ts +0 -5
- package/src/index.ts +14 -77
- package/src/input/change-gesture.ts +104 -0
- package/src/internal.ts +51 -0
- package/src/midi/access.ts +16 -15
- package/src/midi/input.ts +2 -9
- package/src/number-input/stepper-drag.ts +9 -1
- package/src/pointer/drag-value.ts +3 -1
- package/src/pointer/wheel.ts +10 -7
- package/src/points-editor/index.ts +7 -1
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,53 +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
|
-
/**
|
|
272
|
-
* Split files into those that satisfy `accept` and those that do not, keeping
|
|
273
|
-
* their order. See {@link matchesAccept}.
|
|
274
|
-
*/
|
|
275
|
-
function partitionByAccept(files, accept) {
|
|
276
|
-
const accepted = [];
|
|
277
|
-
const rejected = [];
|
|
278
|
-
for (const file of files) if (matchesAccept(file, accept)) accepted.push(file);
|
|
279
|
-
else rejected.push(file);
|
|
280
|
-
return {
|
|
281
|
-
accepted,
|
|
282
|
-
rejected
|
|
283
|
-
};
|
|
284
|
-
}
|
|
285
|
-
//#endregion
|
|
286
237
|
//#region src/file/drop-zone.ts
|
|
287
238
|
/**
|
|
288
239
|
* Take files dropped onto an element.
|
|
@@ -321,7 +272,7 @@ function createDropZone(element, options = {}) {
|
|
|
321
272
|
function anyAcceptable(transfer) {
|
|
322
273
|
const items = Array.from(transfer?.items ?? []).filter((item) => item.kind === "file");
|
|
323
274
|
if (items.length === 0) return true;
|
|
324
|
-
return items.some((item) => matchesAccept({ type: item.type }, opts.accept));
|
|
275
|
+
return items.some((item) => require_xy.matchesAccept({ type: item.type }, opts.accept));
|
|
325
276
|
}
|
|
326
277
|
function onDragEnter(event) {
|
|
327
278
|
if (!carriesFiles(event.dataTransfer)) return;
|
|
@@ -361,7 +312,7 @@ function createDropZone(element, options = {}) {
|
|
|
361
312
|
invalid: false
|
|
362
313
|
});
|
|
363
314
|
if (opts.disabled) return;
|
|
364
|
-
const { accepted, rejected } = partitionByAccept(event.dataTransfer?.files ?? [], opts.accept);
|
|
315
|
+
const { accepted, rejected } = require_xy.partitionByAccept(event.dataTransfer?.files ?? [], opts.accept);
|
|
365
316
|
const taken = opts.multiple ? accepted : accepted.slice(0, 1);
|
|
366
317
|
if (taken.length > 0) opts.onDrop?.(taken, event);
|
|
367
318
|
if (rejected.length > 0) opts.onReject?.(rejected, event);
|
|
@@ -406,295 +357,64 @@ function createDropZone(element, options = {}) {
|
|
|
406
357
|
};
|
|
407
358
|
}
|
|
408
359
|
//#endregion
|
|
409
|
-
//#region src/input/
|
|
410
|
-
/**
|
|
411
|
-
* Checked in this order, and the first one that is both held and configured
|
|
412
|
-
* wins. Fixing an order is what keeps two modifiers held at once from
|
|
413
|
-
* behaving differently between browsers.
|
|
414
|
-
*/
|
|
415
|
-
const MODIFIER_ORDER = [
|
|
416
|
-
"meta",
|
|
417
|
-
"ctrl",
|
|
418
|
-
"alt",
|
|
419
|
-
"shift"
|
|
420
|
-
];
|
|
421
|
-
const MODIFIER_FLAG = {
|
|
422
|
-
meta: "metaKey",
|
|
423
|
-
ctrl: "ctrlKey",
|
|
424
|
-
alt: "altKey",
|
|
425
|
-
shift: "shiftKey"
|
|
426
|
-
};
|
|
427
|
-
/**
|
|
428
|
-
* A map is the only form with a `default` key, which is what tells it apart
|
|
429
|
-
* from a bare setting. Tuples are arrays, so they never match.
|
|
430
|
-
*/
|
|
431
|
-
function isModifierMap(value) {
|
|
432
|
-
return typeof value === "object" && value !== null && !Array.isArray(value) && "default" in value;
|
|
433
|
-
}
|
|
434
|
-
/**
|
|
435
|
-
* Pick the setting that applies, given the modifier keys being held.
|
|
436
|
-
*
|
|
437
|
-
* @example
|
|
438
|
-
* selectModifier({ default: 1, shift: 0.1 }, event)
|
|
439
|
-
*/
|
|
440
|
-
function selectModifier(options, modifiers) {
|
|
441
|
-
if (!isModifierMap(options)) return {
|
|
442
|
-
value: options,
|
|
443
|
-
modifier: null
|
|
444
|
-
};
|
|
445
|
-
if (modifiers) for (const modifier of MODIFIER_ORDER) {
|
|
446
|
-
const value = options[modifier];
|
|
447
|
-
if (value !== void 0 && modifiers[MODIFIER_FLAG[modifier]]) return {
|
|
448
|
-
value,
|
|
449
|
-
modifier
|
|
450
|
-
};
|
|
451
|
-
}
|
|
452
|
-
return {
|
|
453
|
-
value: options.default,
|
|
454
|
-
modifier: null
|
|
455
|
-
};
|
|
456
|
-
}
|
|
457
|
-
/**
|
|
458
|
-
* Turn every entry of a setting into another kind of setting, keeping which
|
|
459
|
-
* modifier each belongs to.
|
|
460
|
-
*
|
|
461
|
-
* A drag sensitivity is a number and a keyboard amount is a tuple, but the two
|
|
462
|
-
* describe the same thing from the caller's side. This carries one over to the
|
|
463
|
-
* other so that a component can hand a sensitivity to {@link applyDelta}
|
|
464
|
-
* without unpicking the modifier map itself — which matters, since naming a
|
|
465
|
-
* modifier is also what takes `step` out of the pipeline.
|
|
466
|
-
*
|
|
467
|
-
* @example
|
|
468
|
-
* mapModifier({ default: 1, shift: 0.1 }, (f) => ['raw', step * f])
|
|
469
|
-
* // { default: ['raw', 1], shift: ['raw', 0.1] }
|
|
470
|
-
*/
|
|
471
|
-
function mapModifier(options, fn) {
|
|
472
|
-
if (!isModifierMap(options)) return fn(options);
|
|
473
|
-
const mapped = { default: fn(options.default) };
|
|
474
|
-
for (const modifier of MODIFIER_ORDER) {
|
|
475
|
-
const value = options[modifier];
|
|
476
|
-
if (value !== void 0) mapped[modifier] = fn(value);
|
|
477
|
-
}
|
|
478
|
-
return mapped;
|
|
479
|
-
}
|
|
480
|
-
//#endregion
|
|
481
|
-
//#region src/input/apply-delta.ts
|
|
482
|
-
/**
|
|
483
|
-
* Move a value by an amount of input, as reported by a wheel or an arrow key.
|
|
484
|
-
*
|
|
485
|
-
* The pipeline matches {@link createDragValue}: scale, then step, then clamp.
|
|
486
|
-
* Which key or which sign of `deltaY` counts as which direction is left to the
|
|
487
|
-
* caller, since it differs per component.
|
|
488
|
-
*
|
|
489
|
-
* @param direction which way, and how many times, to apply the option. The
|
|
490
|
-
* size of one step is `option[1]`, so this is normally `1` or `-1`.
|
|
491
|
-
*
|
|
492
|
-
* @param modifiers the event, for `options` that name a modifier key. See
|
|
493
|
-
* {@link selectModifier}.
|
|
494
|
-
*
|
|
495
|
-
* @example
|
|
496
|
-
* // ArrowDown on a slider whose keyboard option is ['raw', 1]
|
|
497
|
-
* applyDelta(value, -1, keyboard, { min, max, step, scale })
|
|
498
|
-
*
|
|
499
|
-
* @example
|
|
500
|
-
* // Shift+ArrowDown, where `keyboard` is { default: …, shift: ['raw', 0.1] }
|
|
501
|
-
* applyDelta(value, -1, keyboard, range, event)
|
|
502
|
-
*/
|
|
503
|
-
function applyDelta(value, direction, options, { min, max, step, scale = _tremolo_ui_functions.linearScale }, modifiers) {
|
|
504
|
-
if (min >= max) throw new RangeError("requirements: min < max");
|
|
505
|
-
if (step !== void 0 && (!Number.isFinite(step) || step <= 0)) throw new RangeError("applyDelta step: requirements: finite and greater than 0");
|
|
506
|
-
const { value: [mode, amount], modifier } = selectModifier(options, modifiers);
|
|
507
|
-
const x = direction * amount;
|
|
508
|
-
const next = mode === "normalized" ? scale.denormalize(scale.normalize(value, min, max) + x, min, max) : value + x;
|
|
509
|
-
const quantum = modifier === null ? step : void 0;
|
|
510
|
-
const stepped = quantum !== void 0 ? (0, _tremolo_ui_functions.stepValue)(next, quantum) : next;
|
|
511
|
-
return (0, _tremolo_ui_functions.clamp)((0, _tremolo_ui_functions.toPrecision)(stepped), min, max);
|
|
512
|
-
}
|
|
513
|
-
//#endregion
|
|
514
|
-
//#region src/input/check-steps.ts
|
|
515
|
-
/** Positions probed across the travel. The ends are left out so that the
|
|
516
|
-
* clamp at `min` and `max` cannot be mistaken for a press that does nothing. */
|
|
517
|
-
const PROBES = [
|
|
518
|
-
.1,
|
|
519
|
-
.2,
|
|
520
|
-
.3,
|
|
521
|
-
.4,
|
|
522
|
-
.5,
|
|
523
|
-
.6,
|
|
524
|
-
.7,
|
|
525
|
-
.8,
|
|
526
|
-
.9
|
|
527
|
-
];
|
|
528
|
-
const NONE = {
|
|
529
|
-
shiftKey: false,
|
|
530
|
-
altKey: false,
|
|
531
|
-
ctrlKey: false,
|
|
532
|
-
metaKey: false
|
|
533
|
-
};
|
|
534
|
-
const MODIFIER_STATE = {
|
|
535
|
-
shift: {
|
|
536
|
-
...NONE,
|
|
537
|
-
shiftKey: true
|
|
538
|
-
},
|
|
539
|
-
alt: {
|
|
540
|
-
...NONE,
|
|
541
|
-
altKey: true
|
|
542
|
-
},
|
|
543
|
-
ctrl: {
|
|
544
|
-
...NONE,
|
|
545
|
-
ctrlKey: true
|
|
546
|
-
},
|
|
547
|
-
meta: {
|
|
548
|
-
...NONE,
|
|
549
|
-
metaKey: true
|
|
550
|
-
}
|
|
551
|
-
};
|
|
552
|
-
/** The held keys that select each entry of a modifier-aware input option. */
|
|
553
|
-
function entries(options) {
|
|
554
|
-
if (Array.isArray(options)) return [NONE];
|
|
555
|
-
return Object.keys(options).map((key) => MODIFIER_STATE[key] ?? NONE);
|
|
556
|
-
}
|
|
360
|
+
//#region src/input/change-gesture.ts
|
|
557
361
|
/**
|
|
558
|
-
*
|
|
559
|
-
*
|
|
560
|
-
*
|
|
561
|
-
* Run against `applyDelta` itself rather than against a reading of `step`:
|
|
562
|
-
* the whole point is that the amount, the step and the scale interact, and
|
|
563
|
-
* the pipeline is the only thing that knows how.
|
|
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.
|
|
564
365
|
*/
|
|
565
|
-
function
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
const value = scale.denormalize(position, min, max);
|
|
573
|
-
const up = applyDelta(value, 1, options, range, modifiers);
|
|
574
|
-
const down = applyDelta(value, -1, options, range, modifiers);
|
|
575
|
-
if (up !== value || down !== value) outcome.moved = true;
|
|
576
|
-
if (!format) continue;
|
|
577
|
-
const shown = format(value);
|
|
578
|
-
if (format(up) !== shown || format(down) !== shown) outcome.visible = true;
|
|
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;
|
|
579
373
|
}
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
*
|
|
594
|
-
* The second is only reported when the press is invisible at *every* point
|
|
595
|
-
* along the travel. A display that rounds is a deliberate choice and is
|
|
596
|
-
* normally right — it is being too coarse everywhere that makes it a mistake.
|
|
597
|
-
*
|
|
598
|
-
* **Meant for development builds only.** It probes the whole travel, so call
|
|
599
|
-
* it behind an inline `process.env.NODE_ENV` check: the bundler then drops
|
|
600
|
-
* the call, and this function and its messages with it.
|
|
601
|
-
*/
|
|
602
|
-
function checkSteps({ component, axis, range, keyboard, wheel, format }) {
|
|
603
|
-
if (!range || !(range.min < range.max)) return [];
|
|
604
|
-
const where = axis ? `${component} (${axis})` : component;
|
|
605
|
-
const warnings = [];
|
|
606
|
-
for (const [name, options] of [["keyboard", keyboard], ["wheel", wheel]]) {
|
|
607
|
-
if (!options) continue;
|
|
608
|
-
const { moved, visible } = probe(options, range, format);
|
|
609
|
-
if (!moved) warnings.push(`[tremolo-ui] ${where}: \`${name}\` cannot move the value.` + (range.step !== void 0 ? ` Each press is smaller than \`step\` (${range.step}), so it rounds` : " Each press rounds") + " straight back to where it started.");
|
|
610
|
-
else if (format && !visible) warnings.push(`[tremolo-ui] ${where}: \`${name}\` moves the value, but \`format\` shows the same text before and after, everywhere in the range. The display is too coarse for it to be seen.`);
|
|
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);
|
|
611
387
|
}
|
|
612
|
-
return warnings;
|
|
613
|
-
}
|
|
614
|
-
//#endregion
|
|
615
|
-
//#region src/input/defaults.ts
|
|
616
|
-
/**
|
|
617
|
-
* The keyboard amount used by Knob, NumberInput, Slider, and XYPad by default:
|
|
618
|
-
* 1 per press in the units of the value, and 0.1 with shift held. That is one
|
|
619
|
-
* `step` only while `step` is 1; a coarser `step` rounds 1 straight back, which
|
|
620
|
-
* `checkSteps` warns about.
|
|
621
|
-
*
|
|
622
|
-
* A modifier entry is not snapped to `step`, which is what lets the finer
|
|
623
|
-
* amount move at all — see `applyDelta`.
|
|
624
|
-
*/
|
|
625
|
-
const DEFAULT_KEYBOARD_OPTIONS = {
|
|
626
|
-
default: ["raw", 1],
|
|
627
|
-
shift: ["raw", .1]
|
|
628
|
-
};
|
|
629
|
-
/**
|
|
630
|
-
* The wheel amount used by Knob, NumberInput, Slider, and XYPad by default.
|
|
631
|
-
*
|
|
632
|
-
* Browsers turn shift+wheel into horizontal scrolling, which empties `deltaY`
|
|
633
|
-
* and fills `deltaX`, so no modifier is bound here.
|
|
634
|
-
*/
|
|
635
|
-
const DEFAULT_WHEEL_OPTIONS = ["raw", 1];
|
|
636
|
-
/**
|
|
637
|
-
* The drag sensitivity used by value controls by default. Shift makes the
|
|
638
|
-
* same movement cover a tenth of the range, matching the arrow keys.
|
|
639
|
-
*/
|
|
640
|
-
const DEFAULT_DRAG_SENSITIVITY = {
|
|
641
|
-
default: 1,
|
|
642
|
-
shift: .1
|
|
643
|
-
};
|
|
644
|
-
//#endregion
|
|
645
|
-
//#region src/input/direction.ts
|
|
646
|
-
/** Is `key` one of the four arrow keys? */
|
|
647
|
-
function isArrowKey(key) {
|
|
648
|
-
return key === "ArrowRight" || key === "ArrowLeft" || key === "ArrowUp" || key === "ArrowDown";
|
|
649
|
-
}
|
|
650
|
-
/**
|
|
651
|
-
* The direction an arrow key moves a single value: right and up raise it.
|
|
652
|
-
* `null` for any other key.
|
|
653
|
-
*/
|
|
654
|
-
function arrowKeyDirection(key) {
|
|
655
|
-
if (!isArrowKey(key)) return null;
|
|
656
|
-
return key === "ArrowRight" || key === "ArrowUp" ? 1 : -1;
|
|
657
|
-
}
|
|
658
|
-
/**
|
|
659
|
-
* The axis and direction an arrow key moves a position on screen. `null` for
|
|
660
|
-
* any other key.
|
|
661
|
-
*
|
|
662
|
-
* The key picks the axis, whichever element inside the control holds the
|
|
663
|
-
* focus: a two-dimensional control is one control to the person moving it.
|
|
664
|
-
*/
|
|
665
|
-
function arrowKeyMove(key) {
|
|
666
|
-
if (!isArrowKey(key)) return null;
|
|
667
388
|
return {
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
direction: delta < 0 ? -1 : 1
|
|
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
|
|
698
418
|
};
|
|
699
419
|
}
|
|
700
420
|
//#endregion
|
|
@@ -753,12 +473,6 @@ function knobArcPath(startAngle, endAngle, radius) {
|
|
|
753
473
|
}
|
|
754
474
|
//#endregion
|
|
755
475
|
//#region src/midi/access.ts
|
|
756
|
-
/** @private */
|
|
757
|
-
const PERMISSION_DENIED = "PERMISSION_DENIED";
|
|
758
|
-
/** @private */
|
|
759
|
-
const NOT_SUPPORTED = "NOT_SUPPORTED";
|
|
760
|
-
/** @private */
|
|
761
|
-
const UNAVAILABLE = "UNAVAILABLE";
|
|
762
476
|
const INITIAL_STATE = {
|
|
763
477
|
midiAccess: null,
|
|
764
478
|
error: null,
|
|
@@ -772,9 +486,9 @@ const INITIAL_STATE = {
|
|
|
772
486
|
*/
|
|
773
487
|
function toError(reason) {
|
|
774
488
|
const name = typeof reason === "object" && reason !== null && "name" in reason ? String(reason.name) : "";
|
|
775
|
-
if (name === "SecurityError" || name === "NotAllowedError") return PERMISSION_DENIED;
|
|
776
|
-
if (name === "NotSupportedError" || name === "TypeError") return NOT_SUPPORTED;
|
|
777
|
-
return UNAVAILABLE;
|
|
489
|
+
if (name === "SecurityError" || name === "NotAllowedError") return "PERMISSION_DENIED";
|
|
490
|
+
if (name === "NotSupportedError" || name === "TypeError") return "NOT_SUPPORTED";
|
|
491
|
+
return "UNAVAILABLE";
|
|
778
492
|
}
|
|
779
493
|
/**
|
|
780
494
|
* Request MIDI access in the browser.
|
|
@@ -813,7 +527,7 @@ function createMIDIAccess() {
|
|
|
813
527
|
if (typeof navigator === "undefined" || !navigator.requestMIDIAccess) {
|
|
814
528
|
setState({
|
|
815
529
|
...state,
|
|
816
|
-
error: NOT_SUPPORTED
|
|
530
|
+
error: "NOT_SUPPORTED"
|
|
817
531
|
});
|
|
818
532
|
return;
|
|
819
533
|
}
|
|
@@ -911,13 +625,6 @@ const STATUS = {
|
|
|
911
625
|
PITCH_BEND: 224
|
|
912
626
|
};
|
|
913
627
|
/**
|
|
914
|
-
* Centre of the 14-bit pitch bend range: no bend.
|
|
915
|
-
*
|
|
916
|
-
* The range is not symmetric — 0 is 8192 below centre and 16383 is 8191 above
|
|
917
|
-
* — so a wheel at rest reports exactly this rather than half of the maximum.
|
|
918
|
-
*/
|
|
919
|
-
const PITCH_BEND_CENTER = 8192;
|
|
920
|
-
/**
|
|
921
628
|
* Handle the channel voice messages of every connected input. To be used with
|
|
922
629
|
* {@link createMIDIAccess}. Internally uses {@link createMIDIMessage}, so
|
|
923
630
|
* devices plugged in later are picked up.
|
|
@@ -965,619 +672,6 @@ function createMIDIInput(midiAccess, handlers) {
|
|
|
965
672
|
};
|
|
966
673
|
}
|
|
967
674
|
//#endregion
|
|
968
|
-
//#region src/pointer/drag.ts
|
|
969
|
-
/**
|
|
970
|
-
* Applied to the element for the lifetime of the instance.
|
|
971
|
-
*
|
|
972
|
-
* `touch-action` stops touch dragging from scrolling the page. That alone makes
|
|
973
|
-
* the browser treat a long press as the start of a text selection, so selection
|
|
974
|
-
* and the iOS callout are suppressed here too.
|
|
975
|
-
*/
|
|
976
|
-
const MANAGED_STYLES = [
|
|
977
|
-
["touch-action", "none"],
|
|
978
|
-
["user-select", "none"],
|
|
979
|
-
["-webkit-user-select", "none"],
|
|
980
|
-
["-webkit-touch-callout", "none"]
|
|
981
|
-
];
|
|
982
|
-
const managedStyleStates = /* @__PURE__ */ new WeakMap();
|
|
983
|
-
/**
|
|
984
|
-
* Track a pointer drag on an element.
|
|
985
|
-
*
|
|
986
|
-
* Uses Pointer Events only, and pointer capture so that the drag keeps working
|
|
987
|
-
* once the pointer leaves the element. For the lifetime of the instance the
|
|
988
|
-
* element gets `touch-action: none` so that touch dragging does not scroll the
|
|
989
|
-
* page, plus `user-select: none` so that a long press does not start a text
|
|
990
|
-
* selection instead.
|
|
991
|
-
*
|
|
992
|
-
* One pointer at a time by default; see {@link DragOptions.multiPointer}.
|
|
993
|
-
*/
|
|
994
|
-
function createDrag(element, options = {}) {
|
|
995
|
-
let opts = options;
|
|
996
|
-
const multiPointer = options.multiPointer ?? false;
|
|
997
|
-
const capture = element;
|
|
998
|
-
const style = element.style;
|
|
999
|
-
let managedStyles = managedStyleStates.get(element);
|
|
1000
|
-
if (style) {
|
|
1001
|
-
if (managedStyles) managedStyles.count += 1;
|
|
1002
|
-
else {
|
|
1003
|
-
const previous = /* @__PURE__ */ new Map();
|
|
1004
|
-
for (const [property, value] of MANAGED_STYLES) {
|
|
1005
|
-
previous.set(property, {
|
|
1006
|
-
value: style.getPropertyValue(property),
|
|
1007
|
-
priority: style.getPropertyPriority(property)
|
|
1008
|
-
});
|
|
1009
|
-
style.setProperty(property, value);
|
|
1010
|
-
}
|
|
1011
|
-
managedStyles = {
|
|
1012
|
-
count: 1,
|
|
1013
|
-
previous
|
|
1014
|
-
};
|
|
1015
|
-
managedStyleStates.set(element, managedStyles);
|
|
1016
|
-
}
|
|
1017
|
-
}
|
|
1018
|
-
const pointers = /* @__PURE__ */ new Map();
|
|
1019
|
-
/**
|
|
1020
|
-
* How many pointers each target carries. Adding the same listener twice is a
|
|
1021
|
-
* no-op and removing it once removes it for good, so a target is subscribed
|
|
1022
|
-
* to while at least one pointer is on it and no longer.
|
|
1023
|
-
*/
|
|
1024
|
-
const targets = /* @__PURE__ */ new Map();
|
|
1025
|
-
let previousCursor;
|
|
1026
|
-
/** The pointer that asked for the lock, while it is still down. */
|
|
1027
|
-
let lockedPointerId = null;
|
|
1028
|
-
let lockRequest = 0;
|
|
1029
|
-
let destroyed = false;
|
|
1030
|
-
function state(event, pointer, deltaX, deltaY) {
|
|
1031
|
-
return {
|
|
1032
|
-
x: pointer.lockBaseX !== void 0 ? pointer.lockBaseX + pointer.lockMoveX : event.screenX - pointer.startX,
|
|
1033
|
-
y: pointer.lockBaseY !== void 0 ? pointer.lockBaseY + pointer.lockMoveY : event.screenY - pointer.startY,
|
|
1034
|
-
deltaX,
|
|
1035
|
-
deltaY,
|
|
1036
|
-
clientX: event.clientX,
|
|
1037
|
-
clientY: event.clientY,
|
|
1038
|
-
pointerId: event.pointerId,
|
|
1039
|
-
event
|
|
1040
|
-
};
|
|
1041
|
-
}
|
|
1042
|
-
/**
|
|
1043
|
-
* `user-select: none` only makes the element's own text unselectable; the
|
|
1044
|
-
* browser still starts a selection on long press and grabs whatever text it
|
|
1045
|
-
* finds nearby. Cancelling the selection outright is what actually stops it.
|
|
1046
|
-
*/
|
|
1047
|
-
function preventSelectStart(event) {
|
|
1048
|
-
event.preventDefault();
|
|
1049
|
-
}
|
|
1050
|
-
function retainTarget(target) {
|
|
1051
|
-
const count = targets.get(target) ?? 0;
|
|
1052
|
-
if (count === 0) {
|
|
1053
|
-
target.addEventListener("pointermove", handlePointerMove);
|
|
1054
|
-
target.addEventListener("pointerup", handlePointerUp);
|
|
1055
|
-
target.addEventListener("pointercancel", handlePointerUp);
|
|
1056
|
-
if (target === element) target.addEventListener("lostpointercapture", handleLostPointerCapture);
|
|
1057
|
-
}
|
|
1058
|
-
targets.set(target, count + 1);
|
|
1059
|
-
}
|
|
1060
|
-
function releaseTarget(target) {
|
|
1061
|
-
const count = targets.get(target) ?? 0;
|
|
1062
|
-
if (count > 1) {
|
|
1063
|
-
targets.set(target, count - 1);
|
|
1064
|
-
return;
|
|
1065
|
-
}
|
|
1066
|
-
target.removeEventListener("pointermove", handlePointerMove);
|
|
1067
|
-
target.removeEventListener("pointerup", handlePointerUp);
|
|
1068
|
-
target.removeEventListener("pointercancel", handlePointerUp);
|
|
1069
|
-
if (target === element) target.removeEventListener("lostpointercapture", handleLostPointerCapture);
|
|
1070
|
-
targets.delete(target);
|
|
1071
|
-
}
|
|
1072
|
-
function handlePointerDown(event) {
|
|
1073
|
-
const pointerEvent = event;
|
|
1074
|
-
if (pointerEvent.button !== 0) return;
|
|
1075
|
-
const pointerId = pointerEvent.pointerId;
|
|
1076
|
-
if (pointers.has(pointerId)) return;
|
|
1077
|
-
if (!multiPointer && pointers.size > 0) return;
|
|
1078
|
-
if (opts.shouldStart && !opts.shouldStart(pointerEvent)) return;
|
|
1079
|
-
const isFirst = pointers.size === 0;
|
|
1080
|
-
if (isFirst && opts.cursor && style) {
|
|
1081
|
-
previousCursor = style.cursor;
|
|
1082
|
-
style.cursor = opts.cursor;
|
|
1083
|
-
}
|
|
1084
|
-
capture.setPointerCapture?.(pointerId);
|
|
1085
|
-
const moveTarget = capture.hasPointerCapture?.(pointerId) === true ? element : globalThis.window ?? element;
|
|
1086
|
-
const pointer = {
|
|
1087
|
-
moveTarget,
|
|
1088
|
-
startX: pointerEvent.screenX,
|
|
1089
|
-
startY: pointerEvent.screenY,
|
|
1090
|
-
lastX: pointerEvent.screenX,
|
|
1091
|
-
lastY: pointerEvent.screenY,
|
|
1092
|
-
lockMoveX: 0,
|
|
1093
|
-
lockMoveY: 0,
|
|
1094
|
-
lastEvent: pointerEvent
|
|
1095
|
-
};
|
|
1096
|
-
pointers.set(pointerId, pointer);
|
|
1097
|
-
retainTarget(moveTarget);
|
|
1098
|
-
if (isFirst) globalThis.document?.addEventListener("selectstart", preventSelectStart);
|
|
1099
|
-
if (isFirst && opts.pointerLock) {
|
|
1100
|
-
const requestId = ++lockRequest;
|
|
1101
|
-
lockedPointerId = pointerId;
|
|
1102
|
-
globalThis.document?.addEventListener("pointerlockchange", handleLockChange);
|
|
1103
|
-
try {
|
|
1104
|
-
(capture.requestPointerLock?.())?.then?.(() => {
|
|
1105
|
-
if (requestId === lockRequest && (!pointers.has(pointerId) || destroyed) && globalThis.document?.pointerLockElement === element) globalThis.document.exitPointerLock?.();
|
|
1106
|
-
}, () => {});
|
|
1107
|
-
} catch {}
|
|
1108
|
-
}
|
|
1109
|
-
opts.onDragStart?.(state(pointerEvent, pointer, 0, 0));
|
|
1110
|
-
}
|
|
1111
|
-
function handlePointerMove(event) {
|
|
1112
|
-
const pointerEvent = event;
|
|
1113
|
-
const pointer = pointers.get(pointerEvent.pointerId);
|
|
1114
|
-
if (!pointer) return;
|
|
1115
|
-
const locked = pointer.lockBaseX !== void 0;
|
|
1116
|
-
const deltaX = locked ? pointerEvent.movementX ?? 0 : pointerEvent.screenX - pointer.lastX;
|
|
1117
|
-
const deltaY = locked ? pointerEvent.movementY ?? 0 : pointerEvent.screenY - pointer.lastY;
|
|
1118
|
-
const threshold = opts.threshold ?? 0;
|
|
1119
|
-
if (Math.abs(deltaX) < threshold && Math.abs(deltaY) < threshold) return;
|
|
1120
|
-
pointer.lastX = pointerEvent.screenX;
|
|
1121
|
-
pointer.lastY = pointerEvent.screenY;
|
|
1122
|
-
pointer.lastEvent = pointerEvent;
|
|
1123
|
-
if (locked) {
|
|
1124
|
-
pointer.lockMoveX += deltaX;
|
|
1125
|
-
pointer.lockMoveY += deltaY;
|
|
1126
|
-
}
|
|
1127
|
-
opts.onDrag?.(state(pointerEvent, pointer, deltaX, deltaY));
|
|
1128
|
-
}
|
|
1129
|
-
function handleLockChange() {
|
|
1130
|
-
if (lockedPointerId === null) return;
|
|
1131
|
-
const pointer = pointers.get(lockedPointerId);
|
|
1132
|
-
if (!pointer) return;
|
|
1133
|
-
if (globalThis.document?.pointerLockElement === element) {
|
|
1134
|
-
pointer.lockBaseX = pointer.lastX - pointer.startX;
|
|
1135
|
-
pointer.lockBaseY = pointer.lastY - pointer.startY;
|
|
1136
|
-
pointer.lockMoveX = 0;
|
|
1137
|
-
pointer.lockMoveY = 0;
|
|
1138
|
-
return;
|
|
1139
|
-
}
|
|
1140
|
-
if (pointer.lockBaseX === void 0) return;
|
|
1141
|
-
finishDrag(lockedPointerId);
|
|
1142
|
-
}
|
|
1143
|
-
function handlePointerUp(event) {
|
|
1144
|
-
const pointerEvent = event;
|
|
1145
|
-
finishDrag(pointerEvent.pointerId, pointerEvent);
|
|
1146
|
-
}
|
|
1147
|
-
function handleLostPointerCapture(event) {
|
|
1148
|
-
finishDrag(event.pointerId);
|
|
1149
|
-
}
|
|
1150
|
-
function finishDrag(pointerId, event) {
|
|
1151
|
-
const pointer = pointers.get(pointerId);
|
|
1152
|
-
if (!pointer) return;
|
|
1153
|
-
const finalState = event ? state(event, pointer, event.screenX - pointer.lastX, event.screenY - pointer.lastY) : state(pointer.lastEvent, pointer, 0, 0);
|
|
1154
|
-
stopTracking(pointerId);
|
|
1155
|
-
opts.onDragEnd?.(finalState);
|
|
1156
|
-
}
|
|
1157
|
-
function stopTracking(pointerId) {
|
|
1158
|
-
const pointer = pointers.get(pointerId);
|
|
1159
|
-
if (!pointer) return;
|
|
1160
|
-
pointers.delete(pointerId);
|
|
1161
|
-
releaseTarget(pointer.moveTarget);
|
|
1162
|
-
try {
|
|
1163
|
-
if (capture.hasPointerCapture?.(pointerId) === true) capture.releasePointerCapture?.(pointerId);
|
|
1164
|
-
} catch {}
|
|
1165
|
-
if (lockedPointerId === pointerId) {
|
|
1166
|
-
lockedPointerId = null;
|
|
1167
|
-
const document = globalThis.document;
|
|
1168
|
-
document?.removeEventListener("pointerlockchange", handleLockChange);
|
|
1169
|
-
if (document?.pointerLockElement === element) document.exitPointerLock?.();
|
|
1170
|
-
}
|
|
1171
|
-
if (pointers.size > 0) return;
|
|
1172
|
-
globalThis.document?.removeEventListener("selectstart", preventSelectStart);
|
|
1173
|
-
if (previousCursor !== void 0 && style) {
|
|
1174
|
-
style.cursor = previousCursor;
|
|
1175
|
-
previousCursor = void 0;
|
|
1176
|
-
}
|
|
1177
|
-
}
|
|
1178
|
-
element.addEventListener("pointerdown", handlePointerDown);
|
|
1179
|
-
return {
|
|
1180
|
-
update: (next) => {
|
|
1181
|
-
opts = {
|
|
1182
|
-
...opts,
|
|
1183
|
-
...next,
|
|
1184
|
-
multiPointer
|
|
1185
|
-
};
|
|
1186
|
-
},
|
|
1187
|
-
destroy: () => {
|
|
1188
|
-
if (destroyed) return;
|
|
1189
|
-
destroyed = true;
|
|
1190
|
-
element.removeEventListener("pointerdown", handlePointerDown);
|
|
1191
|
-
for (const pointerId of [...pointers.keys()]) finishDrag(pointerId);
|
|
1192
|
-
if (style && managedStyles && --managedStyles.count === 0) {
|
|
1193
|
-
for (const [property] of MANAGED_STYLES) {
|
|
1194
|
-
const previous = managedStyles.previous.get(property);
|
|
1195
|
-
if (previous?.value) style.setProperty(property, previous.value, previous.priority);
|
|
1196
|
-
else style.removeProperty(property);
|
|
1197
|
-
}
|
|
1198
|
-
managedStyleStates.delete(element);
|
|
1199
|
-
}
|
|
1200
|
-
}
|
|
1201
|
-
};
|
|
1202
|
-
}
|
|
1203
|
-
//#endregion
|
|
1204
|
-
//#region src/number-input/stepper-drag.ts
|
|
1205
|
-
/**
|
|
1206
|
-
* Drag up and down on the steppers of a number input to move its value, one
|
|
1207
|
-
* `step` every `pixels` — up raises it, as on a knob.
|
|
1208
|
-
*
|
|
1209
|
-
* Counted from where the drag started moving rather than added up per
|
|
1210
|
-
* event, so rounding cannot accumulate. The start is taken on the first
|
|
1211
|
-
* move, not on pointerdown: a stepper button acts on pointerdown, so by then
|
|
1212
|
-
* the value may already have been nudged once, and the drag carries on from
|
|
1213
|
-
* there.
|
|
1214
|
-
*/
|
|
1215
|
-
function createStepperDrag(element, options) {
|
|
1216
|
-
let opts = options;
|
|
1217
|
-
let origin = null;
|
|
1218
|
-
let moved = false;
|
|
1219
|
-
/**
|
|
1220
|
-
* Which sensitivity the drag is counting at, and where the previous event
|
|
1221
|
-
* was — a key produces no pointer event of its own, so a change is only
|
|
1222
|
-
* seen on the next move and has to be dated back to the one before it.
|
|
1223
|
-
*/
|
|
1224
|
-
let factor = 1;
|
|
1225
|
-
let previousY = 0;
|
|
1226
|
-
const drag = createDrag(element, {
|
|
1227
|
-
threshold: 1,
|
|
1228
|
-
cursor: "ns-resize",
|
|
1229
|
-
pointerLock: opts.pointerLock,
|
|
1230
|
-
onDragStart: (state) => {
|
|
1231
|
-
origin = null;
|
|
1232
|
-
moved = false;
|
|
1233
|
-
factor = selectModifier(opts.sensitivity ?? DEFAULT_DRAG_SENSITIVITY, state.event).value;
|
|
1234
|
-
},
|
|
1235
|
-
onDrag: (state) => {
|
|
1236
|
-
const { y } = state;
|
|
1237
|
-
if (!origin) {
|
|
1238
|
-
origin = {
|
|
1239
|
-
y,
|
|
1240
|
-
value: opts.getValue()
|
|
1241
|
-
};
|
|
1242
|
-
previousY = y;
|
|
1243
|
-
return;
|
|
1244
|
-
}
|
|
1245
|
-
const sensitivity = opts.sensitivity ?? DEFAULT_DRAG_SENSITIVITY;
|
|
1246
|
-
const next = selectModifier(sensitivity, state.event).value;
|
|
1247
|
-
if (next !== factor) {
|
|
1248
|
-
origin = {
|
|
1249
|
-
y: previousY,
|
|
1250
|
-
value: opts.getValue()
|
|
1251
|
-
};
|
|
1252
|
-
factor = next;
|
|
1253
|
-
}
|
|
1254
|
-
previousY = y;
|
|
1255
|
-
const steps = Math.round(-(y - origin.y) / (opts.pixels ?? 1));
|
|
1256
|
-
if (steps === 0) return;
|
|
1257
|
-
moved = true;
|
|
1258
|
-
const step = opts.range.step ?? 1;
|
|
1259
|
-
const amounts = mapModifier(sensitivity, (f) => ["raw", step * f]);
|
|
1260
|
-
opts.onChange(applyDelta(origin.value, steps, amounts, opts.range, state.event));
|
|
1261
|
-
},
|
|
1262
|
-
onDragEnd: () => {
|
|
1263
|
-
moved = false;
|
|
1264
|
-
}
|
|
1265
|
-
});
|
|
1266
|
-
return {
|
|
1267
|
-
update: (next) => {
|
|
1268
|
-
opts = {
|
|
1269
|
-
...opts,
|
|
1270
|
-
...next
|
|
1271
|
-
};
|
|
1272
|
-
if ("pointerLock" in next) drag.update({ pointerLock: opts.pointerLock });
|
|
1273
|
-
},
|
|
1274
|
-
moved: () => moved,
|
|
1275
|
-
destroy: () => drag.destroy()
|
|
1276
|
-
};
|
|
1277
|
-
}
|
|
1278
|
-
//#endregion
|
|
1279
|
-
//#region src/number-input/value.ts
|
|
1280
|
-
/**
|
|
1281
|
-
* The ranges a number input moves its value across, with the open ends
|
|
1282
|
-
* filled in.
|
|
1283
|
-
*
|
|
1284
|
-
* A normalized amount needs a finite span even when an end is unbounded or
|
|
1285
|
-
* clamping is off. Safe integers provide one without overflowing the span a
|
|
1286
|
-
* scale calculates. A raw amount needs no span, so its open ends can cover
|
|
1287
|
-
* every finite number instead of stopping at the safe-integer range.
|
|
1288
|
-
*/
|
|
1289
|
-
function numberInputRanges({ min, max, step, scale, clampValue = true }) {
|
|
1290
|
-
const lo = clampValue ? min : void 0;
|
|
1291
|
-
const hi = clampValue ? max : void 0;
|
|
1292
|
-
return {
|
|
1293
|
-
normalized: {
|
|
1294
|
-
min: lo ?? Number.MIN_SAFE_INTEGER,
|
|
1295
|
-
max: hi ?? Number.MAX_SAFE_INTEGER,
|
|
1296
|
-
step,
|
|
1297
|
-
scale
|
|
1298
|
-
},
|
|
1299
|
-
raw: {
|
|
1300
|
-
min: lo ?? -Number.MAX_VALUE,
|
|
1301
|
-
max: hi ?? Number.MAX_VALUE,
|
|
1302
|
-
step,
|
|
1303
|
-
scale
|
|
1304
|
-
}
|
|
1305
|
-
};
|
|
1306
|
-
}
|
|
1307
|
-
/**
|
|
1308
|
-
* Move a number input's value by one press of a key, a wheel notch or a
|
|
1309
|
-
* stepper, picking the range that suits the kind of amount. See `applyDelta`.
|
|
1310
|
-
*/
|
|
1311
|
-
function nudgeNumberInput(value, direction, options, ranges, modifiers) {
|
|
1312
|
-
const [mode] = selectModifier(options, modifiers).value;
|
|
1313
|
-
return applyDelta(value, direction, options, mode === "raw" ? ranges.raw : ranges.normalized, modifiers);
|
|
1314
|
-
}
|
|
1315
|
-
/**
|
|
1316
|
-
* Where the value stands against the ends: whether it can go no further
|
|
1317
|
-
* down or up, and whether it lies outside them — which only an unclamped
|
|
1318
|
-
* input, or a value set from outside, can do.
|
|
1319
|
-
*/
|
|
1320
|
-
function numberInputBounds(value, { min, max, clampValue = true }) {
|
|
1321
|
-
return {
|
|
1322
|
-
atMin: clampValue && min !== void 0 && value <= min,
|
|
1323
|
-
atMax: clampValue && max !== void 0 && value >= max,
|
|
1324
|
-
outOfRange: min !== void 0 && value < min || max !== void 0 && value > max
|
|
1325
|
-
};
|
|
1326
|
-
}
|
|
1327
|
-
/**
|
|
1328
|
-
* The value typed text commits to, or `null` when there is no number in it.
|
|
1329
|
-
*
|
|
1330
|
-
* Text with no number is not a value: the input should go back to what it
|
|
1331
|
-
* was showing rather than commit a zero the user never typed. What is read
|
|
1332
|
-
* is clamped here and not while typing, since clamping as the user types
|
|
1333
|
-
* would make "1500" impossible to enter into an input whose max is 100.
|
|
1334
|
-
*/
|
|
1335
|
-
function commitNumberInputText(text, parse, { min, max, clampValue = true }) {
|
|
1336
|
-
const parsed = parse(text);
|
|
1337
|
-
if (!Number.isFinite(parsed)) return null;
|
|
1338
|
-
let committed = parsed;
|
|
1339
|
-
if (clampValue && min !== void 0) committed = Math.max(committed, min);
|
|
1340
|
-
if (clampValue && max !== void 0) committed = Math.min(committed, max);
|
|
1341
|
-
return committed;
|
|
1342
|
-
}
|
|
1343
|
-
//#endregion
|
|
1344
|
-
//#region src/number-input/text.ts
|
|
1345
|
-
/**
|
|
1346
|
-
* Reading a number out of the text of a number input, and keeping the caret
|
|
1347
|
-
* in place while the number under it changes.
|
|
1348
|
-
*
|
|
1349
|
-
* The text is whatever `format` made of the value — `"440 Hz"`, `"-6.0 dB"` —
|
|
1350
|
-
* or a half-typed entry, so none of this assumes the text is a number alone.
|
|
1351
|
-
*/
|
|
1352
|
-
/** The signs a number can carry. U+2212 is what some `Intl.NumberFormat` locales write. */
|
|
1353
|
-
const SIGNS = "+-−";
|
|
1354
|
-
/**
|
|
1355
|
-
* Where the number is in the text: from its first digit to its last, with the
|
|
1356
|
-
* sign and the decimal point in front of it. Whatever is left on either side
|
|
1357
|
-
* is taken for the unit, so it does not matter whether a space separates them,
|
|
1358
|
-
* or what the number looks like — `+6.0`, `1e+21`, `1,000` and `1:30` are each
|
|
1359
|
-
* one number. `null` when the text has no digit at all.
|
|
1360
|
-
*/
|
|
1361
|
-
function numberSpan(text) {
|
|
1362
|
-
const first = text.search(/\d/);
|
|
1363
|
-
if (first === -1) return null;
|
|
1364
|
-
let start = first;
|
|
1365
|
-
if (text[start - 1] === ".") start -= 1;
|
|
1366
|
-
if (start > 0 && SIGNS.includes(text[start - 1])) start -= 1;
|
|
1367
|
-
return {
|
|
1368
|
-
start,
|
|
1369
|
-
end: text.search(/\d\D*$/) + 1
|
|
1370
|
-
};
|
|
1371
|
-
}
|
|
1372
|
-
/** A plain number, and nothing else: no grouping, no other separators. */
|
|
1373
|
-
const PLAIN_NUMBER = /^[+\-\u2212]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+\-\u2212]?\d+)?$/;
|
|
1374
|
-
/**
|
|
1375
|
-
* The number in the text, with the unit after it ignored: the default `parse`
|
|
1376
|
-
* of a number input.
|
|
1377
|
-
*
|
|
1378
|
-
* `NaN`, which leaves the value alone, whenever the number cannot be read
|
|
1379
|
-
* safely, rather than a part of it: text with no number, a number that is not
|
|
1380
|
-
* a plain one (`1,000` would otherwise read as 1, and `1:30` as 1), and text
|
|
1381
|
-
* with something in front of the number, which can change what it means (the
|
|
1382
|
-
* `L` of a pan reading `L 30`). A `format` that writes any of those needs its
|
|
1383
|
-
* own `parse`.
|
|
1384
|
-
*/
|
|
1385
|
-
function parseNumberText(text) {
|
|
1386
|
-
const span = numberSpan(text);
|
|
1387
|
-
if (!span || text.slice(0, span.start).trim() !== "") return NaN;
|
|
1388
|
-
const number = text.slice(span.start, span.end);
|
|
1389
|
-
return PLAIN_NUMBER.test(number) ? Number(number.replace(/\u2212/g, "-")) : NaN;
|
|
1390
|
-
}
|
|
1391
|
-
/**
|
|
1392
|
-
* The index the caret is measured against: the decimal point, or where one
|
|
1393
|
-
* would go if the number has none — in front of an exponent, if there is one.
|
|
1394
|
-
*
|
|
1395
|
-
* Measuring from an end instead would slide the caret across a digit whenever
|
|
1396
|
-
* the number changed length — `9.9` to `10.0` gains a character in front, `10`
|
|
1397
|
-
* to `9` loses one, and so does `9e+9` to `1e+10` behind — which is exactly
|
|
1398
|
-
* what stepping does.
|
|
1399
|
-
*/
|
|
1400
|
-
function decimalAnchor(text, span) {
|
|
1401
|
-
const number = text.slice(span.start, span.end);
|
|
1402
|
-
const exponent = number.search(/[eE][+\-\u2212]?\d/);
|
|
1403
|
-
const mantissa = exponent === -1 ? number : number.slice(0, exponent);
|
|
1404
|
-
const dot = mantissa.indexOf(".");
|
|
1405
|
-
return span.start + (dot === -1 ? mantissa.length : dot);
|
|
1406
|
-
}
|
|
1407
|
-
const NO_NUMBER = {
|
|
1408
|
-
start: 0,
|
|
1409
|
-
end: 0
|
|
1410
|
-
};
|
|
1411
|
-
/**
|
|
1412
|
-
* Where the caret is, relative to the decimal point, so that it can be put
|
|
1413
|
-
* back at the same digit once the value has changed. See
|
|
1414
|
-
* {@link caretAtDecimalOffset}.
|
|
1415
|
-
*/
|
|
1416
|
-
function caretDecimalOffset(text, caret) {
|
|
1417
|
-
return caret - decimalAnchor(text, numberSpan(text) ?? NO_NUMBER);
|
|
1418
|
-
}
|
|
1419
|
-
/**
|
|
1420
|
-
* The caret position `offset` characters from the decimal point of the new
|
|
1421
|
-
* text, kept within the number.
|
|
1422
|
-
*
|
|
1423
|
-
* @example
|
|
1424
|
-
* // The caret sits in front of the point of "9.9" when ArrowUp turns it
|
|
1425
|
-
* // into "10.0"
|
|
1426
|
-
* const offset = caretDecimalOffset('9.9', 1) // 0
|
|
1427
|
-
* caretAtDecimalOffset('10.0', offset) // 2: still in front of the point
|
|
1428
|
-
*/
|
|
1429
|
-
function caretAtDecimalOffset(text, offset) {
|
|
1430
|
-
const span = numberSpan(text) ?? NO_NUMBER;
|
|
1431
|
-
const place = decimalAnchor(text, span) + offset;
|
|
1432
|
-
return Math.max(span.start, Math.min(place, span.end));
|
|
1433
|
-
}
|
|
1434
|
-
//#endregion
|
|
1435
|
-
//#region src/options/replace.ts
|
|
1436
|
-
/**
|
|
1437
|
-
* The `update()` argument that leaves an instance with exactly `next` as its
|
|
1438
|
-
* options.
|
|
1439
|
-
*
|
|
1440
|
-
* `update()` merges into what the instance has, which suits a wrapper that
|
|
1441
|
-
* pushes one setting at a time. A wrapper handed the whole new set instead —
|
|
1442
|
-
* a Svelte action, a Vue composable — has to clear what the new set no longer
|
|
1443
|
-
* carries, or a handler taken away, or an option left to its default, keeps
|
|
1444
|
-
* working.
|
|
1445
|
-
*
|
|
1446
|
-
* @example
|
|
1447
|
-
* instance.update(replaceOptions(previous, next))
|
|
1448
|
-
*/
|
|
1449
|
-
function replaceOptions(previous, next) {
|
|
1450
|
-
return {
|
|
1451
|
-
...Object.fromEntries(Object.keys(previous ?? {}).map((key) => [key, void 0])),
|
|
1452
|
-
...next
|
|
1453
|
-
};
|
|
1454
|
-
}
|
|
1455
|
-
//#endregion
|
|
1456
|
-
//#region src/piano/layout.ts
|
|
1457
|
-
/**
|
|
1458
|
-
* `[noteRange.first, noteRange.first + 1, ..., noteRange.last]`
|
|
1459
|
-
*/
|
|
1460
|
-
function getNoteRangeArray(noteRange) {
|
|
1461
|
-
return Array.from({ length: noteRange.last - noteRange.first + 1 }, (_, i) => i + noteRange.first);
|
|
1462
|
-
}
|
|
1463
|
-
const DEFAULT_KEY_GAP = 1;
|
|
1464
|
-
const DEFAULT_BLACK_KEY_WIDTH_RATIO = .65;
|
|
1465
|
-
const DEFAULT_BLACK_KEY_HEIGHT_RATIO = .6;
|
|
1466
|
-
/**
|
|
1467
|
-
* How many white keys sit at or before each pitch class, counting from C.
|
|
1468
|
-
*
|
|
1469
|
-
* A black key shares the number of the white key to its left plus one, which
|
|
1470
|
-
* puts it on the boundary between the two; {@link notePosition} then shifts it
|
|
1471
|
-
* back by half its width to centre it there.
|
|
1472
|
-
*/
|
|
1473
|
-
const whiteKeysBefore = {
|
|
1474
|
-
C: 0,
|
|
1475
|
-
"C#": 1,
|
|
1476
|
-
D: 1,
|
|
1477
|
-
"D#": 2,
|
|
1478
|
-
E: 2,
|
|
1479
|
-
F: 3,
|
|
1480
|
-
"F#": 4,
|
|
1481
|
-
G: 4,
|
|
1482
|
-
"G#": 5,
|
|
1483
|
-
A: 5,
|
|
1484
|
-
"A#": 6,
|
|
1485
|
-
B: 6
|
|
1486
|
-
};
|
|
1487
|
-
/** Width of a black key in pixels. */
|
|
1488
|
-
function blackKeyWidth(layout) {
|
|
1489
|
-
return layout.whiteKeyWidth * (layout.blackKeyWidthRatio ?? DEFAULT_BLACK_KEY_WIDTH_RATIO);
|
|
1490
|
-
}
|
|
1491
|
-
function rawNotePosition(note, layout) {
|
|
1492
|
-
const slot = layout.whiteKeyWidth + (layout.keyGap ?? DEFAULT_KEY_GAP);
|
|
1493
|
-
const target = (0, _tremolo_ui_functions.noteKey)(note);
|
|
1494
|
-
const first = (0, _tremolo_ui_functions.noteKey)(layout.noteRange.first);
|
|
1495
|
-
const octave = Math.floor((note - layout.noteRange.first) / 12);
|
|
1496
|
-
const octaveOffset = _tremolo_ui_functions.noteKeys.indexOf(first) > _tremolo_ui_functions.noteKeys.indexOf(target) ? 1 : 0;
|
|
1497
|
-
const whiteKeysIn = whiteKeysBefore[target] - whiteKeysBefore[first] + (octave + octaveOffset) * 7;
|
|
1498
|
-
return (0, _tremolo_ui_functions.isBlackKey)(note) ? whiteKeysIn * slot - blackKeyWidth(layout) / 2 : whiteKeysIn * slot;
|
|
1499
|
-
}
|
|
1500
|
-
function pianoBounds(layout) {
|
|
1501
|
-
const notes = getNoteRangeArray(layout.noteRange);
|
|
1502
|
-
if (notes.length === 0) return {
|
|
1503
|
-
left: 0,
|
|
1504
|
-
right: 0
|
|
1505
|
-
};
|
|
1506
|
-
let left = Infinity;
|
|
1507
|
-
let right = -Infinity;
|
|
1508
|
-
const whiteWidth = layout.whiteKeyWidth + (layout.keyGap ?? DEFAULT_KEY_GAP);
|
|
1509
|
-
for (const note of notes) {
|
|
1510
|
-
const noteLeft = rawNotePosition(note, layout);
|
|
1511
|
-
const width = (0, _tremolo_ui_functions.isBlackKey)(note) ? blackKeyWidth(layout) : whiteWidth;
|
|
1512
|
-
left = Math.min(left, noteLeft);
|
|
1513
|
-
right = Math.max(right, noteLeft + width);
|
|
1514
|
-
}
|
|
1515
|
-
return {
|
|
1516
|
-
left,
|
|
1517
|
-
right
|
|
1518
|
-
};
|
|
1519
|
-
}
|
|
1520
|
-
/** Width of the whole keyboard in pixels. */
|
|
1521
|
-
function pianoWidth(layout) {
|
|
1522
|
-
const { left, right } = pianoBounds(layout);
|
|
1523
|
-
return right - left;
|
|
1524
|
-
}
|
|
1525
|
-
/**
|
|
1526
|
-
* Offset of the left edge of a key from the left edge of the keyboard, in
|
|
1527
|
-
* pixels.
|
|
1528
|
-
*
|
|
1529
|
-
* Notes outside `noteRange` are placed too, so the value is negative below
|
|
1530
|
-
* `noteRange.first`.
|
|
1531
|
-
*/
|
|
1532
|
-
function notePosition(note, layout) {
|
|
1533
|
-
return rawNotePosition(note, layout) - pianoBounds(layout).left;
|
|
1534
|
-
}
|
|
1535
|
-
/**
|
|
1536
|
-
* The note drawn at a point, or null where there is none.
|
|
1537
|
-
*
|
|
1538
|
-
* Black keys are tested first, so they win where they overlap a white one. A
|
|
1539
|
-
* white key covers its gap as well as its width, so the whole width of the
|
|
1540
|
-
* keyboard belongs to some key and a click cannot fall between two.
|
|
1541
|
-
*
|
|
1542
|
-
* @param x offset from the left edge of the keyboard, in pixels
|
|
1543
|
-
* @param y offset from its top edge, in pixels
|
|
1544
|
-
* @param height height of the keyboard, in pixels
|
|
1545
|
-
*/
|
|
1546
|
-
function noteAt(x, y, height, layout) {
|
|
1547
|
-
if (x < 0 || x >= pianoWidth(layout) || y < 0 || y >= height) return null;
|
|
1548
|
-
const notes = getNoteRangeArray(layout.noteRange);
|
|
1549
|
-
if (y < height * (layout.blackKeyHeightRatio ?? DEFAULT_BLACK_KEY_HEIGHT_RATIO)) for (const note of notes) {
|
|
1550
|
-
if ((0, _tremolo_ui_functions.isWhiteKey)(note)) continue;
|
|
1551
|
-
const left = notePosition(note, layout);
|
|
1552
|
-
if (left <= x && x < left + blackKeyWidth(layout)) return note;
|
|
1553
|
-
}
|
|
1554
|
-
const slot = layout.whiteKeyWidth + (layout.keyGap ?? DEFAULT_KEY_GAP);
|
|
1555
|
-
for (const note of notes) {
|
|
1556
|
-
if ((0, _tremolo_ui_functions.isBlackKey)(note)) continue;
|
|
1557
|
-
const left = notePosition(note, layout);
|
|
1558
|
-
if (left <= x && x < left + slot) return note;
|
|
1559
|
-
}
|
|
1560
|
-
return null;
|
|
1561
|
-
}
|
|
1562
|
-
/**
|
|
1563
|
-
* The white key width that makes the keyboard exactly `width` wide, for a
|
|
1564
|
-
* keyboard that follows the size of its container.
|
|
1565
|
-
*
|
|
1566
|
-
* Solved from {@link pianoWidth} rather than by dividing among the white
|
|
1567
|
-
* keys, so that a range that starts or ends on a black key — which sticks out
|
|
1568
|
-
* by a fraction of a white key — still fills the container. The width grows
|
|
1569
|
-
* linearly with the white key width, so two samples pin it down.
|
|
1570
|
-
*/
|
|
1571
|
-
function fitWhiteKeyWidth(width, layout) {
|
|
1572
|
-
const at = (whiteKeyWidth) => pianoWidth({
|
|
1573
|
-
...layout,
|
|
1574
|
-
whiteKeyWidth
|
|
1575
|
-
});
|
|
1576
|
-
const slope = at(2) - at(1);
|
|
1577
|
-
if (slope <= 0) return 0;
|
|
1578
|
-
return (width - (at(1) - slope)) / slope;
|
|
1579
|
-
}
|
|
1580
|
-
//#endregion
|
|
1581
675
|
//#region src/piano/shortcuts.ts
|
|
1582
676
|
/**
|
|
1583
677
|
* Ready-made keyboard layouts. Both assume `noteRange.first` is a C.
|
|
@@ -1690,7 +784,7 @@ function createPianoInput(element, options) {
|
|
|
1690
784
|
/** The note under a pointer, or null where the pointer is off the keys. */
|
|
1691
785
|
function noteUnder(clientX, clientY) {
|
|
1692
786
|
const { left, top, height } = element.getBoundingClientRect();
|
|
1693
|
-
return noteAt(clientX - left, clientY - top, height, opts.layout);
|
|
787
|
+
return require_xy.noteAt(clientX - left, clientY - top, height, opts.layout);
|
|
1694
788
|
}
|
|
1695
789
|
/** Move a pointer onto a note, releasing whatever it held before. */
|
|
1696
790
|
function movePointer(pointerId, note) {
|
|
@@ -1778,7 +872,7 @@ function createPianoInput(element, options) {
|
|
|
1778
872
|
].join("");
|
|
1779
873
|
}
|
|
1780
874
|
bindShortcuts();
|
|
1781
|
-
const drag = createDrag(element, {
|
|
875
|
+
const drag = require_xy.createDrag(element, {
|
|
1782
876
|
multiPointer: true,
|
|
1783
877
|
onDragStart: (state) => movePointer(state.pointerId, noteUnder(state.clientX, state.clientY)),
|
|
1784
878
|
onDrag: (state) => {
|
|
@@ -1824,285 +918,6 @@ function createPianoInput(element, options) {
|
|
|
1824
918
|
};
|
|
1825
919
|
}
|
|
1826
920
|
//#endregion
|
|
1827
|
-
//#region src/selection/box.ts
|
|
1828
|
-
function rectOf(from, to) {
|
|
1829
|
-
return {
|
|
1830
|
-
x: Math.min(from[0], to[0]),
|
|
1831
|
-
y: Math.min(from[1], to[1]),
|
|
1832
|
-
width: Math.abs(to[0] - from[0]),
|
|
1833
|
-
height: Math.abs(to[1] - from[1])
|
|
1834
|
-
};
|
|
1835
|
-
}
|
|
1836
|
-
/** Whether the box covers the point, edges included. */
|
|
1837
|
-
function selectionBoxCovers(box, [x, y]) {
|
|
1838
|
-
return x >= box.x && x <= box.x + box.width && y >= box.y && y <= box.y + box.height;
|
|
1839
|
-
}
|
|
1840
|
-
/**
|
|
1841
|
-
* Selecting by dragging a box over a set of items.
|
|
1842
|
-
*
|
|
1843
|
-
* The drag itself is not here: the caller owns the pointer, and reports where
|
|
1844
|
-
* it went in the same 0..1 space the items are given in.
|
|
1845
|
-
*/
|
|
1846
|
-
function createSelectionBox(options) {
|
|
1847
|
-
let opts = options;
|
|
1848
|
-
let drag = null;
|
|
1849
|
-
let box = null;
|
|
1850
|
-
function setBox(next) {
|
|
1851
|
-
box = next;
|
|
1852
|
-
opts.onBoxChange?.(next);
|
|
1853
|
-
}
|
|
1854
|
-
function apply(rect, base) {
|
|
1855
|
-
const inside = [];
|
|
1856
|
-
for (const [id, at] of opts.items()) if (selectionBoxCovers(rect, at) && !base.includes(id)) inside.push(id);
|
|
1857
|
-
opts.onSelectionChange?.([...base, ...inside]);
|
|
1858
|
-
}
|
|
1859
|
-
return {
|
|
1860
|
-
update: (next) => {
|
|
1861
|
-
opts = {
|
|
1862
|
-
...opts,
|
|
1863
|
-
...next
|
|
1864
|
-
};
|
|
1865
|
-
},
|
|
1866
|
-
begin: (at, { additive = false, selection = [] } = {}) => {
|
|
1867
|
-
drag = {
|
|
1868
|
-
from: at,
|
|
1869
|
-
base: additive ? [...selection] : []
|
|
1870
|
-
};
|
|
1871
|
-
setBox(rectOf(at, at));
|
|
1872
|
-
if (!additive) opts.onSelectionChange?.([]);
|
|
1873
|
-
},
|
|
1874
|
-
move: (to) => {
|
|
1875
|
-
if (!drag) return;
|
|
1876
|
-
const rect = rectOf(drag.from, to);
|
|
1877
|
-
setBox(rect);
|
|
1878
|
-
apply(rect, drag.base);
|
|
1879
|
-
},
|
|
1880
|
-
end: () => {
|
|
1881
|
-
const running = drag !== null;
|
|
1882
|
-
drag = null;
|
|
1883
|
-
if (box !== null) setBox(null);
|
|
1884
|
-
return running;
|
|
1885
|
-
},
|
|
1886
|
-
box: () => box,
|
|
1887
|
-
destroy: () => {
|
|
1888
|
-
drag = null;
|
|
1889
|
-
box = null;
|
|
1890
|
-
}
|
|
1891
|
-
};
|
|
1892
|
-
}
|
|
1893
|
-
//#endregion
|
|
1894
|
-
//#region src/points-editor/index.ts
|
|
1895
|
-
/**
|
|
1896
|
-
* A point's value is its position in the editor, so its range is the editor:
|
|
1897
|
-
* no scaling, and no rounding to a step.
|
|
1898
|
-
*/
|
|
1899
|
-
const POINT_AXIS = {
|
|
1900
|
-
min: 0,
|
|
1901
|
-
max: 1
|
|
1902
|
-
};
|
|
1903
|
-
/**
|
|
1904
|
-
* How much one wheel notch moves a point by default: a hundredth of the
|
|
1905
|
-
* editor, whatever its pixel size.
|
|
1906
|
-
*/
|
|
1907
|
-
const POINTS_EDITOR_DEFAULT_WHEEL = ["normalized", .01];
|
|
1908
|
-
/**
|
|
1909
|
-
* How much one arrow key press moves a point by default. Shift is the
|
|
1910
|
-
* fine-adjustment key everywhere else, so it is bound here too — but only on
|
|
1911
|
-
* the keyboard. On the wheel it already means the x axis, and browsers hand
|
|
1912
|
-
* shift+wheel over as horizontal scrolling anyway.
|
|
1913
|
-
*/
|
|
1914
|
-
const POINTS_EDITOR_DEFAULT_KEYBOARD = {
|
|
1915
|
-
default: ["normalized", .01],
|
|
1916
|
-
shift: ["normalized", .001]
|
|
1917
|
-
};
|
|
1918
|
-
/** Keep a point within its range. An axis left out runs from 0 to 1. */
|
|
1919
|
-
function clampPoint(point, min, max) {
|
|
1920
|
-
return {
|
|
1921
|
-
x: (0, _tremolo_ui_functions.clamp)(point.x, min?.x ?? 0, max?.x ?? 1),
|
|
1922
|
-
y: (0, _tremolo_ui_functions.clamp)(point.y, min?.y ?? 0, max?.y ?? 1)
|
|
1923
|
-
};
|
|
1924
|
-
}
|
|
1925
|
-
const EMPTY = [];
|
|
1926
|
-
/**
|
|
1927
|
-
* How far a group may move before something in it leaves its range.
|
|
1928
|
-
*
|
|
1929
|
-
* Clamping each point on its own would break the shape of the selection: the
|
|
1930
|
-
* one that reached the edge would stop while the rest carried on. One amount
|
|
1931
|
-
* for all of them means the whole selection stops together.
|
|
1932
|
-
*/
|
|
1933
|
-
function allowedDelta(delta, entries) {
|
|
1934
|
-
let loX = -Infinity;
|
|
1935
|
-
let hiX = Infinity;
|
|
1936
|
-
let loY = -Infinity;
|
|
1937
|
-
let hiY = Infinity;
|
|
1938
|
-
for (const { start, point } of entries) {
|
|
1939
|
-
if (point.readonly) continue;
|
|
1940
|
-
loX = Math.max(loX, (point.min?.x ?? 0) - start.x);
|
|
1941
|
-
hiX = Math.min(hiX, (point.max?.x ?? 1) - start.x);
|
|
1942
|
-
loY = Math.max(loY, (point.min?.y ?? 0) - start.y);
|
|
1943
|
-
hiY = Math.min(hiY, (point.max?.y ?? 1) - start.y);
|
|
1944
|
-
}
|
|
1945
|
-
return {
|
|
1946
|
-
x: hiX < loX ? 0 : (0, _tremolo_ui_functions.clamp)(delta.x, loX, hiX),
|
|
1947
|
-
y: hiY < loY ? 0 : (0, _tremolo_ui_functions.clamp)(delta.y, loY, hiY)
|
|
1948
|
-
};
|
|
1949
|
-
}
|
|
1950
|
-
/**
|
|
1951
|
-
* The selection and the moves of a points editor: which points a press or a
|
|
1952
|
-
* box selects, and how a selection moves as one.
|
|
1953
|
-
*
|
|
1954
|
-
* The points stay with the wrapper, which registers each one; the editor only
|
|
1955
|
-
* reads them when it needs to, so a point's value can change on every frame
|
|
1956
|
-
* of a drag without anything being re-registered.
|
|
1957
|
-
*/
|
|
1958
|
-
function createPointsEditor(options = {}) {
|
|
1959
|
-
let opts = options;
|
|
1960
|
-
const points = /* @__PURE__ */ new Map();
|
|
1961
|
-
/** What the current drag picked up, and where those points started. */
|
|
1962
|
-
let dragged = [];
|
|
1963
|
-
const selection = () => opts.selectable ? opts.selection ?? EMPTY : EMPTY;
|
|
1964
|
-
function changeSelection(next) {
|
|
1965
|
-
opts = {
|
|
1966
|
-
...opts,
|
|
1967
|
-
selection: next
|
|
1968
|
-
};
|
|
1969
|
-
opts.onSelectionChange?.(next);
|
|
1970
|
-
}
|
|
1971
|
-
function snapshot(ids) {
|
|
1972
|
-
return ids.flatMap((id) => {
|
|
1973
|
-
const point = points.get(id)?.();
|
|
1974
|
-
return point ? [{
|
|
1975
|
-
id,
|
|
1976
|
-
start: { ...point.value }
|
|
1977
|
-
}] : [];
|
|
1978
|
-
});
|
|
1979
|
-
}
|
|
1980
|
-
function moveFrom(entries, delta) {
|
|
1981
|
-
const withPoint = entries.flatMap((entry) => {
|
|
1982
|
-
const point = points.get(entry.id)?.();
|
|
1983
|
-
return point ? [{
|
|
1984
|
-
...entry,
|
|
1985
|
-
point
|
|
1986
|
-
}] : [];
|
|
1987
|
-
});
|
|
1988
|
-
const allowed = allowedDelta(delta, withPoint);
|
|
1989
|
-
for (const { start, point } of withPoint) {
|
|
1990
|
-
if (point.readonly) continue;
|
|
1991
|
-
point.onChange?.({
|
|
1992
|
-
x: (0, _tremolo_ui_functions.toPrecision)(start.x + allowed.x),
|
|
1993
|
-
y: (0, _tremolo_ui_functions.toPrecision)(start.y + allowed.y)
|
|
1994
|
-
});
|
|
1995
|
-
}
|
|
1996
|
-
}
|
|
1997
|
-
function nudgeSelection(id, delta) {
|
|
1998
|
-
const current = selection();
|
|
1999
|
-
moveFrom(snapshot(current.includes(id) ? current : [id]), delta);
|
|
2000
|
-
}
|
|
2001
|
-
function nudgePoint(id, axis, direction, option, modifiers) {
|
|
2002
|
-
const point = points.get(id)?.();
|
|
2003
|
-
if (!point) return;
|
|
2004
|
-
const { value } = point;
|
|
2005
|
-
const next = applyDelta(value[axis], direction, option, POINT_AXIS, modifiers);
|
|
2006
|
-
nudgeSelection(id, {
|
|
2007
|
-
x: axis === "x" ? next - value.x : 0,
|
|
2008
|
-
y: axis === "y" ? next - value.y : 0
|
|
2009
|
-
});
|
|
2010
|
-
}
|
|
2011
|
-
const box = createSelectionBox({
|
|
2012
|
-
*items() {
|
|
2013
|
-
for (const [id, read] of points) {
|
|
2014
|
-
const { x, y } = read().value;
|
|
2015
|
-
yield [id, [x, y]];
|
|
2016
|
-
}
|
|
2017
|
-
},
|
|
2018
|
-
onBoxChange: (rect) => opts.onSelectionBoxChange?.(rect),
|
|
2019
|
-
onSelectionChange: changeSelection
|
|
2020
|
-
});
|
|
2021
|
-
return {
|
|
2022
|
-
update: (next) => {
|
|
2023
|
-
opts = {
|
|
2024
|
-
...opts,
|
|
2025
|
-
...next
|
|
2026
|
-
};
|
|
2027
|
-
},
|
|
2028
|
-
registerPoint: (id, read) => {
|
|
2029
|
-
points.set(id, read);
|
|
2030
|
-
return () => {
|
|
2031
|
-
if (points.get(id) === read) points.delete(id);
|
|
2032
|
-
};
|
|
2033
|
-
},
|
|
2034
|
-
isPointElement: (element) => {
|
|
2035
|
-
if (!element) return false;
|
|
2036
|
-
for (const read of points.values()) if (read().element?.contains(element)) return true;
|
|
2037
|
-
return false;
|
|
2038
|
-
},
|
|
2039
|
-
beginPointDrag: (id, modifiers) => {
|
|
2040
|
-
if (!opts.selectable) {
|
|
2041
|
-
dragged = snapshot([id]);
|
|
2042
|
-
return;
|
|
2043
|
-
}
|
|
2044
|
-
const current = selection();
|
|
2045
|
-
const additive = modifiers.ctrlKey || modifiers.metaKey;
|
|
2046
|
-
let next;
|
|
2047
|
-
if (additive) next = current.includes(id) ? current.filter((x) => x !== id) : [...current, id];
|
|
2048
|
-
else if (current.includes(id)) next = current;
|
|
2049
|
-
else next = [id];
|
|
2050
|
-
changeSelection([...next]);
|
|
2051
|
-
dragged = next.includes(id) ? snapshot(next) : [];
|
|
2052
|
-
},
|
|
2053
|
-
movePointDrag: (delta) => moveFrom(dragged, delta),
|
|
2054
|
-
nudgeSelection,
|
|
2055
|
-
nudgePoint,
|
|
2056
|
-
nudgeFocusedPoint: (axis, direction, modifiers) => {
|
|
2057
|
-
for (const [id, read] of points) {
|
|
2058
|
-
const { element, wheel, readonly, onChange } = read();
|
|
2059
|
-
const active = element?.ownerDocument.activeElement;
|
|
2060
|
-
if (!active || !element?.contains(active)) continue;
|
|
2061
|
-
if (!wheel || readonly || !onChange) return false;
|
|
2062
|
-
nudgePoint(id, axis, direction, wheel, modifiers);
|
|
2063
|
-
return true;
|
|
2064
|
-
}
|
|
2065
|
-
return false;
|
|
2066
|
-
},
|
|
2067
|
-
beginSelectionBox: (at, modifiers) => {
|
|
2068
|
-
if (!opts.selectable) return;
|
|
2069
|
-
box.begin([at.x, at.y], {
|
|
2070
|
-
additive: modifiers.ctrlKey || modifiers.metaKey,
|
|
2071
|
-
selection: selection()
|
|
2072
|
-
});
|
|
2073
|
-
},
|
|
2074
|
-
moveSelectionBox: (to) => box.move([to.x, to.y]),
|
|
2075
|
-
endSelectionBox: () => {
|
|
2076
|
-
if (!box.end()) return;
|
|
2077
|
-
const [first] = selection();
|
|
2078
|
-
const element = first ? points.get(first)?.().element : null;
|
|
2079
|
-
if (element && "focus" in element) element.focus();
|
|
2080
|
-
},
|
|
2081
|
-
destroy: () => {
|
|
2082
|
-
box.destroy();
|
|
2083
|
-
dragged = [];
|
|
2084
|
-
}
|
|
2085
|
-
};
|
|
2086
|
-
}
|
|
2087
|
-
//#endregion
|
|
2088
|
-
//#region src/xy.ts
|
|
2089
|
-
function isPair(value) {
|
|
2090
|
-
return Array.isArray(value);
|
|
2091
|
-
}
|
|
2092
|
-
/**
|
|
2093
|
-
* Spread a setting that may have been given as a single value.
|
|
2094
|
-
*
|
|
2095
|
-
* The parameter is written out rather than taken as `XYInput<T>`: a
|
|
2096
|
-
* conditional type cannot be narrowed, so the constraint stays where it is
|
|
2097
|
-
* declared and this takes both forms.
|
|
2098
|
-
*
|
|
2099
|
-
* The pair is copied rather than passed along, so that the result is a tuple
|
|
2100
|
-
* the caller owns even when a readonly one was given.
|
|
2101
|
-
*/
|
|
2102
|
-
function toXY(value) {
|
|
2103
|
-
return isPair(value) ? [value[0], value[1]] : [value, value];
|
|
2104
|
-
}
|
|
2105
|
-
//#endregion
|
|
2106
921
|
//#region src/pointer/drag-value.ts
|
|
2107
922
|
/**
|
|
2108
923
|
* Map the pointer onto the bounding rect of an element: the value *is* the
|
|
@@ -2156,7 +971,7 @@ function elementMapping(getElement, { sensitivity } = {}) {
|
|
|
2156
971
|
* value can be adjusted from anywhere on the screen.
|
|
2157
972
|
*/
|
|
2158
973
|
function relativeMapping({ pixelRange = 100, sensitivity } = {}) {
|
|
2159
|
-
const [baseX, baseY] = toXY(pixelRange);
|
|
974
|
+
const [baseX, baseY] = require_xy.toXY(pixelRange);
|
|
2160
975
|
let origin = [0, 0];
|
|
2161
976
|
/** Where the current sensitivity took over, in drag coordinates. */
|
|
2162
977
|
let anchor = [0, 0];
|
|
@@ -2195,7 +1010,7 @@ function createDragValue(element, options) {
|
|
|
2195
1010
|
let opts = options;
|
|
2196
1011
|
let lastValue = [0, 0];
|
|
2197
1012
|
let active = false;
|
|
2198
|
-
const axes = () => toXY(opts.axis);
|
|
1013
|
+
const axes = () => require_xy.toXY(opts.axis);
|
|
2199
1014
|
function valueOf(position) {
|
|
2200
1015
|
return axes().map((axis, i) => {
|
|
2201
1016
|
const p = axis.reverse ? 1 - position[i] : position[i];
|
|
@@ -2213,7 +1028,7 @@ function createDragValue(element, options) {
|
|
|
2213
1028
|
return axis.reverse ? 1 - n : n;
|
|
2214
1029
|
});
|
|
2215
1030
|
} };
|
|
2216
|
-
const drag = createDrag(element, {
|
|
1031
|
+
const drag = require_xy.createDrag(element, {
|
|
2217
1032
|
threshold: opts.threshold,
|
|
2218
1033
|
cursor: opts.cursor,
|
|
2219
1034
|
pointerLock: opts.pointerLock,
|
|
@@ -2223,8 +1038,8 @@ function createDragValue(element, options) {
|
|
|
2223
1038
|
if (!position) return;
|
|
2224
1039
|
active = true;
|
|
2225
1040
|
lastValue = valueOf(position);
|
|
2226
|
-
if (opts.updateOnPointerDown) opts.onChange?.(lastValue, state);
|
|
2227
1041
|
opts.onDragStart?.(lastValue, state);
|
|
1042
|
+
if (opts.updateOnPointerDown) opts.onChange?.(lastValue, state);
|
|
2228
1043
|
},
|
|
2229
1044
|
onDrag: (state) => {
|
|
2230
1045
|
if (!active) return;
|
|
@@ -2337,18 +1152,15 @@ function createLongPress(options) {
|
|
|
2337
1152
|
* The listener is registered with `passive: false` so that the handler can call
|
|
2338
1153
|
* `preventDefault()` to stop the page from scrolling.
|
|
2339
1154
|
*/
|
|
2340
|
-
function createWheel(element,
|
|
2341
|
-
let opts =
|
|
2342
|
-
...options,
|
|
2343
|
-
onWheel
|
|
2344
|
-
};
|
|
1155
|
+
function createWheel(element, options) {
|
|
1156
|
+
let opts = options;
|
|
2345
1157
|
function hasFocus() {
|
|
2346
1158
|
const active = element.ownerDocument?.activeElement;
|
|
2347
1159
|
return !!active && element.contains(active);
|
|
2348
1160
|
}
|
|
2349
1161
|
const handler = (event) => {
|
|
2350
1162
|
if (opts.requireFocus && !hasFocus()) return;
|
|
2351
|
-
opts.onWheel
|
|
1163
|
+
opts.onWheel(event);
|
|
2352
1164
|
};
|
|
2353
1165
|
element.addEventListener("wheel", handler, { passive: false });
|
|
2354
1166
|
return {
|
|
@@ -2378,114 +1190,14 @@ function valuePercent(value, { min, max, scale = _tremolo_ui_functions.linearSca
|
|
|
2378
1190
|
return (0, _tremolo_ui_functions.toFixed)((reversed ? 1 - normalized : normalized) * 100);
|
|
2379
1191
|
}
|
|
2380
1192
|
//#endregion
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
*
|
|
2385
|
-
* `Slider.Marks` builds its labels by multiplying an interval, which leaves
|
|
2386
|
-
* binary debris in the last digits (`0.1 * 3` is `0.30000000000000004`). The
|
|
2387
|
-
* count is what tells `toFixed` how far to round that back.
|
|
2388
|
-
*
|
|
2389
|
-
* The exponent form has to be handled separately: `String(1e-7)` is `'1e-7'`,
|
|
2390
|
-
* which has no decimal point at all, so reading the text after the point
|
|
2391
|
-
* reports no digits and the interval rounds away to whole numbers.
|
|
2392
|
-
*/
|
|
2393
|
-
function decimalDigits(x) {
|
|
2394
|
-
if (!Number.isFinite(x)) return 0;
|
|
2395
|
-
const text = String(x);
|
|
2396
|
-
const e = text.indexOf("e");
|
|
2397
|
-
if (e === -1) return text.split(".")[1]?.length ?? 0;
|
|
2398
|
-
const fraction = text.slice(0, e).split(".")[1]?.length ?? 0;
|
|
2399
|
-
return Math.max(0, fraction - Number(text.slice(e + 1)));
|
|
2400
|
-
}
|
|
2401
|
-
//#endregion
|
|
2402
|
-
//#region src/slider/marks.ts
|
|
2403
|
-
/**
|
|
2404
|
-
* The marks `options` asks for between `min` and `max`, in ascending order.
|
|
2405
|
-
*
|
|
2406
|
-
* Each value is a whole multiple of the interval, rounded to the digits the
|
|
2407
|
-
* interval has: stepping by 0.1 would otherwise put binary debris in the
|
|
2408
|
-
* labels (`0.1 * 3` is `0.30000000000000004`).
|
|
2409
|
-
*/
|
|
2410
|
-
function sliderMarks(options, min, max, step) {
|
|
2411
|
-
const { per, mark = true, label = true } = typeof options === "object" ? options : { per: options };
|
|
2412
|
-
const optionsList = [];
|
|
2413
|
-
const interval = per === "step" ? step : per;
|
|
2414
|
-
const count = Math.floor(max / interval) - Math.ceil(min / interval) + 1;
|
|
2415
|
-
for (let i = 0; i < count; i++) {
|
|
2416
|
-
const value = (0, _tremolo_ui_functions.toFixed)(interval * (Math.ceil(min / interval) + i), decimalDigits(interval));
|
|
2417
|
-
optionsList.push({
|
|
2418
|
-
value,
|
|
2419
|
-
mark,
|
|
2420
|
-
label
|
|
2421
|
-
});
|
|
2422
|
-
}
|
|
2423
|
-
return optionsList;
|
|
2424
|
-
}
|
|
2425
|
-
//#endregion
|
|
2426
|
-
//#region src/style.ts
|
|
2427
|
-
/**
|
|
2428
|
-
* A number as a CSS length, for a custom property.
|
|
2429
|
-
*
|
|
2430
|
-
* A custom property takes whatever text it is given, so `--size: 50` comes out
|
|
2431
|
-
* as the invalid `50` rather than `50px`. React appends `px` to a bare number
|
|
2432
|
-
* only for the properties it knows take a length, and a custom property is
|
|
2433
|
-
* never one of them; Vue and Svelte append nothing at all. Anything already a
|
|
2434
|
-
* string is passed through, so `'3rem'` and `'100%'` still work.
|
|
2435
|
-
*/
|
|
2436
|
-
function cssLength(value) {
|
|
2437
|
-
return typeof value === "number" ? `${value}px` : value;
|
|
2438
|
-
}
|
|
2439
|
-
/**
|
|
2440
|
-
* Take an element out of sight while leaving it in the accessibility tree and
|
|
2441
|
-
* in the tab order.
|
|
2442
|
-
*
|
|
2443
|
-
* `display: none` and `visibility: hidden` would remove it from both, and the
|
|
2444
|
-
* native control underneath a headless component is what carries the ARIA and
|
|
2445
|
-
* the keyboard behaviour. `pointer-events: none` is safe because nothing is
|
|
2446
|
-
* ever clicked here directly: a `<label>` forwards its click, and a drag is
|
|
2447
|
-
* handled by the part that is visible.
|
|
2448
|
-
*
|
|
2449
|
-
* Every value is a string with its unit, so the object can be handed to any
|
|
2450
|
-
* framework's `style` binding as it is.
|
|
2451
|
-
*/
|
|
2452
|
-
const visuallyHiddenStyle = {
|
|
2453
|
-
position: "absolute",
|
|
2454
|
-
width: "1px",
|
|
2455
|
-
height: "1px",
|
|
2456
|
-
padding: "0",
|
|
2457
|
-
margin: "-1px",
|
|
2458
|
-
overflow: "hidden",
|
|
2459
|
-
clip: "rect(0, 0, 0, 0)",
|
|
2460
|
-
clipPath: "inset(50%)",
|
|
2461
|
-
whiteSpace: "nowrap",
|
|
2462
|
-
border: "0",
|
|
2463
|
-
pointerEvents: "none"
|
|
2464
|
-
};
|
|
2465
|
-
//#endregion
|
|
2466
|
-
exports.DEFAULT_DRAG_SENSITIVITY = DEFAULT_DRAG_SENSITIVITY;
|
|
2467
|
-
exports.DEFAULT_KEYBOARD_OPTIONS = DEFAULT_KEYBOARD_OPTIONS;
|
|
2468
|
-
exports.DEFAULT_WHEEL_OPTIONS = DEFAULT_WHEEL_OPTIONS;
|
|
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;
|
|
2469
1196
|
exports.KNOB_VIEWBOX_SIZE = KNOB_VIEWBOX_SIZE;
|
|
2470
|
-
exports.NOT_SUPPORTED = NOT_SUPPORTED;
|
|
2471
|
-
exports.PERMISSION_DENIED = PERMISSION_DENIED;
|
|
2472
|
-
exports.PITCH_BEND_CENTER = PITCH_BEND_CENTER;
|
|
2473
|
-
exports.POINTS_EDITOR_DEFAULT_KEYBOARD = POINTS_EDITOR_DEFAULT_KEYBOARD;
|
|
2474
|
-
exports.POINTS_EDITOR_DEFAULT_WHEEL = POINTS_EDITOR_DEFAULT_WHEEL;
|
|
2475
|
-
exports.POINT_AXIS = POINT_AXIS;
|
|
2476
1197
|
exports.SHORTCUTS = SHORTCUTS;
|
|
2477
|
-
exports.UNAVAILABLE = UNAVAILABLE;
|
|
2478
|
-
exports.applyDelta = applyDelta;
|
|
2479
|
-
exports.arrowKeyDirection = arrowKeyDirection;
|
|
2480
|
-
exports.arrowKeyMove = arrowKeyMove;
|
|
2481
|
-
exports.blackKeyWidth = blackKeyWidth;
|
|
2482
|
-
exports.caretAtDecimalOffset = caretAtDecimalOffset;
|
|
2483
|
-
exports.caretDecimalOffset = caretDecimalOffset;
|
|
2484
|
-
exports.checkSteps = checkSteps;
|
|
2485
|
-
exports.clampPoint = clampPoint;
|
|
2486
|
-
exports.commitNumberInputText = commitNumberInputText;
|
|
2487
1198
|
exports.createAnimationCanvas = createAnimationCanvas;
|
|
2488
|
-
exports.
|
|
1199
|
+
exports.createChangeGesture = createChangeGesture;
|
|
1200
|
+
exports.createDrag = require_xy.createDrag;
|
|
2489
1201
|
exports.createDragValue = createDragValue;
|
|
2490
1202
|
exports.createDropZone = createDropZone;
|
|
2491
1203
|
exports.createLongPress = createLongPress;
|
|
@@ -2493,41 +1205,14 @@ exports.createMIDIAccess = createMIDIAccess;
|
|
|
2493
1205
|
exports.createMIDIInput = createMIDIInput;
|
|
2494
1206
|
exports.createMIDIMessage = createMIDIMessage;
|
|
2495
1207
|
exports.createPianoInput = createPianoInput;
|
|
2496
|
-
exports.createPointsEditor = createPointsEditor;
|
|
2497
|
-
exports.createSelectionBox = createSelectionBox;
|
|
2498
|
-
exports.createStepperDrag = createStepperDrag;
|
|
1208
|
+
exports.createPointsEditor = require_xy.createPointsEditor;
|
|
2499
1209
|
exports.createWheel = createWheel;
|
|
2500
|
-
exports.cssLength = cssLength;
|
|
2501
|
-
exports.drawingState = drawingState;
|
|
2502
1210
|
exports.elementMapping = elementMapping;
|
|
2503
|
-
exports.fitWhiteKeyWidth = fitWhiteKeyWidth;
|
|
2504
|
-
exports.getNoteRangeArray = getNoteRangeArray;
|
|
2505
|
-
exports.isArrowKey = isArrowKey;
|
|
2506
|
-
exports.isDrawingState = isDrawingState;
|
|
2507
1211
|
exports.knobAngles = knobAngles;
|
|
2508
1212
|
exports.knobArcPath = knobArcPath;
|
|
2509
|
-
exports.knobArcPoint = knobArcPoint;
|
|
2510
1213
|
exports.knobArcRadius = knobArcRadius;
|
|
2511
|
-
exports.
|
|
2512
|
-
exports.matchesAccept = matchesAccept;
|
|
2513
|
-
exports.noteAt = noteAt;
|
|
2514
|
-
exports.notePosition = notePosition;
|
|
2515
|
-
exports.nudgeNumberInput = nudgeNumberInput;
|
|
2516
|
-
exports.numberInputBounds = numberInputBounds;
|
|
2517
|
-
exports.numberInputRanges = numberInputRanges;
|
|
2518
|
-
exports.numberSpan = numberSpan;
|
|
2519
|
-
exports.parseNumberText = parseNumberText;
|
|
2520
|
-
exports.partitionByAccept = partitionByAccept;
|
|
2521
|
-
exports.pianoWidth = pianoWidth;
|
|
1214
|
+
exports.notePosition = require_xy.notePosition;
|
|
2522
1215
|
exports.relativeMapping = relativeMapping;
|
|
2523
|
-
exports.replaceOptions = replaceOptions;
|
|
2524
|
-
exports.selectModifier = selectModifier;
|
|
2525
|
-
exports.selectionBoxCovers = selectionBoxCovers;
|
|
2526
|
-
exports.sliderMarks = sliderMarks;
|
|
2527
|
-
exports.toXY = toXY;
|
|
2528
1216
|
exports.valuePercent = valuePercent;
|
|
2529
|
-
exports.visuallyHiddenStyle = visuallyHiddenStyle;
|
|
2530
|
-
exports.wheelDirection = wheelDirection;
|
|
2531
|
-
exports.wheelMove = wheelMove;
|
|
2532
1217
|
|
|
2533
1218
|
//# sourceMappingURL=index.cjs.map
|