@tremolo-ui/functions 0.5.0 → 0.7.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 +195 -204
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +101 -149
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +101 -149
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +194 -190
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/src/index.ts +6 -14
- package/src/math.ts +50 -13
- package/src/midi.ts +45 -16
- package/src/scales.ts +43 -50
- package/src/unit.ts +182 -51
- package/src/piano.ts +0 -163
- package/src/types.ts +0 -4
- package/src/util.ts +0 -42
package/dist/index.cjs
CHANGED
|
@@ -25,23 +25,54 @@ function rawValue(normalizedValue, min, max) {
|
|
|
25
25
|
if (min >= max) throw new RangeError("requirements: min < max");
|
|
26
26
|
return min + clamp(normalizedValue, 0, 1) * (max - min);
|
|
27
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Put a value on the grid the caller asked for, rounding a half step upwards.
|
|
30
|
+
*
|
|
31
|
+
* The rounding is done on the quotient rather than by comparing the distance
|
|
32
|
+
* to the two neighbours, because both of those carry error of their own. The
|
|
33
|
+
* quotient is cleared of its artefact first: `0.15 / 0.1` is 1.4999999999999998,
|
|
34
|
+
* and a value sitting exactly on a half step would otherwise fall to whichever
|
|
35
|
+
* side the last bit happened to land on — 0.25 rounded up while 0.15 and 0.35
|
|
36
|
+
* rounded down.
|
|
37
|
+
*/
|
|
28
38
|
function stepValue(value, step) {
|
|
29
39
|
if (step <= 0) throw new RangeError("requirements: step > 0");
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
const v = toFixed(quotient * step, decimalDigits);
|
|
33
|
-
const next = toFixed((quotient + 1) * step, decimalDigits);
|
|
34
|
-
return Math.abs(value - v) < Math.abs(value - next) ? v : next;
|
|
40
|
+
const stepped = toPrecision(Math.round(toPrecision(value / step)) * step);
|
|
41
|
+
return stepped === 0 ? 0 : stepped;
|
|
35
42
|
}
|
|
36
43
|
function toFixed(x, fractionDigits) {
|
|
37
44
|
return Number(x.toFixed(fractionDigits));
|
|
38
45
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
46
|
+
/**
|
|
47
|
+
* The significant decimal digits a double actually carries. A double holds a
|
|
48
|
+
* little under 16, so anything past this is the binary representation showing
|
|
49
|
+
* through rather than information.
|
|
50
|
+
*/
|
|
51
|
+
const SIGNIFICANT_DIGITS = 15;
|
|
52
|
+
/**
|
|
53
|
+
* Drop the binary artefact from a computed value.
|
|
54
|
+
*
|
|
55
|
+
* Arithmetic on doubles leaves debris in the last couple of digits, and it
|
|
56
|
+
* accumulates: adding 0.1 to 5 twelve times gives 5.699999999999998 rather
|
|
57
|
+
* than 5.7, and the display of a control shows exactly that. Rounding to the
|
|
58
|
+
* digits a double can carry removes it, and adds nothing back — the value was
|
|
59
|
+
* already the result of a calculation whose own error is that size or larger.
|
|
60
|
+
*
|
|
61
|
+
* This is not the same as rounding to a `step`. {@link stepValue} puts a value
|
|
62
|
+
* on a grid the caller asked for and is a decision about the value; this only
|
|
63
|
+
* removes what was never in the value to begin with.
|
|
64
|
+
*
|
|
65
|
+
* @param significantDigits how many digits to keep. The default is the only
|
|
66
|
+
* one that is purely artefact removal; a smaller number starts discarding real
|
|
67
|
+
* precision.
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* toPrecision(5.1 + 0.1) // 5.2, rather than 5.199999999999999
|
|
71
|
+
*/
|
|
72
|
+
function toPrecision(x, significantDigits = SIGNIFICANT_DIGITS) {
|
|
73
|
+
if (x === 0 || !Number.isFinite(x)) return x;
|
|
74
|
+
const rounded = Number(x.toPrecision(significantDigits));
|
|
75
|
+
return Number.isFinite(rounded) ? rounded : x;
|
|
45
76
|
}
|
|
46
77
|
function radian(degree) {
|
|
47
78
|
return Math.PI * degree / 180;
|
|
@@ -63,6 +94,9 @@ function gainToDb(gain) {
|
|
|
63
94
|
function assertRange(min, max) {
|
|
64
95
|
if (min >= max) throw new RangeError("requirements: min < max");
|
|
65
96
|
}
|
|
97
|
+
function assertPositiveFinite(value, name) {
|
|
98
|
+
if (!Number.isFinite(value) || value <= 0) throw new RangeError(`${name}: requirements: finite and greater than 0`);
|
|
99
|
+
}
|
|
66
100
|
/**
|
|
67
101
|
* Equal travel gives an equal change in value.
|
|
68
102
|
*
|
|
@@ -90,6 +124,7 @@ const linearScale = {
|
|
|
90
124
|
* @param skew the JUCE skew factor
|
|
91
125
|
*/
|
|
92
126
|
function skewScale(skew) {
|
|
127
|
+
assertPositiveFinite(skew, "skewScale");
|
|
93
128
|
return {
|
|
94
129
|
normalize: (value, min, max) => Math.pow(normalizeValue(value, min, max), skew),
|
|
95
130
|
denormalize: (position, min, max) => rawValue(skew === 1 ? position : Math.exp(Math.log(clamp(position, 0, 1)) / skew), min, max)
|
|
@@ -100,7 +135,8 @@ function skewScale(skew) {
|
|
|
100
135
|
* of the travel — JUCE's `NormalisableRange::setSkewForCentre`.
|
|
101
136
|
*/
|
|
102
137
|
function skewWithCenterValue(centerValue, min, max) {
|
|
103
|
-
|
|
138
|
+
assertRange(min, max);
|
|
139
|
+
if (!(min < centerValue && centerValue < max)) throw new RangeError("requirements: min < centerValue < max");
|
|
104
140
|
return Math.log(.5) / Math.log((centerValue - min) / (max - min));
|
|
105
141
|
}
|
|
106
142
|
/**
|
|
@@ -117,11 +153,15 @@ function skewWithCenterValue(centerValue, min, max) {
|
|
|
117
153
|
const exponentialScale = {
|
|
118
154
|
normalize: (value, min, max) => {
|
|
119
155
|
assertExponentialRange(min, max);
|
|
120
|
-
|
|
156
|
+
const start = Math.log(Math.abs(min));
|
|
157
|
+
const end = Math.log(Math.abs(max));
|
|
158
|
+
return clamp((Math.log(Math.abs(clamp(value, min, max))) - start) / (end - start), 0, 1);
|
|
121
159
|
},
|
|
122
160
|
denormalize: (position, min, max) => {
|
|
123
161
|
assertExponentialRange(min, max);
|
|
124
|
-
|
|
162
|
+
const start = Math.log(Math.abs(min));
|
|
163
|
+
const magnitude = Math.exp(start + (Math.log(Math.abs(max)) - start) * clamp(position, 0, 1));
|
|
164
|
+
return Math.sign(min) * magnitude;
|
|
125
165
|
}
|
|
126
166
|
};
|
|
127
167
|
function assertExponentialRange(min, max) {
|
|
@@ -150,25 +190,23 @@ function assertExponentialRange(min, max) {
|
|
|
150
190
|
* @param curve how hard the curve bends, and in which direction
|
|
151
191
|
*/
|
|
152
192
|
function curveScale(curve) {
|
|
193
|
+
if (!Number.isFinite(curve) || Math.abs(curve) > 32) throw new RangeError("curveScale: requirements: finite curve from -32 to 32");
|
|
153
194
|
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
195
|
return {
|
|
163
196
|
normalize: (value, min, max) => {
|
|
164
197
|
assertRange(min, max);
|
|
165
|
-
const
|
|
166
|
-
|
|
198
|
+
const proportion = clamp((value - min) / (max - min), 0, 1);
|
|
199
|
+
if (proportion === 0 || proportion === 1) return proportion;
|
|
200
|
+
if (curve > 0) return 1 + Math.log(proportion + (1 - proportion) * Math.exp(-curve)) / curve;
|
|
201
|
+
return Math.log1p(proportion * Math.expm1(curve)) / curve;
|
|
167
202
|
},
|
|
168
203
|
denormalize: (position, min, max) => {
|
|
169
204
|
assertRange(min, max);
|
|
170
|
-
const
|
|
171
|
-
|
|
205
|
+
const p = clamp(position, 0, 1);
|
|
206
|
+
if (p === 0) return min;
|
|
207
|
+
if (p === 1) return max;
|
|
208
|
+
const proportion = curve > 0 ? Math.exp(curve * (p - 1)) * (1 - Math.exp(-curve * p)) / (1 - Math.exp(-curve)) : Math.expm1(curve * p) / Math.expm1(curve);
|
|
209
|
+
return min + (max - min) * proportion;
|
|
172
210
|
}
|
|
173
211
|
};
|
|
174
212
|
}
|
|
@@ -185,6 +223,7 @@ function curveScale(curve) {
|
|
|
185
223
|
* @param skew the JUCE skew factor
|
|
186
224
|
*/
|
|
187
225
|
function symmetricSkewScale(skew) {
|
|
226
|
+
assertPositiveFinite(skew, "symmetricSkewScale");
|
|
188
227
|
return {
|
|
189
228
|
normalize: (value, min, max) => {
|
|
190
229
|
assertRange(min, max);
|
|
@@ -211,48 +250,18 @@ function curveWithCenterValue(centerValue, min, max) {
|
|
|
211
250
|
const proportion = (centerValue - min) / (max - min);
|
|
212
251
|
return 2 * Math.log(1 / proportion - 1);
|
|
213
252
|
}
|
|
253
|
+
//#endregion
|
|
254
|
+
//#region src/midi.ts
|
|
214
255
|
/**
|
|
215
|
-
*
|
|
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.
|
|
256
|
+
* Remainder that follows the sign of the divisor, rather than of the dividend.
|
|
220
257
|
*
|
|
221
|
-
*
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
* @example
|
|
225
|
-
* // ArrowDown on a slider whose keyboard option is ['raw', 1]
|
|
226
|
-
* applyDelta(value, -1, keyboard, { min, max, step, scale })
|
|
258
|
+
* `%` in JavaScript keeps the sign of the left operand, so `-13 % 12` is `-1`
|
|
259
|
+
* and cannot index a 12-element table. Every note here is placed by its
|
|
260
|
+
* position within the octave, and note numbers below C-1 are negative.
|
|
227
261
|
*/
|
|
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
|
|
234
|
-
//#region src/util.ts
|
|
235
|
-
function styleHelper(value, op, influencer) {
|
|
236
|
-
if (op && influencer) if (typeof value == "number") {
|
|
237
|
-
if (op == "+") return `${value + influencer}px`;
|
|
238
|
-
if (op == "-") return `${value - influencer}px`;
|
|
239
|
-
if (op == "*") return `${value * influencer}px`;
|
|
240
|
-
if (op == "/") return `${value / influencer}px`;
|
|
241
|
-
} else return `calc(${value}px ${op} ${influencer})`;
|
|
242
|
-
else if (typeof value == "number") return `${value}px`;
|
|
243
|
-
else return value;
|
|
244
|
-
}
|
|
245
|
-
function isEmpty(obj) {
|
|
246
|
-
return Object.keys(obj).length == 0;
|
|
247
|
-
}
|
|
248
262
|
function mod(n, m) {
|
|
249
263
|
return (n % m + m) % m;
|
|
250
264
|
}
|
|
251
|
-
function xor(a = false, b = false) {
|
|
252
|
-
return (a || b) && a != b;
|
|
253
|
-
}
|
|
254
|
-
//#endregion
|
|
255
|
-
//#region src/midi.ts
|
|
256
265
|
const whiteKeys = [
|
|
257
266
|
"A",
|
|
258
267
|
"B",
|
|
@@ -276,14 +285,19 @@ const noteKeys = [
|
|
|
276
285
|
"A#",
|
|
277
286
|
"B"
|
|
278
287
|
];
|
|
288
|
+
function assertSafeInteger(value, name) {
|
|
289
|
+
if (!Number.isSafeInteger(value)) throw new RangeError(`${name}: requirements: a safe integer`);
|
|
290
|
+
}
|
|
279
291
|
function parseNoteName(noteName) {
|
|
280
292
|
const m = noteName.match(/^([a-g])(#{0,2}|b{0,2})(-?\d+)$/i);
|
|
281
293
|
if (!m) throw new Error("Invalid note name");
|
|
282
294
|
const [, letter, accidental, octave] = m;
|
|
295
|
+
const parsedOctave = Number(octave);
|
|
296
|
+
assertSafeInteger(parsedOctave, "octave");
|
|
283
297
|
return {
|
|
284
298
|
letter: letter.toLocaleUpperCase(),
|
|
285
299
|
accidental,
|
|
286
|
-
octave:
|
|
300
|
+
octave: parsedOctave
|
|
287
301
|
};
|
|
288
302
|
}
|
|
289
303
|
/**
|
|
@@ -292,8 +306,10 @@ function parseNoteName(noteName) {
|
|
|
292
306
|
function noteNumber(noteName) {
|
|
293
307
|
const { letter, accidental, octave } = parseNoteName(noteName);
|
|
294
308
|
const noteIndex = noteKeys.indexOf(letter.toLocaleUpperCase());
|
|
295
|
-
const accidentalValue = (accidental[0]
|
|
296
|
-
|
|
309
|
+
const accidentalValue = (accidental[0] === "b" ? -1 : 1) * accidental.length;
|
|
310
|
+
const result = noteIndex + 12 * (octave + 1) + accidentalValue;
|
|
311
|
+
assertSafeInteger(result, "note number");
|
|
312
|
+
return result;
|
|
297
313
|
}
|
|
298
314
|
/**
|
|
299
315
|
* Convert noteNumber to noteName
|
|
@@ -302,6 +318,7 @@ function noteNumber(noteName) {
|
|
|
302
318
|
* @param noteNumber noteNumber
|
|
303
319
|
*/
|
|
304
320
|
function noteName(noteNumber) {
|
|
321
|
+
assertSafeInteger(noteNumber, "note number");
|
|
305
322
|
const noteIndex = mod(noteNumber, 12);
|
|
306
323
|
const octave = Math.floor(noteNumber / 12) - 1;
|
|
307
324
|
return `${noteKeys[noteIndex]}${octave}`;
|
|
@@ -310,14 +327,16 @@ function noteName(noteNumber) {
|
|
|
310
327
|
* Convert noteNumber to noteKey
|
|
311
328
|
*/
|
|
312
329
|
function noteKey(noteNumber) {
|
|
330
|
+
assertSafeInteger(noteNumber, "note number");
|
|
313
331
|
return noteKeys[mod(noteNumber, 12)];
|
|
314
332
|
}
|
|
315
333
|
/**
|
|
316
334
|
* @param note noteNumber: 0 ~ 127 or noteName e.g. 'C3'
|
|
317
335
|
*/
|
|
318
336
|
function isWhiteKey(note) {
|
|
319
|
-
const n = typeof note
|
|
320
|
-
|
|
337
|
+
const n = typeof note === "string" ? noteNumber(note) : note;
|
|
338
|
+
assertSafeInteger(n, "note number");
|
|
339
|
+
return mod(n, 12) === 0 || mod(n, 12) === 2 || mod(n, 12) === 4 || mod(n, 12) === 5 || mod(n, 12) === 7 || mod(n, 12) === 9 || mod(n, 12) === 11;
|
|
321
340
|
}
|
|
322
341
|
/**
|
|
323
342
|
* @param note noteNumber: 0 ~ 127 or noteName e.g. 'C3'
|
|
@@ -332,7 +351,8 @@ function isBlackKey(note) {
|
|
|
332
351
|
* @returns frequency [Hz]
|
|
333
352
|
*/
|
|
334
353
|
function noteToFrequency(note, detune = 0, a4 = 440) {
|
|
335
|
-
const n = typeof note
|
|
354
|
+
const n = typeof note === "string" ? noteNumber(note) : note;
|
|
355
|
+
assertSafeInteger(n, "note number");
|
|
336
356
|
return a4 / 32 * 2 ** ((n - 9 + detune / 100) / 12);
|
|
337
357
|
}
|
|
338
358
|
/**
|
|
@@ -503,8 +523,10 @@ const scaleIntervals = {
|
|
|
503
523
|
* ```
|
|
504
524
|
*/
|
|
505
525
|
function inScale(note, root, name) {
|
|
506
|
-
const n = typeof note
|
|
507
|
-
const r = typeof root
|
|
526
|
+
const n = typeof note === "string" ? noteNumber(note) : note;
|
|
527
|
+
const r = typeof root === "string" ? noteNumber(root) : root;
|
|
528
|
+
assertSafeInteger(n, "note number");
|
|
529
|
+
assertSafeInteger(r, "root note number");
|
|
508
530
|
return scaleIntervals[name].includes(mod(n - r, 12));
|
|
509
531
|
}
|
|
510
532
|
/**
|
|
@@ -522,189 +544,158 @@ function inScale(note, root, name) {
|
|
|
522
544
|
* ```
|
|
523
545
|
*/
|
|
524
546
|
function scaleNotes(root, name, octaves = 1) {
|
|
525
|
-
const r = typeof root
|
|
547
|
+
const r = typeof root === "string" ? noteNumber(root) : root;
|
|
548
|
+
assertSafeInteger(r, "root note number");
|
|
549
|
+
if (!Number.isSafeInteger(octaves) || octaves < 0) throw new RangeError("octaves: requirements: a non-negative safe integer");
|
|
526
550
|
const intervals = scaleIntervals[name];
|
|
527
551
|
return Array.from({ length: octaves }, (_, octave) => intervals.map((interval) => r + octave * 12 + interval)).flat();
|
|
528
552
|
}
|
|
529
553
|
//#endregion
|
|
530
|
-
//#region src/
|
|
531
|
-
/**
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
554
|
+
//#region src/unit.ts
|
|
555
|
+
/** Ordered small to large. The empty symbol is the base unit. */
|
|
556
|
+
const PREFIXES = [
|
|
557
|
+
["p", 1e-12],
|
|
558
|
+
["n", 1e-9],
|
|
559
|
+
["µ", 1e-6],
|
|
560
|
+
["m", .001],
|
|
561
|
+
["", 1],
|
|
562
|
+
["k", 1e3],
|
|
563
|
+
["M", 1e6],
|
|
564
|
+
["G", 1e9]
|
|
565
|
+
];
|
|
566
|
+
const PREFIX_SCALE = new Map(PREFIXES);
|
|
540
567
|
/**
|
|
541
|
-
*
|
|
542
|
-
*
|
|
543
|
-
*
|
|
544
|
-
*
|
|
545
|
-
* back by half its width to centre it there.
|
|
568
|
+
* Micro is written three ways. `µ` (U+00B5 MICRO SIGN) is what `format`
|
|
569
|
+
* writes and what d3-format uses, `μ` (U+03BC GREEK SMALL LETTER MU) looks
|
|
570
|
+
* identical and is what a Greek keyboard produces, and `u` is what everyone
|
|
571
|
+
* actually types. All three read back the same.
|
|
546
572
|
*/
|
|
547
|
-
const
|
|
548
|
-
|
|
549
|
-
"
|
|
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
|
|
573
|
+
const MICRO_ALIASES = {
|
|
574
|
+
μ: "µ",
|
|
575
|
+
u: "µ"
|
|
560
576
|
};
|
|
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
577
|
/**
|
|
571
|
-
*
|
|
572
|
-
* pixels.
|
|
578
|
+
* Divide by a prefix scale without showing the result of doing so in binary.
|
|
573
579
|
*
|
|
574
|
-
*
|
|
575
|
-
*
|
|
580
|
+
* `0.0005 / 1e-6` is 500.00000000000006, and with no `digits` to round it that
|
|
581
|
+
* lands in the input as written.
|
|
576
582
|
*/
|
|
577
|
-
function
|
|
578
|
-
|
|
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;
|
|
583
|
+
function scaleBy(value, scale) {
|
|
584
|
+
return toPrecision(value / scale);
|
|
585
585
|
}
|
|
586
|
+
/** A number, then whatever followed it. */
|
|
587
|
+
const NUMBER_THEN_REST = /^([+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?)\s*(.*)$/;
|
|
586
588
|
/**
|
|
587
|
-
*
|
|
589
|
+
* Build the `format` and `parse` of a unit, as one pair.
|
|
588
590
|
*
|
|
589
|
-
*
|
|
590
|
-
*
|
|
591
|
-
*
|
|
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.
|
|
591
|
+
* They are returned together because they have to agree: a `format` that
|
|
592
|
+
* writes `1.23kHz` is only useful next to a `parse` that reads it back as
|
|
593
|
+
* 1230. Spread the result into the input.
|
|
629
594
|
*
|
|
630
595
|
* @example
|
|
631
|
-
*
|
|
632
|
-
*
|
|
633
|
-
|
|
634
|
-
|
|
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.
|
|
596
|
+
* unitFormat('Hz') // 1234 -> '1.23kHz'
|
|
597
|
+
* unitFormat('s', { base: 'm' }) // value in ms. 1500 -> '1.5s'
|
|
598
|
+
* unitFormat('s', { base: 'm', digits: 2 }) // 1500 -> '1.50s'
|
|
599
|
+
* unitFormat('dB', { prefixes: false, digits: 1 }) // -6.25 -> '-6.3dB'
|
|
647
600
|
*
|
|
648
601
|
* @example
|
|
649
|
-
*
|
|
650
|
-
* parseValue('4abc') // 4
|
|
602
|
+
* <NumberInput.Root {...unitFormat('Hz', { digits: 2 })} value={v} onChange={setV}>
|
|
651
603
|
*/
|
|
652
|
-
function
|
|
653
|
-
const
|
|
654
|
-
if (
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
604
|
+
function unitFormat(unit, options = {}) {
|
|
605
|
+
const { base = "", prefixes = true, digits, separator = "" } = options;
|
|
606
|
+
if (unit === "" && base !== "") throw new RangeError("unitFormat: base requires a non-empty unit");
|
|
607
|
+
const baseScale = PREFIX_SCALE.get(base) ?? 1;
|
|
608
|
+
/**
|
|
609
|
+
* `toFixed` renders anything that rounds to zero from below as `-0`, which
|
|
610
|
+
* is never what a control should show.
|
|
611
|
+
*/
|
|
612
|
+
const fixed = (value) => {
|
|
613
|
+
const text = digits !== void 0 ? value.toFixed(digits) : String(value);
|
|
614
|
+
return Number(text) === 0 ? text.replace("-", "") : text;
|
|
615
|
+
};
|
|
616
|
+
if (!prefixes) {
|
|
617
|
+
const symbol = base + unit;
|
|
618
|
+
return {
|
|
619
|
+
format: (value) => Number.isFinite(value) ? fixed(value) + separator + symbol : String(value),
|
|
620
|
+
parse: (text) => {
|
|
621
|
+
const match = text.trim().match(NUMBER_THEN_REST);
|
|
622
|
+
if (!match) return NaN;
|
|
623
|
+
const value = Number(match[1]);
|
|
624
|
+
return Number.isFinite(value) ? value : NaN;
|
|
625
|
+
}
|
|
626
|
+
};
|
|
658
627
|
}
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
628
|
+
return {
|
|
629
|
+
format: (value) => {
|
|
630
|
+
if (!Number.isFinite(value)) return String(value);
|
|
631
|
+
const si = value * baseScale;
|
|
632
|
+
let index = PREFIXES.findIndex(([, scale]) => scale === 1);
|
|
633
|
+
if (si !== 0) {
|
|
634
|
+
const magnitude = Math.abs(si);
|
|
635
|
+
index = 0;
|
|
636
|
+
for (let i = PREFIXES.length - 1; i >= 0; i--) if (magnitude >= PREFIXES[i][1]) {
|
|
637
|
+
index = i;
|
|
638
|
+
break;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
let text = fixed(scaleBy(si, PREFIXES[index][1]));
|
|
642
|
+
if (Math.abs(Number(text)) >= 1e3 && index < PREFIXES.length - 1) {
|
|
643
|
+
index += 1;
|
|
644
|
+
text = fixed(scaleBy(si, PREFIXES[index][1]));
|
|
645
|
+
}
|
|
646
|
+
return text + separator + PREFIXES[index][0] + unit;
|
|
647
|
+
},
|
|
648
|
+
parse: (text) => {
|
|
649
|
+
const match = text.trim().match(NUMBER_THEN_REST);
|
|
650
|
+
if (!match) return NaN;
|
|
651
|
+
const number = Number(match[1]);
|
|
652
|
+
if (!Number.isFinite(number)) return NaN;
|
|
653
|
+
let suffix = match[2].trim();
|
|
654
|
+
const separatorText = separator.trim();
|
|
655
|
+
if (separatorText !== "" && suffix.startsWith(separatorText)) suffix = suffix.slice(separatorText.length).trim();
|
|
656
|
+
if (suffix === "") return number;
|
|
657
|
+
let prefix = null;
|
|
658
|
+
if (unit !== "" && suffix.endsWith(unit)) prefix = suffix.slice(0, suffix.length - unit.length);
|
|
659
|
+
else if (suffix.length <= 1) prefix = suffix;
|
|
660
|
+
if (prefix === null) return number;
|
|
661
|
+
const normalized = MICRO_ALIASES[prefix] ?? prefix;
|
|
662
|
+
const scale = PREFIX_SCALE.get(normalized);
|
|
663
|
+
if (scale === void 0) return number;
|
|
664
|
+
return number * scale / baseScale;
|
|
665
|
+
}
|
|
666
|
+
};
|
|
663
667
|
}
|
|
664
668
|
//#endregion
|
|
665
|
-
exports.applyDelta = applyDelta;
|
|
666
|
-
exports.blackKeyWidth = blackKeyWidth;
|
|
667
669
|
exports.clamp = clamp;
|
|
668
670
|
exports.curveScale = curveScale;
|
|
669
671
|
exports.curveWithCenterValue = curveWithCenterValue;
|
|
670
672
|
exports.dbToGain = dbToGain;
|
|
671
|
-
exports.decimalPart = decimalPart;
|
|
672
673
|
exports.degree = degree;
|
|
673
674
|
exports.exponentialScale = exponentialScale;
|
|
674
|
-
exports.formatValue = formatValue;
|
|
675
675
|
exports.gainToDb = gainToDb;
|
|
676
|
-
exports.getNoteRangeArray = getNoteRangeArray;
|
|
677
676
|
exports.inScale = inScale;
|
|
678
|
-
exports.integerPart = integerPart;
|
|
679
677
|
exports.isBlackKey = isBlackKey;
|
|
680
|
-
exports.isEmpty = isEmpty;
|
|
681
678
|
exports.isWhiteKey = isWhiteKey;
|
|
682
679
|
exports.linearScale = linearScale;
|
|
683
680
|
exports.mapValue = mapValue;
|
|
684
|
-
exports.mod = mod;
|
|
685
681
|
exports.normalizeValue = normalizeValue;
|
|
686
|
-
exports.noteAt = noteAt;
|
|
687
682
|
exports.noteKey = noteKey;
|
|
688
683
|
exports.noteKeys = noteKeys;
|
|
689
684
|
exports.noteName = noteName;
|
|
690
685
|
exports.noteNumber = noteNumber;
|
|
691
|
-
exports.notePosition = notePosition;
|
|
692
686
|
exports.noteToFrequency = noteToFrequency;
|
|
693
687
|
exports.parseNoteName = parseNoteName;
|
|
694
|
-
exports.parseValue = parseValue;
|
|
695
|
-
exports.pianoWidth = pianoWidth;
|
|
696
688
|
exports.radian = radian;
|
|
697
689
|
exports.rawValue = rawValue;
|
|
698
690
|
exports.scaleIntervals = scaleIntervals;
|
|
699
691
|
exports.scaleNotes = scaleNotes;
|
|
700
|
-
exports.selectUnit = selectUnit;
|
|
701
692
|
exports.skewScale = skewScale;
|
|
702
693
|
exports.skewWithCenterValue = skewWithCenterValue;
|
|
703
694
|
exports.stepValue = stepValue;
|
|
704
|
-
exports.styleHelper = styleHelper;
|
|
705
695
|
exports.symmetricSkewScale = symmetricSkewScale;
|
|
706
696
|
exports.toFixed = toFixed;
|
|
697
|
+
exports.toPrecision = toPrecision;
|
|
698
|
+
exports.unitFormat = unitFormat;
|
|
707
699
|
exports.whiteKeys = whiteKeys;
|
|
708
|
-
exports.xor = xor;
|
|
709
700
|
|
|
710
701
|
//# sourceMappingURL=index.cjs.map
|