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