@xsynaptic/sonic-ui 0.28.1 → 0.28.2
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/CHANGELOG.md +7 -0
- package/dist/default/elements/define-once.js +4 -1
- package/dist/default/lib/clock.js +3 -4
- package/dist/default/lib/number-list.js +2 -2
- package/dist/default/lib/taper.js +12 -2
- package/dist/default/lib/value-mapping.js +28 -11
- package/dist/dev/elements/define-once.js +7 -1
- package/dist/dev/elements/define-once.js.map +1 -1
- package/dist/dev/lib/clock.js +3 -4
- package/dist/dev/lib/clock.js.map +1 -1
- package/dist/dev/lib/number-list.js +2 -2
- package/dist/dev/lib/number-list.js.map +1 -1
- package/dist/dev/lib/taper.js +12 -2
- package/dist/dev/lib/taper.js.map +1 -1
- package/dist/dev/lib/value-mapping.js +28 -11
- package/dist/dev/lib/value-mapping.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# @xsynaptic/sonic-ui
|
|
2
2
|
|
|
3
|
+
## 0.28.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- An endless dial now spaces its `positions` evenly round the turn and wraps from the last to the first, and an endless value that rounds onto `max` wraps to `min`. The waveform and wavestrip ignore a typed time that isn't in clock form, such as `1:75` or `1e2`.
|
|
8
|
+
- In development, a warning now flags a second copy of the package defining a tag that's already registered.
|
|
9
|
+
|
|
3
10
|
## 0.28.1
|
|
4
11
|
|
|
5
12
|
### Patch Changes
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
//#region src/elements/define-once.ts
|
|
2
2
|
function defineOnce(tag, elementClass) {
|
|
3
|
-
|
|
3
|
+
const registered = customElements.get(tag);
|
|
4
|
+
if (registered === elementClass) return;
|
|
5
|
+
if (registered) return;
|
|
6
|
+
customElements.define(tag, elementClass);
|
|
4
7
|
}
|
|
5
8
|
|
|
6
9
|
//#endregion
|
|
@@ -10,13 +10,12 @@ function formatClock(seconds) {
|
|
|
10
10
|
if (hours === 0) return `${prefix}${String(minutes)}:${rest}`;
|
|
11
11
|
return `${prefix}${String(hours)}:${String(minutes).padStart(2, "0")}:${rest}`;
|
|
12
12
|
}
|
|
13
|
+
const clockPattern = /^[-−]?\d+(?::[0-5]?\d){0,2}(?:\.\d+)?$/;
|
|
13
14
|
function parseClock(text) {
|
|
14
15
|
const clock = text.trim();
|
|
16
|
+
if (!clockPattern.test(clock)) return NaN;
|
|
15
17
|
let seconds = 0;
|
|
16
|
-
for (const part of clock.replace(sign, "").split(":"))
|
|
17
|
-
if (part.trim() === "") return NaN;
|
|
18
|
-
seconds = seconds * 60 + Number(part);
|
|
19
|
-
}
|
|
18
|
+
for (const part of clock.replace(sign, "").split(":")) seconds = seconds * 60 + Number(part);
|
|
20
19
|
return sign.test(clock) ? -seconds : seconds;
|
|
21
20
|
}
|
|
22
21
|
|
|
@@ -4,9 +4,9 @@ function parseNumberList(text) {
|
|
|
4
4
|
const numbers = [...new Set(text.split(/[\s,]+/).filter(Boolean).map(Number).filter((entry) => Number.isFinite(entry)))].toSorted((first, second) => first - second);
|
|
5
5
|
return numbers.length >= 2 ? numbers : void 0;
|
|
6
6
|
}
|
|
7
|
-
function nearestPosition(positions, value) {
|
|
7
|
+
function nearestPosition(positions, value, distance = (position) => Math.abs(position - value)) {
|
|
8
8
|
let nearest = positions[0] ?? value;
|
|
9
|
-
for (const position of positions) if (
|
|
9
|
+
for (const position of positions) if (distance(position) < distance(nearest)) nearest = position;
|
|
10
10
|
return nearest;
|
|
11
11
|
}
|
|
12
12
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { clampProportion } from "./math.js";
|
|
1
|
+
import { clampProportion, wrapProportion } from "./math.js";
|
|
2
2
|
|
|
3
3
|
//#region src/lib/taper.ts
|
|
4
4
|
function linearTaper(min, max) {
|
|
@@ -46,6 +46,16 @@ function listTaper(positions) {
|
|
|
46
46
|
valueAt: (proportion) => positions[Math.round(clampProportion(proportion) * last)] ?? NaN
|
|
47
47
|
};
|
|
48
48
|
}
|
|
49
|
+
function ringTaper(positions, span) {
|
|
50
|
+
const count = positions.length;
|
|
51
|
+
const first = positions[0] ?? 0;
|
|
52
|
+
const firstAfterOneTurn = first + span;
|
|
53
|
+
const list = listTaper([...positions, firstAfterOneTurn]);
|
|
54
|
+
return {
|
|
55
|
+
proportionOf: (value) => wrapProportion(list.proportionOf(value < first ? value + span : value)),
|
|
56
|
+
valueAt: (proportion) => positions[Math.round(wrapProportion(proportion) * count) % count] ?? NaN
|
|
57
|
+
};
|
|
58
|
+
}
|
|
49
59
|
|
|
50
60
|
//#endregion
|
|
51
|
-
export { linearTaper, listTaper, logTaper, skewTaper };
|
|
61
|
+
export { linearTaper, listTaper, logTaper, ringTaper, skewTaper };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { clamp, decimalPlaces, roundTo, trimFloat } from "./math.js";
|
|
2
2
|
import { nearestPosition } from "./number-list.js";
|
|
3
|
-
import { linearTaper, listTaper, logTaper, skewTaper } from "./taper.js";
|
|
3
|
+
import { linearTaper, listTaper, logTaper, ringTaper, skewTaper } from "./taper.js";
|
|
4
4
|
|
|
5
5
|
//#region src/lib/value-mapping.ts
|
|
6
6
|
const resetKeys = /* @__PURE__ */ new Set(["Backspace", "Delete"]);
|
|
@@ -12,9 +12,14 @@ const keySteps = /* @__PURE__ */ new Map([
|
|
|
12
12
|
["PageDown", -10],
|
|
13
13
|
["PageUp", 10]
|
|
14
14
|
]);
|
|
15
|
-
function
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
function positionsIn(spec, max) {
|
|
16
|
+
if (!spec.isWrapping) return spec.positions;
|
|
17
|
+
const withinTurn = spec.positions?.filter((position) => position >= spec.min && position < max);
|
|
18
|
+
return withinTurn && withinTurn.length >= 2 ? withinTurn : void 0;
|
|
19
|
+
}
|
|
20
|
+
function curveOf(spec, max, positions) {
|
|
21
|
+
const { min } = spec;
|
|
22
|
+
if (positions) return spec.isWrapping ? ringTaper(positions, max - min) : listTaper(positions);
|
|
18
23
|
if (spec.isNotched || spec.isWrapping) return void 0;
|
|
19
24
|
return (spec.taper === "log" ? logTaper(min, max) : void 0) ?? skewTaper(min, max, spec.midpoint ?? NaN);
|
|
20
25
|
}
|
|
@@ -28,15 +33,16 @@ function countSteps(spec, range) {
|
|
|
28
33
|
return spec.isWrapping ? Math.max(1, Math.ceil(fit)) : Math.floor(fit) + 1;
|
|
29
34
|
}
|
|
30
35
|
function valueMapping(spec) {
|
|
31
|
-
const { min,
|
|
36
|
+
const { min, step } = spec;
|
|
32
37
|
const max = Math.max(min, spec.max);
|
|
33
38
|
const range = max - min;
|
|
39
|
+
const positions = positionsIn(spec, max);
|
|
34
40
|
const count = countSteps(spec, range);
|
|
35
41
|
const isStepped = step > 0 && !positions;
|
|
36
|
-
const curve = curveOf(spec, max);
|
|
42
|
+
const curve = curveOf(spec, max, positions);
|
|
37
43
|
const taper = curve ?? linearTaper(min, max);
|
|
38
44
|
function round(value) {
|
|
39
|
-
if (
|
|
45
|
+
if (!isStepped) return trimFloat(value);
|
|
40
46
|
return roundTo(value, Math.max(decimalPlaces(step), decimalPlaces(min)));
|
|
41
47
|
}
|
|
42
48
|
const top = round((count - 1) * step);
|
|
@@ -52,7 +58,8 @@ function valueMapping(spec) {
|
|
|
52
58
|
function bound(value) {
|
|
53
59
|
if (!spec.isWrapping) return clamp(value, ...bounds);
|
|
54
60
|
if (range <= 0) return min;
|
|
55
|
-
|
|
61
|
+
const wrapped = round(min + wrap(value, range));
|
|
62
|
+
return wrapped >= max ? min : wrapped;
|
|
56
63
|
}
|
|
57
64
|
function ring(value, span) {
|
|
58
65
|
if (range <= 0) return min;
|
|
@@ -60,15 +67,25 @@ function valueMapping(spec) {
|
|
|
60
67
|
if (offset > top) return span - offset <= offset - top ? min : round(min + top);
|
|
61
68
|
return round(min + Math.round(offset / step) * step);
|
|
62
69
|
}
|
|
70
|
+
function snapPosition(list, value) {
|
|
71
|
+
if (!spec.isWrapping) return nearestPosition(list, value);
|
|
72
|
+
const wrapped = bound(value);
|
|
73
|
+
return nearestPosition(list, wrapped, (position) => {
|
|
74
|
+
const gap = Math.abs(position - wrapped);
|
|
75
|
+
return Math.min(gap, range - gap);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
63
78
|
function snap(value) {
|
|
64
|
-
if (positions) return
|
|
79
|
+
if (positions) return snapPosition(positions, value);
|
|
65
80
|
if (step <= 0) return bound(round(value));
|
|
66
81
|
if (spec.isWrapping) return ring(value, range);
|
|
67
82
|
return bound(round(min + Math.round((value - min) / step) * step));
|
|
68
83
|
}
|
|
69
84
|
function stepBy(from, steps, size) {
|
|
70
85
|
if (positions) {
|
|
71
|
-
const
|
|
86
|
+
const { length } = positions;
|
|
87
|
+
const moved = positions.indexOf(from) + steps;
|
|
88
|
+
const index = spec.isWrapping ? (moved % length + length) % length : clamp(moved, 0, length - 1);
|
|
72
89
|
return positions[index] ?? from;
|
|
73
90
|
}
|
|
74
91
|
if (!curve) return from + steps * size;
|
|
@@ -87,7 +104,7 @@ function valueMapping(spec) {
|
|
|
87
104
|
return step > 0 ? Math.max(1, Math.round(trimFloat(keyStep / step))) * step : keyStep;
|
|
88
105
|
}
|
|
89
106
|
function end(size) {
|
|
90
|
-
if (!spec.isWrapping) return bounds[1];
|
|
107
|
+
if (positions || !spec.isWrapping) return bounds[1];
|
|
91
108
|
return min + (isStepped ? top : (Math.ceil(range / size) - 1) * size);
|
|
92
109
|
}
|
|
93
110
|
function target(key, from, keyStep) {
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
//#region src/elements/define-once.ts
|
|
2
2
|
function defineOnce(tag, elementClass) {
|
|
3
|
-
|
|
3
|
+
const registered = customElements.get(tag);
|
|
4
|
+
if (registered === elementClass) return;
|
|
5
|
+
if (registered) {
|
|
6
|
+
console.warn(`<${tag}> is already registered by another class, so this copy of sonic-ui never runs; two copies of the package are probably loaded`);
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
customElements.define(tag, elementClass);
|
|
4
10
|
}
|
|
5
11
|
|
|
6
12
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"define-once.js","names":[],"sources":["../../../src/elements/define-once.ts"],"sourcesContent":["
|
|
1
|
+
{"version":3,"file":"define-once.js","names":[],"sources":["../../../src/elements/define-once.ts"],"sourcesContent":["declare const __DEV__: boolean;\n\nexport function defineOnce(tag: string, elementClass: CustomElementConstructor): void {\n\tconst registered = customElements.get(tag);\n\n\tif (registered === elementClass) return;\n\n\tif (registered) {\n\t\tif (__DEV__) {\n\t\t\tconsole.warn(\n\t\t\t\t`<${tag}> is already registered by another class, so this copy of sonic-ui never runs; two copies of the package are probably loaded`,\n\t\t\t);\n\t\t}\n\t\treturn;\n\t}\n\n\tcustomElements.define(tag, elementClass);\n}\n"],"mappings":";AAEA,SAAgB,WAAW,KAAa,cAA8C;CACrF,MAAM,aAAa,eAAe,IAAI,GAAG;CAEzC,IAAI,eAAe,cAAc;CAEjC,IAAI,YAAY;EAEd,QAAQ,KACP,IAAI,IAAI,6HACT;EAED;CACD;CAEA,eAAe,OAAO,KAAK,YAAY;AACxC"}
|
package/dist/dev/lib/clock.js
CHANGED
|
@@ -10,13 +10,12 @@ function formatClock(seconds) {
|
|
|
10
10
|
if (hours === 0) return `${prefix}${String(minutes)}:${rest}`;
|
|
11
11
|
return `${prefix}${String(hours)}:${String(minutes).padStart(2, "0")}:${rest}`;
|
|
12
12
|
}
|
|
13
|
+
const clockPattern = /^[-−]?\d+(?::[0-5]?\d){0,2}(?:\.\d+)?$/;
|
|
13
14
|
function parseClock(text) {
|
|
14
15
|
const clock = text.trim();
|
|
16
|
+
if (!clockPattern.test(clock)) return NaN;
|
|
15
17
|
let seconds = 0;
|
|
16
|
-
for (const part of clock.replace(sign, "").split(":"))
|
|
17
|
-
if (part.trim() === "") return NaN;
|
|
18
|
-
seconds = seconds * 60 + Number(part);
|
|
19
|
-
}
|
|
18
|
+
for (const part of clock.replace(sign, "").split(":")) seconds = seconds * 60 + Number(part);
|
|
20
19
|
return sign.test(clock) ? -seconds : seconds;
|
|
21
20
|
}
|
|
22
21
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"clock.js","names":[],"sources":["../../../src/lib/clock.ts"],"sourcesContent":["const sign = /^[-−]/;\n\n// A time not yet known writes as nothing, the text `parseClock` reads back as NaN\nexport function formatClock(seconds: number): string {\n\tif (!Number.isFinite(seconds)) return '';\n\n\tconst total = Math.floor(Math.abs(seconds));\n\tconst hours = Math.floor(total / 3600);\n\tconst minutes = Math.floor(total / 60) % 60;\n\tconst rest = String(total % 60).padStart(2, '0');\n\tconst prefix = seconds < 0 ? '−' : '';\n\n\tif (hours === 0) return `${prefix}${String(minutes)}:${rest}`;\n\n\treturn `${prefix}${String(hours)}:${String(minutes).padStart(2, '0')}:${rest}`;\n}\n\
|
|
1
|
+
{"version":3,"file":"clock.js","names":[],"sources":["../../../src/lib/clock.ts"],"sourcesContent":["const sign = /^[-−]/;\n\n// A time not yet known writes as nothing, the text `parseClock` reads back as NaN\nexport function formatClock(seconds: number): string {\n\tif (!Number.isFinite(seconds)) return '';\n\n\tconst total = Math.floor(Math.abs(seconds));\n\tconst hours = Math.floor(total / 3600);\n\tconst minutes = Math.floor(total / 60) % 60;\n\tconst rest = String(total % 60).padStart(2, '0');\n\tconst prefix = seconds < 0 ? '−' : '';\n\n\tif (hours === 0) return `${prefix}${String(minutes)}:${rest}`;\n\n\treturn `${prefix}${String(hours)}:${String(minutes).padStart(2, '0')}:${rest}`;\n}\n\nconst clockPattern = /^[-−]?\\d+(?::[0-5]?\\d){0,2}(?:\\.\\d+)?$/;\n\nexport function parseClock(text: string): number {\n\tconst clock = text.trim();\n\tif (!clockPattern.test(clock)) return NaN;\n\n\tlet seconds = 0;\n\n\tfor (const part of clock.replace(sign, '').split(':')) seconds = seconds * 60 + Number(part);\n\n\treturn sign.test(clock) ? -seconds : seconds;\n}\n"],"mappings":";AAAA,MAAM,OAAO;AAGb,SAAgB,YAAY,SAAyB;CACpD,IAAI,CAAC,OAAO,SAAS,OAAO,GAAG,OAAO;CAEtC,MAAM,QAAQ,KAAK,MAAM,KAAK,IAAI,OAAO,CAAC;CAC1C,MAAM,QAAQ,KAAK,MAAM,QAAQ,IAAI;CACrC,MAAM,UAAU,KAAK,MAAM,QAAQ,EAAE,IAAI;CACzC,MAAM,OAAO,OAAO,QAAQ,EAAE,CAAC,CAAC,SAAS,GAAG,GAAG;CAC/C,MAAM,SAAS,UAAU,IAAI,MAAM;CAEnC,IAAI,UAAU,GAAG,OAAO,GAAG,SAAS,OAAO,OAAO,EAAE,GAAG;CAEvD,OAAO,GAAG,SAAS,OAAO,KAAK,EAAE,GAAG,OAAO,OAAO,CAAC,CAAC,SAAS,GAAG,GAAG,EAAE,GAAG;AACzE;AAEA,MAAM,eAAe;AAErB,SAAgB,WAAW,MAAsB;CAChD,MAAM,QAAQ,KAAK,KAAK;CACxB,IAAI,CAAC,aAAa,KAAK,KAAK,GAAG,OAAO;CAEtC,IAAI,UAAU;CAEd,KAAK,MAAM,QAAQ,MAAM,QAAQ,MAAM,EAAE,CAAC,CAAC,MAAM,GAAG,GAAG,UAAU,UAAU,KAAK,OAAO,IAAI;CAE3F,OAAO,KAAK,KAAK,KAAK,IAAI,CAAC,UAAU;AACtC"}
|
|
@@ -4,9 +4,9 @@ function parseNumberList(text) {
|
|
|
4
4
|
const numbers = [...new Set(text.split(/[\s,]+/).filter(Boolean).map(Number).filter((entry) => Number.isFinite(entry)))].toSorted((first, second) => first - second);
|
|
5
5
|
return numbers.length >= 2 ? numbers : void 0;
|
|
6
6
|
}
|
|
7
|
-
function nearestPosition(positions, value) {
|
|
7
|
+
function nearestPosition(positions, value, distance = (position) => Math.abs(position - value)) {
|
|
8
8
|
let nearest = positions[0] ?? value;
|
|
9
|
-
for (const position of positions) if (
|
|
9
|
+
for (const position of positions) if (distance(position) < distance(nearest)) nearest = position;
|
|
10
10
|
return nearest;
|
|
11
11
|
}
|
|
12
12
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"number-list.js","names":[],"sources":["../../../src/lib/number-list.ts"],"sourcesContent":["export function parseNumberList(text: null | string): Array<number> | undefined {\n\tif (text === null) return undefined;\n\n\tconst numbers = [\n\t\t...new Set(\n\t\t\ttext\n\t\t\t\t.split(/[\\s,]+/)\n\t\t\t\t.filter(Boolean)\n\t\t\t\t.map(Number)\n\t\t\t\t.filter((entry) => Number.isFinite(entry)),\n\t\t),\n\t].toSorted((first, second) => first - second);\n\n\treturn numbers.length >= 2 ? numbers : undefined;\n}\n\nexport function nearestPosition(
|
|
1
|
+
{"version":3,"file":"number-list.js","names":[],"sources":["../../../src/lib/number-list.ts"],"sourcesContent":["export function parseNumberList(text: null | string): Array<number> | undefined {\n\tif (text === null) return undefined;\n\n\tconst numbers = [\n\t\t...new Set(\n\t\t\ttext\n\t\t\t\t.split(/[\\s,]+/)\n\t\t\t\t.filter(Boolean)\n\t\t\t\t.map(Number)\n\t\t\t\t.filter((entry) => Number.isFinite(entry)),\n\t\t),\n\t].toSorted((first, second) => first - second);\n\n\treturn numbers.length >= 2 ? numbers : undefined;\n}\n\nexport function nearestPosition(\n\tpositions: ReadonlyArray<number>,\n\tvalue: number,\n\tdistance = (position: number) => Math.abs(position - value),\n): number {\n\tlet nearest = positions[0] ?? value;\n\n\tfor (const position of positions) {\n\t\tif (distance(position) < distance(nearest)) nearest = position;\n\t}\n\n\treturn nearest;\n}\n"],"mappings":";AAAA,SAAgB,gBAAgB,MAAgD;CAC/E,IAAI,SAAS,MAAM,OAAO;CAE1B,MAAM,UAAU,CACf,GAAG,IAAI,IACN,KACE,MAAM,QAAQ,CAAC,CACf,OAAO,OAAO,CAAC,CACf,IAAI,MAAM,CAAC,CACX,QAAQ,UAAU,OAAO,SAAS,KAAK,CAAC,CAC3C,CACD,CAAC,CAAC,UAAU,OAAO,WAAW,QAAQ,MAAM;CAE5C,OAAO,QAAQ,UAAU,IAAI,UAAU;AACxC;AAEA,SAAgB,gBACf,WACA,OACA,YAAY,aAAqB,KAAK,IAAI,WAAW,KAAK,GACjD;CACT,IAAI,UAAU,UAAU,MAAM;CAE9B,KAAK,MAAM,YAAY,WACtB,IAAI,SAAS,QAAQ,IAAI,SAAS,OAAO,GAAG,UAAU;CAGvD,OAAO;AACR"}
|
package/dist/dev/lib/taper.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { clampProportion } from "./math.js";
|
|
1
|
+
import { clampProportion, wrapProportion } from "./math.js";
|
|
2
2
|
|
|
3
3
|
//#region src/lib/taper.ts
|
|
4
4
|
function linearTaper(min, max) {
|
|
@@ -46,7 +46,17 @@ function listTaper(positions) {
|
|
|
46
46
|
valueAt: (proportion) => positions[Math.round(clampProportion(proportion) * last)] ?? NaN
|
|
47
47
|
};
|
|
48
48
|
}
|
|
49
|
+
function ringTaper(positions, span) {
|
|
50
|
+
const count = positions.length;
|
|
51
|
+
const first = positions[0] ?? 0;
|
|
52
|
+
const firstAfterOneTurn = first + span;
|
|
53
|
+
const list = listTaper([...positions, firstAfterOneTurn]);
|
|
54
|
+
return {
|
|
55
|
+
proportionOf: (value) => wrapProportion(list.proportionOf(value < first ? value + span : value)),
|
|
56
|
+
valueAt: (proportion) => positions[Math.round(wrapProportion(proportion) * count) % count] ?? NaN
|
|
57
|
+
};
|
|
58
|
+
}
|
|
49
59
|
|
|
50
60
|
//#endregion
|
|
51
|
-
export { linearTaper, listTaper, logTaper, skewTaper };
|
|
61
|
+
export { linearTaper, listTaper, logTaper, ringTaper, skewTaper };
|
|
52
62
|
//# sourceMappingURL=taper.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"taper.js","names":[],"sources":["../../../src/lib/taper.ts"],"sourcesContent":["import { clampProportion } from '#lib/math.ts';\n\nexport interface Taper {\n\tproportionOf: (value: number) => number;\n\tvalueAt: (proportion: number) => number;\n}\n\nexport function linearTaper(min: number, max: number): Taper {\n\tconst range = max - min;\n\n\treturn {\n\t\tproportionOf: (value) => (range > 0 ? clampProportion((value - min) / range) : 0),\n\t\tvalueAt: (proportion) => min + clampProportion(proportion) * range,\n\t};\n}\n\nexport function logTaper(min: number, max: number): Taper | undefined {\n\tif (min <= 0 || max <= min) return undefined;\n\n\tconst span = Math.log(max / min);\n\n\treturn {\n\t\tproportionOf: (value) => clampProportion(Math.log(Math.max(min, value) / min) / span),\n\t\tvalueAt: (proportion) => {\n\t\t\tconst clamped = clampProportion(proportion);\n\t\t\tif (clamped === 0) return min;\n\t\t\tif (clamped === 1) return max;\n\n\t\t\treturn min * Math.exp(clamped * span);\n\t\t},\n\t};\n}\n\n// JUCE's skew: a power curve through `midpoint` at half the travel\nexport function skewTaper(min: number, max: number, midpoint: number): Taper | undefined {\n\tconst range = max - min;\n\tconst midpointProportion = (midpoint - min) / range;\n\n\tif (midpointProportion === 0.5 || !(midpointProportion > 0 && midpointProportion < 1))\n\t\treturn undefined;\n\n\tconst skew = Math.log(0.5) / Math.log(midpointProportion);\n\n\treturn {\n\t\tproportionOf: (value) => clampProportion((value - min) / range) ** skew,\n\t\tvalueAt: (proportion) => min + range * clampProportion(proportion) ** (1 / skew),\n\t};\n}\n\nexport function listTaper(positions: ReadonlyArray<number>): Taper {\n\tconst last = positions.length - 1;\n\n\treturn {\n\t\tproportionOf: (value) => {\n\t\t\tif (Number.isNaN(value)) return NaN;\n\n\t\t\tconst above = positions.findIndex((position) => position >= value);\n\t\t\tif (above === -1) return 1;\n\n\t\t\tconst high = positions[above];\n\t\t\tconst low = positions[above - 1];\n\t\t\tif (high === undefined || low === undefined) return 0;\n\n\t\t\treturn (above - 1 + (value - low) / (high - low)) / last;\n\t\t},\n\t\tvalueAt: (proportion) => positions[Math.round(clampProportion(proportion) * last)] ?? NaN,\n\t};\n}\n"],"mappings":";;;AAOA,SAAgB,YAAY,KAAa,KAAoB;CAC5D,MAAM,QAAQ,MAAM;CAEpB,OAAO;EACN,eAAe,UAAW,QAAQ,IAAI,iBAAiB,QAAQ,OAAO,KAAK,IAAI;EAC/E,UAAU,eAAe,MAAM,gBAAgB,UAAU,IAAI;CAC9D;AACD;AAEA,SAAgB,SAAS,KAAa,KAAgC;CACrE,IAAI,OAAO,KAAK,OAAO,KAAK,OAAO;CAEnC,MAAM,OAAO,KAAK,IAAI,MAAM,GAAG;CAE/B,OAAO;EACN,eAAe,UAAU,gBAAgB,KAAK,IAAI,KAAK,IAAI,KAAK,KAAK,IAAI,GAAG,IAAI,IAAI;EACpF,UAAU,eAAe;GACxB,MAAM,UAAU,gBAAgB,UAAU;GAC1C,IAAI,YAAY,GAAG,OAAO;GAC1B,IAAI,YAAY,GAAG,OAAO;GAE1B,OAAO,MAAM,KAAK,IAAI,UAAU,IAAI;EACrC;CACD;AACD;AAGA,SAAgB,UAAU,KAAa,KAAa,UAAqC;CACxF,MAAM,QAAQ,MAAM;CACpB,MAAM,sBAAsB,WAAW,OAAO;CAE9C,IAAI,uBAAuB,MAAO,EAAE,qBAAqB,KAAK,qBAAqB,IAClF,OAAO;CAER,MAAM,OAAO,KAAK,IAAI,EAAG,IAAI,KAAK,IAAI,kBAAkB;CAExD,OAAO;EACN,eAAe,UAAU,iBAAiB,QAAQ,OAAO,KAAK,KAAK;EACnE,UAAU,eAAe,MAAM,QAAQ,gBAAgB,UAAU,MAAM,IAAI;CAC5E;AACD;AAEA,SAAgB,UAAU,WAAyC;CAClE,MAAM,OAAO,UAAU,SAAS;CAEhC,OAAO;EACN,eAAe,UAAU;GACxB,IAAI,OAAO,MAAM,KAAK,GAAG,OAAO;GAEhC,MAAM,QAAQ,UAAU,WAAW,aAAa,YAAY,KAAK;GACjE,IAAI,UAAU,IAAI,OAAO;GAEzB,MAAM,OAAO,UAAU;GACvB,MAAM,MAAM,UAAU,QAAQ;GAC9B,IAAI,SAAS,UAAa,QAAQ,QAAW,OAAO;GAEpD,QAAQ,QAAQ,KAAK,QAAQ,QAAQ,OAAO,QAAQ;EACrD;EACA,UAAU,eAAe,UAAU,KAAK,MAAM,gBAAgB,UAAU,IAAI,IAAI,MAAM;CACvF;AACD"}
|
|
1
|
+
{"version":3,"file":"taper.js","names":[],"sources":["../../../src/lib/taper.ts"],"sourcesContent":["import { clampProportion, wrapProportion } from '#lib/math.ts';\n\nexport interface Taper {\n\tproportionOf: (value: number) => number;\n\tvalueAt: (proportion: number) => number;\n}\n\nexport function linearTaper(min: number, max: number): Taper {\n\tconst range = max - min;\n\n\treturn {\n\t\tproportionOf: (value) => (range > 0 ? clampProportion((value - min) / range) : 0),\n\t\tvalueAt: (proportion) => min + clampProportion(proportion) * range,\n\t};\n}\n\nexport function logTaper(min: number, max: number): Taper | undefined {\n\tif (min <= 0 || max <= min) return undefined;\n\n\tconst span = Math.log(max / min);\n\n\treturn {\n\t\tproportionOf: (value) => clampProportion(Math.log(Math.max(min, value) / min) / span),\n\t\tvalueAt: (proportion) => {\n\t\t\tconst clamped = clampProportion(proportion);\n\t\t\tif (clamped === 0) return min;\n\t\t\tif (clamped === 1) return max;\n\n\t\t\treturn min * Math.exp(clamped * span);\n\t\t},\n\t};\n}\n\n// JUCE's skew: a power curve through `midpoint` at half the travel\nexport function skewTaper(min: number, max: number, midpoint: number): Taper | undefined {\n\tconst range = max - min;\n\tconst midpointProportion = (midpoint - min) / range;\n\n\tif (midpointProportion === 0.5 || !(midpointProportion > 0 && midpointProportion < 1))\n\t\treturn undefined;\n\n\tconst skew = Math.log(0.5) / Math.log(midpointProportion);\n\n\treturn {\n\t\tproportionOf: (value) => clampProportion((value - min) / range) ** skew,\n\t\tvalueAt: (proportion) => min + range * clampProportion(proportion) ** (1 / skew),\n\t};\n}\n\nexport function listTaper(positions: ReadonlyArray<number>): Taper {\n\tconst last = positions.length - 1;\n\n\treturn {\n\t\tproportionOf: (value) => {\n\t\t\tif (Number.isNaN(value)) return NaN;\n\n\t\t\tconst above = positions.findIndex((position) => position >= value);\n\t\t\tif (above === -1) return 1;\n\n\t\t\tconst high = positions[above];\n\t\t\tconst low = positions[above - 1];\n\t\t\tif (high === undefined || low === undefined) return 0;\n\n\t\t\treturn (above - 1 + (value - low) / (high - low)) / last;\n\t\t},\n\t\tvalueAt: (proportion) => positions[Math.round(clampProportion(proportion) * last)] ?? NaN,\n\t};\n}\n\nexport function ringTaper(positions: ReadonlyArray<number>, span: number): Taper {\n\tconst count = positions.length;\n\tconst first = positions[0] ?? 0;\n\tconst firstAfterOneTurn = first + span;\n\tconst list = listTaper([...positions, firstAfterOneTurn]);\n\n\treturn {\n\t\tproportionOf: (value) =>\n\t\t\twrapProportion(list.proportionOf(value < first ? value + span : value)),\n\t\tvalueAt: (proportion) =>\n\t\t\tpositions[Math.round(wrapProportion(proportion) * count) % count] ?? NaN,\n\t};\n}\n"],"mappings":";;;AAOA,SAAgB,YAAY,KAAa,KAAoB;CAC5D,MAAM,QAAQ,MAAM;CAEpB,OAAO;EACN,eAAe,UAAW,QAAQ,IAAI,iBAAiB,QAAQ,OAAO,KAAK,IAAI;EAC/E,UAAU,eAAe,MAAM,gBAAgB,UAAU,IAAI;CAC9D;AACD;AAEA,SAAgB,SAAS,KAAa,KAAgC;CACrE,IAAI,OAAO,KAAK,OAAO,KAAK,OAAO;CAEnC,MAAM,OAAO,KAAK,IAAI,MAAM,GAAG;CAE/B,OAAO;EACN,eAAe,UAAU,gBAAgB,KAAK,IAAI,KAAK,IAAI,KAAK,KAAK,IAAI,GAAG,IAAI,IAAI;EACpF,UAAU,eAAe;GACxB,MAAM,UAAU,gBAAgB,UAAU;GAC1C,IAAI,YAAY,GAAG,OAAO;GAC1B,IAAI,YAAY,GAAG,OAAO;GAE1B,OAAO,MAAM,KAAK,IAAI,UAAU,IAAI;EACrC;CACD;AACD;AAGA,SAAgB,UAAU,KAAa,KAAa,UAAqC;CACxF,MAAM,QAAQ,MAAM;CACpB,MAAM,sBAAsB,WAAW,OAAO;CAE9C,IAAI,uBAAuB,MAAO,EAAE,qBAAqB,KAAK,qBAAqB,IAClF,OAAO;CAER,MAAM,OAAO,KAAK,IAAI,EAAG,IAAI,KAAK,IAAI,kBAAkB;CAExD,OAAO;EACN,eAAe,UAAU,iBAAiB,QAAQ,OAAO,KAAK,KAAK;EACnE,UAAU,eAAe,MAAM,QAAQ,gBAAgB,UAAU,MAAM,IAAI;CAC5E;AACD;AAEA,SAAgB,UAAU,WAAyC;CAClE,MAAM,OAAO,UAAU,SAAS;CAEhC,OAAO;EACN,eAAe,UAAU;GACxB,IAAI,OAAO,MAAM,KAAK,GAAG,OAAO;GAEhC,MAAM,QAAQ,UAAU,WAAW,aAAa,YAAY,KAAK;GACjE,IAAI,UAAU,IAAI,OAAO;GAEzB,MAAM,OAAO,UAAU;GACvB,MAAM,MAAM,UAAU,QAAQ;GAC9B,IAAI,SAAS,UAAa,QAAQ,QAAW,OAAO;GAEpD,QAAQ,QAAQ,KAAK,QAAQ,QAAQ,OAAO,QAAQ;EACrD;EACA,UAAU,eAAe,UAAU,KAAK,MAAM,gBAAgB,UAAU,IAAI,IAAI,MAAM;CACvF;AACD;AAEA,SAAgB,UAAU,WAAkC,MAAqB;CAChF,MAAM,QAAQ,UAAU;CACxB,MAAM,QAAQ,UAAU,MAAM;CAC9B,MAAM,oBAAoB,QAAQ;CAClC,MAAM,OAAO,UAAU,CAAC,GAAG,WAAW,iBAAiB,CAAC;CAExD,OAAO;EACN,eAAe,UACd,eAAe,KAAK,aAAa,QAAQ,QAAQ,QAAQ,OAAO,KAAK,CAAC;EACvE,UAAU,eACT,UAAU,KAAK,MAAM,eAAe,UAAU,IAAI,KAAK,IAAI,UAAU;CACvE;AACD"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { clamp, decimalPlaces, roundTo, trimFloat } from "./math.js";
|
|
2
2
|
import { nearestPosition } from "./number-list.js";
|
|
3
|
-
import { linearTaper, listTaper, logTaper, skewTaper } from "./taper.js";
|
|
3
|
+
import { linearTaper, listTaper, logTaper, ringTaper, skewTaper } from "./taper.js";
|
|
4
4
|
|
|
5
5
|
//#region src/lib/value-mapping.ts
|
|
6
6
|
const resetKeys = /* @__PURE__ */ new Set(["Backspace", "Delete"]);
|
|
@@ -12,9 +12,14 @@ const keySteps = /* @__PURE__ */ new Map([
|
|
|
12
12
|
["PageDown", -10],
|
|
13
13
|
["PageUp", 10]
|
|
14
14
|
]);
|
|
15
|
-
function
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
function positionsIn(spec, max) {
|
|
16
|
+
if (!spec.isWrapping) return spec.positions;
|
|
17
|
+
const withinTurn = spec.positions?.filter((position) => position >= spec.min && position < max);
|
|
18
|
+
return withinTurn && withinTurn.length >= 2 ? withinTurn : void 0;
|
|
19
|
+
}
|
|
20
|
+
function curveOf(spec, max, positions) {
|
|
21
|
+
const { min } = spec;
|
|
22
|
+
if (positions) return spec.isWrapping ? ringTaper(positions, max - min) : listTaper(positions);
|
|
18
23
|
if (spec.isNotched || spec.isWrapping) return void 0;
|
|
19
24
|
return (spec.taper === "log" ? logTaper(min, max) : void 0) ?? skewTaper(min, max, spec.midpoint ?? NaN);
|
|
20
25
|
}
|
|
@@ -28,15 +33,16 @@ function countSteps(spec, range) {
|
|
|
28
33
|
return spec.isWrapping ? Math.max(1, Math.ceil(fit)) : Math.floor(fit) + 1;
|
|
29
34
|
}
|
|
30
35
|
function valueMapping(spec) {
|
|
31
|
-
const { min,
|
|
36
|
+
const { min, step } = spec;
|
|
32
37
|
const max = Math.max(min, spec.max);
|
|
33
38
|
const range = max - min;
|
|
39
|
+
const positions = positionsIn(spec, max);
|
|
34
40
|
const count = countSteps(spec, range);
|
|
35
41
|
const isStepped = step > 0 && !positions;
|
|
36
|
-
const curve = curveOf(spec, max);
|
|
42
|
+
const curve = curveOf(spec, max, positions);
|
|
37
43
|
const taper = curve ?? linearTaper(min, max);
|
|
38
44
|
function round(value) {
|
|
39
|
-
if (
|
|
45
|
+
if (!isStepped) return trimFloat(value);
|
|
40
46
|
return roundTo(value, Math.max(decimalPlaces(step), decimalPlaces(min)));
|
|
41
47
|
}
|
|
42
48
|
const top = round((count - 1) * step);
|
|
@@ -52,7 +58,8 @@ function valueMapping(spec) {
|
|
|
52
58
|
function bound(value) {
|
|
53
59
|
if (!spec.isWrapping) return clamp(value, ...bounds);
|
|
54
60
|
if (range <= 0) return min;
|
|
55
|
-
|
|
61
|
+
const wrapped = round(min + wrap(value, range));
|
|
62
|
+
return wrapped >= max ? min : wrapped;
|
|
56
63
|
}
|
|
57
64
|
function ring(value, span) {
|
|
58
65
|
if (range <= 0) return min;
|
|
@@ -60,15 +67,25 @@ function valueMapping(spec) {
|
|
|
60
67
|
if (offset > top) return span - offset <= offset - top ? min : round(min + top);
|
|
61
68
|
return round(min + Math.round(offset / step) * step);
|
|
62
69
|
}
|
|
70
|
+
function snapPosition(list, value) {
|
|
71
|
+
if (!spec.isWrapping) return nearestPosition(list, value);
|
|
72
|
+
const wrapped = bound(value);
|
|
73
|
+
return nearestPosition(list, wrapped, (position) => {
|
|
74
|
+
const gap = Math.abs(position - wrapped);
|
|
75
|
+
return Math.min(gap, range - gap);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
63
78
|
function snap(value) {
|
|
64
|
-
if (positions) return
|
|
79
|
+
if (positions) return snapPosition(positions, value);
|
|
65
80
|
if (step <= 0) return bound(round(value));
|
|
66
81
|
if (spec.isWrapping) return ring(value, range);
|
|
67
82
|
return bound(round(min + Math.round((value - min) / step) * step));
|
|
68
83
|
}
|
|
69
84
|
function stepBy(from, steps, size) {
|
|
70
85
|
if (positions) {
|
|
71
|
-
const
|
|
86
|
+
const { length } = positions;
|
|
87
|
+
const moved = positions.indexOf(from) + steps;
|
|
88
|
+
const index = spec.isWrapping ? (moved % length + length) % length : clamp(moved, 0, length - 1);
|
|
72
89
|
return positions[index] ?? from;
|
|
73
90
|
}
|
|
74
91
|
if (!curve) return from + steps * size;
|
|
@@ -87,7 +104,7 @@ function valueMapping(spec) {
|
|
|
87
104
|
return step > 0 ? Math.max(1, Math.round(trimFloat(keyStep / step))) * step : keyStep;
|
|
88
105
|
}
|
|
89
106
|
function end(size) {
|
|
90
|
-
if (!spec.isWrapping) return bounds[1];
|
|
107
|
+
if (positions || !spec.isWrapping) return bounds[1];
|
|
91
108
|
return min + (isStepped ? top : (Math.ceil(range / size) - 1) * size);
|
|
92
109
|
}
|
|
93
110
|
function target(key, from, keyStep) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"value-mapping.js","names":[],"sources":["../../../src/lib/value-mapping.ts"],"sourcesContent":["import type { Taper } from '#lib/taper.ts';\n\nimport { clamp, decimalPlaces, roundTo, trimFloat } from '#lib/math.ts';\nimport { nearestPosition } from '#lib/number-list.ts';\nimport { linearTaper, listTaper, logTaper, skewTaper } from '#lib/taper.ts';\n\nexport interface ValueSpec {\n\tdetent?: number;\n\tisNotched: boolean;\n\tisWrapping: boolean;\n\tmax: number;\n\tmidpoint?: number;\n\tmin: number;\n\tpositions?: ReadonlyArray<number>;\n\tstep: number;\n\ttaper: 'linear' | 'log';\n}\n\nexport interface ValueMapping {\n\treadonly bounds: [number, number];\n\treadonly isWrapping: boolean;\n\treadonly keyTarget: (key: string, from: number, keyStep?: number) => number | undefined;\n\treadonly positionCount: number | undefined;\n\treadonly proportionOf: (value: number) => number;\n\treadonly snap: (value: number) => number;\n\treadonly valueAt: (proportion: number) => number;\n}\n\n// A Mac keyboard's Delete sends Backspace\nexport const resetKeys = new Set(['Backspace', 'Delete']);\n\nconst pageSteps = 10;\n\nconst keySteps = new Map([\n\t['ArrowDown', -1],\n\t['ArrowLeft', -1],\n\t['ArrowRight', 1],\n\t['ArrowUp', 1],\n\t['PageDown', -pageSteps],\n\t['PageUp', pageSteps],\n]);\n\nfunction curveOf(spec: ValueSpec, max: number): Taper | undefined {\n\tconst { min, positions } = spec;\n\tif (positions) return listTaper(positions);\n\tif (spec.isNotched || spec.isWrapping) return undefined;\n\n\tconst log = spec.taper === 'log' ? logTaper(min, max) : undefined;\n\n\treturn log ?? skewTaper(min, max, spec.midpoint ?? NaN);\n}\n\nfunction countPositions(spec: ValueSpec, count: number): number | undefined {\n\tif (!spec.isNotched || !Number.isFinite(count) || count < 2) return;\n\n\treturn count;\n}\n\n// A wrapping mapping stops a step short of max, which is min again\nfunction countSteps(spec: ValueSpec, range: number): number {\n\tif (spec.step <= 0) return NaN;\n\n\tconst fit = trimFloat(range / spec.step);\n\n\treturn spec.isWrapping ? Math.max(1, Math.ceil(fit)) : Math.floor(fit) + 1;\n}\n\nexport function valueMapping(spec: ValueSpec): ValueMapping {\n\tconst { min, positions, step } = spec;\n\tconst max = Math.max(min, spec.max);\n\tconst range = max - min;\n\tconst count = countSteps(spec, range);\n\tconst isStepped = step > 0 && !positions;\n\tconst curve = curveOf(spec, max);\n\tconst taper = curve ?? linearTaper(min, max);\n\n\t// Decimal places, since significant digits keep the float residue beside 0\n\tfunction round(value: number): number {\n\t\tif (step <= 0) return trimFloat(value);\n\n\t\treturn roundTo(value, Math.max(decimalPlaces(step), decimalPlaces(min)));\n\t}\n\n\tconst top = round((count - 1) * step);\n\n\tfunction limits(): [number, number] {\n\t\tif (positions) return [positions[0] ?? min, positions.at(-1) ?? max];\n\n\t\treturn [min, isStepped && !spec.isWrapping ? round(min + top) : max];\n\t}\n\n\tconst bounds = limits();\n\n\tfunction wrap(value: number, span: number): number {\n\t\tconst offset = round((((value - min) % span) + span) % span);\n\n\t\treturn offset >= span ? 0 : offset;\n\t}\n\n\tfunction bound(value: number): number {\n\t\tif (!spec.isWrapping) return clamp(value, ...bounds);\n\t\tif (range <= 0) return min;\n\n\t\treturn round(min + wrap(value, range));\n\t}\n\n\t// A step that does not divide the range leaves a short seam, where a value goes to the nearer side\n\tfunction ring(value: number, span: number): number {\n\t\tif (range <= 0) return min;\n\n\t\tconst offset = wrap(value, span);\n\t\tif (offset > top) return span - offset <= offset - top ? min : round(min + top);\n\n\t\treturn round(min + Math.round(offset / step) * step);\n\t}\n\n\tfunction snap(value: number): number {\n\t\tif (positions) return nearestPosition(positions, value);\n\t\tif (step <= 0) return bound(round(value));\n\t\tif (spec.isWrapping) return ring(value, range);\n\n\t\treturn bound(round(min + Math.round((value - min) / step) * step));\n\t}\n\n\tfunction stepBy(from: number, steps: number, size: number): number {\n\t\tif (positions) {\n\t\t\tconst index = clamp(positions.indexOf(from) + steps, 0, positions.length - 1);\n\n\t\t\treturn positions[index] ?? from;\n\t\t}\n\t\tif (!curve) return from + steps * size;\n\n\t\tconst next = snap(curve.valueAt(curve.proportionOf(from) + steps / 100));\n\n\t\treturn next === from ? from + Math.sign(steps) * size : next;\n\t}\n\n\tfunction stopAtDetent(from: number, next: number): number {\n\t\tif (spec.detent === undefined) return next;\n\n\t\tconst detent = snap(spec.detent);\n\t\tconst toward = Math.sign(next - from) * (detent - from);\n\t\tconst ahead = spec.isWrapping ? wrap(min + toward, isStepped ? count * step : range) : toward;\n\n\t\treturn ahead > 0 && ahead < Math.abs(next - from) ? detent : next;\n\t}\n\n\tfunction keyStride(keyStep: number): number | undefined {\n\t\tif (positions || !(keyStep > 0)) return undefined;\n\n\t\treturn step > 0 ? Math.max(1, Math.round(trimFloat(keyStep / step))) * step : keyStep;\n\t}\n\n\tfunction end(size: number): number {\n\t\tif (!spec.isWrapping) return bounds[1];\n\n\t\treturn min + (isStepped ? top : (Math.ceil(range / size) - 1) * size);\n\t}\n\n\tfunction target(key: string, from: number, keyStep: number): number | undefined {\n\t\tif (key === 'Home') return bounds[0];\n\n\t\tconst size = step > 0 ? step : range / 100;\n\t\tif (key === 'End') return snap(end(size));\n\n\t\tconst steps = keySteps.get(key);\n\t\tif (steps === undefined) return undefined;\n\n\t\tconst stride = keyStride(keyStep);\n\t\tconst moved = stride === undefined ? stepBy(from, steps, size) : from + steps * stride;\n\t\tconst next = stopAtDetent(from, moved);\n\n\t\treturn isStepped && spec.isWrapping ? ring(next, count * step) : snap(next);\n\t}\n\n\treturn {\n\t\tbounds,\n\t\tisWrapping: spec.isWrapping,\n\t\tkeyTarget: (key, from, keyStep = 0) => target(key, from, keyStep),\n\t\tpositionCount: countPositions(spec, positions?.length ?? count),\n\t\tproportionOf: (value) => taper.proportionOf(bound(value)),\n\t\tsnap,\n\t\tvalueAt: (proportion) => taper.valueAt(proportion),\n\t};\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"value-mapping.js","names":[],"sources":["../../../src/lib/value-mapping.ts"],"sourcesContent":["import type { Taper } from '#lib/taper.ts';\n\nimport { clamp, decimalPlaces, roundTo, trimFloat } from '#lib/math.ts';\nimport { nearestPosition } from '#lib/number-list.ts';\nimport { linearTaper, listTaper, logTaper, ringTaper, skewTaper } from '#lib/taper.ts';\n\nexport interface ValueSpec {\n\tdetent?: number;\n\tisNotched: boolean;\n\tisWrapping: boolean;\n\tmax: number;\n\tmidpoint?: number;\n\tmin: number;\n\tpositions?: ReadonlyArray<number>;\n\tstep: number;\n\ttaper: 'linear' | 'log';\n}\n\nexport interface ValueMapping {\n\treadonly bounds: [number, number];\n\treadonly isWrapping: boolean;\n\treadonly keyTarget: (key: string, from: number, keyStep?: number) => number | undefined;\n\treadonly positionCount: number | undefined;\n\treadonly proportionOf: (value: number) => number;\n\treadonly snap: (value: number) => number;\n\treadonly valueAt: (proportion: number) => number;\n}\n\n// A Mac keyboard's Delete sends Backspace\nexport const resetKeys = new Set(['Backspace', 'Delete']);\n\nconst pageSteps = 10;\n\nconst keySteps = new Map([\n\t['ArrowDown', -1],\n\t['ArrowLeft', -1],\n\t['ArrowRight', 1],\n\t['ArrowUp', 1],\n\t['PageDown', -pageSteps],\n\t['PageUp', pageSteps],\n]);\n\nfunction positionsIn(spec: ValueSpec, max: number): ReadonlyArray<number> | undefined {\n\tif (!spec.isWrapping) return spec.positions;\n\n\tconst withinTurn = spec.positions?.filter((position) => position >= spec.min && position < max);\n\n\treturn withinTurn && withinTurn.length >= 2 ? withinTurn : undefined;\n}\n\nfunction curveOf(\n\tspec: ValueSpec,\n\tmax: number,\n\tpositions: ReadonlyArray<number> | undefined,\n): Taper | undefined {\n\tconst { min } = spec;\n\tif (positions) return spec.isWrapping ? ringTaper(positions, max - min) : listTaper(positions);\n\tif (spec.isNotched || spec.isWrapping) return undefined;\n\n\tconst log = spec.taper === 'log' ? logTaper(min, max) : undefined;\n\n\treturn log ?? skewTaper(min, max, spec.midpoint ?? NaN);\n}\n\nfunction countPositions(spec: ValueSpec, count: number): number | undefined {\n\tif (!spec.isNotched || !Number.isFinite(count) || count < 2) return;\n\n\treturn count;\n}\n\n// A wrapping mapping stops a step short of max, which is min again\nfunction countSteps(spec: ValueSpec, range: number): number {\n\tif (spec.step <= 0) return NaN;\n\n\tconst fit = trimFloat(range / spec.step);\n\n\treturn spec.isWrapping ? Math.max(1, Math.ceil(fit)) : Math.floor(fit) + 1;\n}\n\nexport function valueMapping(spec: ValueSpec): ValueMapping {\n\tconst { min, step } = spec;\n\tconst max = Math.max(min, spec.max);\n\tconst range = max - min;\n\tconst positions = positionsIn(spec, max);\n\tconst count = countSteps(spec, range);\n\tconst isStepped = step > 0 && !positions;\n\tconst curve = curveOf(spec, max, positions);\n\tconst taper = curve ?? linearTaper(min, max);\n\n\t// Decimal places, since significant digits keep the float residue beside 0\n\tfunction round(value: number): number {\n\t\tif (!isStepped) return trimFloat(value);\n\n\t\treturn roundTo(value, Math.max(decimalPlaces(step), decimalPlaces(min)));\n\t}\n\n\tconst top = round((count - 1) * step);\n\n\tfunction limits(): [number, number] {\n\t\tif (positions) return [positions[0] ?? min, positions.at(-1) ?? max];\n\n\t\treturn [min, isStepped && !spec.isWrapping ? round(min + top) : max];\n\t}\n\n\tconst bounds = limits();\n\n\tfunction wrap(value: number, span: number): number {\n\t\tconst offset = round((((value - min) % span) + span) % span);\n\n\t\treturn offset >= span ? 0 : offset;\n\t}\n\n\tfunction bound(value: number): number {\n\t\tif (!spec.isWrapping) return clamp(value, ...bounds);\n\t\tif (range <= 0) return min;\n\n\t\tconst wrapped = round(min + wrap(value, range));\n\n\t\treturn wrapped >= max ? min : wrapped;\n\t}\n\n\t// A step that does not divide the range leaves a short seam, where a value goes to the nearer side\n\tfunction ring(value: number, span: number): number {\n\t\tif (range <= 0) return min;\n\n\t\tconst offset = wrap(value, span);\n\t\tif (offset > top) return span - offset <= offset - top ? min : round(min + top);\n\n\t\treturn round(min + Math.round(offset / step) * step);\n\t}\n\n\tfunction snapPosition(list: ReadonlyArray<number>, value: number): number {\n\t\tif (!spec.isWrapping) return nearestPosition(list, value);\n\n\t\tconst wrapped = bound(value);\n\n\t\treturn nearestPosition(list, wrapped, (position) => {\n\t\t\tconst gap = Math.abs(position - wrapped);\n\n\t\t\treturn Math.min(gap, range - gap);\n\t\t});\n\t}\n\n\tfunction snap(value: number): number {\n\t\tif (positions) return snapPosition(positions, value);\n\t\tif (step <= 0) return bound(round(value));\n\t\tif (spec.isWrapping) return ring(value, range);\n\n\t\treturn bound(round(min + Math.round((value - min) / step) * step));\n\t}\n\n\tfunction stepBy(from: number, steps: number, size: number): number {\n\t\tif (positions) {\n\t\t\tconst { length } = positions;\n\t\t\tconst moved = positions.indexOf(from) + steps;\n\t\t\tconst index = spec.isWrapping\n\t\t\t\t? ((moved % length) + length) % length\n\t\t\t\t: clamp(moved, 0, length - 1);\n\n\t\t\treturn positions[index] ?? from;\n\t\t}\n\t\tif (!curve) return from + steps * size;\n\n\t\tconst next = snap(curve.valueAt(curve.proportionOf(from) + steps / 100));\n\n\t\treturn next === from ? from + Math.sign(steps) * size : next;\n\t}\n\n\tfunction stopAtDetent(from: number, next: number): number {\n\t\tif (spec.detent === undefined) return next;\n\n\t\tconst detent = snap(spec.detent);\n\t\tconst toward = Math.sign(next - from) * (detent - from);\n\t\tconst ahead = spec.isWrapping ? wrap(min + toward, isStepped ? count * step : range) : toward;\n\n\t\treturn ahead > 0 && ahead < Math.abs(next - from) ? detent : next;\n\t}\n\n\tfunction keyStride(keyStep: number): number | undefined {\n\t\tif (positions || !(keyStep > 0)) return undefined;\n\n\t\treturn step > 0 ? Math.max(1, Math.round(trimFloat(keyStep / step))) * step : keyStep;\n\t}\n\n\tfunction end(size: number): number {\n\t\tif (positions || !spec.isWrapping) return bounds[1];\n\n\t\treturn min + (isStepped ? top : (Math.ceil(range / size) - 1) * size);\n\t}\n\n\tfunction target(key: string, from: number, keyStep: number): number | undefined {\n\t\tif (key === 'Home') return bounds[0];\n\n\t\tconst size = step > 0 ? step : range / 100;\n\t\tif (key === 'End') return snap(end(size));\n\n\t\tconst steps = keySteps.get(key);\n\t\tif (steps === undefined) return undefined;\n\n\t\tconst stride = keyStride(keyStep);\n\t\tconst moved = stride === undefined ? stepBy(from, steps, size) : from + steps * stride;\n\t\tconst next = stopAtDetent(from, moved);\n\n\t\treturn isStepped && spec.isWrapping ? ring(next, count * step) : snap(next);\n\t}\n\n\treturn {\n\t\tbounds,\n\t\tisWrapping: spec.isWrapping,\n\t\tkeyTarget: (key, from, keyStep = 0) => target(key, from, keyStep),\n\t\tpositionCount: countPositions(spec, positions?.length ?? count),\n\t\tproportionOf: (value) => taper.proportionOf(bound(value)),\n\t\tsnap,\n\t\tvalueAt: (proportion) => taper.valueAt(proportion),\n\t};\n}\n"],"mappings":"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"}
|