@tremolo-ui/functions 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -7,23 +7,23 @@ function clamp(value, min, max) {
7
7
  return Math.max(min, Math.min(value, max));
8
8
  }
9
9
  /**
10
- * Normalize the value from 0 to 1
10
+ * Normalize the value from 0 to 1, spreading the range evenly.
11
+ *
12
+ * This is the linear mapping and takes no curve of its own; a `Scale` builds
13
+ * whatever curve it needs on top of it.
11
14
  */
12
- function normalizeValue(rawValue, min, max, skew = 1) {
15
+ function normalizeValue(value, min, max) {
13
16
  if (min >= max) throw new RangeError("requirements: min < max");
14
- const v = clamp((rawValue - min) / (max - min), 0, 1);
15
- return Math.pow(v, skew);
17
+ return clamp((value - min) / (max - min), 0, 1);
16
18
  }
17
19
  /**
18
- * Convert normalized values back to raw values.
20
+ * Convert normalized values back to raw values, spreading the range evenly.
21
+ *
22
+ * The inverse of {@link normalizeValue}.
19
23
  */
20
- function rawValue(normalizedValue, min, max, skew = 1) {
24
+ function rawValue(normalizedValue, min, max) {
21
25
  if (min >= max) throw new RangeError("requirements: min < max");
22
- return min + (skew == 1 ? clamp(normalizedValue, 0, 1) : Math.exp(Math.log(clamp(normalizedValue, 0, 1)) / skew)) * (max - min);
23
- }
24
- function skewWithCenterValue(centerValue, min, max) {
25
- if (!(min <= centerValue && centerValue <= max)) throw new RangeError("requirements: min <= centerValue <= max");
26
- return Math.log(.5) / Math.log((centerValue - min) / (max - min));
26
+ return min + clamp(normalizedValue, 0, 1) * (max - min);
27
27
  }
28
28
  function stepValue(value, step) {
29
29
  if (step <= 0) throw new RangeError("requirements: step > 0");
@@ -59,6 +59,178 @@ function gainToDb(gain) {
59
59
  return 20 * (Math.log(gain) / Math.LN10);
60
60
  }
61
61
  //#endregion
62
+ //#region src/scales.ts
63
+ function assertRange(min, max) {
64
+ if (min >= max) throw new RangeError("requirements: min < max");
65
+ }
66
+ /**
67
+ * Equal travel gives an equal change in value.
68
+ *
69
+ * The right default for anything already linear in perception: dB values,
70
+ * pan, percentages, MIDI note numbers, semitones.
71
+ */
72
+ const linearScale = {
73
+ normalize: (value, min, max) => normalizeValue(value, min, max),
74
+ denormalize: (position, min, max) => rawValue(position, min, max)
75
+ };
76
+ /**
77
+ * The power law of JUCE's `NormalisableRange::skew`, applied to `value - min`.
78
+ *
79
+ * Use it when the value has to agree with a JUCE or iPlug2 parameter — a
80
+ * plugin UI in a WebView, say, where the knob must sit exactly where the
81
+ * host's automation curve puts it. {@link skewWithCenterValue} gives the
82
+ * factor that places a chosen value at the middle of the travel.
83
+ *
84
+ * `skew < 1` gives the lower end more travel, `skew > 1` the upper end.
85
+ *
86
+ * For new designs prefer {@link exponentialScale} or {@link curveScale}: the
87
+ * slope of this curve is either zero or infinite at `min`, so the bottom of
88
+ * the range is a dead zone or jumps.
89
+ *
90
+ * @param skew the JUCE skew factor
91
+ */
92
+ function skewScale(skew) {
93
+ return {
94
+ normalize: (value, min, max) => Math.pow(normalizeValue(value, min, max), skew),
95
+ denormalize: (position, min, max) => rawValue(skew === 1 ? position : Math.exp(Math.log(clamp(position, 0, 1)) / skew), min, max)
96
+ };
97
+ }
98
+ /**
99
+ * The skew factor for {@link skewScale} that puts `centerValue` at the middle
100
+ * of the travel — JUCE's `NormalisableRange::setSkewForCentre`.
101
+ */
102
+ function skewWithCenterValue(centerValue, min, max) {
103
+ if (!(min <= centerValue && centerValue <= max)) throw new RangeError("requirements: min <= centerValue <= max");
104
+ return Math.log(.5) / Math.log((centerValue - min) / (max - min));
105
+ }
106
+ /**
107
+ * Equal travel gives an equal *ratio*, so an octave — or a percentage — takes
108
+ * the same distance wherever it falls.
109
+ *
110
+ * The scale for frequency (a filter cutoff over 20-20000 Hz), free running
111
+ * rates, and delay times.
112
+ *
113
+ * Requires `min` and `max` to be non-zero and of the same sign, since no
114
+ * ratio reaches zero or crosses it. Use {@link curveScale} for a range that
115
+ * starts at 0.
116
+ */
117
+ const exponentialScale = {
118
+ normalize: (value, min, max) => {
119
+ assertExponentialRange(min, max);
120
+ return clamp(Math.log(clamp(value, min, max) / min) / Math.log(max / min), 0, 1);
121
+ },
122
+ denormalize: (position, min, max) => {
123
+ assertExponentialRange(min, max);
124
+ return min * Math.pow(max / min, clamp(position, 0, 1));
125
+ }
126
+ };
127
+ function assertExponentialRange(min, max) {
128
+ assertRange(min, max);
129
+ if (min === 0 || max === 0 || Math.sign(min) !== Math.sign(max)) throw new RangeError("exponentialScale: requirements: min and max are non-zero and have the same sign");
130
+ }
131
+ /**
132
+ * An exponential bend that still passes exactly through `min` and `max`, so
133
+ * unlike {@link exponentialScale} it works on a range that starts at 0 or
134
+ * crosses it, and unlike {@link skewScale} its slope is neither zero nor
135
+ * infinite at either end.
136
+ *
137
+ * The general purpose taper, and the same family as the curve of an envelope
138
+ * segment (SuperCollider's `CurveWarp`).
139
+ *
140
+ * - `curve > 0` gives the lower end more travel — envelope times from 0 ms,
141
+ * delay times, anything that wants fine control near the bottom
142
+ * - `curve < 0` gives the upper end more travel — a volume fader over
143
+ * -60..+6 dB that should be precise around 0 dB
144
+ * - near 0 it is indistinguishable from {@link linearScale}, and is treated
145
+ * as linear to avoid dividing by zero
146
+ *
147
+ * {@link curveWithCenterValue} gives the curve that places a chosen value at
148
+ * the middle of the travel.
149
+ *
150
+ * @param curve how hard the curve bends, and in which direction
151
+ */
152
+ function curveScale(curve) {
153
+ if (Math.abs(curve) < .001) return linearScale;
154
+ const grow = Math.exp(curve);
155
+ const coefficients = (min, max) => {
156
+ const a = (max - min) / (1 - grow);
157
+ return {
158
+ a,
159
+ b: min + a
160
+ };
161
+ };
162
+ return {
163
+ normalize: (value, min, max) => {
164
+ assertRange(min, max);
165
+ const { a, b } = coefficients(min, max);
166
+ return clamp(Math.log((b - clamp(value, min, max)) / a) / curve, 0, 1);
167
+ },
168
+ denormalize: (position, min, max) => {
169
+ assertRange(min, max);
170
+ const { a, b } = coefficients(min, max);
171
+ return b - a * Math.pow(grow, clamp(position, 0, 1));
172
+ }
173
+ };
174
+ }
175
+ /**
176
+ * {@link skewScale} mirrored about the middle of the range, so both halves
177
+ * bend the same way — JUCE's `symmetricSkew`.
178
+ *
179
+ * For a bipolar control whose centre matters: detune over -100..+100 cents,
180
+ * or a bipolar filter envelope amount, where the fine adjustment is around 0
181
+ * rather than at either end.
182
+ *
183
+ * `skew < 1` gives the middle more travel, `skew > 1` the two ends.
184
+ *
185
+ * @param skew the JUCE skew factor
186
+ */
187
+ function symmetricSkewScale(skew) {
188
+ return {
189
+ normalize: (value, min, max) => {
190
+ assertRange(min, max);
191
+ const proportion = clamp((value - min) / (max - min), 0, 1);
192
+ if (skew === 1) return proportion;
193
+ const distanceFromMiddle = 2 * proportion - 1;
194
+ return (1 + Math.pow(Math.abs(distanceFromMiddle), skew) * Math.sign(distanceFromMiddle)) / 2;
195
+ },
196
+ denormalize: (position, min, max) => {
197
+ assertRange(min, max);
198
+ let distanceFromMiddle = 2 * clamp(position, 0, 1) - 1;
199
+ if (skew !== 1 && distanceFromMiddle !== 0) distanceFromMiddle = Math.pow(Math.abs(distanceFromMiddle), 1 / skew) * Math.sign(distanceFromMiddle);
200
+ return min + (max - min) / 2 * (1 + distanceFromMiddle);
201
+ }
202
+ };
203
+ }
204
+ /**
205
+ * The curve for {@link curveScale} that puts `centerValue` at the middle of
206
+ * the travel — the counterpart of {@link skewWithCenterValue}.
207
+ */
208
+ function curveWithCenterValue(centerValue, min, max) {
209
+ assertRange(min, max);
210
+ if (!(min < centerValue && centerValue < max)) throw new RangeError("requirements: min < centerValue < max");
211
+ const proportion = (centerValue - min) / (max - min);
212
+ return 2 * Math.log(1 / proportion - 1);
213
+ }
214
+ /**
215
+ * Move a value by an amount of input, as reported by a wheel or an arrow key.
216
+ *
217
+ * The pipeline matches `createDragValue` of `@tremolo-ui/dom`: scale, then
218
+ * step, then clamp. Which key or which sign of `deltaY` counts as which
219
+ * direction is left to the caller, since it differs per component.
220
+ *
221
+ * @param direction which way, and how many times, to apply the option. The
222
+ * size of one step is `option[1]`, so this is normally `1` or `-1`.
223
+ *
224
+ * @example
225
+ * // ArrowDown on a slider whose keyboard option is ['raw', 1]
226
+ * applyDelta(value, -1, keyboard, { min, max, step, scale })
227
+ */
228
+ function applyDelta(value, direction, [mode, amount], { min, max, step, scale = linearScale }) {
229
+ const x = direction * amount;
230
+ const next = mode == "normalized" ? scale.denormalize(scale.normalize(value, min, max) + x, min, max) : value + x;
231
+ return clamp(step ? stepValue(next, step) : next, min, max);
232
+ }
233
+ //#endregion
62
234
  //#region src/util.ts
63
235
  function styleHelper(value, op, influencer) {
64
236
  if (op && influencer) if (typeof value == "number") {
@@ -163,30 +335,374 @@ function noteToFrequency(note, detune = 0, a4 = 440) {
163
335
  const n = typeof note == "string" ? noteNumber(note) : note;
164
336
  return a4 / 32 * 2 ** ((n - 9 + detune / 100) / 12);
165
337
  }
338
+ /**
339
+ * Semitones above the root, for each supported scale.
340
+ *
341
+ * Every entry starts at 0 and stays inside one octave, so a scale is a set of
342
+ * pitch classes rather than a set of notes: {@link inScale} compares against
343
+ * it with the octave taken out.
344
+ *
345
+ * `ionian` and `aeolian` are the same sets as `major` and `naturalMinor`; both
346
+ * spellings are here because both are what someone reaches for depending on
347
+ * whether they are thinking in keys or in modes.
348
+ */
349
+ const scaleIntervals = {
350
+ major: [
351
+ 0,
352
+ 2,
353
+ 4,
354
+ 5,
355
+ 7,
356
+ 9,
357
+ 11
358
+ ],
359
+ naturalMinor: [
360
+ 0,
361
+ 2,
362
+ 3,
363
+ 5,
364
+ 7,
365
+ 8,
366
+ 10
367
+ ],
368
+ harmonicMinor: [
369
+ 0,
370
+ 2,
371
+ 3,
372
+ 5,
373
+ 7,
374
+ 8,
375
+ 11
376
+ ],
377
+ melodicMinor: [
378
+ 0,
379
+ 2,
380
+ 3,
381
+ 5,
382
+ 7,
383
+ 9,
384
+ 11
385
+ ],
386
+ ionian: [
387
+ 0,
388
+ 2,
389
+ 4,
390
+ 5,
391
+ 7,
392
+ 9,
393
+ 11
394
+ ],
395
+ dorian: [
396
+ 0,
397
+ 2,
398
+ 3,
399
+ 5,
400
+ 7,
401
+ 9,
402
+ 10
403
+ ],
404
+ phrygian: [
405
+ 0,
406
+ 1,
407
+ 3,
408
+ 5,
409
+ 7,
410
+ 8,
411
+ 10
412
+ ],
413
+ lydian: [
414
+ 0,
415
+ 2,
416
+ 4,
417
+ 6,
418
+ 7,
419
+ 9,
420
+ 11
421
+ ],
422
+ mixolydian: [
423
+ 0,
424
+ 2,
425
+ 4,
426
+ 5,
427
+ 7,
428
+ 9,
429
+ 10
430
+ ],
431
+ aeolian: [
432
+ 0,
433
+ 2,
434
+ 3,
435
+ 5,
436
+ 7,
437
+ 8,
438
+ 10
439
+ ],
440
+ locrian: [
441
+ 0,
442
+ 1,
443
+ 3,
444
+ 5,
445
+ 6,
446
+ 8,
447
+ 10
448
+ ],
449
+ majorPentatonic: [
450
+ 0,
451
+ 2,
452
+ 4,
453
+ 7,
454
+ 9
455
+ ],
456
+ minorPentatonic: [
457
+ 0,
458
+ 3,
459
+ 5,
460
+ 7,
461
+ 10
462
+ ],
463
+ blues: [
464
+ 0,
465
+ 3,
466
+ 5,
467
+ 6,
468
+ 7,
469
+ 10
470
+ ],
471
+ wholeTone: [
472
+ 0,
473
+ 2,
474
+ 4,
475
+ 6,
476
+ 8,
477
+ 10
478
+ ],
479
+ chromatic: [
480
+ 0,
481
+ 1,
482
+ 2,
483
+ 3,
484
+ 4,
485
+ 5,
486
+ 6,
487
+ 7,
488
+ 8,
489
+ 9,
490
+ 10,
491
+ 11
492
+ ]
493
+ };
494
+ /**
495
+ * Whether a note belongs to a scale, regardless of the octave either sits in.
496
+ *
497
+ * @param note noteNumber: 0 ~ 127 or noteName e.g. 'C3'
498
+ * @param root the note the scale is built on, in the same two forms
499
+ *
500
+ * @example
501
+ * ```ts
502
+ * inScale('F#4', 'D3', 'major') // true
503
+ * ```
504
+ */
505
+ function inScale(note, root, name) {
506
+ const n = typeof note == "string" ? noteNumber(note) : note;
507
+ const r = typeof root == "string" ? noteNumber(root) : root;
508
+ return scaleIntervals[name].includes(mod(n - r, 12));
509
+ }
510
+ /**
511
+ * The notes of a scale, ascending from `root`.
512
+ *
513
+ * The octave above the root is not included: ask for more `octaves` instead, so
514
+ * that concatenating the result of two calls does not repeat a note.
515
+ *
516
+ * @param root noteNumber: 0 ~ 127 or noteName e.g. 'C3'
517
+ * @param octaves how many octaves to cover
518
+ *
519
+ * @example
520
+ * ```ts
521
+ * scaleNotes('C3', 'majorPentatonic') // [48, 50, 52, 55, 57]
522
+ * ```
523
+ */
524
+ function scaleNotes(root, name, octaves = 1) {
525
+ const r = typeof root == "string" ? noteNumber(root) : root;
526
+ const intervals = scaleIntervals[name];
527
+ return Array.from({ length: octaves }, (_, octave) => intervals.map((interval) => r + octave * 12 + interval)).flat();
528
+ }
529
+ //#endregion
530
+ //#region src/piano.ts
531
+ /**
532
+ * `[noteRange.first, noteRange.first + 1, ..., noteRange.last]`
533
+ */
534
+ function getNoteRangeArray(noteRange) {
535
+ return Array.from({ length: noteRange.last - noteRange.first + 1 }, (_, i) => i + noteRange.first);
536
+ }
537
+ const DEFAULT_KEY_GAP = 1;
538
+ const DEFAULT_BLACK_KEY_WIDTH_RATIO = .65;
539
+ const DEFAULT_BLACK_KEY_HEIGHT_RATIO = .6;
540
+ /**
541
+ * How many white keys sit at or before each pitch class, counting from C.
542
+ *
543
+ * A black key shares the number of the white key to its left plus one, which
544
+ * puts it on the boundary between the two; {@link notePosition} then shifts it
545
+ * back by half its width to centre it there.
546
+ */
547
+ const whiteKeysBefore = {
548
+ C: 0,
549
+ "C#": 1,
550
+ D: 1,
551
+ "D#": 2,
552
+ E: 2,
553
+ F: 3,
554
+ "F#": 4,
555
+ G: 4,
556
+ "G#": 5,
557
+ A: 5,
558
+ "A#": 6,
559
+ B: 6
560
+ };
561
+ /** Width of a black key in pixels. */
562
+ function blackKeyWidth(layout) {
563
+ return layout.whiteKeyWidth * (layout.blackKeyWidthRatio ?? DEFAULT_BLACK_KEY_WIDTH_RATIO);
564
+ }
565
+ /** Width of the whole keyboard in pixels. */
566
+ function pianoWidth(layout) {
567
+ const whiteKeys = getNoteRangeArray(layout.noteRange).filter(isWhiteKey);
568
+ return (layout.whiteKeyWidth + (layout.keyGap ?? DEFAULT_KEY_GAP)) * whiteKeys.length;
569
+ }
570
+ /**
571
+ * Offset of the left edge of a key from the left edge of the keyboard, in
572
+ * pixels.
573
+ *
574
+ * Notes outside `noteRange` are placed too, so the value is negative below
575
+ * `noteRange.first`.
576
+ */
577
+ function notePosition(note, layout) {
578
+ const slot = layout.whiteKeyWidth + (layout.keyGap ?? DEFAULT_KEY_GAP);
579
+ const target = noteKey(note);
580
+ const first = noteKey(layout.noteRange.first);
581
+ const octave = Math.floor((note - layout.noteRange.first) / 12);
582
+ const octaveOffset = noteKeys.indexOf(first) > noteKeys.indexOf(target) ? 1 : 0;
583
+ const whiteKeysIn = whiteKeysBefore[target] - whiteKeysBefore[first] + (octave + octaveOffset) * 7;
584
+ return isBlackKey(note) ? whiteKeysIn * slot - blackKeyWidth(layout) / 2 : whiteKeysIn * slot;
585
+ }
586
+ /**
587
+ * The note drawn at a point, or null where there is none.
588
+ *
589
+ * Black keys are tested first, so they win where they overlap a white one. A
590
+ * white key covers its gap as well as its width, so the whole width of the
591
+ * keyboard belongs to some key and a click cannot fall between two.
592
+ *
593
+ * @param x offset from the left edge of the keyboard, in pixels
594
+ * @param y offset from its top edge, in pixels
595
+ * @param height height of the keyboard, in pixels
596
+ */
597
+ function noteAt(x, y, height, layout) {
598
+ if (y < 0 || y >= height) return null;
599
+ const notes = getNoteRangeArray(layout.noteRange);
600
+ if (y < height * (layout.blackKeyHeightRatio ?? DEFAULT_BLACK_KEY_HEIGHT_RATIO)) for (const note of notes) {
601
+ if (isWhiteKey(note)) continue;
602
+ const left = notePosition(note, layout);
603
+ if (left <= x && x < left + blackKeyWidth(layout)) return note;
604
+ }
605
+ const slot = layout.whiteKeyWidth + (layout.keyGap ?? DEFAULT_KEY_GAP);
606
+ for (const note of notes) {
607
+ if (isBlackKey(note)) continue;
608
+ const left = notePosition(note, layout);
609
+ if (left <= x && x < left + slot) return note;
610
+ }
611
+ return null;
612
+ }
613
+ //#endregion
614
+ //#region src/unit.ts
615
+ /**
616
+ * Pick the unit a value is displayed in: the largest one whose scale does not
617
+ * exceed the magnitude of the value.
618
+ */
619
+ function selectUnit(units, value) {
620
+ let i = 0;
621
+ for (; i < units.length; i++) if (Math.abs(units[i][1]) > Math.abs(value)) break;
622
+ return units[Math.max(0, i - 1)];
623
+ }
624
+ /**
625
+ * Render a value as text, in the unit that suits its magnitude.
626
+ *
627
+ * @param units a single symbol appended as-is, or a list to choose from.
628
+ * @param digit digits after the decimal point. Left as-is when omitted.
629
+ *
630
+ * @example
631
+ * formatValue(1234, [['Hz', 1], ['kHz', 1000]], 2) // '1.23kHz'
632
+ * formatValue(1.5, 'Hz') // '1.5Hz'
633
+ */
634
+ function formatValue(value, units, digit) {
635
+ const fixed = (v) => digit != void 0 ? v.toFixed(digit) : String(v);
636
+ if (!units || typeof units == "string") return fixed(value) + (units ?? "");
637
+ const [unit, scale] = selectUnit(units, value);
638
+ return fixed(value / scale) + unit;
639
+ }
640
+ /**
641
+ * Read a value back out of text, undoing the scaling of {@link formatValue}.
642
+ *
643
+ * With a list of units the text has to be a number followed by an optional
644
+ * unit and nothing else, since the unit decides the scale; anything else reads
645
+ * as 0. With a single unit, or none, the first number found anywhere in the
646
+ * text is taken, so a half-typed entry still yields something.
647
+ *
648
+ * @example
649
+ * parseValue('1.23kHz', [['Hz', 1], ['kHz', 1000]]) // 1230
650
+ * parseValue('4abc') // 4
651
+ */
652
+ function parseValue(text, units) {
653
+ const str = text.trim();
654
+ if (!units || typeof units == "string") {
655
+ const m = str.match(/-?\d+(\.\d+)?/);
656
+ const v = Number(m?.[0] ?? "0");
657
+ return isNaN(v) ? 0 : v;
658
+ }
659
+ const m = str.match(/^(-?\d+(\.\d+)?)\s*(\w*)$/);
660
+ if (!m) return 0;
661
+ const found = units.find(([unit]) => unit == m[3]);
662
+ return (Number(m[1]) || 0) * (found ? found[1] : 1);
663
+ }
166
664
  //#endregion
665
+ exports.applyDelta = applyDelta;
666
+ exports.blackKeyWidth = blackKeyWidth;
167
667
  exports.clamp = clamp;
668
+ exports.curveScale = curveScale;
669
+ exports.curveWithCenterValue = curveWithCenterValue;
168
670
  exports.dbToGain = dbToGain;
169
671
  exports.decimalPart = decimalPart;
170
672
  exports.degree = degree;
673
+ exports.exponentialScale = exponentialScale;
674
+ exports.formatValue = formatValue;
171
675
  exports.gainToDb = gainToDb;
676
+ exports.getNoteRangeArray = getNoteRangeArray;
677
+ exports.inScale = inScale;
172
678
  exports.integerPart = integerPart;
173
679
  exports.isBlackKey = isBlackKey;
174
680
  exports.isEmpty = isEmpty;
175
681
  exports.isWhiteKey = isWhiteKey;
682
+ exports.linearScale = linearScale;
176
683
  exports.mapValue = mapValue;
177
684
  exports.mod = mod;
178
685
  exports.normalizeValue = normalizeValue;
686
+ exports.noteAt = noteAt;
179
687
  exports.noteKey = noteKey;
180
688
  exports.noteKeys = noteKeys;
181
689
  exports.noteName = noteName;
182
690
  exports.noteNumber = noteNumber;
691
+ exports.notePosition = notePosition;
183
692
  exports.noteToFrequency = noteToFrequency;
184
693
  exports.parseNoteName = parseNoteName;
694
+ exports.parseValue = parseValue;
695
+ exports.pianoWidth = pianoWidth;
185
696
  exports.radian = radian;
186
697
  exports.rawValue = rawValue;
698
+ exports.scaleIntervals = scaleIntervals;
699
+ exports.scaleNotes = scaleNotes;
700
+ exports.selectUnit = selectUnit;
701
+ exports.skewScale = skewScale;
187
702
  exports.skewWithCenterValue = skewWithCenterValue;
188
703
  exports.stepValue = stepValue;
189
704
  exports.styleHelper = styleHelper;
705
+ exports.symmetricSkewScale = symmetricSkewScale;
190
706
  exports.toFixed = toFixed;
191
707
  exports.whiteKeys = whiteKeys;
192
708
  exports.xor = xor;
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":[],"sources":["../src/math.ts","../src/util.ts","../src/midi.ts"],"sourcesContent":["/**\n * clamp value between min and max\n */\nexport function clamp(value: number, min: number, max: number) {\n return Math.max(min, Math.min(value, max))\n}\n\n/**\n * Normalize the value from 0 to 1\n */\nexport function normalizeValue(\n rawValue: number,\n min: number,\n max: number,\n skew = 1,\n) {\n if (min >= max) throw new RangeError('requirements: min < max')\n const v = clamp((rawValue - min) / (max - min), 0, 1)\n return Math.pow(v, skew)\n}\n\n/**\n * Convert normalized values back to raw values.\n */\nexport function rawValue(\n normalizedValue: number,\n min: number,\n max: number,\n skew = 1,\n) {\n if (min >= max) throw new RangeError('requirements: min < max')\n const v =\n skew == 1\n ? clamp(normalizedValue, 0, 1)\n : Math.exp(Math.log(clamp(normalizedValue, 0, 1)) / skew)\n return min + v * (max - min)\n}\n\nexport function skewWithCenterValue(\n centerValue: number,\n min: number,\n max: number,\n) {\n if (!(min <= centerValue && centerValue <= max))\n throw new RangeError('requirements: min <= centerValue <= max')\n return Math.log(0.5) / Math.log((centerValue - min) / (max - min))\n}\n\nexport function stepValue(value: number, step: number) {\n if (step <= 0) throw new RangeError('requirements: step > 0')\n const quotient = Math.floor(value / step)\n const decimalDigits = decimalPart(step)?.length\n const v = toFixed(quotient * step, decimalDigits)\n const next = toFixed((quotient + 1) * step, decimalDigits)\n return Math.abs(value - v) < Math.abs(value - next) ? v : next\n}\n\nexport function toFixed(x: number, fractionDigits?: number) {\n return Number(x.toFixed(fractionDigits))\n}\n\nexport function integerPart(x: number | string): string | undefined {\n if (Number.isNaN(x)) {\n return undefined\n }\n return String(x).split('.')[0]\n}\n\nexport function decimalPart(x: number | string): string | undefined {\n return String(x).split('.')[1]\n}\n\nexport function radian(degree: number) {\n return (Math.PI * degree) / 180\n}\n\nexport function degree(radian: number) {\n return (180 * radian) / Math.PI\n}\n\nexport function mapValue(\n value: number,\n inMin: number,\n inMax: number,\n outMin: number,\n outMax: number,\n) {\n return ((value - inMin) / (inMax - inMin)) * (outMax - outMin) + outMin\n}\n\nexport function dbToGain(db: number) {\n return Math.pow(10, db / 20)\n}\n\nexport function gainToDb(gain: number) {\n return 20 * (Math.log(gain) / Math.LN10)\n}\n","type Operator = '+' | '-' | '*' | '/'\n\nexport function styleHelper(value: string | number): string\nexport function styleHelper(\n value: string | number,\n op: Operator,\n influencer?: number,\n): string\nexport function styleHelper(\n value: string | number,\n op?: Operator,\n influencer?: number,\n) {\n if (op && influencer) {\n if (typeof value == 'number') {\n if (op == '+') return `${value + influencer}px`\n if (op == '-') return `${value - influencer}px`\n if (op == '*') return `${value * influencer}px`\n if (op == '/') return `${value / influencer}px`\n } else {\n return `calc(${value}px ${op} ${influencer})`\n }\n } else {\n if (typeof value == 'number') {\n return `${value}px`\n } else {\n return value\n }\n }\n}\n\nexport function isEmpty(obj: object) {\n return Object.keys(obj).length == 0\n}\n\nexport function mod(n: number, m: number) {\n return ((n % m) + m) % m\n}\n\nexport function xor(a = false, b = false) {\n return (a || b) && a != b\n}\n","import { mod } from './util'\n\nexport const whiteKeys = ['A', 'B', 'C', 'D', 'E', 'F', 'G'] as const\n\nexport type WhiteKey = (typeof whiteKeys)[number]\n\nexport const noteKeys = [\n 'C',\n 'C#',\n 'D',\n 'D#',\n 'E',\n 'F',\n 'F#',\n 'G',\n 'G#',\n 'A',\n 'A#',\n 'B',\n] as const\n\nexport type NoteKey = (typeof noteKeys)[number]\n\nexport function parseNoteName(noteName: string) {\n const m = noteName.match(/^([a-g])(#{0,2}|b{0,2})(-?\\d+)$/i)\n if (!m) throw new Error('Invalid note name')\n const [, letter, accidental, octave] = m\n return {\n letter: letter.toLocaleUpperCase() as WhiteKey,\n accidental: accidental as '#' | '##' | 'b' | 'bb' | '',\n octave: Number(octave),\n }\n}\n\n/**\n * Convert noteName to noteNumber\n */\nexport function noteNumber(noteName: string) {\n const { letter, accidental, octave } = parseNoteName(noteName)\n const noteIndex = noteKeys.indexOf(letter.toLocaleUpperCase() as NoteKey)\n const accidentalValue = (accidental[0] == 'b' ? -1 : 1) * accidental.length\n return noteIndex + 12 * (Number(octave) + 1) + accidentalValue\n}\n\n/**\n * Convert noteNumber to noteName\n * C-1 = 0\n * G9 = 127\n * @param noteNumber noteNumber\n */\nexport function noteName(noteNumber: number): `${NoteKey}${number}` {\n const noteIndex = mod(noteNumber, 12)\n const octave = Math.floor(noteNumber / 12) - 1\n return `${noteKeys[noteIndex]}${octave}`\n}\n\n/**\n * Convert noteNumber to noteKey\n */\nexport function noteKey(noteNumber: number): NoteKey {\n return noteKeys[mod(noteNumber, 12)]\n}\n\n/**\n * @param note noteNumber: 0 ~ 127 or noteName e.g. 'C3'\n */\nexport function isWhiteKey(note: number | string) {\n const n = typeof note == 'string' ? noteNumber(note) : note\n return (\n mod(n, 12) == 0 ||\n mod(n, 12) == 2 ||\n mod(n, 12) == 4 ||\n mod(n, 12) == 5 ||\n mod(n, 12) == 7 ||\n mod(n, 12) == 9 ||\n mod(n, 12) == 11\n )\n}\n\n/**\n * @param note noteNumber: 0 ~ 127 or noteName e.g. 'C3'\n */\nexport function isBlackKey(note: number | string) {\n return !isWhiteKey(note)\n}\n\n/**\n * @param note noteNumber: 0 ~ 127 or noteName e.g. 'C3'\n * @param detune [cent]\n * @param a4 A4 frequency [Hz]\n * @returns frequency [Hz]\n */\nexport function noteToFrequency(note: number | string, detune = 0, a4 = 440) {\n const n = typeof note == 'string' ? noteNumber(note) : note\n return (a4 / 32) * 2 ** ((n - 9 + detune / 100) / 12)\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","names":[],"sources":["../src/math.ts","../src/scales.ts","../src/util.ts","../src/midi.ts","../src/piano.ts","../src/unit.ts"],"sourcesContent":["/**\n * clamp value between min and max\n */\nexport function clamp(value: number, min: number, max: number) {\n return Math.max(min, Math.min(value, max))\n}\n\n/**\n * Normalize the value from 0 to 1, spreading the range evenly.\n *\n * This is the linear mapping and takes no curve of its own; a `Scale` builds\n * whatever curve it needs on top of it.\n */\nexport function normalizeValue(value: number, min: number, max: number) {\n if (min >= max) throw new RangeError('requirements: min < max')\n return clamp((value - min) / (max - min), 0, 1)\n}\n\n/**\n * Convert normalized values back to raw values, spreading the range evenly.\n *\n * The inverse of {@link normalizeValue}.\n */\nexport function rawValue(normalizedValue: number, min: number, max: number) {\n if (min >= max) throw new RangeError('requirements: min < max')\n return min + clamp(normalizedValue, 0, 1) * (max - min)\n}\n\nexport function stepValue(value: number, step: number) {\n if (step <= 0) throw new RangeError('requirements: step > 0')\n const quotient = Math.floor(value / step)\n const decimalDigits = decimalPart(step)?.length\n const v = toFixed(quotient * step, decimalDigits)\n const next = toFixed((quotient + 1) * step, decimalDigits)\n return Math.abs(value - v) < Math.abs(value - next) ? v : next\n}\n\nexport function toFixed(x: number, fractionDigits?: number) {\n return Number(x.toFixed(fractionDigits))\n}\n\nexport function integerPart(x: number | string): string | undefined {\n if (Number.isNaN(x)) {\n return undefined\n }\n return String(x).split('.')[0]\n}\n\nexport function decimalPart(x: number | string): string | undefined {\n return String(x).split('.')[1]\n}\n\nexport function radian(degree: number) {\n return (Math.PI * degree) / 180\n}\n\nexport function degree(radian: number) {\n return (180 * radian) / Math.PI\n}\n\nexport function mapValue(\n value: number,\n inMin: number,\n inMax: number,\n outMin: number,\n outMax: number,\n) {\n return ((value - inMin) / (inMax - inMin)) * (outMax - outMin) + outMin\n}\n\nexport function dbToGain(db: number) {\n return Math.pow(10, db / 20)\n}\n\nexport function gainToDb(gain: number) {\n return 20 * (Math.log(gain) / Math.LN10)\n}\n","import { clamp, normalizeValue, rawValue, stepValue } from './math'\n\nimport type { InputEventOption } from './types'\n\n/**\n * How a value is distributed across the travel of a control.\n *\n * `normalize` and `denormalize` are inverses of each other: the position is\n * 0 at `min` and 1 at `max`, and everything in between is up to the scale.\n *\n * `min` and `max` are arguments rather than baked into the scale, so a scale\n * holds no state and can be a module level constant. Passing the same object\n * on every render therefore costs nothing.\n *\n * @example\n * ```ts\n * exponentialScale.denormalize(0.5, 20, 20000) // 632.45…\n * ```\n */\nexport interface Scale {\n /** Value to its position on the travel, 0-1. */\n normalize: (value: number, min: number, max: number) => number\n /** Position on the travel, 0-1, back to a value. */\n denormalize: (position: number, min: number, max: number) => number\n}\n\nfunction assertRange(min: number, max: number) {\n if (min >= max) throw new RangeError('requirements: min < max')\n}\n\n/**\n * Equal travel gives an equal change in value.\n *\n * The right default for anything already linear in perception: dB values,\n * pan, percentages, MIDI note numbers, semitones.\n */\nexport const linearScale: Scale = {\n normalize: (value, min, max) => normalizeValue(value, min, max),\n denormalize: (position, min, max) => rawValue(position, min, max),\n}\n\n/**\n * The power law of JUCE's `NormalisableRange::skew`, applied to `value - min`.\n *\n * Use it when the value has to agree with a JUCE or iPlug2 parameter — a\n * plugin UI in a WebView, say, where the knob must sit exactly where the\n * host's automation curve puts it. {@link skewWithCenterValue} gives the\n * factor that places a chosen value at the middle of the travel.\n *\n * `skew < 1` gives the lower end more travel, `skew > 1` the upper end.\n *\n * For new designs prefer {@link exponentialScale} or {@link curveScale}: the\n * slope of this curve is either zero or infinite at `min`, so the bottom of\n * the range is a dead zone or jumps.\n *\n * @param skew the JUCE skew factor\n */\nexport function skewScale(skew: number): Scale {\n return {\n // The two expressions JUCE uses, kept verbatim so the numbers agree with\n // a NormalisableRange: pow() one way, exp(log()) the other.\n normalize: (value, min, max) =>\n Math.pow(normalizeValue(value, min, max), skew),\n denormalize: (position, min, max) =>\n rawValue(\n skew === 1\n ? position\n : Math.exp(Math.log(clamp(position, 0, 1)) / skew),\n min,\n max,\n ),\n }\n}\n\n/**\n * The skew factor for {@link skewScale} that puts `centerValue` at the middle\n * of the travel — JUCE's `NormalisableRange::setSkewForCentre`.\n */\nexport function skewWithCenterValue(\n centerValue: number,\n min: number,\n max: number,\n) {\n if (!(min <= centerValue && centerValue <= max))\n throw new RangeError('requirements: min <= centerValue <= max')\n return Math.log(0.5) / Math.log((centerValue - min) / (max - min))\n}\n\n/**\n * Equal travel gives an equal *ratio*, so an octave — or a percentage — takes\n * the same distance wherever it falls.\n *\n * The scale for frequency (a filter cutoff over 20-20000 Hz), free running\n * rates, and delay times.\n *\n * Requires `min` and `max` to be non-zero and of the same sign, since no\n * ratio reaches zero or crosses it. Use {@link curveScale} for a range that\n * starts at 0.\n */\nexport const exponentialScale: Scale = {\n normalize: (value, min, max) => {\n assertExponentialRange(min, max)\n // The value is clamped before the logarithm, not after: outside the range\n // the ratio can be negative, and log() would give NaN rather than a\n // position to clamp.\n return clamp(\n Math.log(clamp(value, min, max) / min) / Math.log(max / min),\n 0,\n 1,\n )\n },\n denormalize: (position, min, max) => {\n assertExponentialRange(min, max)\n return min * Math.pow(max / min, clamp(position, 0, 1))\n },\n}\n\nfunction assertExponentialRange(min: number, max: number) {\n assertRange(min, max)\n if (min === 0 || max === 0 || Math.sign(min) !== Math.sign(max)) {\n throw new RangeError(\n 'exponentialScale: requirements: min and max are non-zero and have the same sign',\n )\n }\n}\n\n/**\n * An exponential bend that still passes exactly through `min` and `max`, so\n * unlike {@link exponentialScale} it works on a range that starts at 0 or\n * crosses it, and unlike {@link skewScale} its slope is neither zero nor\n * infinite at either end.\n *\n * The general purpose taper, and the same family as the curve of an envelope\n * segment (SuperCollider's `CurveWarp`).\n *\n * - `curve > 0` gives the lower end more travel — envelope times from 0 ms,\n * delay times, anything that wants fine control near the bottom\n * - `curve < 0` gives the upper end more travel — a volume fader over\n * -60..+6 dB that should be precise around 0 dB\n * - near 0 it is indistinguishable from {@link linearScale}, and is treated\n * as linear to avoid dividing by zero\n *\n * {@link curveWithCenterValue} gives the curve that places a chosen value at\n * the middle of the travel.\n *\n * @param curve how hard the curve bends, and in which direction\n */\nexport function curveScale(curve: number): Scale {\n // The two coefficients blow up as the curve flattens: `a` divides by\n // 1 - e^curve, which goes to 0.\n if (Math.abs(curve) < 0.001) return linearScale\n\n const grow = Math.exp(curve)\n\n // value(position) = b - a * e^(curve * position), fixed so that\n // value(0) = min and value(1) = max.\n const coefficients = (min: number, max: number) => {\n const a = (max - min) / (1 - grow)\n return { a, b: min + a }\n }\n\n return {\n normalize: (value, min, max) => {\n assertRange(min, max)\n const { a, b } = coefficients(min, max)\n // Clamped before the logarithm: far outside the range `(b - value) / a`\n // turns negative and log() would give NaN.\n return clamp(Math.log((b - clamp(value, min, max)) / a) / curve, 0, 1)\n },\n denormalize: (position, min, max) => {\n assertRange(min, max)\n const { a, b } = coefficients(min, max)\n return b - a * Math.pow(grow, clamp(position, 0, 1))\n },\n }\n}\n\n/**\n * {@link skewScale} mirrored about the middle of the range, so both halves\n * bend the same way — JUCE's `symmetricSkew`.\n *\n * For a bipolar control whose centre matters: detune over -100..+100 cents,\n * or a bipolar filter envelope amount, where the fine adjustment is around 0\n * rather than at either end.\n *\n * `skew < 1` gives the middle more travel, `skew > 1` the two ends.\n *\n * @param skew the JUCE skew factor\n */\nexport function symmetricSkewScale(skew: number): Scale {\n return {\n normalize: (value, min, max) => {\n assertRange(min, max)\n const proportion = clamp((value - min) / (max - min), 0, 1)\n if (skew === 1) return proportion\n const distanceFromMiddle = 2 * proportion - 1\n return (\n (1 +\n Math.pow(Math.abs(distanceFromMiddle), skew) *\n Math.sign(distanceFromMiddle)) /\n 2\n )\n },\n denormalize: (position, min, max) => {\n assertRange(min, max)\n const p = clamp(position, 0, 1)\n let distanceFromMiddle = 2 * p - 1\n if (skew !== 1 && distanceFromMiddle !== 0) {\n distanceFromMiddle =\n Math.pow(Math.abs(distanceFromMiddle), 1 / skew) *\n Math.sign(distanceFromMiddle)\n }\n return min + ((max - min) / 2) * (1 + distanceFromMiddle)\n },\n }\n}\n\n/**\n * The curve for {@link curveScale} that puts `centerValue` at the middle of\n * the travel — the counterpart of {@link skewWithCenterValue}.\n */\nexport function curveWithCenterValue(\n centerValue: number,\n min: number,\n max: number,\n) {\n assertRange(min, max)\n if (!(min < centerValue && centerValue < max)) {\n throw new RangeError('requirements: min < centerValue < max')\n }\n // value(0.5) - min = range / (1 + e^(curve / 2))\n const proportion = (centerValue - min) / (max - min)\n return 2 * Math.log(1 / proportion - 1)\n}\n\n/**\n * How a value is scaled: the range it lives in, how it is rounded, and how it\n * is distributed across the travel.\n *\n * `AxisOptions` of `@tremolo-ui/dom` extends this, so a drag and a\n * wheel / keyboard nudge run the same value pipeline.\n */\nexport interface ValueRange {\n min: number\n max: number\n /**\n * Rounding applied to the value. Left unrounded when omitted.\n */\n step?: number\n /**\n * How the value is distributed across the travel.\n *\n * @default linearScale\n */\n scale?: Scale\n}\n\n/**\n * Move a value by an amount of input, as reported by a wheel or an arrow key.\n *\n * The pipeline matches `createDragValue` of `@tremolo-ui/dom`: scale, then\n * step, then clamp. Which key or which sign of `deltaY` counts as which\n * direction is left to the caller, since it differs per component.\n *\n * @param direction which way, and how many times, to apply the option. The\n * size of one step is `option[1]`, so this is normally `1` or `-1`.\n *\n * @example\n * // ArrowDown on a slider whose keyboard option is ['raw', 1]\n * applyDelta(value, -1, keyboard, { min, max, step, scale })\n */\nexport function applyDelta(\n value: number,\n direction: number,\n [mode, amount]: InputEventOption,\n { min, max, step, scale = linearScale }: ValueRange,\n): number {\n const x = direction * amount\n const next =\n mode == 'normalized'\n ? scale.denormalize(scale.normalize(value, min, max) + x, min, max)\n : value + x\n return clamp(step ? stepValue(next, step) : next, min, max)\n}\n","type Operator = '+' | '-' | '*' | '/'\n\nexport function styleHelper(value: string | number): string\nexport function styleHelper(\n value: string | number,\n op: Operator,\n influencer?: number,\n): string\nexport function styleHelper(\n value: string | number,\n op?: Operator,\n influencer?: number,\n) {\n if (op && influencer) {\n if (typeof value == 'number') {\n if (op == '+') return `${value + influencer}px`\n if (op == '-') return `${value - influencer}px`\n if (op == '*') return `${value * influencer}px`\n if (op == '/') return `${value / influencer}px`\n } else {\n return `calc(${value}px ${op} ${influencer})`\n }\n } else {\n if (typeof value == 'number') {\n return `${value}px`\n } else {\n return value\n }\n }\n}\n\nexport function isEmpty(obj: object) {\n return Object.keys(obj).length == 0\n}\n\nexport function mod(n: number, m: number) {\n return ((n % m) + m) % m\n}\n\nexport function xor(a = false, b = false) {\n return (a || b) && a != b\n}\n","import { mod } from './util'\n\nexport const whiteKeys = ['A', 'B', 'C', 'D', 'E', 'F', 'G'] as const\n\nexport type WhiteKey = (typeof whiteKeys)[number]\n\nexport const noteKeys = [\n 'C',\n 'C#',\n 'D',\n 'D#',\n 'E',\n 'F',\n 'F#',\n 'G',\n 'G#',\n 'A',\n 'A#',\n 'B',\n] as const\n\nexport type NoteKey = (typeof noteKeys)[number]\n\nexport function parseNoteName(noteName: string) {\n const m = noteName.match(/^([a-g])(#{0,2}|b{0,2})(-?\\d+)$/i)\n if (!m) throw new Error('Invalid note name')\n const [, letter, accidental, octave] = m\n return {\n letter: letter.toLocaleUpperCase() as WhiteKey,\n accidental: accidental as '#' | '##' | 'b' | 'bb' | '',\n octave: Number(octave),\n }\n}\n\n/**\n * Convert noteName to noteNumber\n */\nexport function noteNumber(noteName: string) {\n const { letter, accidental, octave } = parseNoteName(noteName)\n const noteIndex = noteKeys.indexOf(letter.toLocaleUpperCase() as NoteKey)\n const accidentalValue = (accidental[0] == 'b' ? -1 : 1) * accidental.length\n return noteIndex + 12 * (Number(octave) + 1) + accidentalValue\n}\n\n/**\n * Convert noteNumber to noteName\n * C-1 = 0\n * G9 = 127\n * @param noteNumber noteNumber\n */\nexport function noteName(noteNumber: number): `${NoteKey}${number}` {\n const noteIndex = mod(noteNumber, 12)\n const octave = Math.floor(noteNumber / 12) - 1\n return `${noteKeys[noteIndex]}${octave}`\n}\n\n/**\n * Convert noteNumber to noteKey\n */\nexport function noteKey(noteNumber: number): NoteKey {\n return noteKeys[mod(noteNumber, 12)]\n}\n\n/**\n * @param note noteNumber: 0 ~ 127 or noteName e.g. 'C3'\n */\nexport function isWhiteKey(note: number | string) {\n const n = typeof note == 'string' ? noteNumber(note) : note\n return (\n mod(n, 12) == 0 ||\n mod(n, 12) == 2 ||\n mod(n, 12) == 4 ||\n mod(n, 12) == 5 ||\n mod(n, 12) == 7 ||\n mod(n, 12) == 9 ||\n mod(n, 12) == 11\n )\n}\n\n/**\n * @param note noteNumber: 0 ~ 127 or noteName e.g. 'C3'\n */\nexport function isBlackKey(note: number | string) {\n return !isWhiteKey(note)\n}\n\n/**\n * @param note noteNumber: 0 ~ 127 or noteName e.g. 'C3'\n * @param detune [cent]\n * @param a4 A4 frequency [Hz]\n * @returns frequency [Hz]\n */\nexport function noteToFrequency(note: number | string, detune = 0, a4 = 440) {\n const n = typeof note == 'string' ? noteNumber(note) : note\n return (a4 / 32) * 2 ** ((n - 9 + detune / 100) / 12)\n}\n\n/**\n * Semitones above the root, for each supported scale.\n *\n * Every entry starts at 0 and stays inside one octave, so a scale is a set of\n * pitch classes rather than a set of notes: {@link inScale} compares against\n * it with the octave taken out.\n *\n * `ionian` and `aeolian` are the same sets as `major` and `naturalMinor`; both\n * spellings are here because both are what someone reaches for depending on\n * whether they are thinking in keys or in modes.\n */\nexport const scaleIntervals = {\n major: [0, 2, 4, 5, 7, 9, 11],\n naturalMinor: [0, 2, 3, 5, 7, 8, 10],\n harmonicMinor: [0, 2, 3, 5, 7, 8, 11],\n melodicMinor: [0, 2, 3, 5, 7, 9, 11],\n\n ionian: [0, 2, 4, 5, 7, 9, 11],\n dorian: [0, 2, 3, 5, 7, 9, 10],\n phrygian: [0, 1, 3, 5, 7, 8, 10],\n lydian: [0, 2, 4, 6, 7, 9, 11],\n mixolydian: [0, 2, 4, 5, 7, 9, 10],\n aeolian: [0, 2, 3, 5, 7, 8, 10],\n locrian: [0, 1, 3, 5, 6, 8, 10],\n\n majorPentatonic: [0, 2, 4, 7, 9],\n minorPentatonic: [0, 3, 5, 7, 10],\n blues: [0, 3, 5, 6, 7, 10],\n\n wholeTone: [0, 2, 4, 6, 8, 10],\n chromatic: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],\n} as const satisfies Record<string, readonly number[]>\n\nexport type ScaleName = keyof typeof scaleIntervals\n\n/**\n * Whether a note belongs to a scale, regardless of the octave either sits in.\n *\n * @param note noteNumber: 0 ~ 127 or noteName e.g. 'C3'\n * @param root the note the scale is built on, in the same two forms\n *\n * @example\n * ```ts\n * inScale('F#4', 'D3', 'major') // true\n * ```\n */\nexport function inScale(\n note: number | string,\n root: number | string,\n name: ScaleName,\n): boolean {\n const n = typeof note == 'string' ? noteNumber(note) : note\n const r = typeof root == 'string' ? noteNumber(root) : root\n return (scaleIntervals[name] as readonly number[]).includes(mod(n - r, 12))\n}\n\n/**\n * The notes of a scale, ascending from `root`.\n *\n * The octave above the root is not included: ask for more `octaves` instead, so\n * that concatenating the result of two calls does not repeat a note.\n *\n * @param root noteNumber: 0 ~ 127 or noteName e.g. 'C3'\n * @param octaves how many octaves to cover\n *\n * @example\n * ```ts\n * scaleNotes('C3', 'majorPentatonic') // [48, 50, 52, 55, 57]\n * ```\n */\nexport function scaleNotes(\n root: number | string,\n name: ScaleName,\n octaves = 1,\n): number[] {\n const r = typeof root == 'string' ? noteNumber(root) : root\n const intervals = scaleIntervals[name] as readonly number[]\n return Array.from({ length: octaves }, (_, octave) =>\n intervals.map((interval) => r + octave * 12 + interval),\n ).flat()\n}\n","import { isBlackKey, isWhiteKey, noteKey, noteKeys, type NoteKey } from './midi'\n\nexport type NoteRange = {\n first: number\n last: number\n}\n\n/**\n * `[noteRange.first, noteRange.first + 1, ..., noteRange.last]`\n */\nexport function getNoteRangeArray(noteRange: NoteRange): number[] {\n return Array.from(\n { length: noteRange.last - noteRange.first + 1 },\n (_, i) => i + noteRange.first,\n )\n}\n\n/**\n * The geometry of a drawn keyboard.\n *\n * One description is shared by the drawing and the hit testing, so a key cannot\n * be drawn somewhere other than where it responds.\n */\nexport interface PianoLayout {\n noteRange: NoteRange\n\n /** Width of a white key, excluding {@link PianoLayout.keyGap}. */\n whiteKeyWidth: number\n\n /**\n * Space between two white keys. Part of the slot a white key occupies, so it\n * still belongs to one of the keys for the purpose of hit testing.\n *\n * @default 1\n */\n keyGap?: number\n\n /**\n * Width of a black key, as a fraction of {@link PianoLayout.whiteKeyWidth}.\n *\n * @default 0.65\n */\n blackKeyWidthRatio?: number\n\n /**\n * Height of a black key, as a fraction of the height of the keyboard.\n *\n * @default 0.6\n */\n blackKeyHeightRatio?: number\n}\n\nconst DEFAULT_KEY_GAP = 1\nconst DEFAULT_BLACK_KEY_WIDTH_RATIO = 0.65\nconst DEFAULT_BLACK_KEY_HEIGHT_RATIO = 0.6\n\n/**\n * How many white keys sit at or before each pitch class, counting from C.\n *\n * A black key shares the number of the white key to its left plus one, which\n * puts it on the boundary between the two; {@link notePosition} then shifts it\n * back by half its width to centre it there.\n */\nconst whiteKeysBefore: Record<NoteKey, number> = {\n C: 0,\n 'C#': 1,\n D: 1,\n 'D#': 2,\n E: 2,\n F: 3,\n 'F#': 4,\n G: 4,\n 'G#': 5,\n A: 5,\n 'A#': 6,\n B: 6,\n}\n\n/** Width of a black key in pixels. */\nexport function blackKeyWidth(layout: PianoLayout): number {\n return (\n layout.whiteKeyWidth *\n (layout.blackKeyWidthRatio ?? DEFAULT_BLACK_KEY_WIDTH_RATIO)\n )\n}\n\n/** Width of the whole keyboard in pixels. */\nexport function pianoWidth(layout: PianoLayout): number {\n const whiteKeys = getNoteRangeArray(layout.noteRange).filter(isWhiteKey)\n return (\n (layout.whiteKeyWidth + (layout.keyGap ?? DEFAULT_KEY_GAP)) *\n whiteKeys.length\n )\n}\n\n/**\n * Offset of the left edge of a key from the left edge of the keyboard, in\n * pixels.\n *\n * Notes outside `noteRange` are placed too, so the value is negative below\n * `noteRange.first`.\n */\nexport function notePosition(note: number, layout: PianoLayout): number {\n const slot = layout.whiteKeyWidth + (layout.keyGap ?? DEFAULT_KEY_GAP)\n const target = noteKey(note)\n const first = noteKey(layout.noteRange.first)\n\n const octave = Math.floor((note - layout.noteRange.first) / 12)\n // A note whose pitch class comes before the first one belongs to the octave\n // above the one the division above gives.\n const octaveOffset =\n noteKeys.indexOf(first) > noteKeys.indexOf(target) ? 1 : 0\n\n const whiteKeysIn =\n whiteKeysBefore[target] -\n whiteKeysBefore[first] +\n (octave + octaveOffset) * 7\n\n return isBlackKey(note)\n ? whiteKeysIn * slot - blackKeyWidth(layout) / 2\n : whiteKeysIn * slot\n}\n\n/**\n * The note drawn at a point, or null where there is none.\n *\n * Black keys are tested first, so they win where they overlap a white one. A\n * white key covers its gap as well as its width, so the whole width of the\n * keyboard belongs to some key and a click cannot fall between two.\n *\n * @param x offset from the left edge of the keyboard, in pixels\n * @param y offset from its top edge, in pixels\n * @param height height of the keyboard, in pixels\n */\nexport function noteAt(\n x: number,\n y: number,\n height: number,\n layout: PianoLayout,\n): number | null {\n if (y < 0 || y >= height) return null\n\n const notes = getNoteRangeArray(layout.noteRange)\n const blackHeight =\n height * (layout.blackKeyHeightRatio ?? DEFAULT_BLACK_KEY_HEIGHT_RATIO)\n\n if (y < blackHeight) {\n for (const note of notes) {\n if (isWhiteKey(note)) continue\n const left = notePosition(note, layout)\n if (left <= x && x < left + blackKeyWidth(layout)) return note\n }\n }\n\n const slot = layout.whiteKeyWidth + (layout.keyGap ?? DEFAULT_KEY_GAP)\n for (const note of notes) {\n if (isBlackKey(note)) continue\n const left = notePosition(note, layout)\n if (left <= x && x < left + slot) return note\n }\n\n return null\n}\n","/**\n * Units a value can be displayed in, as `[symbol, scale]` pairs ordered from\n * the smallest scale up. A value is shown in the largest unit that does not\n * exceed it.\n *\n * @example\n * [['Hz', 1], ['kHz', 1000]]\n * [['ms', 1], ['s', 1000]]\n */\nexport type Units = [string, number][]\n\n/**\n * Pick the unit a value is displayed in: the largest one whose scale does not\n * exceed the magnitude of the value.\n */\nexport function selectUnit(units: Units, value: number): [string, number] {\n let i = 0\n for (; i < units.length; i++) {\n if (Math.abs(units[i][1]) > Math.abs(value)) break\n }\n return units[Math.max(0, i - 1)]\n}\n\n/**\n * Render a value as text, in the unit that suits its magnitude.\n *\n * @param units a single symbol appended as-is, or a list to choose from.\n * @param digit digits after the decimal point. Left as-is when omitted.\n *\n * @example\n * formatValue(1234, [['Hz', 1], ['kHz', 1000]], 2) // '1.23kHz'\n * formatValue(1.5, 'Hz') // '1.5Hz'\n */\nexport function formatValue(\n value: number,\n units?: string | Units,\n digit?: number,\n): string {\n const fixed = (v: number) =>\n digit != undefined ? v.toFixed(digit) : String(v)\n if (!units || typeof units == 'string') {\n return fixed(value) + (units ?? '')\n }\n const [unit, scale] = selectUnit(units, value)\n return fixed(value / scale) + unit\n}\n\n/**\n * Read a value back out of text, undoing the scaling of {@link formatValue}.\n *\n * With a list of units the text has to be a number followed by an optional\n * unit and nothing else, since the unit decides the scale; anything else reads\n * as 0. With a single unit, or none, the first number found anywhere in the\n * text is taken, so a half-typed entry still yields something.\n *\n * @example\n * parseValue('1.23kHz', [['Hz', 1], ['kHz', 1000]]) // 1230\n * parseValue('4abc') // 4\n */\nexport function parseValue(text: string, units?: string | Units): number {\n const str = text.trim()\n\n if (!units || typeof units == 'string') {\n const m = str.match(/-?\\d+(\\.\\d+)?/)\n const v = Number(m?.[0] ?? '0')\n return isNaN(v) ? 0 : v\n }\n\n const m = str.match(/^(-?\\d+(\\.\\d+)?)\\s*(\\w*)$/)\n if (!m) return 0\n const found = units.find(([unit]) => unit == m[3])\n return (Number(m[1]) || 0) * (found ? found[1] : 1)\n}\n"],"mappings":";;;;;AAGA,SAAgB,MAAM,OAAe,KAAa,KAAa;AAC7D,QAAO,KAAK,IAAI,KAAK,KAAK,IAAI,OAAO,IAAI,CAAC;;;;;;;;AAS5C,SAAgB,eAAe,OAAe,KAAa,KAAa;AACtE,KAAI,OAAO,IAAK,OAAM,IAAI,WAAW,0BAA0B;AAC/D,QAAO,OAAO,QAAQ,QAAQ,MAAM,MAAM,GAAG,EAAE;;;;;;;AAQjD,SAAgB,SAAS,iBAAyB,KAAa,KAAa;AAC1E,KAAI,OAAO,IAAK,OAAM,IAAI,WAAW,0BAA0B;AAC/D,QAAO,MAAM,MAAM,iBAAiB,GAAG,EAAE,IAAI,MAAM;;AAGrD,SAAgB,UAAU,OAAe,MAAc;AACrD,KAAI,QAAQ,EAAG,OAAM,IAAI,WAAW,yBAAyB;CAC7D,MAAM,WAAW,KAAK,MAAM,QAAQ,KAAK;CACzC,MAAM,gBAAgB,YAAY,KAAK,EAAE;CACzC,MAAM,IAAI,QAAQ,WAAW,MAAM,cAAc;CACjD,MAAM,OAAO,SAAS,WAAW,KAAK,MAAM,cAAc;AAC1D,QAAO,KAAK,IAAI,QAAQ,EAAE,GAAG,KAAK,IAAI,QAAQ,KAAK,GAAG,IAAI;;AAG5D,SAAgB,QAAQ,GAAW,gBAAyB;AAC1D,QAAO,OAAO,EAAE,QAAQ,eAAe,CAAC;;AAG1C,SAAgB,YAAY,GAAwC;AAClE,KAAI,OAAO,MAAM,EAAE,CACjB;AAEF,QAAO,OAAO,EAAE,CAAC,MAAM,IAAI,CAAC;;AAG9B,SAAgB,YAAY,GAAwC;AAClE,QAAO,OAAO,EAAE,CAAC,MAAM,IAAI,CAAC;;AAG9B,SAAgB,OAAO,QAAgB;AACrC,QAAQ,KAAK,KAAK,SAAU;;AAG9B,SAAgB,OAAO,QAAgB;AACrC,QAAQ,MAAM,SAAU,KAAK;;AAG/B,SAAgB,SACd,OACA,OACA,OACA,QACA,QACA;AACA,SAAS,QAAQ,UAAU,QAAQ,UAAW,SAAS,UAAU;;AAGnE,SAAgB,SAAS,IAAY;AACnC,QAAO,KAAK,IAAI,IAAI,KAAK,GAAG;;AAG9B,SAAgB,SAAS,MAAc;AACrC,QAAO,MAAM,KAAK,IAAI,KAAK,GAAG,KAAK;;;;ACjDrC,SAAS,YAAY,KAAa,KAAa;AAC7C,KAAI,OAAO,IAAK,OAAM,IAAI,WAAW,0BAA0B;;;;;;;;AASjE,MAAa,cAAqB;CAChC,YAAY,OAAO,KAAK,QAAQ,eAAe,OAAO,KAAK,IAAI;CAC/D,cAAc,UAAU,KAAK,QAAQ,SAAS,UAAU,KAAK,IAAI;CAClE;;;;;;;;;;;;;;;;;AAkBD,SAAgB,UAAU,MAAqB;AAC7C,QAAO;EAGL,YAAY,OAAO,KAAK,QACtB,KAAK,IAAI,eAAe,OAAO,KAAK,IAAI,EAAE,KAAK;EACjD,cAAc,UAAU,KAAK,QAC3B,SACE,SAAS,IACL,WACA,KAAK,IAAI,KAAK,IAAI,MAAM,UAAU,GAAG,EAAE,CAAC,GAAG,KAAK,EACpD,KACA,IACD;EACJ;;;;;;AAOH,SAAgB,oBACd,aACA,KACA,KACA;AACA,KAAI,EAAE,OAAO,eAAe,eAAe,KACzC,OAAM,IAAI,WAAW,0CAA0C;AACjE,QAAO,KAAK,IAAI,GAAI,GAAG,KAAK,KAAK,cAAc,QAAQ,MAAM,KAAK;;;;;;;;;;;;;AAcpE,MAAa,mBAA0B;CACrC,YAAY,OAAO,KAAK,QAAQ;AAC9B,yBAAuB,KAAK,IAAI;AAIhC,SAAO,MACL,KAAK,IAAI,MAAM,OAAO,KAAK,IAAI,GAAG,IAAI,GAAG,KAAK,IAAI,MAAM,IAAI,EAC5D,GACA,EACD;;CAEH,cAAc,UAAU,KAAK,QAAQ;AACnC,yBAAuB,KAAK,IAAI;AAChC,SAAO,MAAM,KAAK,IAAI,MAAM,KAAK,MAAM,UAAU,GAAG,EAAE,CAAC;;CAE1D;AAED,SAAS,uBAAuB,KAAa,KAAa;AACxD,aAAY,KAAK,IAAI;AACrB,KAAI,QAAQ,KAAK,QAAQ,KAAK,KAAK,KAAK,IAAI,KAAK,KAAK,KAAK,IAAI,CAC7D,OAAM,IAAI,WACR,kFACD;;;;;;;;;;;;;;;;;;;;;;;AAyBL,SAAgB,WAAW,OAAsB;AAG/C,KAAI,KAAK,IAAI,MAAM,GAAG,KAAO,QAAO;CAEpC,MAAM,OAAO,KAAK,IAAI,MAAM;CAI5B,MAAM,gBAAgB,KAAa,QAAgB;EACjD,MAAM,KAAK,MAAM,QAAQ,IAAI;AAC7B,SAAO;GAAE;GAAG,GAAG,MAAM;GAAG;;AAG1B,QAAO;EACL,YAAY,OAAO,KAAK,QAAQ;AAC9B,eAAY,KAAK,IAAI;GACrB,MAAM,EAAE,GAAG,MAAM,aAAa,KAAK,IAAI;AAGvC,UAAO,MAAM,KAAK,KAAK,IAAI,MAAM,OAAO,KAAK,IAAI,IAAI,EAAE,GAAG,OAAO,GAAG,EAAE;;EAExE,cAAc,UAAU,KAAK,QAAQ;AACnC,eAAY,KAAK,IAAI;GACrB,MAAM,EAAE,GAAG,MAAM,aAAa,KAAK,IAAI;AACvC,UAAO,IAAI,IAAI,KAAK,IAAI,MAAM,MAAM,UAAU,GAAG,EAAE,CAAC;;EAEvD;;;;;;;;;;;;;;AAeH,SAAgB,mBAAmB,MAAqB;AACtD,QAAO;EACL,YAAY,OAAO,KAAK,QAAQ;AAC9B,eAAY,KAAK,IAAI;GACrB,MAAM,aAAa,OAAO,QAAQ,QAAQ,MAAM,MAAM,GAAG,EAAE;AAC3D,OAAI,SAAS,EAAG,QAAO;GACvB,MAAM,qBAAqB,IAAI,aAAa;AAC5C,WACG,IACC,KAAK,IAAI,KAAK,IAAI,mBAAmB,EAAE,KAAK,GAC1C,KAAK,KAAK,mBAAmB,IACjC;;EAGJ,cAAc,UAAU,KAAK,QAAQ;AACnC,eAAY,KAAK,IAAI;GAErB,IAAI,qBAAqB,IADf,MAAM,UAAU,GAAG,EAAE,GACE;AACjC,OAAI,SAAS,KAAK,uBAAuB,EACvC,sBACE,KAAK,IAAI,KAAK,IAAI,mBAAmB,EAAE,IAAI,KAAK,GAChD,KAAK,KAAK,mBAAmB;AAEjC,UAAO,OAAQ,MAAM,OAAO,KAAM,IAAI;;EAEzC;;;;;;AAOH,SAAgB,qBACd,aACA,KACA,KACA;AACA,aAAY,KAAK,IAAI;AACrB,KAAI,EAAE,MAAM,eAAe,cAAc,KACvC,OAAM,IAAI,WAAW,wCAAwC;CAG/D,MAAM,cAAc,cAAc,QAAQ,MAAM;AAChD,QAAO,IAAI,KAAK,IAAI,IAAI,aAAa,EAAE;;;;;;;;;;;;;;;;AAuCzC,SAAgB,WACd,OACA,WACA,CAAC,MAAM,SACP,EAAE,KAAK,KAAK,MAAM,QAAQ,eAClB;CACR,MAAM,IAAI,YAAY;CACtB,MAAM,OACJ,QAAQ,eACJ,MAAM,YAAY,MAAM,UAAU,OAAO,KAAK,IAAI,GAAG,GAAG,KAAK,IAAI,GACjE,QAAQ;AACd,QAAO,MAAM,OAAO,UAAU,MAAM,KAAK,GAAG,MAAM,KAAK,IAAI;;;;AClR7D,SAAgB,YACd,OACA,IACA,YACA;AACA,KAAI,MAAM,WACR,KAAI,OAAO,SAAS,UAAU;AAC5B,MAAI,MAAM,IAAK,QAAO,GAAG,QAAQ,WAAW;AAC5C,MAAI,MAAM,IAAK,QAAO,GAAG,QAAQ,WAAW;AAC5C,MAAI,MAAM,IAAK,QAAO,GAAG,QAAQ,WAAW;AAC5C,MAAI,MAAM,IAAK,QAAO,GAAG,QAAQ,WAAW;OAE5C,QAAO,QAAQ,MAAM,KAAK,GAAG,GAAG,WAAW;UAGzC,OAAO,SAAS,SAClB,QAAO,GAAG,MAAM;KAEhB,QAAO;;AAKb,SAAgB,QAAQ,KAAa;AACnC,QAAO,OAAO,KAAK,IAAI,CAAC,UAAU;;AAGpC,SAAgB,IAAI,GAAW,GAAW;AACxC,SAAS,IAAI,IAAK,KAAK;;AAGzB,SAAgB,IAAI,IAAI,OAAO,IAAI,OAAO;AACxC,SAAQ,KAAK,MAAM,KAAK;;;;ACtC1B,MAAa,YAAY;CAAC;CAAK;CAAK;CAAK;CAAK;CAAK;CAAK;CAAI;AAI5D,MAAa,WAAW;CACtB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD;AAID,SAAgB,cAAc,UAAkB;CAC9C,MAAM,IAAI,SAAS,MAAM,mCAAmC;AAC5D,KAAI,CAAC,EAAG,OAAM,IAAI,MAAM,oBAAoB;CAC5C,MAAM,GAAG,QAAQ,YAAY,UAAU;AACvC,QAAO;EACL,QAAQ,OAAO,mBAAmB;EACtB;EACZ,QAAQ,OAAO,OAAO;EACvB;;;;;AAMH,SAAgB,WAAW,UAAkB;CAC3C,MAAM,EAAE,QAAQ,YAAY,WAAW,cAAc,SAAS;CAC9D,MAAM,YAAY,SAAS,QAAQ,OAAO,mBAAmB,CAAY;CACzE,MAAM,mBAAmB,WAAW,MAAM,MAAM,KAAK,KAAK,WAAW;AACrE,QAAO,YAAY,MAAM,OAAO,OAAO,GAAG,KAAK;;;;;;;;AASjD,SAAgB,SAAS,YAA2C;CAClE,MAAM,YAAY,IAAI,YAAY,GAAG;CACrC,MAAM,SAAS,KAAK,MAAM,aAAa,GAAG,GAAG;AAC7C,QAAO,GAAG,SAAS,aAAa;;;;;AAMlC,SAAgB,QAAQ,YAA6B;AACnD,QAAO,SAAS,IAAI,YAAY,GAAG;;;;;AAMrC,SAAgB,WAAW,MAAuB;CAChD,MAAM,IAAI,OAAO,QAAQ,WAAW,WAAW,KAAK,GAAG;AACvD,QACE,IAAI,GAAG,GAAG,IAAI,KACd,IAAI,GAAG,GAAG,IAAI,KACd,IAAI,GAAG,GAAG,IAAI,KACd,IAAI,GAAG,GAAG,IAAI,KACd,IAAI,GAAG,GAAG,IAAI,KACd,IAAI,GAAG,GAAG,IAAI,KACd,IAAI,GAAG,GAAG,IAAI;;;;;AAOlB,SAAgB,WAAW,MAAuB;AAChD,QAAO,CAAC,WAAW,KAAK;;;;;;;;AAS1B,SAAgB,gBAAgB,MAAuB,SAAS,GAAG,KAAK,KAAK;CAC3E,MAAM,IAAI,OAAO,QAAQ,WAAW,WAAW,KAAK,GAAG;AACvD,QAAQ,KAAK,KAAM,OAAO,IAAI,IAAI,SAAS,OAAO;;;;;;;;;;;;;AAcpD,MAAa,iBAAiB;CAC5B,OAAO;EAAC;EAAG;EAAG;EAAG;EAAG;EAAG;EAAG;EAAG;CAC7B,cAAc;EAAC;EAAG;EAAG;EAAG;EAAG;EAAG;EAAG;EAAG;CACpC,eAAe;EAAC;EAAG;EAAG;EAAG;EAAG;EAAG;EAAG;EAAG;CACrC,cAAc;EAAC;EAAG;EAAG;EAAG;EAAG;EAAG;EAAG;EAAG;CAEpC,QAAQ;EAAC;EAAG;EAAG;EAAG;EAAG;EAAG;EAAG;EAAG;CAC9B,QAAQ;EAAC;EAAG;EAAG;EAAG;EAAG;EAAG;EAAG;EAAG;CAC9B,UAAU;EAAC;EAAG;EAAG;EAAG;EAAG;EAAG;EAAG;EAAG;CAChC,QAAQ;EAAC;EAAG;EAAG;EAAG;EAAG;EAAG;EAAG;EAAG;CAC9B,YAAY;EAAC;EAAG;EAAG;EAAG;EAAG;EAAG;EAAG;EAAG;CAClC,SAAS;EAAC;EAAG;EAAG;EAAG;EAAG;EAAG;EAAG;EAAG;CAC/B,SAAS;EAAC;EAAG;EAAG;EAAG;EAAG;EAAG;EAAG;EAAG;CAE/B,iBAAiB;EAAC;EAAG;EAAG;EAAG;EAAG;EAAE;CAChC,iBAAiB;EAAC;EAAG;EAAG;EAAG;EAAG;EAAG;CACjC,OAAO;EAAC;EAAG;EAAG;EAAG;EAAG;EAAG;EAAG;CAE1B,WAAW;EAAC;EAAG;EAAG;EAAG;EAAG;EAAG;EAAG;CAC9B,WAAW;EAAC;EAAG;EAAG;EAAG;EAAG;EAAG;EAAG;EAAG;EAAG;EAAG;EAAG;EAAI;EAAG;CAClD;;;;;;;;;;;;AAeD,SAAgB,QACd,MACA,MACA,MACS;CACT,MAAM,IAAI,OAAO,QAAQ,WAAW,WAAW,KAAK,GAAG;CACvD,MAAM,IAAI,OAAO,QAAQ,WAAW,WAAW,KAAK,GAAG;AACvD,QAAQ,eAAe,MAA4B,SAAS,IAAI,IAAI,GAAG,GAAG,CAAC;;;;;;;;;;;;;;;;AAiB7E,SAAgB,WACd,MACA,MACA,UAAU,GACA;CACV,MAAM,IAAI,OAAO,QAAQ,WAAW,WAAW,KAAK,GAAG;CACvD,MAAM,YAAY,eAAe;AACjC,QAAO,MAAM,KAAK,EAAE,QAAQ,SAAS,GAAG,GAAG,WACzC,UAAU,KAAK,aAAa,IAAI,SAAS,KAAK,SAAS,CACxD,CAAC,MAAM;;;;;;;ACtKV,SAAgB,kBAAkB,WAAgC;AAChE,QAAO,MAAM,KACX,EAAE,QAAQ,UAAU,OAAO,UAAU,QAAQ,GAAG,GAC/C,GAAG,MAAM,IAAI,UAAU,MACzB;;AAsCH,MAAM,kBAAkB;AACxB,MAAM,gCAAgC;AACtC,MAAM,iCAAiC;;;;;;;;AASvC,MAAM,kBAA2C;CAC/C,GAAG;CACH,MAAM;CACN,GAAG;CACH,MAAM;CACN,GAAG;CACH,GAAG;CACH,MAAM;CACN,GAAG;CACH,MAAM;CACN,GAAG;CACH,MAAM;CACN,GAAG;CACJ;;AAGD,SAAgB,cAAc,QAA6B;AACzD,QACE,OAAO,iBACN,OAAO,sBAAsB;;;AAKlC,SAAgB,WAAW,QAA6B;CACtD,MAAM,YAAY,kBAAkB,OAAO,UAAU,CAAC,OAAO,WAAW;AACxE,SACG,OAAO,iBAAiB,OAAO,UAAU,oBAC1C,UAAU;;;;;;;;;AAWd,SAAgB,aAAa,MAAc,QAA6B;CACtE,MAAM,OAAO,OAAO,iBAAiB,OAAO,UAAU;CACtD,MAAM,SAAS,QAAQ,KAAK;CAC5B,MAAM,QAAQ,QAAQ,OAAO,UAAU,MAAM;CAE7C,MAAM,SAAS,KAAK,OAAO,OAAO,OAAO,UAAU,SAAS,GAAG;CAG/D,MAAM,eACJ,SAAS,QAAQ,MAAM,GAAG,SAAS,QAAQ,OAAO,GAAG,IAAI;CAE3D,MAAM,cACJ,gBAAgB,UAChB,gBAAgB,UACf,SAAS,gBAAgB;AAE5B,QAAO,WAAW,KAAK,GACnB,cAAc,OAAO,cAAc,OAAO,GAAG,IAC7C,cAAc;;;;;;;;;;;;;AAcpB,SAAgB,OACd,GACA,GACA,QACA,QACe;AACf,KAAI,IAAI,KAAK,KAAK,OAAQ,QAAO;CAEjC,MAAM,QAAQ,kBAAkB,OAAO,UAAU;AAIjD,KAAI,IAFF,UAAU,OAAO,uBAAuB,gCAGxC,MAAK,MAAM,QAAQ,OAAO;AACxB,MAAI,WAAW,KAAK,CAAE;EACtB,MAAM,OAAO,aAAa,MAAM,OAAO;AACvC,MAAI,QAAQ,KAAK,IAAI,OAAO,cAAc,OAAO,CAAE,QAAO;;CAI9D,MAAM,OAAO,OAAO,iBAAiB,OAAO,UAAU;AACtD,MAAK,MAAM,QAAQ,OAAO;AACxB,MAAI,WAAW,KAAK,CAAE;EACtB,MAAM,OAAO,aAAa,MAAM,OAAO;AACvC,MAAI,QAAQ,KAAK,IAAI,OAAO,KAAM,QAAO;;AAG3C,QAAO;;;;;;;;AClJT,SAAgB,WAAW,OAAc,OAAiC;CACxE,IAAI,IAAI;AACR,QAAO,IAAI,MAAM,QAAQ,IACvB,KAAI,KAAK,IAAI,MAAM,GAAG,GAAG,GAAG,KAAK,IAAI,MAAM,CAAE;AAE/C,QAAO,MAAM,KAAK,IAAI,GAAG,IAAI,EAAE;;;;;;;;;;;;AAajC,SAAgB,YACd,OACA,OACA,OACQ;CACR,MAAM,SAAS,MACb,SAAS,KAAA,IAAY,EAAE,QAAQ,MAAM,GAAG,OAAO,EAAE;AACnD,KAAI,CAAC,SAAS,OAAO,SAAS,SAC5B,QAAO,MAAM,MAAM,IAAI,SAAS;CAElC,MAAM,CAAC,MAAM,SAAS,WAAW,OAAO,MAAM;AAC9C,QAAO,MAAM,QAAQ,MAAM,GAAG;;;;;;;;;;;;;;AAehC,SAAgB,WAAW,MAAc,OAAgC;CACvE,MAAM,MAAM,KAAK,MAAM;AAEvB,KAAI,CAAC,SAAS,OAAO,SAAS,UAAU;EACtC,MAAM,IAAI,IAAI,MAAM,gBAAgB;EACpC,MAAM,IAAI,OAAO,IAAI,MAAM,IAAI;AAC/B,SAAO,MAAM,EAAE,GAAG,IAAI;;CAGxB,MAAM,IAAI,IAAI,MAAM,4BAA4B;AAChD,KAAI,CAAC,EAAG,QAAO;CACf,MAAM,QAAQ,MAAM,MAAM,CAAC,UAAU,QAAQ,EAAE,GAAG;AAClD,SAAQ,OAAO,EAAE,GAAG,IAAI,MAAM,QAAQ,MAAM,KAAK"}