@xsynaptic/sonic-ui 0.28.0 → 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 +13 -0
- package/dist/default/elements/define-once.js +4 -1
- package/dist/default/elements/ticker.js +24 -6
- 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/elements/meter.d.ts +1 -0
- package/dist/dev/elements/position-group.d.ts +1 -0
- package/dist/dev/elements/spectrum.d.ts +1 -0
- package/dist/dev/elements/ticker.d.ts +1 -1
- package/dist/dev/elements/ticker.js +24 -6
- package/dist/dev/elements/ticker.js.map +1 -1
- package/dist/dev/elements/value-element.d.ts +1 -0
- package/dist/dev/elements/wavestrip.d.ts +1 -0
- package/dist/dev/lib/clock.js +3 -4
- package/dist/dev/lib/clock.js.map +1 -1
- package/dist/dev/lib/crossfade.d.ts +1 -0
- 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/dist/styles/ticker.css +3 -3
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
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
|
+
|
|
10
|
+
## 0.28.1
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- Ticker: setting `text` to the text it already shows no longer sends the tape back to the start, and `disabled` now holds the ticker at rest with no scrolling and no drag.
|
|
15
|
+
|
|
3
16
|
## 0.28.0
|
|
4
17
|
|
|
5
18
|
### Minor 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
|
|
@@ -17,7 +17,7 @@ const fadePx = 12;
|
|
|
17
17
|
const holdMs = 2e3;
|
|
18
18
|
const ratePxPerSecond = 40;
|
|
19
19
|
var SonicTicker = class extends SonicElement {
|
|
20
|
-
static observedAttributes = ["paused"];
|
|
20
|
+
static observedAttributes = ["disabled", "paused"];
|
|
21
21
|
get cancellable() {
|
|
22
22
|
return this.hasAttribute("cancellable");
|
|
23
23
|
}
|
|
@@ -34,12 +34,15 @@ var SonicTicker = class extends SonicElement {
|
|
|
34
34
|
return this.#originals().map((original) => original.textContent).join("");
|
|
35
35
|
}
|
|
36
36
|
set text(text) {
|
|
37
|
+
const next = text ?? "";
|
|
38
|
+
if (this.#holds(next)) return;
|
|
37
39
|
for (const original of this.#originals()) original.remove();
|
|
38
|
-
if (
|
|
40
|
+
if (next !== "") this.prepend(next);
|
|
39
41
|
}
|
|
40
42
|
control = renderTicker();
|
|
41
43
|
sheet = "ticker.css";
|
|
42
44
|
sheetToken = "--_sonic-ticker-fade";
|
|
45
|
+
#drag;
|
|
43
46
|
#fade = fadePx;
|
|
44
47
|
#heading = 1;
|
|
45
48
|
#isClickSwallowed = false;
|
|
@@ -52,22 +55,31 @@ var SonicTicker = class extends SonicElement {
|
|
|
52
55
|
#position = 0;
|
|
53
56
|
#reducedMotion = matchMedia("(prefers-reduced-motion: reduce)");
|
|
54
57
|
#running;
|
|
58
|
+
#tape = requireChild(this.control, ".sonic-ticker-tape", HTMLSpanElement);
|
|
55
59
|
#travel = {
|
|
56
60
|
distance: 0,
|
|
57
61
|
hold: holdMs,
|
|
58
62
|
rate: ratePxPerSecond
|
|
59
63
|
};
|
|
60
|
-
attributeChangedCallback() {
|
|
64
|
+
attributeChangedCallback(name) {
|
|
65
|
+
if (name === "disabled" && this.isBound()) {
|
|
66
|
+
this.#drag?.end();
|
|
67
|
+
this.#run(this.#tape, {
|
|
68
|
+
from: 0,
|
|
69
|
+
heading: 1
|
|
70
|
+
});
|
|
71
|
+
}
|
|
61
72
|
this.render();
|
|
62
73
|
}
|
|
63
74
|
connect(signal) {
|
|
64
75
|
const control = this.control;
|
|
65
|
-
const tape =
|
|
76
|
+
const tape = this.#tape;
|
|
66
77
|
const measure = () => {
|
|
67
78
|
this.#measure(tape);
|
|
68
79
|
};
|
|
69
80
|
const observer = new ResizeObserver(measure);
|
|
70
81
|
const drag = this.#bindDrag(tape, signal);
|
|
82
|
+
this.#drag = drag;
|
|
71
83
|
this.upgradeProperties("cancellable", "paused", "text");
|
|
72
84
|
mirrorChildren(this, {
|
|
73
85
|
canCopyLinks: true,
|
|
@@ -117,7 +129,7 @@ var SonicTicker = class extends SonicElement {
|
|
|
117
129
|
}, { signal });
|
|
118
130
|
return bindDrag(control, {
|
|
119
131
|
grab: (event) => {
|
|
120
|
-
if (this.#travel.distance <= 0) return;
|
|
132
|
+
if (this.#travel.distance <= 0 || this.isDisabled()) return;
|
|
121
133
|
return {
|
|
122
134
|
from: 0,
|
|
123
135
|
isDragging: false,
|
|
@@ -193,6 +205,12 @@ var SonicTicker = class extends SonicElement {
|
|
|
193
205
|
view.disconnect();
|
|
194
206
|
}, { once: true });
|
|
195
207
|
}
|
|
208
|
+
#holds(text) {
|
|
209
|
+
const [only, ...rest] = this.#originals();
|
|
210
|
+
if (only === void 0) return text === "";
|
|
211
|
+
if (rest.length > 0 || !(only instanceof Text)) return false;
|
|
212
|
+
return only.data === text;
|
|
213
|
+
}
|
|
196
214
|
#isPaused() {
|
|
197
215
|
return this.paused || this.#isHovered || this.#isPressed || this.#isOffScreen || this.control.closest(":focus-visible") !== null;
|
|
198
216
|
}
|
|
@@ -226,7 +244,7 @@ var SonicTicker = class extends SonicElement {
|
|
|
226
244
|
this.#writeFades(this.#position);
|
|
227
245
|
}
|
|
228
246
|
#run(tape, departure = this.#where()) {
|
|
229
|
-
const leg = !this.#isFocusInside && !this.#reducedMotion.matches && !this.hasState("dragging") ? tickerLeg(this.#travel, departure) : void 0;
|
|
247
|
+
const leg = !this.#isFocusInside && !this.#reducedMotion.matches && !this.hasState("dragging") && !this.isDisabled() ? tickerLeg(this.#travel, departure) : void 0;
|
|
230
248
|
this.#stop();
|
|
231
249
|
this.#heading = departure.heading;
|
|
232
250
|
if (leg) this.#start(tape, leg);
|
|
@@ -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"}
|
|
@@ -17,7 +17,7 @@ export declare class SonicTicker extends SonicElement {
|
|
|
17
17
|
protected readonly control: HTMLSpanElement;
|
|
18
18
|
protected readonly sheet = "ticker.css";
|
|
19
19
|
protected readonly sheetToken = "--_sonic-ticker-fade";
|
|
20
|
-
attributeChangedCallback(): void;
|
|
20
|
+
attributeChangedCallback(name: string): void;
|
|
21
21
|
protected connect(signal: AbortSignal): void;
|
|
22
22
|
protected render(): void;
|
|
23
23
|
}
|
|
@@ -17,7 +17,7 @@ const fadePx = 12;
|
|
|
17
17
|
const holdMs = 2e3;
|
|
18
18
|
const ratePxPerSecond = 40;
|
|
19
19
|
var SonicTicker = class extends SonicElement {
|
|
20
|
-
static observedAttributes = ["paused"];
|
|
20
|
+
static observedAttributes = ["disabled", "paused"];
|
|
21
21
|
get cancellable() {
|
|
22
22
|
return this.hasAttribute("cancellable");
|
|
23
23
|
}
|
|
@@ -34,12 +34,15 @@ var SonicTicker = class extends SonicElement {
|
|
|
34
34
|
return this.#originals().map((original) => original.textContent).join("");
|
|
35
35
|
}
|
|
36
36
|
set text(text) {
|
|
37
|
+
const next = text ?? "";
|
|
38
|
+
if (this.#holds(next)) return;
|
|
37
39
|
for (const original of this.#originals()) original.remove();
|
|
38
|
-
if (
|
|
40
|
+
if (next !== "") this.prepend(next);
|
|
39
41
|
}
|
|
40
42
|
control = renderTicker();
|
|
41
43
|
sheet = "ticker.css";
|
|
42
44
|
sheetToken = "--_sonic-ticker-fade";
|
|
45
|
+
#drag;
|
|
43
46
|
#fade = fadePx;
|
|
44
47
|
#heading = 1;
|
|
45
48
|
#isClickSwallowed = false;
|
|
@@ -52,22 +55,31 @@ var SonicTicker = class extends SonicElement {
|
|
|
52
55
|
#position = 0;
|
|
53
56
|
#reducedMotion = matchMedia("(prefers-reduced-motion: reduce)");
|
|
54
57
|
#running;
|
|
58
|
+
#tape = requireChild(this.control, ".sonic-ticker-tape", HTMLSpanElement);
|
|
55
59
|
#travel = {
|
|
56
60
|
distance: 0,
|
|
57
61
|
hold: holdMs,
|
|
58
62
|
rate: ratePxPerSecond
|
|
59
63
|
};
|
|
60
|
-
attributeChangedCallback() {
|
|
64
|
+
attributeChangedCallback(name) {
|
|
65
|
+
if (name === "disabled" && this.isBound()) {
|
|
66
|
+
this.#drag?.end();
|
|
67
|
+
this.#run(this.#tape, {
|
|
68
|
+
from: 0,
|
|
69
|
+
heading: 1
|
|
70
|
+
});
|
|
71
|
+
}
|
|
61
72
|
this.render();
|
|
62
73
|
}
|
|
63
74
|
connect(signal) {
|
|
64
75
|
const control = this.control;
|
|
65
|
-
const tape =
|
|
76
|
+
const tape = this.#tape;
|
|
66
77
|
const measure = () => {
|
|
67
78
|
this.#measure(tape);
|
|
68
79
|
};
|
|
69
80
|
const observer = new ResizeObserver(measure);
|
|
70
81
|
const drag = this.#bindDrag(tape, signal);
|
|
82
|
+
this.#drag = drag;
|
|
71
83
|
this.upgradeProperties("cancellable", "paused", "text");
|
|
72
84
|
mirrorChildren(this, {
|
|
73
85
|
canCopyLinks: true,
|
|
@@ -117,7 +129,7 @@ var SonicTicker = class extends SonicElement {
|
|
|
117
129
|
}, { signal });
|
|
118
130
|
return bindDrag(control, {
|
|
119
131
|
grab: (event) => {
|
|
120
|
-
if (this.#travel.distance <= 0) return;
|
|
132
|
+
if (this.#travel.distance <= 0 || this.isDisabled()) return;
|
|
121
133
|
return {
|
|
122
134
|
from: 0,
|
|
123
135
|
isDragging: false,
|
|
@@ -193,6 +205,12 @@ var SonicTicker = class extends SonicElement {
|
|
|
193
205
|
view.disconnect();
|
|
194
206
|
}, { once: true });
|
|
195
207
|
}
|
|
208
|
+
#holds(text) {
|
|
209
|
+
const [only, ...rest] = this.#originals();
|
|
210
|
+
if (only === void 0) return text === "";
|
|
211
|
+
if (rest.length > 0 || !(only instanceof Text)) return false;
|
|
212
|
+
return only.data === text;
|
|
213
|
+
}
|
|
196
214
|
#isPaused() {
|
|
197
215
|
return this.paused || this.#isHovered || this.#isPressed || this.#isOffScreen || this.control.closest(":focus-visible") !== null;
|
|
198
216
|
}
|
|
@@ -226,7 +244,7 @@ var SonicTicker = class extends SonicElement {
|
|
|
226
244
|
this.#writeFades(this.#position);
|
|
227
245
|
}
|
|
228
246
|
#run(tape, departure = this.#where()) {
|
|
229
|
-
const leg = !this.#isFocusInside && !this.#reducedMotion.matches && !this.hasState("dragging") ? tickerLeg(this.#travel, departure) : void 0;
|
|
247
|
+
const leg = !this.#isFocusInside && !this.#reducedMotion.matches && !this.hasState("dragging") && !this.isDisabled() ? tickerLeg(this.#travel, departure) : void 0;
|
|
230
248
|
this.#stop();
|
|
231
249
|
this.#heading = departure.heading;
|
|
232
250
|
if (leg) this.#start(tape, leg);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ticker.js","names":[],"sources":["../../../src/elements/ticker.ts"],"sourcesContent":["import type { TickerDeparture, TickerHeading, TickerLeg, TickerTravel } from '#lib/ticker-leg.ts';\n\nimport { SonicElement } from '#elements/sonic-element.ts';\nimport { outsideCancelZone } from '#lib/cancel-zone.ts';\nimport { copyNode } from '#lib/copy-node.ts';\nimport { dragThresholdPx } from '#lib/drag-step.ts';\nimport { clamp } from '#lib/math.ts';\nimport { mirrorChildren } from '#lib/mirror-children.ts';\nimport { bindDrag, capturePointer } from '#lib/pointer-drag.ts';\nimport { readMsProperty } from '#lib/read-ms-property.ts';\nimport { readPxProperty } from '#lib/read-px-property.ts';\nimport { placeChildren, requireChild, template } from '#lib/render.ts';\nimport {\n\tclippedEnds,\n\tfadeWidth,\n\tlegDeparture,\n\tlegFades,\n\tlegPosition,\n\ttickerLeg,\n} from '#lib/ticker-leg.ts';\nimport { writeAttribute } from '#lib/write-attribute.ts';\n\ndeclare global {\n\tinterface HTMLElementTagNameMap {\n\t\t'sonic-ticker': SonicTicker;\n\t}\n}\n\nconst renderTicker = template(\n\t/* HTML */ `<span class=\"sonic-ticker\"><span class=\"sonic-ticker-tape\"></span></span>`,\n\tHTMLSpanElement,\n);\n\nconst fadePx = 12;\n\nconst holdMs = 2000;\n\nconst ratePxPerSecond = 40;\n\ninterface Running {\n\tanimation: Animation;\n\tfades: Array<Animation>;\n\tleg: TickerLeg;\n}\n\ninterface TapeDrag {\n\tfrom: number;\n\tisDragging: boolean;\n\toutside: ((event: PointerEvent) => boolean) | undefined;\n\tthresholdPx: number;\n\tx: number;\n}\n\nexport class SonicTicker extends SonicElement {\n\tstatic override readonly observedAttributes = ['paused'];\n\n\t// fallow-ignore-next-line code-duplication -- one accessor pair per reflected attribute\n\tget cancellable(): boolean {\n\t\treturn this.hasAttribute('cancellable');\n\t}\n\n\tset cancellable(isCancellable: boolean) {\n\t\tthis.reflect('cancellable', isCancellable);\n\t}\n\n\tget paused(): boolean {\n\t\treturn this.hasAttribute('paused');\n\t}\n\n\tset paused(isPaused: boolean) {\n\t\tthis.reflect('paused', isPaused);\n\t}\n\n\tget text(): string {\n\t\treturn this.#originals()\n\t\t\t.map((original) => original.textContent)\n\t\t\t.join('');\n\t}\n\n\tset text(text: string | undefined) {\n\t\tfor (const original of this.#originals()) original.remove();\n\t\tif (text !== undefined && text !== '') this.prepend(text);\n\t}\n\n\tprotected override readonly control = renderTicker();\n\n\tprotected override readonly sheet = 'ticker.css';\n\n\tprotected override readonly sheetToken = '--_sonic-ticker-fade';\n\n\t#fade = fadePx;\n\n\t#heading: TickerHeading = 1;\n\n\t#isClickSwallowed = false;\n\n\t#isFocusInside = false;\n\n\t#isHovered = false;\n\n\t#isOffScreen = false;\n\n\t#isPressed = false;\n\n\t#mark: ReturnType<typeof setTimeout> | undefined;\n\n\t#measured = '';\n\n\t#position = 0;\n\n\treadonly #reducedMotion = matchMedia('(prefers-reduced-motion: reduce)');\n\n\t#running: Running | undefined;\n\n\t#travel: TickerTravel = { distance: 0, hold: holdMs, rate: ratePxPerSecond };\n\n\tattributeChangedCallback(): void {\n\t\tthis.render();\n\t}\n\n\tprotected connect(signal: AbortSignal): void {\n\t\tconst control = this.control;\n\t\tconst tape = requireChild(control, '.sonic-ticker-tape', HTMLSpanElement);\n\t\tconst measure = (): void => {\n\t\t\tthis.#measure(tape);\n\t\t};\n\t\tconst observer = new ResizeObserver(measure);\n\t\tconst drag = this.#bindDrag(tape, signal);\n\n\t\tthis.upgradeProperties('cancellable', 'paused', 'text');\n\t\tmirrorChildren(\n\t\t\tthis,\n\t\t\t{\n\t\t\t\tcanCopyLinks: true,\n\t\t\t\tcontrol,\n\t\t\t\tcopy: copyNode,\n\t\t\t\tplace: (copies) => {\n\t\t\t\t\tplaceChildren(tape, copies);\n\t\t\t\t\tdrag.end();\n\t\t\t\t\tthis.#measure(tape, { from: 0, heading: 1 });\n\t\t\t\t},\n\t\t\t},\n\t\t\tsignal,\n\t\t);\n\t\tthis.render();\n\n\t\tobserver.observe(control);\n\t\tobserver.observe(tape);\n\t\tif ('fonts' in document) document.fonts.addEventListener('loadingdone', measure, { signal });\n\t\tthis.#bindFocus(tape, signal);\n\t\tthis.#bindPauses(signal);\n\t\tthis.#reducedMotion.addEventListener(\n\t\t\t'change',\n\t\t\t() => {\n\t\t\t\tthis.#run(tape);\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t\tsignal.addEventListener(\n\t\t\t'abort',\n\t\t\t() => {\n\t\t\t\tobserver.disconnect();\n\t\t\t\tthis.#stop();\n\t\t\t},\n\t\t\t{ once: true },\n\t\t);\n\t}\n\n\tprotected render(): void {\n\t\tif (!this.isBound()) return;\n\n\t\tthis.toggleState('paused', this.paused);\n\t\tthis.#syncPause();\n\t}\n\n\t#bindDrag(tape: HTMLElement, signal: AbortSignal): { end: () => void } {\n\t\tconst control = this.control;\n\n\t\tcontrol.addEventListener(\n\t\t\t'click',\n\t\t\t(event) => {\n\t\t\t\tif (!this.#isClickSwallowed) return;\n\n\t\t\t\tthis.#isClickSwallowed = false;\n\t\t\t\tevent.preventDefault();\n\t\t\t\tevent.stopPropagation();\n\t\t\t},\n\t\t\t{ capture: true, signal },\n\t\t);\n\t\t// In WebKit, dragging a link that wraps the ticker fires `dragstart` on the link, outside the control\n\t\tthis.getRootNode().addEventListener(\n\t\t\t'dragstart',\n\t\t\t(event) => {\n\t\t\t\tif (this.#isPressed) event.preventDefault();\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\n\t\treturn bindDrag<TapeDrag>(\n\t\t\tcontrol,\n\t\t\t{\n\t\t\t\tgrab: (event) => {\n\t\t\t\t\tif (this.#travel.distance <= 0) return;\n\n\t\t\t\t\treturn {\n\t\t\t\t\t\tfrom: 0,\n\t\t\t\t\t\tisDragging: false,\n\t\t\t\t\t\toutside: undefined,\n\t\t\t\t\t\tthresholdPx: dragThresholdPx(event.pointerType),\n\t\t\t\t\t\tx: event.clientX,\n\t\t\t\t\t};\n\t\t\t\t},\n\t\t\t\tisCaptureLate: true,\n\t\t\t\tmove: (held, event) => {\n\t\t\t\t\tif (!held.isDragging) {\n\t\t\t\t\t\tif (Math.abs(event.clientX - held.x) < held.thresholdPx) return;\n\n\t\t\t\t\t\tconst { from, heading } = this.#where();\n\n\t\t\t\t\t\theld.isDragging = true;\n\t\t\t\t\t\theld.from = from;\n\t\t\t\t\t\theld.outside = this.cancellable\n\t\t\t\t\t\t\t? outsideCancelZone(control, '--_sonic-ticker-cancel-zone')\n\t\t\t\t\t\t\t: undefined;\n\t\t\t\t\t\theld.x = event.clientX;\n\t\t\t\t\t\tcapturePointer(control, event.pointerId);\n\t\t\t\t\t\tthis.toggleState('dragging', true);\n\t\t\t\t\t\tthis.#stop();\n\t\t\t\t\t\tthis.#heading = heading;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst isOutside = held.outside?.(event) === true;\n\n\t\t\t\t\tthis.toggleState('cancelling', isOutside);\n\t\t\t\t\tthis.#place(tape, isOutside ? held.from : held.from - (event.clientX - held.x));\n\t\t\t\t},\n\t\t\t\trelease: (held) => {\n\t\t\t\t\tif (!held.isDragging) return;\n\n\t\t\t\t\t// Safari sends no click after a drag, so the flag is cleared on a timer or it would cancel the next real click\n\t\t\t\t\tthis.#isClickSwallowed = true;\n\t\t\t\t\tsetTimeout(() => {\n\t\t\t\t\t\tthis.#isClickSwallowed = false;\n\t\t\t\t\t}, 0);\n\t\t\t\t\tthis.toggleState('dragging', false);\n\t\t\t\t\tthis.toggleState('cancelling', false);\n\t\t\t\t\tthis.#run(tape);\n\t\t\t\t},\n\t\t\t\ttoggle: (isPressed) => {\n\t\t\t\t\tthis.#isPressed = isPressed;\n\t\t\t\t\tthis.#syncPause();\n\t\t\t\t},\n\t\t\t},\n\t\t\tsignal,\n\t\t);\n\t}\n\n\t#bindFocus(tape: HTMLElement, signal: AbortSignal): void {\n\t\tconst control = this.control;\n\n\t\tcontrol.addEventListener(\n\t\t\t'focusout',\n\t\t\t(event) => {\n\t\t\t\tif (event.relatedTarget instanceof Node && control.contains(event.relatedTarget)) return;\n\n\t\t\t\tcontrol.scrollLeft = 0;\n\t\t\t\tthis.#isFocusInside = false;\n\t\t\t\tthis.#run(tape);\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t\tcontrol.addEventListener(\n\t\t\t'focusin',\n\t\t\t(event) => {\n\t\t\t\tthis.#isFocusInside = true;\n\t\t\t\tthis.#run(tape);\n\t\t\t\tif (event.target instanceof Element) event.target.scrollIntoView({ block: 'nearest' });\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t}\n\n\t#bindPauses(signal: AbortSignal): void {\n\t\tconst control = this.control;\n\n\t\tfor (const type of ['pointerenter', 'pointerleave'] as const) {\n\t\t\tcontrol.addEventListener(\n\t\t\t\ttype,\n\t\t\t\t(event) => {\n\t\t\t\t\tif (event.pointerType !== 'mouse') return;\n\n\t\t\t\t\tthis.#isHovered = type === 'pointerenter';\n\t\t\t\t\tthis.#syncPause();\n\t\t\t\t},\n\t\t\t\t{ signal },\n\t\t\t);\n\t\t}\n\t\tfor (const type of ['focusin', 'focusout'] as const) {\n\t\t\tthis.getRootNode().addEventListener(\n\t\t\t\ttype,\n\t\t\t\t() => {\n\t\t\t\t\tthis.#syncPause();\n\t\t\t\t},\n\t\t\t\t{ signal },\n\t\t\t);\n\t\t}\n\n\t\tconst view = new IntersectionObserver((entries) => {\n\t\t\tthis.#isOffScreen = entries.at(-1)?.isIntersecting === false;\n\t\t\tthis.#syncPause();\n\t\t});\n\n\t\tview.observe(control);\n\t\tsignal.addEventListener(\n\t\t\t'abort',\n\t\t\t() => {\n\t\t\t\tview.disconnect();\n\t\t\t},\n\t\t\t{ once: true },\n\t\t);\n\t}\n\n\t#isPaused(): boolean {\n\t\treturn (\n\t\t\tthis.paused ||\n\t\t\tthis.#isHovered ||\n\t\t\tthis.#isPressed ||\n\t\t\tthis.#isOffScreen ||\n\t\t\tthis.control.closest(':focus-visible') !== null\n\t\t);\n\t}\n\n\t#measure(tape: HTMLElement, departure?: TickerDeparture): void {\n\t\tconst control = this.control;\n\t\tconst styles = getComputedStyle(control);\n\t\tconst box =\n\t\t\tcontrol.clientWidth -\n\t\t\treadPxProperty(styles, 'padding-left', 0) -\n\t\t\treadPxProperty(styles, 'padding-right', 0);\n\t\tconst travel = {\n\t\t\tdistance: Math.max(0, Math.round(tape.offsetWidth - box)),\n\t\t\thold: readMsProperty(styles, '--_sonic-ticker-hold', holdMs),\n\t\t\trate: readPxProperty(styles, '--_sonic-ticker-rate', ratePxPerSecond),\n\t\t};\n\t\tconst fade = readPxProperty(styles, '--_sonic-ticker-fade', fadePx);\n\t\tconst measured = [...Object.values(travel), fade].join(' ');\n\t\tif (!departure && measured === this.#measured) return;\n\n\t\tconst from = departure ?? this.#where();\n\n\t\tthis.#fade = fade;\n\t\tthis.#measured = measured;\n\t\tthis.#travel = travel;\n\t\tthis.toggleState('overflowing', travel.distance > 0);\n\t\tthis.#run(tape, from);\n\t}\n\n\t#originals(): Array<ChildNode> {\n\t\treturn [...this.childNodes].filter((child) => child !== this.control);\n\t}\n\n\t#place(tape: HTMLElement, position: number): void {\n\t\tconst { distance } = this.#travel;\n\n\t\tthis.#position = clamp(position, 0, distance);\n\t\ttape.style.translate = this.#position > 0 ? `${String(-this.#position)}px` : '';\n\t\twriteAttribute(this.control, 'data-sonic-clipped', clippedEnds(this.#position, distance));\n\t\tthis.#writeFades(this.#position);\n\t}\n\n\t#run(tape: HTMLElement, departure: TickerDeparture = this.#where()): void {\n\t\tconst canTravel =\n\t\t\t!this.#isFocusInside && !this.#reducedMotion.matches && !this.hasState('dragging');\n\t\tconst leg = canTravel ? tickerLeg(this.#travel, departure) : undefined;\n\n\t\tthis.#stop();\n\t\tthis.#heading = departure.heading;\n\t\tif (leg) this.#start(tape, leg);\n\t\telse this.#place(tape, this.#isFocusInside ? 0 : departure.from);\n\t}\n\n\t#start(tape: HTMLElement, leg: TickerLeg): void {\n\t\tconst animation = tape.animate(\n\t\t\t{ translate: [`${String(-leg.from)}px`, `${String(-leg.to)}px`] },\n\t\t\t{ delay: leg.hold, duration: leg.duration, fill: 'both' },\n\t\t);\n\n\t\tanimation.addEventListener('finish', () => {\n\t\t\tthis.#run(tape, { from: leg.to, heading: leg.heading });\n\t\t});\n\t\tthis.#running = { animation, fades: [], leg };\n\t\ttape.style.translate = '';\n\t\twriteAttribute(\n\t\t\tthis.control,\n\t\t\t'data-sonic-clipped',\n\t\t\tclippedEnds(leg.from, this.#travel.distance),\n\t\t);\n\t\tthis.#syncPause();\n\t}\n\n\t#startFades(leg: TickerLeg, elapsed: number): Array<Animation> {\n\t\tconst fades = legFades(leg, this.#travel.distance, this.#fade);\n\n\t\treturn (['start', 'end'] as const).flatMap((end) => {\n\t\t\tconst { delay, duration, widths } = fades[end];\n\t\t\tif (duration <= 0 || delay + duration <= elapsed) return [];\n\n\t\t\treturn this.control.animate(\n\t\t\t\t{ [`--_sonic-ticker-fade-${end}`]: widths.map((width) => `${String(width)}px`) },\n\t\t\t\t{ delay: delay - elapsed, duration, fill: 'both' },\n\t\t\t);\n\t\t});\n\t}\n\n\t#stop(): void {\n\t\tconst running = this.#running;\n\n\t\tclearTimeout(this.#mark);\n\t\tif (!running) return;\n\n\t\tfor (const fade of running.fades) fade.cancel();\n\t\trunning.animation.cancel();\n\t\tthis.#running = undefined;\n\t}\n\n\t// Calling `play()` on a finished animation restarts it, so the fades are recreated on resume, not paused\n\t#syncPause(): void {\n\t\tconst running = this.#running;\n\t\tif (!running) return;\n\n\t\tconst { animation, leg } = running;\n\t\tconst elapsed = Number(animation.currentTime);\n\n\t\tclearTimeout(this.#mark);\n\t\tfor (const fade of running.fades) fade.cancel();\n\t\trunning.fades = [];\n\t\tthis.#writeFades(legPosition(leg, elapsed));\n\t\tif (elapsed >= leg.hold) writeAttribute(this.control, 'data-sonic-clipped', 'start end');\n\t\tif (this.#isPaused()) {\n\t\t\tanimation.pause();\n\t\t\treturn;\n\t\t}\n\t\tif (animation.playState === 'paused') animation.play();\n\t\trunning.fades = this.#startFades(leg, elapsed);\n\t\tif (elapsed >= leg.hold) return;\n\n\t\tthis.#mark = setTimeout(() => {\n\t\t\twriteAttribute(this.control, 'data-sonic-clipped', 'start end');\n\t\t}, leg.hold - elapsed);\n\t}\n\n\t#where(): TickerDeparture {\n\t\tconst running = this.#running;\n\t\tif (!running) return { from: this.#position, heading: this.#heading };\n\n\t\treturn legDeparture(running.leg, Number(running.animation.currentTime));\n\t}\n\n\t#writeFades(position: number): void {\n\t\tconst { style } = this.control;\n\n\t\tstyle.setProperty('--_sonic-ticker-fade-start', `${String(fadeWidth(position, this.#fade))}px`);\n\t\tstyle.setProperty(\n\t\t\t'--_sonic-ticker-fade-end',\n\t\t\t`${String(fadeWidth(this.#travel.distance - position, this.#fade))}px`,\n\t\t);\n\t}\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ticker.js","names":[],"sources":["../../../src/elements/ticker.ts"],"sourcesContent":["import type { TickerDeparture, TickerHeading, TickerLeg, TickerTravel } from '#lib/ticker-leg.ts';\n\nimport { SonicElement } from '#elements/sonic-element.ts';\nimport { outsideCancelZone } from '#lib/cancel-zone.ts';\nimport { copyNode } from '#lib/copy-node.ts';\nimport { dragThresholdPx } from '#lib/drag-step.ts';\nimport { clamp } from '#lib/math.ts';\nimport { mirrorChildren } from '#lib/mirror-children.ts';\nimport { bindDrag, capturePointer } from '#lib/pointer-drag.ts';\nimport { readMsProperty } from '#lib/read-ms-property.ts';\nimport { readPxProperty } from '#lib/read-px-property.ts';\nimport { placeChildren, requireChild, template } from '#lib/render.ts';\nimport {\n\tclippedEnds,\n\tfadeWidth,\n\tlegDeparture,\n\tlegFades,\n\tlegPosition,\n\ttickerLeg,\n} from '#lib/ticker-leg.ts';\nimport { writeAttribute } from '#lib/write-attribute.ts';\n\ndeclare global {\n\tinterface HTMLElementTagNameMap {\n\t\t'sonic-ticker': SonicTicker;\n\t}\n}\n\nconst renderTicker = template(\n\t/* HTML */ `<span class=\"sonic-ticker\"><span class=\"sonic-ticker-tape\"></span></span>`,\n\tHTMLSpanElement,\n);\n\nconst fadePx = 12;\n\nconst holdMs = 2000;\n\nconst ratePxPerSecond = 40;\n\ninterface Running {\n\tanimation: Animation;\n\tfades: Array<Animation>;\n\tleg: TickerLeg;\n}\n\ninterface TapeDrag {\n\tfrom: number;\n\tisDragging: boolean;\n\toutside: ((event: PointerEvent) => boolean) | undefined;\n\tthresholdPx: number;\n\tx: number;\n}\n\nexport class SonicTicker extends SonicElement {\n\tstatic override readonly observedAttributes = ['disabled', 'paused'];\n\n\t// fallow-ignore-next-line code-duplication -- one accessor pair per reflected attribute\n\tget cancellable(): boolean {\n\t\treturn this.hasAttribute('cancellable');\n\t}\n\n\tset cancellable(isCancellable: boolean) {\n\t\tthis.reflect('cancellable', isCancellable);\n\t}\n\n\tget paused(): boolean {\n\t\treturn this.hasAttribute('paused');\n\t}\n\n\tset paused(isPaused: boolean) {\n\t\tthis.reflect('paused', isPaused);\n\t}\n\n\tget text(): string {\n\t\treturn this.#originals()\n\t\t\t.map((original) => original.textContent)\n\t\t\t.join('');\n\t}\n\n\tset text(text: string | undefined) {\n\t\tconst next = text ?? '';\n\n\t\t// A host that re-renders the same title would otherwise restart the tape\n\t\tif (this.#holds(next)) return;\n\n\t\tfor (const original of this.#originals()) original.remove();\n\t\tif (next !== '') this.prepend(next);\n\t}\n\n\tprotected override readonly control = renderTicker();\n\n\tprotected override readonly sheet = 'ticker.css';\n\n\tprotected override readonly sheetToken = '--_sonic-ticker-fade';\n\n\t#drag: undefined | { end: () => void };\n\n\t#fade = fadePx;\n\n\t#heading: TickerHeading = 1;\n\n\t#isClickSwallowed = false;\n\n\t#isFocusInside = false;\n\n\t#isHovered = false;\n\n\t#isOffScreen = false;\n\n\t#isPressed = false;\n\n\t#mark: ReturnType<typeof setTimeout> | undefined;\n\n\t#measured = '';\n\n\t#position = 0;\n\n\treadonly #reducedMotion = matchMedia('(prefers-reduced-motion: reduce)');\n\n\t#running: Running | undefined;\n\n\treadonly #tape = requireChild(this.control, '.sonic-ticker-tape', HTMLSpanElement);\n\n\t#travel: TickerTravel = { distance: 0, hold: holdMs, rate: ratePxPerSecond };\n\n\tattributeChangedCallback(name: string): void {\n\t\tif (name === 'disabled' && this.isBound()) {\n\t\t\tthis.#drag?.end();\n\t\t\tthis.#run(this.#tape, { from: 0, heading: 1 });\n\t\t}\n\n\t\tthis.render();\n\t}\n\n\tprotected connect(signal: AbortSignal): void {\n\t\tconst control = this.control;\n\t\tconst tape = this.#tape;\n\t\tconst measure = (): void => {\n\t\t\tthis.#measure(tape);\n\t\t};\n\t\tconst observer = new ResizeObserver(measure);\n\t\tconst drag = this.#bindDrag(tape, signal);\n\n\t\tthis.#drag = drag;\n\n\t\tthis.upgradeProperties('cancellable', 'paused', 'text');\n\t\tmirrorChildren(\n\t\t\tthis,\n\t\t\t{\n\t\t\t\tcanCopyLinks: true,\n\t\t\t\tcontrol,\n\t\t\t\tcopy: copyNode,\n\t\t\t\tplace: (copies) => {\n\t\t\t\t\tplaceChildren(tape, copies);\n\t\t\t\t\tdrag.end();\n\t\t\t\t\tthis.#measure(tape, { from: 0, heading: 1 });\n\t\t\t\t},\n\t\t\t},\n\t\t\tsignal,\n\t\t);\n\t\tthis.render();\n\n\t\tobserver.observe(control);\n\t\tobserver.observe(tape);\n\t\tif ('fonts' in document) document.fonts.addEventListener('loadingdone', measure, { signal });\n\t\tthis.#bindFocus(tape, signal);\n\t\tthis.#bindPauses(signal);\n\t\tthis.#reducedMotion.addEventListener(\n\t\t\t'change',\n\t\t\t() => {\n\t\t\t\tthis.#run(tape);\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t\tsignal.addEventListener(\n\t\t\t'abort',\n\t\t\t() => {\n\t\t\t\tobserver.disconnect();\n\t\t\t\tthis.#stop();\n\t\t\t},\n\t\t\t{ once: true },\n\t\t);\n\t}\n\n\tprotected render(): void {\n\t\tif (!this.isBound()) return;\n\n\t\tthis.toggleState('paused', this.paused);\n\t\tthis.#syncPause();\n\t}\n\n\t#bindDrag(tape: HTMLElement, signal: AbortSignal): { end: () => void } {\n\t\tconst control = this.control;\n\n\t\tcontrol.addEventListener(\n\t\t\t'click',\n\t\t\t(event) => {\n\t\t\t\tif (!this.#isClickSwallowed) return;\n\n\t\t\t\tthis.#isClickSwallowed = false;\n\t\t\t\tevent.preventDefault();\n\t\t\t\tevent.stopPropagation();\n\t\t\t},\n\t\t\t{ capture: true, signal },\n\t\t);\n\t\t// In WebKit, dragging a link that wraps the ticker fires `dragstart` on the link, outside the control\n\t\tthis.getRootNode().addEventListener(\n\t\t\t'dragstart',\n\t\t\t(event) => {\n\t\t\t\tif (this.#isPressed) event.preventDefault();\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\n\t\treturn bindDrag<TapeDrag>(\n\t\t\tcontrol,\n\t\t\t{\n\t\t\t\tgrab: (event) => {\n\t\t\t\t\tif (this.#travel.distance <= 0 || this.isDisabled()) return;\n\n\t\t\t\t\treturn {\n\t\t\t\t\t\tfrom: 0,\n\t\t\t\t\t\tisDragging: false,\n\t\t\t\t\t\toutside: undefined,\n\t\t\t\t\t\tthresholdPx: dragThresholdPx(event.pointerType),\n\t\t\t\t\t\tx: event.clientX,\n\t\t\t\t\t};\n\t\t\t\t},\n\t\t\t\tisCaptureLate: true,\n\t\t\t\tmove: (held, event) => {\n\t\t\t\t\tif (!held.isDragging) {\n\t\t\t\t\t\tif (Math.abs(event.clientX - held.x) < held.thresholdPx) return;\n\n\t\t\t\t\t\tconst { from, heading } = this.#where();\n\n\t\t\t\t\t\theld.isDragging = true;\n\t\t\t\t\t\theld.from = from;\n\t\t\t\t\t\theld.outside = this.cancellable\n\t\t\t\t\t\t\t? outsideCancelZone(control, '--_sonic-ticker-cancel-zone')\n\t\t\t\t\t\t\t: undefined;\n\t\t\t\t\t\theld.x = event.clientX;\n\t\t\t\t\t\tcapturePointer(control, event.pointerId);\n\t\t\t\t\t\tthis.toggleState('dragging', true);\n\t\t\t\t\t\tthis.#stop();\n\t\t\t\t\t\tthis.#heading = heading;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst isOutside = held.outside?.(event) === true;\n\n\t\t\t\t\tthis.toggleState('cancelling', isOutside);\n\t\t\t\t\tthis.#place(tape, isOutside ? held.from : held.from - (event.clientX - held.x));\n\t\t\t\t},\n\t\t\t\trelease: (held) => {\n\t\t\t\t\tif (!held.isDragging) return;\n\n\t\t\t\t\t// Safari sends no click after a drag, so the flag is cleared on a timer or it would cancel the next real click\n\t\t\t\t\tthis.#isClickSwallowed = true;\n\t\t\t\t\tsetTimeout(() => {\n\t\t\t\t\t\tthis.#isClickSwallowed = false;\n\t\t\t\t\t}, 0);\n\t\t\t\t\tthis.toggleState('dragging', false);\n\t\t\t\t\tthis.toggleState('cancelling', false);\n\t\t\t\t\tthis.#run(tape);\n\t\t\t\t},\n\t\t\t\ttoggle: (isPressed) => {\n\t\t\t\t\tthis.#isPressed = isPressed;\n\t\t\t\t\tthis.#syncPause();\n\t\t\t\t},\n\t\t\t},\n\t\t\tsignal,\n\t\t);\n\t}\n\n\t#bindFocus(tape: HTMLElement, signal: AbortSignal): void {\n\t\tconst control = this.control;\n\n\t\tcontrol.addEventListener(\n\t\t\t'focusout',\n\t\t\t(event) => {\n\t\t\t\tif (event.relatedTarget instanceof Node && control.contains(event.relatedTarget)) return;\n\n\t\t\t\tcontrol.scrollLeft = 0;\n\t\t\t\tthis.#isFocusInside = false;\n\t\t\t\tthis.#run(tape);\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t\tcontrol.addEventListener(\n\t\t\t'focusin',\n\t\t\t(event) => {\n\t\t\t\tthis.#isFocusInside = true;\n\t\t\t\tthis.#run(tape);\n\t\t\t\tif (event.target instanceof Element) event.target.scrollIntoView({ block: 'nearest' });\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t}\n\n\t#bindPauses(signal: AbortSignal): void {\n\t\tconst control = this.control;\n\n\t\tfor (const type of ['pointerenter', 'pointerleave'] as const) {\n\t\t\tcontrol.addEventListener(\n\t\t\t\ttype,\n\t\t\t\t(event) => {\n\t\t\t\t\tif (event.pointerType !== 'mouse') return;\n\n\t\t\t\t\tthis.#isHovered = type === 'pointerenter';\n\t\t\t\t\tthis.#syncPause();\n\t\t\t\t},\n\t\t\t\t{ signal },\n\t\t\t);\n\t\t}\n\t\tfor (const type of ['focusin', 'focusout'] as const) {\n\t\t\tthis.getRootNode().addEventListener(\n\t\t\t\ttype,\n\t\t\t\t() => {\n\t\t\t\t\tthis.#syncPause();\n\t\t\t\t},\n\t\t\t\t{ signal },\n\t\t\t);\n\t\t}\n\n\t\tconst view = new IntersectionObserver((entries) => {\n\t\t\tthis.#isOffScreen = entries.at(-1)?.isIntersecting === false;\n\t\t\tthis.#syncPause();\n\t\t});\n\n\t\tview.observe(control);\n\t\tsignal.addEventListener(\n\t\t\t'abort',\n\t\t\t() => {\n\t\t\t\tview.disconnect();\n\t\t\t},\n\t\t\t{ once: true },\n\t\t);\n\t}\n\n\t#holds(text: string): boolean {\n\t\tconst [only, ...rest] = this.#originals();\n\t\tif (only === undefined) return text === '';\n\t\tif (rest.length > 0 || !(only instanceof Text)) return false;\n\n\t\treturn only.data === text;\n\t}\n\n\t#isPaused(): boolean {\n\t\treturn (\n\t\t\tthis.paused ||\n\t\t\tthis.#isHovered ||\n\t\t\tthis.#isPressed ||\n\t\t\tthis.#isOffScreen ||\n\t\t\tthis.control.closest(':focus-visible') !== null\n\t\t);\n\t}\n\n\t#measure(tape: HTMLElement, departure?: TickerDeparture): void {\n\t\tconst control = this.control;\n\t\tconst styles = getComputedStyle(control);\n\t\tconst box =\n\t\t\tcontrol.clientWidth -\n\t\t\treadPxProperty(styles, 'padding-left', 0) -\n\t\t\treadPxProperty(styles, 'padding-right', 0);\n\t\tconst travel = {\n\t\t\tdistance: Math.max(0, Math.round(tape.offsetWidth - box)),\n\t\t\thold: readMsProperty(styles, '--_sonic-ticker-hold', holdMs),\n\t\t\trate: readPxProperty(styles, '--_sonic-ticker-rate', ratePxPerSecond),\n\t\t};\n\t\tconst fade = readPxProperty(styles, '--_sonic-ticker-fade', fadePx);\n\t\tconst measured = [...Object.values(travel), fade].join(' ');\n\t\tif (!departure && measured === this.#measured) return;\n\n\t\tconst from = departure ?? this.#where();\n\n\t\tthis.#fade = fade;\n\t\tthis.#measured = measured;\n\t\tthis.#travel = travel;\n\t\tthis.toggleState('overflowing', travel.distance > 0);\n\t\tthis.#run(tape, from);\n\t}\n\n\t#originals(): Array<ChildNode> {\n\t\treturn [...this.childNodes].filter((child) => child !== this.control);\n\t}\n\n\t#place(tape: HTMLElement, position: number): void {\n\t\tconst { distance } = this.#travel;\n\n\t\tthis.#position = clamp(position, 0, distance);\n\t\ttape.style.translate = this.#position > 0 ? `${String(-this.#position)}px` : '';\n\t\twriteAttribute(this.control, 'data-sonic-clipped', clippedEnds(this.#position, distance));\n\t\tthis.#writeFades(this.#position);\n\t}\n\n\t#run(tape: HTMLElement, departure: TickerDeparture = this.#where()): void {\n\t\tconst canTravel =\n\t\t\t!this.#isFocusInside &&\n\t\t\t!this.#reducedMotion.matches &&\n\t\t\t!this.hasState('dragging') &&\n\t\t\t!this.isDisabled();\n\t\tconst leg = canTravel ? tickerLeg(this.#travel, departure) : undefined;\n\n\t\tthis.#stop();\n\t\tthis.#heading = departure.heading;\n\t\tif (leg) this.#start(tape, leg);\n\t\telse this.#place(tape, this.#isFocusInside ? 0 : departure.from);\n\t}\n\n\t#start(tape: HTMLElement, leg: TickerLeg): void {\n\t\tconst animation = tape.animate(\n\t\t\t{ translate: [`${String(-leg.from)}px`, `${String(-leg.to)}px`] },\n\t\t\t{ delay: leg.hold, duration: leg.duration, fill: 'both' },\n\t\t);\n\n\t\tanimation.addEventListener('finish', () => {\n\t\t\tthis.#run(tape, { from: leg.to, heading: leg.heading });\n\t\t});\n\t\tthis.#running = { animation, fades: [], leg };\n\t\ttape.style.translate = '';\n\t\twriteAttribute(\n\t\t\tthis.control,\n\t\t\t'data-sonic-clipped',\n\t\t\tclippedEnds(leg.from, this.#travel.distance),\n\t\t);\n\t\tthis.#syncPause();\n\t}\n\n\t#startFades(leg: TickerLeg, elapsed: number): Array<Animation> {\n\t\tconst fades = legFades(leg, this.#travel.distance, this.#fade);\n\n\t\treturn (['start', 'end'] as const).flatMap((end) => {\n\t\t\tconst { delay, duration, widths } = fades[end];\n\t\t\tif (duration <= 0 || delay + duration <= elapsed) return [];\n\n\t\t\treturn this.control.animate(\n\t\t\t\t{ [`--_sonic-ticker-fade-${end}`]: widths.map((width) => `${String(width)}px`) },\n\t\t\t\t{ delay: delay - elapsed, duration, fill: 'both' },\n\t\t\t);\n\t\t});\n\t}\n\n\t#stop(): void {\n\t\tconst running = this.#running;\n\n\t\tclearTimeout(this.#mark);\n\t\tif (!running) return;\n\n\t\tfor (const fade of running.fades) fade.cancel();\n\t\trunning.animation.cancel();\n\t\tthis.#running = undefined;\n\t}\n\n\t// Calling `play()` on a finished animation restarts it, so the fades are recreated on resume, not paused\n\t#syncPause(): void {\n\t\tconst running = this.#running;\n\t\tif (!running) return;\n\n\t\tconst { animation, leg } = running;\n\t\tconst elapsed = Number(animation.currentTime);\n\n\t\tclearTimeout(this.#mark);\n\t\tfor (const fade of running.fades) fade.cancel();\n\t\trunning.fades = [];\n\t\tthis.#writeFades(legPosition(leg, elapsed));\n\t\tif (elapsed >= leg.hold) writeAttribute(this.control, 'data-sonic-clipped', 'start end');\n\t\tif (this.#isPaused()) {\n\t\t\tanimation.pause();\n\t\t\treturn;\n\t\t}\n\t\tif (animation.playState === 'paused') animation.play();\n\t\trunning.fades = this.#startFades(leg, elapsed);\n\t\tif (elapsed >= leg.hold) return;\n\n\t\tthis.#mark = setTimeout(() => {\n\t\t\twriteAttribute(this.control, 'data-sonic-clipped', 'start end');\n\t\t}, leg.hold - elapsed);\n\t}\n\n\t#where(): TickerDeparture {\n\t\tconst running = this.#running;\n\t\tif (!running) return { from: this.#position, heading: this.#heading };\n\n\t\treturn legDeparture(running.leg, Number(running.animation.currentTime));\n\t}\n\n\t#writeFades(position: number): void {\n\t\tconst { style } = this.control;\n\n\t\tstyle.setProperty('--_sonic-ticker-fade-start', `${String(fadeWidth(position, this.#fade))}px`);\n\t\tstyle.setProperty(\n\t\t\t'--_sonic-ticker-fade-end',\n\t\t\t`${String(fadeWidth(this.#travel.distance - position, this.#fade))}px`,\n\t\t);\n\t}\n}\n"],"mappings":"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"}
|
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"}
|
|
@@ -10,4 +10,5 @@ interface CrossfadeGains {
|
|
|
10
10
|
/** `position` runs from 0, all a, to 1, all b; `sharpness` from 0 to 1 shortens the overlap */
|
|
11
11
|
export declare function crossfadeGains(position: number, options?: CrossfadeOptions): CrossfadeGains;
|
|
12
12
|
//#endregion
|
|
13
|
+
export {};
|
|
13
14
|
//# sourceMappingURL=crossfade.d.ts.map
|
|
@@ -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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"}
|
package/dist/styles/ticker.css
CHANGED
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
user-select: none;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
.sonic-ticker[data-sonic-overflowing] {
|
|
64
|
+
.sonic-ticker[data-sonic-overflowing]:not([disabled] > *) {
|
|
65
65
|
text-overflow: clip;
|
|
66
66
|
touch-action: var(--sonic-touch-action, pan-y);
|
|
67
67
|
-webkit-mask: linear-gradient(
|
|
@@ -80,11 +80,11 @@
|
|
|
80
80
|
);
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
.sonic-ticker[data-sonic-overflowing] > .sonic-ticker-tape {
|
|
83
|
+
.sonic-ticker[data-sonic-overflowing]:not([disabled] > *) > .sonic-ticker-tape {
|
|
84
84
|
display: inline-block;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
.sonic-ticker[data-sonic-overflowing]:not(:any-link *) {
|
|
87
|
+
.sonic-ticker[data-sonic-overflowing]:not(:any-link *, [disabled] > *) {
|
|
88
88
|
cursor: grab;
|
|
89
89
|
}
|
|
90
90
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xsynaptic/sonic-ui",
|
|
3
|
-
"version": "0.28.
|
|
3
|
+
"version": "0.28.2",
|
|
4
4
|
"description": "Native custom elements for audio interfaces, skinned with CSS custom properties",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"custom-elements",
|
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
],
|
|
80
80
|
"devDependencies": {
|
|
81
81
|
"publint": "^0.3.25",
|
|
82
|
-
"tsdown": "^0.23.
|
|
82
|
+
"tsdown": "^0.23.1",
|
|
83
83
|
"typescript": "^6.0.3"
|
|
84
84
|
},
|
|
85
85
|
"scripts": {
|