@xsynaptic/sonic-ui 0.10.4 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/README.md +5 -16
- package/dist/default/define/toggle.js +7 -0
- package/dist/default/define.js +1 -0
- package/dist/default/elements/button.js +2 -0
- package/dist/default/elements/position-group.js +163 -0
- package/dist/default/elements/radio-group.js +34 -12
- package/dist/default/elements/switch.js +33 -167
- package/dist/default/elements/toggle.js +189 -0
- package/dist/default/elements/waveform.js +30 -10
- package/dist/default/elements/wavestrip.js +4 -0
- package/dist/default/index.js +2 -1
- package/dist/default/lib/key-press.js +27 -0
- package/dist/default/lib/toggle-travel.js +19 -0
- package/dist/dev/define/toggle.d.ts +2 -0
- package/dist/dev/define/toggle.js +8 -0
- package/dist/dev/define/toggle.js.map +1 -0
- package/dist/dev/define.d.ts +3 -1
- package/dist/dev/define.js +1 -0
- package/dist/dev/elements/button.js +2 -0
- package/dist/dev/elements/button.js.map +1 -1
- package/dist/dev/elements/position-group.d.ts +38 -0
- package/dist/dev/elements/position-group.js +164 -0
- package/dist/dev/elements/position-group.js.map +1 -0
- package/dist/dev/elements/radio-group.d.ts +4 -0
- package/dist/dev/elements/radio-group.js +34 -12
- package/dist/dev/elements/radio-group.js.map +1 -1
- package/dist/dev/elements/switch.d.ts +4 -19
- package/dist/dev/elements/switch.js +33 -167
- package/dist/dev/elements/switch.js.map +1 -1
- package/dist/dev/elements/toggle.d.ts +21 -0
- package/dist/dev/elements/toggle.js +197 -0
- package/dist/dev/elements/toggle.js.map +1 -0
- package/dist/dev/elements/waveform.d.ts +1 -1
- package/dist/dev/elements/waveform.js +30 -10
- package/dist/dev/elements/waveform.js.map +1 -1
- package/dist/dev/elements/wavestrip.d.ts +1 -0
- package/dist/dev/elements/wavestrip.js +4 -0
- package/dist/dev/elements/wavestrip.js.map +1 -1
- package/dist/dev/index.d.ts +3 -2
- package/dist/dev/index.js +2 -1
- package/dist/dev/lib/key-press.js +28 -0
- package/dist/dev/lib/key-press.js.map +1 -0
- package/dist/dev/lib/toggle-travel.js +20 -0
- package/dist/dev/lib/toggle-travel.js.map +1 -0
- package/dist/styles/button.css +2 -2
- package/dist/styles/controls.css +1 -0
- package/dist/styles/led.css +16 -13
- package/dist/styles/material.css +36 -9
- package/dist/styles/meter.css +7 -6
- package/dist/styles/segmented.css +8 -4
- package/dist/styles/spectrum.css +2 -2
- package/dist/styles/switch.css +6 -2
- package/dist/styles/toggle.css +481 -0
- package/package.json +2 -1
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { placeChildren, requireChild, template } from "../lib/render.js";
|
|
2
|
+
import { bindDrag } from "../lib/pointer-drag.js";
|
|
3
|
+
import { dragThresholdPx } from "../lib/drag-step.js";
|
|
4
|
+
import { isUnder, optionValue } from "./radio-group.js";
|
|
5
|
+
import { SonicPositionGroupElement } from "./position-group.js";
|
|
6
|
+
import { positionAt, travelFrom } from "../lib/toggle-travel.js";
|
|
7
|
+
|
|
8
|
+
//#region src/elements/toggle.ts
|
|
9
|
+
const renderToggle = template(`
|
|
10
|
+
<div class="sonic-toggle">
|
|
11
|
+
<span aria-hidden="true" class="sonic-toggle-well"></span>
|
|
12
|
+
<span aria-hidden="true" class="sonic-toggle-cap"></span>
|
|
13
|
+
</div>
|
|
14
|
+
`, HTMLDivElement);
|
|
15
|
+
const renderPosition = template(`
|
|
16
|
+
<button class="sonic-toggle-position" role="radio" type="button">
|
|
17
|
+
<span class="sonic-toggle-legend"></span>
|
|
18
|
+
</button>
|
|
19
|
+
`, HTMLButtonElement);
|
|
20
|
+
const renderBare = template(`<button class="sonic-toggle-position" role="switch" type="button"></button>`, HTMLButtonElement);
|
|
21
|
+
var SonicToggle = class extends SonicPositionGroupElement {
|
|
22
|
+
bare = renderBare();
|
|
23
|
+
defaultOrientation = "horizontal";
|
|
24
|
+
group = renderToggle();
|
|
25
|
+
#cap = requireChild(this.group, ".sonic-toggle-cap", HTMLSpanElement);
|
|
26
|
+
#draggedTo;
|
|
27
|
+
#hasWarnedOfLegends = false;
|
|
28
|
+
#well = requireChild(this.group, ".sonic-toggle-well", HTMLSpanElement);
|
|
29
|
+
claimPress() {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
connect(signal) {
|
|
33
|
+
super.connect(signal);
|
|
34
|
+
this.checkStyles(this.group, "toggle.css");
|
|
35
|
+
this.bare.addEventListener("click", (event) => {
|
|
36
|
+
if (event instanceof PointerEvent && event.pointerType !== "") return;
|
|
37
|
+
this.setChecked(!this.checked);
|
|
38
|
+
}, { signal });
|
|
39
|
+
this.#bindCap(signal);
|
|
40
|
+
}
|
|
41
|
+
isMirrored(child) {
|
|
42
|
+
return super.isMirrored(child) || child === this.#fixedLegend();
|
|
43
|
+
}
|
|
44
|
+
placeCopies(copies) {
|
|
45
|
+
const positions = copies.filter((copy) => super.isMirrored(copy));
|
|
46
|
+
placeChildren(this.#cap, copies.filter((copy) => !positions.includes(copy)));
|
|
47
|
+
super.placeCopies(positions);
|
|
48
|
+
}
|
|
49
|
+
render() {
|
|
50
|
+
if (!this.isBound()) return;
|
|
51
|
+
this.#checkLegends();
|
|
52
|
+
this.group.style.setProperty("--_sonic-toggle-count", String(this.#stopCount()));
|
|
53
|
+
this.#placeCap(this.#draggedTo ?? this.#restingAt());
|
|
54
|
+
super.render();
|
|
55
|
+
for (const [index, option] of this.options().entries()) option.style.setProperty("--_sonic-toggle-index", String(index));
|
|
56
|
+
}
|
|
57
|
+
renderOption() {
|
|
58
|
+
return renderPosition();
|
|
59
|
+
}
|
|
60
|
+
#bindCap(signal) {
|
|
61
|
+
bindDrag(this.group, {
|
|
62
|
+
grab: (event) => this.#grab(event),
|
|
63
|
+
lift: (drag, event) => {
|
|
64
|
+
if (drag.hasMoved || drag.isHolding || this.isDisabled()) return;
|
|
65
|
+
if (!isUnder(this.group, event.clientX, event.clientY)) return;
|
|
66
|
+
this.#press(drag.position);
|
|
67
|
+
},
|
|
68
|
+
move: (drag, event) => {
|
|
69
|
+
if (drag.isHolding || this.isDisabled()) return;
|
|
70
|
+
const pointer = this.along(event);
|
|
71
|
+
if (!drag.hasMoved && Math.abs(pointer - drag.start) < drag.thresholdPx) return;
|
|
72
|
+
drag.hasMoved = true;
|
|
73
|
+
this.toggleState("dragging", true);
|
|
74
|
+
this.#slideTo(positionAt(drag.travel, pointer), event.pointerId);
|
|
75
|
+
},
|
|
76
|
+
release: (drag) => {
|
|
77
|
+
delete (drag.position ?? this.bare).dataset.sonicPressed;
|
|
78
|
+
this.#draggedTo = void 0;
|
|
79
|
+
this.toggleState("dragging", false);
|
|
80
|
+
this.render();
|
|
81
|
+
}
|
|
82
|
+
}, signal);
|
|
83
|
+
}
|
|
84
|
+
#checkLegends() {
|
|
85
|
+
if (this.#hasWarnedOfLegends) return;
|
|
86
|
+
const count = this.options().length;
|
|
87
|
+
if (count <= 3 || this.getAttribute("legends") !== "outside") return;
|
|
88
|
+
this.#hasWarnedOfLegends = true;
|
|
89
|
+
console.warn(`<sonic-toggle> lays out legends="outside" for two or three positions, and this one has ${String(count)}`);
|
|
90
|
+
}
|
|
91
|
+
#enabledEnds() {
|
|
92
|
+
if (this.isBare()) return [0, 1];
|
|
93
|
+
const options = this.options();
|
|
94
|
+
const isEnabled = (option) => !this.isOptionDisabled(option);
|
|
95
|
+
return [options.findIndex(isEnabled), options.findLastIndex(isEnabled)];
|
|
96
|
+
}
|
|
97
|
+
#fixedLegend() {
|
|
98
|
+
return [...this.children].find((child) => child !== this.group && !super.isMirrored(child));
|
|
99
|
+
}
|
|
100
|
+
#grab(event) {
|
|
101
|
+
const position = this.optionOf(event.target);
|
|
102
|
+
if (this.isDisabled() || !position && event.target !== this.bare) return void 0;
|
|
103
|
+
const isOnCap = isUnder(this.#cap, event.clientX, event.clientY);
|
|
104
|
+
const travel = this.#travel(event, isOnCap);
|
|
105
|
+
if (!travel || this.#isBlocked(position, isOnCap)) return void 0;
|
|
106
|
+
(position ?? this.bare).toggleAttribute("data-sonic-pressed", true);
|
|
107
|
+
return {
|
|
108
|
+
hasMoved: false,
|
|
109
|
+
isHolding: this.#holdFrom(position, event.pointerId),
|
|
110
|
+
position,
|
|
111
|
+
start: this.along(event),
|
|
112
|
+
thresholdPx: dragThresholdPx(event.pointerType),
|
|
113
|
+
travel
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
#holdFrom(position, pointerId) {
|
|
117
|
+
const target = position && this.pressTarget(position);
|
|
118
|
+
if (!target || !this.isMomentary(target) || this.isOptionDisabled(target)) return false;
|
|
119
|
+
this.hold(target, pointerId);
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
#isBlocked(position, isOnCap) {
|
|
123
|
+
return !isOnCap && position !== void 0 && this.isOptionDisabled(position);
|
|
124
|
+
}
|
|
125
|
+
#onAt() {
|
|
126
|
+
return this.orientation === "vertical" ? 0 : 1;
|
|
127
|
+
}
|
|
128
|
+
#placeCap(at) {
|
|
129
|
+
this.group.style.setProperty("--_sonic-toggle-at", String(at));
|
|
130
|
+
}
|
|
131
|
+
#press(position) {
|
|
132
|
+
if (!position) {
|
|
133
|
+
this.setChecked(!this.checked);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const target = this.pressTarget(position);
|
|
137
|
+
if (this.isOptionDisabled(target)) return;
|
|
138
|
+
this.select(target);
|
|
139
|
+
this.#refocus(target);
|
|
140
|
+
}
|
|
141
|
+
#refocus(position) {
|
|
142
|
+
if (this.group.matches(":focus-within")) position.focus();
|
|
143
|
+
}
|
|
144
|
+
#restingAt() {
|
|
145
|
+
if (this.isBare()) return this.checked ? this.#onAt() : 1 - this.#onAt();
|
|
146
|
+
return Math.max(0, this.checkedIndex(this.options()));
|
|
147
|
+
}
|
|
148
|
+
#slideTo(at, pointerId) {
|
|
149
|
+
const crossed = this.options()[Math.round(at)];
|
|
150
|
+
this.#draggedTo = at;
|
|
151
|
+
this.#placeCap(at);
|
|
152
|
+
if (this.isBare()) {
|
|
153
|
+
this.setChecked(Math.round(at) === this.#onAt());
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (!crossed || optionValue(crossed) === this.value) return;
|
|
157
|
+
this.releaseHold();
|
|
158
|
+
if (this.isOptionDisabled(crossed) || optionValue(crossed) === this.value) return;
|
|
159
|
+
if (this.isMomentary(crossed)) this.hold(crossed, pointerId);
|
|
160
|
+
else this.select(crossed);
|
|
161
|
+
this.#refocus(crossed);
|
|
162
|
+
}
|
|
163
|
+
#stopCount() {
|
|
164
|
+
return this.options().length || 2;
|
|
165
|
+
}
|
|
166
|
+
#travel(event, isOnCap) {
|
|
167
|
+
const [first, last] = this.#enabledEnds();
|
|
168
|
+
if (first < 0) return void 0;
|
|
169
|
+
const box = this.#well.getBoundingClientRect();
|
|
170
|
+
const [start, length, thickness] = this.orientation === "vertical" ? [
|
|
171
|
+
box.top,
|
|
172
|
+
box.height,
|
|
173
|
+
box.width
|
|
174
|
+
] : [
|
|
175
|
+
box.left,
|
|
176
|
+
box.width,
|
|
177
|
+
box.height
|
|
178
|
+
];
|
|
179
|
+
return travelFrom({
|
|
180
|
+
at: this.#restingAt(),
|
|
181
|
+
count: this.#stopCount(),
|
|
182
|
+
first,
|
|
183
|
+
isOnCap,
|
|
184
|
+
last,
|
|
185
|
+
pointer: this.along(event),
|
|
186
|
+
well: {
|
|
187
|
+
length,
|
|
188
|
+
start,
|
|
189
|
+
thickness
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
//#endregion
|
|
196
|
+
export { SonicToggle };
|
|
197
|
+
//# sourceMappingURL=toggle.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"toggle.js","names":[],"sources":["../../../src/elements/toggle.ts"],"sourcesContent":["import type { ToggleTravel } from '#lib/toggle-travel.ts';\n\nimport { SonicPositionGroupElement } from '#elements/position-group.ts';\nimport { isUnder, optionValue } from '#elements/radio-group.ts';\nimport { dragThresholdPx } from '#lib/drag-step.ts';\nimport { bindDrag } from '#lib/pointer-drag.ts';\nimport { placeChildren, requireChild, template } from '#lib/render.ts';\nimport { positionAt, travelFrom } from '#lib/toggle-travel.ts';\n\n// eslint-disable-next-line unicorn/consistent-boolean-name -- the name bundlers and other kits use\ndeclare const __DEV__: boolean;\n\ndeclare global {\n\tinterface HTMLElementTagNameMap {\n\t\t'sonic-toggle': SonicToggle;\n\t}\n}\n\n// fallow-ignore-next-line code-duplication -- each control draws its own parts\nconst renderToggle = template(\n\t/* HTML */ `\n\t\t<div class=\"sonic-toggle\">\n\t\t\t<span aria-hidden=\"true\" class=\"sonic-toggle-well\"></span>\n\t\t\t<span aria-hidden=\"true\" class=\"sonic-toggle-cap\"></span>\n\t\t</div>\n\t`,\n\tHTMLDivElement,\n);\n\nconst renderPosition = template(\n\t/* HTML */ `\n\t\t<button class=\"sonic-toggle-position\" role=\"radio\" type=\"button\">\n\t\t\t<span class=\"sonic-toggle-legend\"></span>\n\t\t</button>\n\t`,\n\tHTMLButtonElement,\n);\n\nconst renderBare = template(\n\t/* HTML */ `<button class=\"sonic-toggle-position\" role=\"switch\" type=\"button\"></button>`,\n\tHTMLButtonElement,\n);\n\ninterface CapDrag {\n\thasMoved: boolean;\n\tisHolding: boolean;\n\tposition: HTMLButtonElement | undefined;\n\tstart: number;\n\tthresholdPx: number;\n\ttravel: ToggleTravel;\n}\n\nexport class SonicToggle extends SonicPositionGroupElement {\n\tprotected readonly bare = renderBare();\n\n\tprotected readonly defaultOrientation = 'horizontal';\n\n\tprotected readonly group = renderToggle();\n\n\treadonly #cap = requireChild(this.group, '.sonic-toggle-cap', HTMLSpanElement);\n\n\t#draggedTo: number | undefined;\n\n\t#hasWarnedOfLegends = false;\n\n\treadonly #well = requireChild(this.group, '.sonic-toggle-well', HTMLSpanElement);\n\n\t// Every press is the toggle's own, so the base's slide-off never starts\n\tprotected override claimPress(): boolean {\n\t\treturn true;\n\t}\n\n\tprotected override connect(signal: AbortSignal): void {\n\t\tsuper.connect(signal);\n\t\tthis.checkStyles(this.group, 'toggle.css');\n\t\tthis.bare.addEventListener(\n\t\t\t'click',\n\t\t\t(event) => {\n\t\t\t\t// A pointer's press is taken on its release, before the click it sends\n\t\t\t\tif (event instanceof PointerEvent && event.pointerType !== '') return;\n\n\t\t\t\tthis.setChecked(!this.checked);\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t\tthis.#bindCap(signal);\n\t}\n\n\tprotected override isMirrored(child: Node): boolean {\n\t\treturn super.isMirrored(child) || child === this.#fixedLegend();\n\t}\n\n\tprotected override placeCopies(copies: Array<Node>): void {\n\t\tconst positions = copies.filter((copy) => super.isMirrored(copy));\n\n\t\tplaceChildren(\n\t\t\tthis.#cap,\n\t\t\tcopies.filter((copy) => !positions.includes(copy)),\n\t\t);\n\t\tsuper.placeCopies(positions);\n\t}\n\n\tprotected override render(): void {\n\t\tif (!this.isBound()) return;\n\n\t\tif (__DEV__) this.#checkLegends();\n\t\tthis.group.style.setProperty('--_sonic-toggle-count', String(this.#stopCount()));\n\t\tthis.#placeCap(this.#draggedTo ?? this.#restingAt());\n\t\tsuper.render();\n\t\tfor (const [index, option] of this.options().entries()) {\n\t\t\toption.style.setProperty('--_sonic-toggle-index', String(index));\n\t\t}\n\t}\n\n\tprotected renderOption(): HTMLButtonElement {\n\t\treturn renderPosition();\n\t}\n\n\t#bindCap(signal: AbortSignal): void {\n\t\tbindDrag<CapDrag>(\n\t\t\tthis.group,\n\t\t\t{\n\t\t\t\tgrab: (event) => this.#grab(event),\n\t\t\t\tlift: (drag, event) => {\n\t\t\t\t\tif (drag.hasMoved || drag.isHolding || this.isDisabled()) return;\n\t\t\t\t\tif (!isUnder(this.group, event.clientX, event.clientY)) return;\n\n\t\t\t\t\tthis.#press(drag.position);\n\t\t\t\t},\n\t\t\t\tmove: (drag, event) => {\n\t\t\t\t\t// A held finger wobbles, and a drag back across halfway would let go\n\t\t\t\t\tif (drag.isHolding || this.isDisabled()) return;\n\t\t\t\t\tconst pointer = this.along(event);\n\t\t\t\t\tif (!drag.hasMoved && Math.abs(pointer - drag.start) < drag.thresholdPx) return;\n\n\t\t\t\t\tdrag.hasMoved = true;\n\t\t\t\t\tthis.toggleState('dragging', true);\n\t\t\t\t\tthis.#slideTo(positionAt(drag.travel, pointer), event.pointerId);\n\t\t\t\t},\n\t\t\t\trelease: (drag) => {\n\t\t\t\t\tdelete (drag.position ?? this.bare).dataset.sonicPressed;\n\t\t\t\t\tthis.#draggedTo = undefined;\n\t\t\t\t\tthis.toggleState('dragging', false);\n\t\t\t\t\tthis.render();\n\t\t\t\t},\n\t\t\t},\n\t\t\tsignal,\n\t\t);\n\t}\n\n\t#checkLegends(): void {\n\t\tif (!__DEV__ || this.#hasWarnedOfLegends) return;\n\n\t\tconst count = this.options().length;\n\t\tif (count <= 3 || this.getAttribute('legends') !== 'outside') return;\n\n\t\tthis.#hasWarnedOfLegends = true;\n\t\tconsole.warn(\n\t\t\t`<sonic-toggle> lays out legends=\"outside\" for two or three positions, and this one has ${String(count)}`,\n\t\t);\n\t}\n\n\t#enabledEnds(): [number, number] {\n\t\tif (this.isBare()) return [0, 1];\n\n\t\tconst options = this.options();\n\t\tconst isEnabled = (option: HTMLButtonElement): boolean => !this.isOptionDisabled(option);\n\n\t\treturn [options.findIndex(isEnabled), options.findLastIndex(isEnabled)];\n\t}\n\n\t#fixedLegend(): Element | undefined {\n\t\treturn [...this.children].find((child) => child !== this.group && !super.isMirrored(child));\n\t}\n\n\t#grab(event: PointerEvent): CapDrag | undefined {\n\t\tconst position = this.optionOf(event.target);\n\t\tif (this.isDisabled() || (!position && event.target !== this.bare)) return undefined;\n\n\t\tconst isOnCap = isUnder(this.#cap, event.clientX, event.clientY);\n\t\tconst travel = this.#travel(event, isOnCap);\n\t\tif (!travel || this.#isBlocked(position, isOnCap)) return undefined;\n\n\t\t(position ?? this.bare).toggleAttribute('data-sonic-pressed', true);\n\n\t\treturn {\n\t\t\thasMoved: false,\n\t\t\tisHolding: this.#holdFrom(position, event.pointerId),\n\t\t\tposition,\n\t\t\tstart: this.along(event),\n\t\t\tthresholdPx: dragThresholdPx(event.pointerType),\n\t\t\ttravel,\n\t\t};\n\t}\n\n\t#holdFrom(position: HTMLButtonElement | undefined, pointerId: number): boolean {\n\t\tconst target = position && this.pressTarget(position);\n\t\tif (!target || !this.isMomentary(target) || this.isOptionDisabled(target)) return false;\n\n\t\tthis.hold(target, pointerId);\n\n\t\treturn true;\n\t}\n\n\t#isBlocked(position: HTMLButtonElement | undefined, isOnCap: boolean): boolean {\n\t\treturn !isOnCap && position !== undefined && this.isOptionDisabled(position);\n\t}\n\n\t// On is up, as on hardware\n\t#onAt(): number {\n\t\treturn this.orientation === 'vertical' ? 0 : 1;\n\t}\n\n\t#placeCap(at: number): void {\n\t\tthis.group.style.setProperty('--_sonic-toggle-at', String(at));\n\t}\n\n\t#press(position: HTMLButtonElement | undefined): void {\n\t\tif (!position) {\n\t\t\tthis.setChecked(!this.checked);\n\t\t\treturn;\n\t\t}\n\n\t\tconst target = this.pressTarget(position);\n\t\tif (this.isOptionDisabled(target)) return;\n\n\t\tthis.select(target);\n\t\tthis.#refocus(target);\n\t}\n\n\t#refocus(position: HTMLButtonElement): void {\n\t\tif (this.group.matches(':focus-within')) position.focus();\n\t}\n\n\t#restingAt(): number {\n\t\tif (this.isBare()) return this.checked ? this.#onAt() : 1 - this.#onAt();\n\n\t\treturn Math.max(0, this.checkedIndex(this.options()));\n\t}\n\n\t#slideTo(at: number, pointerId: number): void {\n\t\tconst crossed = this.options()[Math.round(at)];\n\n\t\tthis.#draggedTo = at;\n\t\tthis.#placeCap(at);\n\t\tif (this.isBare()) {\n\t\t\tthis.setChecked(Math.round(at) === this.#onAt());\n\t\t\treturn;\n\t\t}\n\t\tif (!crossed || optionValue(crossed) === this.value) return;\n\n\t\tthis.releaseHold();\n\t\tif (this.isOptionDisabled(crossed) || optionValue(crossed) === this.value) return;\n\n\t\tif (this.isMomentary(crossed)) this.hold(crossed, pointerId);\n\t\telse this.select(crossed);\n\t\tthis.#refocus(crossed);\n\t}\n\n\t#stopCount(): number {\n\t\treturn this.options().length || 2;\n\t}\n\n\t#travel(event: PointerEvent, isOnCap: boolean): ToggleTravel | undefined {\n\t\tconst [first, last] = this.#enabledEnds();\n\t\tif (first < 0) return undefined;\n\n\t\tconst box = this.#well.getBoundingClientRect();\n\t\tconst [start, length, thickness] =\n\t\t\tthis.orientation === 'vertical'\n\t\t\t\t? [box.top, box.height, box.width]\n\t\t\t\t: [box.left, box.width, box.height];\n\n\t\treturn travelFrom({\n\t\t\tat: this.#restingAt(),\n\t\t\tcount: this.#stopCount(),\n\t\t\tfirst,\n\t\t\tisOnCap,\n\t\t\tlast,\n\t\t\tpointer: this.along(event),\n\t\t\twell: { length, start, thickness },\n\t\t});\n\t}\n}\n"],"mappings":"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"}
|
|
@@ -9,7 +9,7 @@ declare global {
|
|
|
9
9
|
}
|
|
10
10
|
}
|
|
11
11
|
type Colour = keyof typeof colours;
|
|
12
|
-
type PeaksRequest = (fromSeconds: number, toSeconds: number) => unknown;
|
|
12
|
+
export type PeaksRequest = (fromSeconds: number, toSeconds: number) => Iterable<Promise<unknown>> | Promise<unknown> | undefined;
|
|
13
13
|
declare const colours: {
|
|
14
14
|
readonly ends: "--_sonic-waveform-ends";
|
|
15
15
|
readonly grid: "--_sonic-waveform-grid";
|
|
@@ -125,20 +125,22 @@ var SonicWaveform = class extends SonicWaveElement {
|
|
|
125
125
|
set peaks(peaks) {
|
|
126
126
|
this.#peaks = peaks;
|
|
127
127
|
this.renderEmpty();
|
|
128
|
-
this
|
|
128
|
+
this.#repaint();
|
|
129
129
|
}
|
|
130
130
|
get pending() {
|
|
131
131
|
return this.#pending.map(([start, end]) => [start, end]);
|
|
132
132
|
}
|
|
133
133
|
set pending(regions) {
|
|
134
|
+
const pending = regions ? readRegions(regions) : [];
|
|
135
|
+
if (String(pending) === String(this.#pending)) return;
|
|
134
136
|
const listedMs = this.#pendingListedMs;
|
|
135
137
|
const nowMs = performance.now();
|
|
136
|
-
this.#pending =
|
|
138
|
+
this.#pending = pending;
|
|
137
139
|
this.#pendingListedMs = new Map(this.#pending.map((region) => {
|
|
138
140
|
const key = region.join(":");
|
|
139
141
|
return [key, listedMs.get(key) ?? nowMs];
|
|
140
142
|
}));
|
|
141
|
-
this
|
|
143
|
+
this.#repaint();
|
|
142
144
|
}
|
|
143
145
|
get pendingDelay() {
|
|
144
146
|
return Math.max(0, this.numberAttribute("pending-delay", 0));
|
|
@@ -177,6 +179,7 @@ var SonicWaveform = class extends SonicWaveElement {
|
|
|
177
179
|
}
|
|
178
180
|
set requestPeaks(request) {
|
|
179
181
|
this.#requestPeaks = request;
|
|
182
|
+
this.#askedKey = void 0;
|
|
180
183
|
this.surface()?.invalidate();
|
|
181
184
|
}
|
|
182
185
|
get zoom() {
|
|
@@ -192,11 +195,13 @@ var SonicWaveform = class extends SonicWaveElement {
|
|
|
192
195
|
lengths = {};
|
|
193
196
|
sheet = "waveform.css";
|
|
194
197
|
sizeProperty = "--_sonic-waveform-size";
|
|
195
|
-
#asked = /* @__PURE__ */ new
|
|
198
|
+
#asked = /* @__PURE__ */ new WeakSet();
|
|
199
|
+
#askedKey;
|
|
196
200
|
#clock = createTrackingClock();
|
|
197
201
|
#drawn;
|
|
198
202
|
#ghost = requireChild(this.control, ".sonic-waveform-ghost", HTMLDivElement);
|
|
199
203
|
#grabbed;
|
|
204
|
+
#isAsking = false;
|
|
200
205
|
#kindColours = /* @__PURE__ */ new Map();
|
|
201
206
|
#paintedKey = "";
|
|
202
207
|
#peaks;
|
|
@@ -287,10 +292,22 @@ var SonicWaveform = class extends SonicWaveElement {
|
|
|
287
292
|
this.surface()?.rebuild();
|
|
288
293
|
}
|
|
289
294
|
#ask(wanted) {
|
|
290
|
-
const
|
|
291
|
-
if (
|
|
292
|
-
|
|
293
|
-
this.#
|
|
295
|
+
const key = wanted.join(":");
|
|
296
|
+
if (key === this.#askedKey) return;
|
|
297
|
+
let asked;
|
|
298
|
+
this.#askedKey = key;
|
|
299
|
+
this.#isAsking = true;
|
|
300
|
+
try {
|
|
301
|
+
asked = this.#requestPeaks?.(...wanted);
|
|
302
|
+
} finally {
|
|
303
|
+
this.#isAsking = false;
|
|
304
|
+
}
|
|
305
|
+
const promises = asked instanceof Promise ? [asked] : asked ?? [];
|
|
306
|
+
for (const promise of promises) {
|
|
307
|
+
if (this.#asked.has(promise)) continue;
|
|
308
|
+
this.#asked.add(promise);
|
|
309
|
+
this.#repaintAfter(promise);
|
|
310
|
+
}
|
|
294
311
|
}
|
|
295
312
|
#duePending() {
|
|
296
313
|
const due = dueRegions(this.#pending, this.#pendingListedMs, [performance.now(), this.pendingDelay]);
|
|
@@ -363,10 +380,13 @@ var SonicWaveform = class extends SonicWaveElement {
|
|
|
363
380
|
for (const probe of probes) probe.remove();
|
|
364
381
|
return new Map(kinds.map((kind, index) => [kind, read[index] ?? ""]));
|
|
365
382
|
}
|
|
383
|
+
#repaint() {
|
|
384
|
+
if (!this.#isAsking) this.#askedKey = void 0;
|
|
385
|
+
this.surface()?.invalidate();
|
|
386
|
+
}
|
|
366
387
|
async #repaintAfter(asked) {
|
|
367
388
|
await Promise.allSettled([asked]);
|
|
368
|
-
this.#
|
|
369
|
-
this.surface()?.invalidate();
|
|
389
|
+
this.#repaint();
|
|
370
390
|
}
|
|
371
391
|
#sourceSeconds() {
|
|
372
392
|
return this.#readTime?.() ?? this.playback();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"waveform.js","names":[],"sources":["../../../src/elements/waveform.ts"],"sourcesContent":["import type { ValueAxis } from '#elements/value-gestures.ts';\nimport type { SurfaceFrame, SurfaceLook } from '#lib/canvas-surface.ts';\nimport type { Drawn, View } from '#lib/frame-timeline.ts';\nimport type { TimeRegions } from '#lib/time-regions.ts';\nimport type { WaveformPeaks } from '#lib/waveform-buckets.ts';\n\nimport { SonicWaveElement } from '#elements/wave-element.ts';\nimport { frameTimeline } from '#lib/frame-timeline.ts';\nimport { isKind, writeKind } from '#lib/marker-band.ts';\nimport { createLabelRider } from '#lib/marker-rider.ts';\nimport { clamp } from '#lib/math.ts';\nimport { requireChild, template } from '#lib/render.ts';\nimport { dueRegions, readRegions } from '#lib/time-regions.ts';\nimport { createTrackingClock } from '#lib/tracking-clock.ts';\nimport { waveformBuckets } from '#lib/waveform-buckets.ts';\n\ndeclare global {\n\tinterface HTMLElementTagNameMap {\n\t\t'sonic-waveform': SonicWaveform;\n\t}\n}\n\ntype Colour = keyof typeof colours;\n\ntype PeaksRequest = (fromSeconds: number, toSeconds: number) => unknown;\n\ninterface Scene extends View {\n\tcolours: SurfaceLook<Colour>['colours'];\n}\n\nconst colours = {\n\tends: '--_sonic-waveform-ends',\n\tgrid: '--_sonic-waveform-grid',\n\tmarker: '--_sonic-waveform-marker',\n\tplaceholder: '--_sonic-waveform-placeholder',\n\twave: '--_sonic-waveform-wave',\n\twaveEdge: '--_sonic-waveform-wave-edge',\n} as const;\n\nconst defaultZoom = 70;\n\nconst amplitudeMargin = 0.94;\n\nconst hatchPitchPx = 8;\nconst placeholderWavelengthPx = 48;\nconst placeholderAmplitude = 0.2;\n\nconst renderWaveform = template(\n\t/* HTML */ `\n\t\t<div class=\"sonic-waveform\" role=\"slider\" tabindex=\"0\">\n\t\t\t<canvas class=\"sonic-waveform-canvas\" aria-hidden=\"true\"></canvas>\n\t\t\t<div class=\"sonic-waveform-scrim\" aria-hidden=\"true\"></div>\n\t\t\t<div class=\"sonic-waveform-ghost\" aria-hidden=\"true\" hidden></div>\n\t\t\t<div class=\"sonic-waveform-playhead\" aria-hidden=\"true\"></div>\n\t\t\t<div class=\"sonic-waveform-label\" aria-hidden=\"true\" hidden></div>\n\t\t\t<div class=\"sonic-waveform-label\" aria-hidden=\"true\" hidden></div>\n\t\t\t<div class=\"sonic-waveform-readout\" popover=\"manual\">\n\t\t\t\t<span></span>\n\t\t\t\t<input\n\t\t\t\t\tautocomplete=\"off\"\n\t\t\t\t\tclass=\"sonic-waveform-entry\"\n\t\t\t\t\tenterkeyhint=\"done\"\n\t\t\t\t\thidden\n\t\t\t\t\tspellcheck=\"false\"\n\t\t\t\t\ttype=\"text\"\n\t\t\t\t/>\n\t\t\t</div>\n\t\t</div>\n\t`,\n\tHTMLDivElement,\n);\n\nfunction placeLine(line: HTMLElement, at: number | undefined): void {\n\tconst isHidden = at === undefined || at < 0 || at > 1;\n\n\tif (line.hidden !== isHidden) line.hidden = isHidden;\n\tif (isHidden) return;\n\n\tconst translate = `${String(at * 100)}cqi`;\n\n\tif (line.style.getPropertyValue('translate') !== translate) {\n\t\tline.style.setProperty('translate', translate);\n\t}\n}\n\nfunction paintHatch(\n\tcontext: CanvasRenderingContext2D,\n\tscene: Scene,\n\t[from, to]: [number, number],\n): void {\n\tif (to <= from) return;\n\n\tconst { dpr, height } = scene;\n\tconst pitch = Math.max(2, Math.round(hatchPitchPx * dpr));\n\tconst offset = scene.startSeconds * scene.pixelsPerSecond;\n\n\tcontext.save();\n\tcontext.beginPath();\n\tcontext.rect(from, 0, to - from, height);\n\tcontext.clip();\n\tcontext.beginPath();\n\tfor (let x = Math.floor((from - height + offset) / pitch) * pitch - offset; x < to; x += pitch) {\n\t\tcontext.moveTo(x, height);\n\t\tcontext.lineTo(x + height, 0);\n\t}\n\tcontext.lineWidth = Math.max(1, Math.round(dpr));\n\tcontext.strokeStyle = scene.colours.ends;\n\tcontext.stroke();\n\tcontext.restore();\n}\n\nfunction paintPlaceholders(context: CanvasRenderingContext2D, scene: Scene): void {\n\tif (scene.pending.length === 0) return;\n\n\tconst { dpr, height, pixelsPerSecond, startSeconds } = scene;\n\tconst wavelength = placeholderWavelengthPx * dpr;\n\tconst center = height / 2;\n\tconst origin = startSeconds * pixelsPerSecond;\n\tconst waveY = (x: number): number =>\n\t\tcenter -\n\t\tMath.sin(((x + origin) / wavelength + scene.phase) * 2 * Math.PI) *\n\t\t\tcenter *\n\t\t\tplaceholderAmplitude;\n\n\tcontext.beginPath();\n\tfor (const [from, to] of scene.pending) {\n\t\tconst opening = (from - startSeconds) * pixelsPerSecond;\n\t\tconst closing = (to - startSeconds) * pixelsPerSecond;\n\n\t\tcontext.moveTo(opening, waveY(opening));\n\t\tfor (let x = opening + dpr; x < closing; x += dpr) context.lineTo(x, waveY(x));\n\t\tcontext.lineTo(closing, waveY(closing));\n\t}\n\tcontext.lineWidth = Math.max(1, Math.round(dpr));\n\tcontext.strokeStyle = scene.colours.placeholder;\n\tcontext.stroke();\n}\n\nfunction waveFill(context: CanvasRenderingContext2D, scene: Scene): CanvasGradient {\n\tconst { wave, waveEdge } = scene.colours;\n\tconst gradient = context.createLinearGradient(0, 0, 0, scene.height);\n\n\tgradient.addColorStop(0, waveEdge);\n\tgradient.addColorStop(0.5, wave);\n\tgradient.addColorStop(1, waveEdge);\n\n\treturn gradient;\n}\n\nfunction paintWave(context: CanvasRenderingContext2D, scene: Scene, peaks: WaveformPeaks): void {\n\tconst buckets = waveformBuckets(peaks, scene);\n\tconst [first] = buckets;\n\tif (!first) return;\n\n\tconst center = scene.height / 2;\n\tconst amplitudePx = center * amplitudeMargin;\n\n\tcontext.fillStyle = waveFill(context, scene);\n\tcontext.beginPath();\n\tcontext.moveTo(first.x, center);\n\tfor (const bucket of buckets) context.lineTo(bucket.x, center - bucket.high * amplitudePx);\n\tfor (const bucket of buckets.toReversed())\n\t\tcontext.lineTo(bucket.x, center - bucket.low * amplitudePx);\n\tcontext.closePath();\n\tcontext.fill();\n}\n\nexport class SonicWaveform extends SonicWaveElement<Colour> {\n\tstatic override readonly observedAttributes = [\n\t\t...SonicWaveElement.observedAttributes,\n\t\t'pending-delay',\n\t\t'playing',\n\t\t'playback-rate',\n\t\t'reduced-motion',\n\t\t'zoom',\n\t];\n\n\tget peaks(): undefined | WaveformPeaks {\n\t\treturn this.#peaks;\n\t}\n\n\tset peaks(peaks: undefined | WaveformPeaks) {\n\t\tthis.#peaks = peaks;\n\t\tthis.renderEmpty();\n\t\tthis.surface()?.invalidate();\n\t}\n\n\tget pending(): Array<[number, number]> {\n\t\treturn this.#pending.map(([start, end]) => [start, end]);\n\t}\n\n\tset pending(regions: TimeRegions | undefined) {\n\t\tconst listedMs = this.#pendingListedMs;\n\t\tconst nowMs = performance.now();\n\n\t\tthis.#pending = regions ? readRegions(regions) : [];\n\t\tthis.#pendingListedMs = new Map(\n\t\t\tthis.#pending.map((region) => {\n\t\t\t\tconst key = region.join(':');\n\n\t\t\t\treturn [key, listedMs.get(key) ?? nowMs];\n\t\t\t}),\n\t\t);\n\t\tthis.surface()?.invalidate();\n\t}\n\n\tget pendingDelay(): number {\n\t\treturn Math.max(0, this.numberAttribute('pending-delay', 0));\n\t}\n\n\tset pendingDelay(value: number | undefined) {\n\t\tthis.reflect('pending-delay', value);\n\t}\n\n\tget playbackRate(): number {\n\t\tconst playbackRate = this.numberAttribute('playback-rate', 1);\n\n\t\treturn playbackRate > 0 ? playbackRate : 1;\n\t}\n\n\tset playbackRate(value: number | undefined) {\n\t\tthis.reflect('playback-rate', value);\n\t}\n\n\tget playing(): boolean {\n\t\treturn this.hasAttribute('playing');\n\t}\n\n\tset playing(isPlaying: boolean) {\n\t\tthis.reflect('playing', isPlaying);\n\t}\n\n\tget readTime(): (() => number | undefined) | undefined {\n\t\treturn this.#readTime;\n\t}\n\n\tset readTime(read: (() => number | undefined) | undefined) {\n\t\tthis.#readTime = read;\n\t\tthis.render();\n\t}\n\n\tget reducedMotion(): 'page' | 'scroll' {\n\t\treturn this.getAttribute('reduced-motion') === 'scroll' ? 'scroll' : 'page';\n\t}\n\n\tset reducedMotion(motion: 'page' | 'scroll' | undefined) {\n\t\tthis.reflect('reduced-motion', motion);\n\t}\n\n\tget requestPeaks(): PeaksRequest | undefined {\n\t\treturn this.#requestPeaks;\n\t}\n\n\tset requestPeaks(request: PeaksRequest | undefined) {\n\t\tthis.#requestPeaks = request;\n\t\tthis.surface()?.invalidate();\n\t}\n\n\tget zoom(): number {\n\t\tconst zoom = this.numberAttribute('zoom', defaultZoom);\n\n\t\treturn zoom > 0 ? zoom : defaultZoom;\n\t}\n\n\tset zoom(value: number | undefined) {\n\t\tthis.reflect('zoom', value);\n\t}\n\n\tprotected readonly control = renderWaveform();\n\n\tprotected readonly canvas = requireChild(\n\t\tthis.control,\n\t\t'.sonic-waveform-canvas',\n\t\tHTMLCanvasElement,\n\t);\n\n\tprotected readonly colours = colours;\n\n\tprotected readonly lengths = {};\n\n\tprotected readonly sheet = 'waveform.css';\n\n\tprotected readonly sizeProperty = '--_sonic-waveform-size';\n\n\treadonly #asked = new Set<Promise<unknown>>();\n\n\treadonly #clock = createTrackingClock();\n\n\t#drawn: Drawn | undefined;\n\n\treadonly #ghost = requireChild(this.control, '.sonic-waveform-ghost', HTMLDivElement);\n\n\t#grabbed: Drawn | undefined;\n\n\t#kindColours = new Map<string, string>();\n\n\t#paintedKey = '';\n\n\t#peaks: undefined | WaveformPeaks;\n\n\t#pending: Array<[number, number]> = [];\n\n\t#pendingListedMs = new Map<string, number>();\n\n\treadonly #playhead = requireChild(this.control, '.sonic-waveform-playhead', HTMLDivElement);\n\n\t#readTime: (() => number | undefined) | undefined;\n\n\t#requestPeaks: PeaksRequest | undefined;\n\n\treadonly #rider = createLabelRider(this.control, {\n\t\tclassName: 'sonic-waveform-label',\n\t\tcolourProperty: '--_sonic-waveform-marker',\n\t\tfadeProperty: '--_sonic-waveform-label-fade',\n\t\tinsetProperty: '--_sonic-waveform-label-inset',\n\t\tparkProperty: '--_sonic-waveform-label-park',\n\t});\n\n\toverride connectedCallback(): void {\n\t\tthis.upgradeProperties(\n\t\t\t'peaks',\n\t\t\t'pending',\n\t\t\t'pendingDelay',\n\t\t\t'playing',\n\t\t\t'playbackRate',\n\t\t\t'readTime',\n\t\t\t'reducedMotion',\n\t\t\t'requestPeaks',\n\t\t\t'zoom',\n\t\t);\n\t\tsuper.connectedCallback();\n\t}\n\n\tprotected override connect(signal: AbortSignal): void {\n\t\tsuper.connect(signal);\n\t\tthis.bindGestures(this.control, signal, () => this.#grab());\n\t\tif (!('fonts' in document)) return;\n\n\t\tdocument.fonts.addEventListener(\n\t\t\t'loadingdone',\n\t\t\t() => {\n\t\t\t\tthis.renderMarkers();\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t}\n\n\tprotected draw(): void {\n\t\tconst surface = this.surface();\n\n\t\tsurface?.requestFrame();\n\t\t// No frame follows the position out of view, or before the first one paints\n\t\tif (surface?.isVisible === false || !this.#drawn) this.showCurrentMarker(this.#sourceSeconds());\n\t}\n\n\tprotected isEmpty(): boolean {\n\t\treturn !this.#peaks || this.#peaks.samples.length === 0;\n\t}\n\n\tprotected paint(\n\t\tcontext: CanvasRenderingContext2D,\n\t\t{ frameMs, isDirty, isRebuilt, look, size }: SurfaceFrame<Colour>,\n\t): void {\n\t\tconst held = this.#held();\n\t\tconst pending = this.#duePending();\n\t\tconst timeline = frameTimeline({\n\t\t\tclockSeconds: this.#clock.read(frameMs, {\n\t\t\t\tisPlaying: this.playing,\n\t\t\t\tplaybackRate: this.playbackRate,\n\t\t\t\tseconds: this.#sourceSeconds(),\n\t\t\t}),\n\t\t\tframeMs,\n\t\t\t...(held ? { held } : {}),\n\t\t\tisPaged: look.isReducedMotion && this.reducedMotion === 'page',\n\t\t\tisPlaying: this.playing,\n\t\t\tisStill: look.isReducedMotion,\n\t\t\tpending,\n\t\t\trange: [this.min, Math.max(this.min, this.max)],\n\t\t\tsize,\n\t\t\tzoom: this.zoom,\n\t\t});\n\t\tconst { seconds, view } = timeline;\n\n\t\tthis.#drawn = { seconds, startSeconds: view.startSeconds };\n\t\tplaceLine(this.#playhead, timeline.playheadAt);\n\t\tplaceLine(this.#ghost, timeline.ghostAt);\n\t\tthis.showCurrentMarker(seconds);\n\t\tthis.toggleState('pending', view.pending.length > 0);\n\t\tthis.#rider.place({\n\t\t\tplayheadSeconds: seconds,\n\t\t\tstartSeconds: view.startSeconds,\n\t\t\twidthPx: view.width / view.dpr,\n\t\t\twindowSeconds: view.width / view.pixelsPerSecond,\n\t\t});\n\t\tif (isDirty || timeline.paintKey !== this.#paintedKey) {\n\t\t\tthis.#paintedKey = timeline.paintKey;\n\t\t\tthis.#paintScene(context, { ...view, colours: look.colours }, isRebuilt);\n\t\t\tif (timeline.wanted) this.#ask(timeline.wanted);\n\t\t}\n\t\tif (timeline.isMoving) this.surface()?.requestFrame();\n\t}\n\n\tprotected override renderMarkers(): void {\n\t\tconst bounds = this.mapping().bounds;\n\n\t\tthis.#rider.measure(\n\t\t\tthis.markers.flatMap(({ dimmed, kind, label, start }) =>\n\t\t\t\tlabel === undefined || label === ''\n\t\t\t\t\t? []\n\t\t\t\t\t: [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tisDimmed: dimmed === true,\n\t\t\t\t\t\t\t\t...(kind === undefined ? {} : { kind }),\n\t\t\t\t\t\t\t\tstart: clamp(start, ...bounds),\n\t\t\t\t\t\t\t\ttext: label,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t],\n\t\t\t),\n\t\t);\n\t\tthis.surface()?.rebuild();\n\t}\n\n\t#ask(wanted: [number, number]): void {\n\t\tconst asked = this.#requestPeaks?.(...wanted);\n\t\tif (!(asked instanceof Promise) || this.#asked.has(asked)) return;\n\n\t\tthis.#asked.add(asked);\n\t\tvoid this.#repaintAfter(asked);\n\t}\n\n\t#duePending(): Array<[number, number]> {\n\t\tconst due = dueRegions(this.#pending, this.#pendingListedMs, [\n\t\t\tperformance.now(),\n\t\t\tthis.pendingDelay,\n\t\t]);\n\n\t\tif (due.length < this.#pending.length) this.surface()?.requestFrame();\n\n\t\treturn due;\n\t}\n\n\t#grab(): ValueAxis {\n\t\tconst drawn = this.#drawn ?? { seconds: this.value, startSeconds: this.value };\n\n\t\tthis.#grabbed = drawn;\n\n\t\tconst mapping = this.mapping();\n\t\tconst [low, high] = mapping.bounds;\n\n\t\treturn {\n\t\t\tfromProportion: mapping.proportionOf(drawn.seconds),\n\t\t\tisKeptOnCancel: true,\n\t\t\tposition: (event) => -event.clientX,\n\t\t\ttravelPx: Math.max(1, (high - low) * this.zoom),\n\t\t};\n\t}\n\n\t#held(): undefined | { grabbed: Drawn; seconds: number } {\n\t\tconst grabbed = this.#grabbed;\n\t\tconst from = this.heldFrom();\n\t\tif (!grabbed || from === undefined) return undefined;\n\n\t\treturn { grabbed, seconds: this.value === from ? grabbed.seconds : this.value };\n\t}\n\n\t#paintMarkers(context: CanvasRenderingContext2D, scene: Scene, isRebuilt: boolean): void {\n\t\tif (isRebuilt) this.#kindColours = this.#readKindColours();\n\n\t\tconst line = Math.max(1, Math.round(scene.dpr));\n\n\t\tfor (const { dimmed, kind, start } of this.markers) {\n\t\t\tconst x = (start - scene.startSeconds) * scene.pixelsPerSecond;\n\n\t\t\tcontext.fillStyle =\n\t\t\t\t(isKind(kind) ? this.#kindColours.get(kind) : undefined) ?? scene.colours.marker;\n\t\t\tcontext.globalAlpha = dimmed === true ? 0.5 : 1;\n\t\t\tcontext.fillRect(x - line / 2, 0, line, scene.height);\n\t\t}\n\t\tcontext.globalAlpha = 1;\n\t}\n\n\t#paintScene(context: CanvasRenderingContext2D, scene: Scene, isRebuilt: boolean): void {\n\t\tconst { dpr, height, pixelsPerSecond, startSeconds, width } = scene;\n\t\tconst x = (seconds: number): number => (seconds - startSeconds) * pixelsPerSecond;\n\t\tconst { ends, grid } = scene.colours;\n\t\tconst line = Math.max(1, Math.round(dpr));\n\t\tconst [opening, closing] = [x(this.min), x(Math.max(this.min, this.max))];\n\t\tconst [from, to] = [clamp(opening, 0, width), clamp(closing, 0, width)];\n\n\t\tcontext.clearRect(0, 0, width, height);\n\t\tpaintHatch(context, scene, [0, from]);\n\t\tpaintHatch(context, scene, [to, width]);\n\t\tcontext.fillStyle = grid;\n\t\tif (to > from) context.fillRect(from, Math.round((height - line) / 2), to - from, line);\n\t\tpaintPlaceholders(context, scene);\n\t\tif (this.#peaks) paintWave(context, scene, this.#peaks);\n\t\t// Scrolling lines sit at fractional x; rounding them judders\n\t\tcontext.fillStyle = ends;\n\t\tfor (const edge of [opening, closing]) context.fillRect(edge - line / 2, 0, line, height);\n\t\tthis.#paintMarkers(context, scene, isRebuilt);\n\t}\n\n\t#readKindColours(): Map<string, string> {\n\t\tconst kinds = [...new Set(this.markers.map(({ kind }) => kind).filter((kind) => isKind(kind)))];\n\t\tconst probes = kinds.map((kind) => {\n\t\t\tconst probe = document.createElement('div');\n\n\t\t\tprobe.className = 'sonic-waveform-label';\n\t\t\twriteKind(probe, kind, '--_sonic-waveform-marker');\n\n\t\t\treturn probe;\n\t\t});\n\n\t\tthis.control.append(...probes);\n\n\t\tconst read = probes.map((probe) => getComputedStyle(probe).color);\n\n\t\tfor (const probe of probes) probe.remove();\n\n\t\treturn new Map(kinds.map((kind, index) => [kind, read[index] ?? '']));\n\t}\n\n\tasync #repaintAfter(asked: Promise<unknown>): Promise<void> {\n\t\tawait Promise.allSettled([asked]);\n\t\tthis.#asked.delete(asked);\n\t\tthis.surface()?.invalidate();\n\t}\n\n\t#sourceSeconds(): number {\n\t\treturn this.#readTime?.() ?? this.playback();\n\t}\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"waveform.js","names":[],"sources":["../../../src/elements/waveform.ts"],"sourcesContent":["import type { ValueAxis } from '#elements/value-gestures.ts';\nimport type { SurfaceFrame, SurfaceLook } from '#lib/canvas-surface.ts';\nimport type { Drawn, View } from '#lib/frame-timeline.ts';\nimport type { TimeRegions } from '#lib/time-regions.ts';\nimport type { WaveformPeaks } from '#lib/waveform-buckets.ts';\n\nimport { SonicWaveElement } from '#elements/wave-element.ts';\nimport { frameTimeline } from '#lib/frame-timeline.ts';\nimport { isKind, writeKind } from '#lib/marker-band.ts';\nimport { createLabelRider } from '#lib/marker-rider.ts';\nimport { clamp } from '#lib/math.ts';\nimport { requireChild, template } from '#lib/render.ts';\nimport { dueRegions, readRegions } from '#lib/time-regions.ts';\nimport { createTrackingClock } from '#lib/tracking-clock.ts';\nimport { waveformBuckets } from '#lib/waveform-buckets.ts';\n\ndeclare global {\n\tinterface HTMLElementTagNameMap {\n\t\t'sonic-waveform': SonicWaveform;\n\t}\n}\n\ntype Colour = keyof typeof colours;\n\nexport type PeaksRequest = (\n\tfromSeconds: number,\n\ttoSeconds: number,\n) => Iterable<Promise<unknown>> | Promise<unknown> | undefined;\n\ninterface Scene extends View {\n\tcolours: SurfaceLook<Colour>['colours'];\n}\n\nconst colours = {\n\tends: '--_sonic-waveform-ends',\n\tgrid: '--_sonic-waveform-grid',\n\tmarker: '--_sonic-waveform-marker',\n\tplaceholder: '--_sonic-waveform-placeholder',\n\twave: '--_sonic-waveform-wave',\n\twaveEdge: '--_sonic-waveform-wave-edge',\n} as const;\n\nconst defaultZoom = 70;\n\nconst amplitudeMargin = 0.94;\n\nconst hatchPitchPx = 8;\nconst placeholderWavelengthPx = 48;\nconst placeholderAmplitude = 0.2;\n\nconst renderWaveform = template(\n\t/* HTML */ `\n\t\t<div class=\"sonic-waveform\" role=\"slider\" tabindex=\"0\">\n\t\t\t<canvas class=\"sonic-waveform-canvas\" aria-hidden=\"true\"></canvas>\n\t\t\t<div class=\"sonic-waveform-scrim\" aria-hidden=\"true\"></div>\n\t\t\t<div class=\"sonic-waveform-ghost\" aria-hidden=\"true\" hidden></div>\n\t\t\t<div class=\"sonic-waveform-playhead\" aria-hidden=\"true\"></div>\n\t\t\t<div class=\"sonic-waveform-label\" aria-hidden=\"true\" hidden></div>\n\t\t\t<div class=\"sonic-waveform-label\" aria-hidden=\"true\" hidden></div>\n\t\t\t<div class=\"sonic-waveform-readout\" popover=\"manual\">\n\t\t\t\t<span></span>\n\t\t\t\t<input\n\t\t\t\t\tautocomplete=\"off\"\n\t\t\t\t\tclass=\"sonic-waveform-entry\"\n\t\t\t\t\tenterkeyhint=\"done\"\n\t\t\t\t\thidden\n\t\t\t\t\tspellcheck=\"false\"\n\t\t\t\t\ttype=\"text\"\n\t\t\t\t/>\n\t\t\t</div>\n\t\t</div>\n\t`,\n\tHTMLDivElement,\n);\n\nfunction placeLine(line: HTMLElement, at: number | undefined): void {\n\tconst isHidden = at === undefined || at < 0 || at > 1;\n\n\tif (line.hidden !== isHidden) line.hidden = isHidden;\n\tif (isHidden) return;\n\n\tconst translate = `${String(at * 100)}cqi`;\n\n\tif (line.style.getPropertyValue('translate') !== translate) {\n\t\tline.style.setProperty('translate', translate);\n\t}\n}\n\nfunction paintHatch(\n\tcontext: CanvasRenderingContext2D,\n\tscene: Scene,\n\t[from, to]: [number, number],\n): void {\n\tif (to <= from) return;\n\n\tconst { dpr, height } = scene;\n\tconst pitch = Math.max(2, Math.round(hatchPitchPx * dpr));\n\tconst offset = scene.startSeconds * scene.pixelsPerSecond;\n\n\tcontext.save();\n\tcontext.beginPath();\n\tcontext.rect(from, 0, to - from, height);\n\tcontext.clip();\n\tcontext.beginPath();\n\tfor (let x = Math.floor((from - height + offset) / pitch) * pitch - offset; x < to; x += pitch) {\n\t\tcontext.moveTo(x, height);\n\t\tcontext.lineTo(x + height, 0);\n\t}\n\tcontext.lineWidth = Math.max(1, Math.round(dpr));\n\tcontext.strokeStyle = scene.colours.ends;\n\tcontext.stroke();\n\tcontext.restore();\n}\n\nfunction paintPlaceholders(context: CanvasRenderingContext2D, scene: Scene): void {\n\tif (scene.pending.length === 0) return;\n\n\tconst { dpr, height, pixelsPerSecond, startSeconds } = scene;\n\tconst wavelength = placeholderWavelengthPx * dpr;\n\tconst center = height / 2;\n\tconst origin = startSeconds * pixelsPerSecond;\n\tconst waveY = (x: number): number =>\n\t\tcenter -\n\t\tMath.sin(((x + origin) / wavelength + scene.phase) * 2 * Math.PI) *\n\t\t\tcenter *\n\t\t\tplaceholderAmplitude;\n\n\tcontext.beginPath();\n\tfor (const [from, to] of scene.pending) {\n\t\tconst opening = (from - startSeconds) * pixelsPerSecond;\n\t\tconst closing = (to - startSeconds) * pixelsPerSecond;\n\n\t\tcontext.moveTo(opening, waveY(opening));\n\t\tfor (let x = opening + dpr; x < closing; x += dpr) context.lineTo(x, waveY(x));\n\t\tcontext.lineTo(closing, waveY(closing));\n\t}\n\tcontext.lineWidth = Math.max(1, Math.round(dpr));\n\tcontext.strokeStyle = scene.colours.placeholder;\n\tcontext.stroke();\n}\n\nfunction waveFill(context: CanvasRenderingContext2D, scene: Scene): CanvasGradient {\n\tconst { wave, waveEdge } = scene.colours;\n\tconst gradient = context.createLinearGradient(0, 0, 0, scene.height);\n\n\tgradient.addColorStop(0, waveEdge);\n\tgradient.addColorStop(0.5, wave);\n\tgradient.addColorStop(1, waveEdge);\n\n\treturn gradient;\n}\n\nfunction paintWave(context: CanvasRenderingContext2D, scene: Scene, peaks: WaveformPeaks): void {\n\tconst buckets = waveformBuckets(peaks, scene);\n\tconst [first] = buckets;\n\tif (!first) return;\n\n\tconst center = scene.height / 2;\n\tconst amplitudePx = center * amplitudeMargin;\n\n\tcontext.fillStyle = waveFill(context, scene);\n\tcontext.beginPath();\n\tcontext.moveTo(first.x, center);\n\tfor (const bucket of buckets) context.lineTo(bucket.x, center - bucket.high * amplitudePx);\n\tfor (const bucket of buckets.toReversed())\n\t\tcontext.lineTo(bucket.x, center - bucket.low * amplitudePx);\n\tcontext.closePath();\n\tcontext.fill();\n}\n\nexport class SonicWaveform extends SonicWaveElement<Colour> {\n\tstatic override readonly observedAttributes = [\n\t\t...SonicWaveElement.observedAttributes,\n\t\t'pending-delay',\n\t\t'playing',\n\t\t'playback-rate',\n\t\t'reduced-motion',\n\t\t'zoom',\n\t];\n\n\tget peaks(): undefined | WaveformPeaks {\n\t\treturn this.#peaks;\n\t}\n\n\tset peaks(peaks: undefined | WaveformPeaks) {\n\t\tthis.#peaks = peaks;\n\t\tthis.renderEmpty();\n\t\tthis.#repaint();\n\t}\n\n\tget pending(): Array<[number, number]> {\n\t\treturn this.#pending.map(([start, end]) => [start, end]);\n\t}\n\n\tset pending(regions: TimeRegions | undefined) {\n\t\tconst pending = regions ? readRegions(regions) : [];\n\t\tif (String(pending) === String(this.#pending)) return;\n\n\t\tconst listedMs = this.#pendingListedMs;\n\t\tconst nowMs = performance.now();\n\n\t\tthis.#pending = pending;\n\t\tthis.#pendingListedMs = new Map(\n\t\t\tthis.#pending.map((region) => {\n\t\t\t\tconst key = region.join(':');\n\n\t\t\t\treturn [key, listedMs.get(key) ?? nowMs];\n\t\t\t}),\n\t\t);\n\t\tthis.#repaint();\n\t}\n\n\tget pendingDelay(): number {\n\t\treturn Math.max(0, this.numberAttribute('pending-delay', 0));\n\t}\n\n\tset pendingDelay(value: number | undefined) {\n\t\tthis.reflect('pending-delay', value);\n\t}\n\n\tget playbackRate(): number {\n\t\tconst playbackRate = this.numberAttribute('playback-rate', 1);\n\n\t\treturn playbackRate > 0 ? playbackRate : 1;\n\t}\n\n\tset playbackRate(value: number | undefined) {\n\t\tthis.reflect('playback-rate', value);\n\t}\n\n\tget playing(): boolean {\n\t\treturn this.hasAttribute('playing');\n\t}\n\n\tset playing(isPlaying: boolean) {\n\t\tthis.reflect('playing', isPlaying);\n\t}\n\n\tget readTime(): (() => number | undefined) | undefined {\n\t\treturn this.#readTime;\n\t}\n\n\tset readTime(read: (() => number | undefined) | undefined) {\n\t\tthis.#readTime = read;\n\t\tthis.render();\n\t}\n\n\tget reducedMotion(): 'page' | 'scroll' {\n\t\treturn this.getAttribute('reduced-motion') === 'scroll' ? 'scroll' : 'page';\n\t}\n\n\tset reducedMotion(motion: 'page' | 'scroll' | undefined) {\n\t\tthis.reflect('reduced-motion', motion);\n\t}\n\n\tget requestPeaks(): PeaksRequest | undefined {\n\t\treturn this.#requestPeaks;\n\t}\n\n\tset requestPeaks(request: PeaksRequest | undefined) {\n\t\tthis.#requestPeaks = request;\n\t\tthis.#askedKey = undefined;\n\t\tthis.surface()?.invalidate();\n\t}\n\n\tget zoom(): number {\n\t\tconst zoom = this.numberAttribute('zoom', defaultZoom);\n\n\t\treturn zoom > 0 ? zoom : defaultZoom;\n\t}\n\n\tset zoom(value: number | undefined) {\n\t\tthis.reflect('zoom', value);\n\t}\n\n\tprotected readonly control = renderWaveform();\n\n\tprotected readonly canvas = requireChild(\n\t\tthis.control,\n\t\t'.sonic-waveform-canvas',\n\t\tHTMLCanvasElement,\n\t);\n\n\tprotected readonly colours = colours;\n\n\tprotected readonly lengths = {};\n\n\tprotected readonly sheet = 'waveform.css';\n\n\tprotected readonly sizeProperty = '--_sonic-waveform-size';\n\n\treadonly #asked = new WeakSet<Promise<unknown>>();\n\n\t#askedKey: string | undefined;\n\n\treadonly #clock = createTrackingClock();\n\n\t#drawn: Drawn | undefined;\n\n\treadonly #ghost = requireChild(this.control, '.sonic-waveform-ghost', HTMLDivElement);\n\n\t#grabbed: Drawn | undefined;\n\n\t#isAsking = false;\n\n\t#kindColours = new Map<string, string>();\n\n\t#paintedKey = '';\n\n\t#peaks: undefined | WaveformPeaks;\n\n\t#pending: Array<[number, number]> = [];\n\n\t#pendingListedMs = new Map<string, number>();\n\n\treadonly #playhead = requireChild(this.control, '.sonic-waveform-playhead', HTMLDivElement);\n\n\t#readTime: (() => number | undefined) | undefined;\n\n\t#requestPeaks: PeaksRequest | undefined;\n\n\treadonly #rider = createLabelRider(this.control, {\n\t\tclassName: 'sonic-waveform-label',\n\t\tcolourProperty: '--_sonic-waveform-marker',\n\t\tfadeProperty: '--_sonic-waveform-label-fade',\n\t\tinsetProperty: '--_sonic-waveform-label-inset',\n\t\tparkProperty: '--_sonic-waveform-label-park',\n\t});\n\n\toverride connectedCallback(): void {\n\t\tthis.upgradeProperties(\n\t\t\t'peaks',\n\t\t\t'pending',\n\t\t\t'pendingDelay',\n\t\t\t'playing',\n\t\t\t'playbackRate',\n\t\t\t'readTime',\n\t\t\t'reducedMotion',\n\t\t\t'requestPeaks',\n\t\t\t'zoom',\n\t\t);\n\t\tsuper.connectedCallback();\n\t}\n\n\tprotected override connect(signal: AbortSignal): void {\n\t\tsuper.connect(signal);\n\t\tthis.bindGestures(this.control, signal, () => this.#grab());\n\t\tif (!('fonts' in document)) return;\n\n\t\tdocument.fonts.addEventListener(\n\t\t\t'loadingdone',\n\t\t\t() => {\n\t\t\t\tthis.renderMarkers();\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t}\n\n\tprotected draw(): void {\n\t\tconst surface = this.surface();\n\n\t\tsurface?.requestFrame();\n\t\t// No frame follows the position out of view, or before the first one paints\n\t\tif (surface?.isVisible === false || !this.#drawn) this.showCurrentMarker(this.#sourceSeconds());\n\t}\n\n\tprotected isEmpty(): boolean {\n\t\treturn !this.#peaks || this.#peaks.samples.length === 0;\n\t}\n\n\tprotected paint(\n\t\tcontext: CanvasRenderingContext2D,\n\t\t{ frameMs, isDirty, isRebuilt, look, size }: SurfaceFrame<Colour>,\n\t): void {\n\t\tconst held = this.#held();\n\t\tconst pending = this.#duePending();\n\t\tconst timeline = frameTimeline({\n\t\t\tclockSeconds: this.#clock.read(frameMs, {\n\t\t\t\tisPlaying: this.playing,\n\t\t\t\tplaybackRate: this.playbackRate,\n\t\t\t\tseconds: this.#sourceSeconds(),\n\t\t\t}),\n\t\t\tframeMs,\n\t\t\t...(held ? { held } : {}),\n\t\t\tisPaged: look.isReducedMotion && this.reducedMotion === 'page',\n\t\t\tisPlaying: this.playing,\n\t\t\tisStill: look.isReducedMotion,\n\t\t\tpending,\n\t\t\trange: [this.min, Math.max(this.min, this.max)],\n\t\t\tsize,\n\t\t\tzoom: this.zoom,\n\t\t});\n\t\tconst { seconds, view } = timeline;\n\n\t\tthis.#drawn = { seconds, startSeconds: view.startSeconds };\n\t\tplaceLine(this.#playhead, timeline.playheadAt);\n\t\tplaceLine(this.#ghost, timeline.ghostAt);\n\t\tthis.showCurrentMarker(seconds);\n\t\tthis.toggleState('pending', view.pending.length > 0);\n\t\tthis.#rider.place({\n\t\t\tplayheadSeconds: seconds,\n\t\t\tstartSeconds: view.startSeconds,\n\t\t\twidthPx: view.width / view.dpr,\n\t\t\twindowSeconds: view.width / view.pixelsPerSecond,\n\t\t});\n\t\tif (isDirty || timeline.paintKey !== this.#paintedKey) {\n\t\t\tthis.#paintedKey = timeline.paintKey;\n\t\t\tthis.#paintScene(context, { ...view, colours: look.colours }, isRebuilt);\n\t\t\tif (timeline.wanted) this.#ask(timeline.wanted);\n\t\t}\n\t\tif (timeline.isMoving) this.surface()?.requestFrame();\n\t}\n\n\tprotected override renderMarkers(): void {\n\t\tconst bounds = this.mapping().bounds;\n\n\t\tthis.#rider.measure(\n\t\t\tthis.markers.flatMap(({ dimmed, kind, label, start }) =>\n\t\t\t\tlabel === undefined || label === ''\n\t\t\t\t\t? []\n\t\t\t\t\t: [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tisDimmed: dimmed === true,\n\t\t\t\t\t\t\t\t...(kind === undefined ? {} : { kind }),\n\t\t\t\t\t\t\t\tstart: clamp(start, ...bounds),\n\t\t\t\t\t\t\t\ttext: label,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t],\n\t\t\t),\n\t\t);\n\t\tthis.surface()?.rebuild();\n\t}\n\n\t#ask(wanted: [number, number]): void {\n\t\tconst key = wanted.join(':');\n\t\tif (key === this.#askedKey) return;\n\n\t\tlet asked: ReturnType<PeaksRequest>;\n\n\t\tthis.#askedKey = key;\n\t\tthis.#isAsking = true;\n\t\ttry {\n\t\t\tasked = this.#requestPeaks?.(...wanted);\n\t\t} finally {\n\t\t\tthis.#isAsking = false;\n\t\t}\n\n\t\tconst promises = asked instanceof Promise ? [asked] : (asked ?? []);\n\n\t\tfor (const promise of promises) {\n\t\t\tif (this.#asked.has(promise)) continue;\n\n\t\t\tthis.#asked.add(promise);\n\t\t\tvoid this.#repaintAfter(promise);\n\t\t}\n\t}\n\n\t#duePending(): Array<[number, number]> {\n\t\tconst due = dueRegions(this.#pending, this.#pendingListedMs, [\n\t\t\tperformance.now(),\n\t\t\tthis.pendingDelay,\n\t\t]);\n\n\t\tif (due.length < this.#pending.length) this.surface()?.requestFrame();\n\n\t\treturn due;\n\t}\n\n\t#grab(): ValueAxis {\n\t\tconst drawn = this.#drawn ?? { seconds: this.value, startSeconds: this.value };\n\n\t\tthis.#grabbed = drawn;\n\n\t\tconst mapping = this.mapping();\n\t\tconst [low, high] = mapping.bounds;\n\n\t\treturn {\n\t\t\tfromProportion: mapping.proportionOf(drawn.seconds),\n\t\t\tisKeptOnCancel: true,\n\t\t\tposition: (event) => -event.clientX,\n\t\t\ttravelPx: Math.max(1, (high - low) * this.zoom),\n\t\t};\n\t}\n\n\t#held(): undefined | { grabbed: Drawn; seconds: number } {\n\t\tconst grabbed = this.#grabbed;\n\t\tconst from = this.heldFrom();\n\t\tif (!grabbed || from === undefined) return undefined;\n\n\t\treturn { grabbed, seconds: this.value === from ? grabbed.seconds : this.value };\n\t}\n\n\t#paintMarkers(context: CanvasRenderingContext2D, scene: Scene, isRebuilt: boolean): void {\n\t\tif (isRebuilt) this.#kindColours = this.#readKindColours();\n\n\t\tconst line = Math.max(1, Math.round(scene.dpr));\n\n\t\tfor (const { dimmed, kind, start } of this.markers) {\n\t\t\tconst x = (start - scene.startSeconds) * scene.pixelsPerSecond;\n\n\t\t\tcontext.fillStyle =\n\t\t\t\t(isKind(kind) ? this.#kindColours.get(kind) : undefined) ?? scene.colours.marker;\n\t\t\tcontext.globalAlpha = dimmed === true ? 0.5 : 1;\n\t\t\tcontext.fillRect(x - line / 2, 0, line, scene.height);\n\t\t}\n\t\tcontext.globalAlpha = 1;\n\t}\n\n\t#paintScene(context: CanvasRenderingContext2D, scene: Scene, isRebuilt: boolean): void {\n\t\tconst { dpr, height, pixelsPerSecond, startSeconds, width } = scene;\n\t\tconst x = (seconds: number): number => (seconds - startSeconds) * pixelsPerSecond;\n\t\tconst { ends, grid } = scene.colours;\n\t\tconst line = Math.max(1, Math.round(dpr));\n\t\tconst [opening, closing] = [x(this.min), x(Math.max(this.min, this.max))];\n\t\tconst [from, to] = [clamp(opening, 0, width), clamp(closing, 0, width)];\n\n\t\tcontext.clearRect(0, 0, width, height);\n\t\tpaintHatch(context, scene, [0, from]);\n\t\tpaintHatch(context, scene, [to, width]);\n\t\tcontext.fillStyle = grid;\n\t\tif (to > from) context.fillRect(from, Math.round((height - line) / 2), to - from, line);\n\t\tpaintPlaceholders(context, scene);\n\t\tif (this.#peaks) paintWave(context, scene, this.#peaks);\n\t\t// Scrolling lines sit at fractional x; rounding them judders\n\t\tcontext.fillStyle = ends;\n\t\tfor (const edge of [opening, closing]) context.fillRect(edge - line / 2, 0, line, height);\n\t\tthis.#paintMarkers(context, scene, isRebuilt);\n\t}\n\n\t#readKindColours(): Map<string, string> {\n\t\tconst kinds = [...new Set(this.markers.map(({ kind }) => kind).filter((kind) => isKind(kind)))];\n\t\tconst probes = kinds.map((kind) => {\n\t\t\tconst probe = document.createElement('div');\n\n\t\t\tprobe.className = 'sonic-waveform-label';\n\t\t\twriteKind(probe, kind, '--_sonic-waveform-marker');\n\n\t\t\treturn probe;\n\t\t});\n\n\t\tthis.control.append(...probes);\n\n\t\tconst read = probes.map((probe) => getComputedStyle(probe).color);\n\n\t\tfor (const probe of probes) probe.remove();\n\n\t\treturn new Map(kinds.map((kind, index) => [kind, read[index] ?? '']));\n\t}\n\n\t// A write from inside the consumer's call repaints without asking for the same window again\n\t#repaint(): void {\n\t\tif (!this.#isAsking) this.#askedKey = undefined;\n\t\tthis.surface()?.invalidate();\n\t}\n\n\tasync #repaintAfter(asked: Promise<unknown>): Promise<void> {\n\t\tawait Promise.allSettled([asked]);\n\t\tthis.#repaint();\n\t}\n\n\t#sourceSeconds(): number {\n\t\treturn this.#readTime?.() ?? this.playback();\n\t}\n}\n"],"mappings":"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"}
|
|
@@ -47,6 +47,7 @@ export declare class SonicWavestrip extends SonicWaveElement<Colour, Length> {
|
|
|
47
47
|
};
|
|
48
48
|
protected readonly sheet = "wavestrip.css";
|
|
49
49
|
protected readonly sizeProperty = "--_sonic-wavestrip-size";
|
|
50
|
+
clientXOf(value: number): number;
|
|
50
51
|
connectedCallback(): void;
|
|
51
52
|
protected connect(signal: AbortSignal): void;
|
|
52
53
|
protected draw(): void;
|
|
@@ -80,6 +80,10 @@ var SonicWavestrip = class extends SonicWaveElement {
|
|
|
80
80
|
#markerBand = createMarkerBand(requireChild(this.control, ".sonic-wavestrip-markers", HTMLDivElement));
|
|
81
81
|
#paintedKey = "";
|
|
82
82
|
#peaks;
|
|
83
|
+
clientXOf(value) {
|
|
84
|
+
const { startPx, travelPx } = this.#axis();
|
|
85
|
+
return startPx + this.mapping().proportionOf(value) * travelPx;
|
|
86
|
+
}
|
|
83
87
|
connectedCallback() {
|
|
84
88
|
this.upgradeProperties("buffered", "cancellable", "peaks");
|
|
85
89
|
super.connectedCallback();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wavestrip.js","names":[],"sources":["../../../src/elements/wavestrip.ts"],"sourcesContent":["import type { ValueAxis } from '#elements/value-gestures.ts';\nimport type { SurfaceFrame } from '#lib/canvas-surface.ts';\nimport type { StripRegions } from '#lib/strip-scene.ts';\nimport type { TimeRegions } from '#lib/time-regions.ts';\n\nimport { SonicWaveElement } from '#elements/wave-element.ts';\nimport { createMarkerBand } from '#lib/marker-band.ts';\nimport { clampProportion } from '#lib/math.ts';\nimport { readPxProperty } from '#lib/read-px-property.ts';\nimport { requireChild, template } from '#lib/render.ts';\nimport { stripBars, stripGroove, stripRegions } from '#lib/strip-scene.ts';\nimport { sortedRegions } from '#lib/time-regions.ts';\n\ndeclare global {\n\tinterface HTMLElementTagNameMap {\n\t\t'sonic-wavestrip': SonicWavestrip;\n\t}\n}\n\ntype Colour = keyof typeof colours;\n\ntype Length = keyof typeof lengths;\n\nconst colours = {\n\tbuffered: '--_sonic-wavestrip-buffered',\n\tplayed: '--_sonic-wavestrip-played',\n\tscrub: '--_sonic-wavestrip-scrub',\n\twave: '--_sonic-wavestrip-wave',\n} as const;\n\nconst lengths = {\n\tgapRatio: '--_sonic-wavestrip-bar-gap-ratio',\n\tgrooveRatio: '--_sonic-wavestrip-groove-ratio',\n\tgrooveSize: '--_sonic-wavestrip-groove-size',\n\tpitch: '--_sonic-wavestrip-bar-pitch',\n\tradiusRatio: '--_sonic-wavestrip-bar-radius-ratio',\n} as const;\n\nconst cancelZonePx = 48;\n\nconst renderWavestrip = template(\n\t/* HTML */ `\n\t\t<div class=\"sonic-wavestrip\" role=\"slider\" tabindex=\"0\">\n\t\t\t<canvas class=\"sonic-wavestrip-canvas\" aria-hidden=\"true\"></canvas>\n\t\t\t<div class=\"sonic-wavestrip-markers\" aria-hidden=\"true\"></div>\n\t\t\t<div class=\"sonic-wavestrip-readout\" popover=\"manual\">\n\t\t\t\t<span></span>\n\t\t\t\t<input\n\t\t\t\t\tautocomplete=\"off\"\n\t\t\t\t\tclass=\"sonic-wavestrip-entry\"\n\t\t\t\t\tenterkeyhint=\"done\"\n\t\t\t\t\thidden\n\t\t\t\t\tspellcheck=\"false\"\n\t\t\t\t\ttype=\"text\"\n\t\t\t\t/>\n\t\t\t</div>\n\t\t</div>\n\t`,\n\tHTMLDivElement,\n);\n\nfunction fillRegion(\n\tcontext: CanvasRenderingContext2D,\n\tpath: Path2D,\n\t[from, to, style]: [number, number, string],\n): void {\n\tcontext.save();\n\tcontext.beginPath();\n\tcontext.rect(from, 0, to - from, context.canvas.height);\n\tcontext.clip();\n\tcontext.fillStyle = style;\n\tcontext.fill(path);\n\tcontext.restore();\n}\n\nexport class SonicWavestrip extends SonicWaveElement<Colour, Length> {\n\t// fallow-ignore-next-line code-duplication -- one accessor pair per property\n\tget buffered(): Array<[number, number]> {\n\t\treturn this.#buffered.map(([start, end]) => [start, end]);\n\t}\n\n\tset buffered(regions: TimeRegions | undefined) {\n\t\tthis.#buffered = sortedRegions(regions);\n\t\tthis.render();\n\t}\n\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 peaks(): ArrayLike<number> | undefined {\n\t\treturn this.#peaks;\n\t}\n\n\tset peaks(peaks: ArrayLike<number> | undefined) {\n\t\tthis.#peaks = peaks;\n\t\tthis.renderEmpty();\n\t\tthis.surface()?.invalidate();\n\t}\n\n\tprotected readonly control = renderWavestrip();\n\n\tprotected readonly canvas = requireChild(\n\t\tthis.control,\n\t\t'.sonic-wavestrip-canvas',\n\t\tHTMLCanvasElement,\n\t);\n\n\tprotected readonly colours = colours;\n\n\tprotected readonly lengths = lengths;\n\n\tprotected readonly sheet = 'wavestrip.css';\n\n\tprotected readonly sizeProperty = '--_sonic-wavestrip-size';\n\n\t#bars: Path2D | undefined;\n\n\t#buffered: Array<[number, number]> = [];\n\n\treadonly #markerBand = createMarkerBand(\n\t\trequireChild(this.control, '.sonic-wavestrip-markers', HTMLDivElement),\n\t);\n\n\t#paintedKey = '';\n\n\t#peaks: ArrayLike<number> | undefined;\n\n\toverride connectedCallback(): void {\n\t\tthis.upgradeProperties('buffered', 'cancellable', 'peaks');\n\t\tsuper.connectedCallback();\n\t}\n\n\tprotected override connect(signal: AbortSignal): void {\n\t\tconst { control } = this;\n\n\t\tsuper.connect(signal);\n\t\tthis.bindHover(control, signal, {\n\t\t\tplace: () => {\n\t\t\t\tthis.#placeReadout();\n\t\t\t},\n\t\t\tvalueAt: (event) => {\n\t\t\t\tconst axis = this.#axis();\n\t\t\t\tconst at = clampProportion((event.clientX - axis.startPx) / axis.travelPx);\n\t\t\t\tconst mapping = this.mapping();\n\n\t\t\t\treturn this.#markerAt(event, axis) ?? mapping.snap(mapping.valueAt(at));\n\t\t\t},\n\t\t});\n\t\tthis.bindGestures(control, signal, (event) => this.#grab(event));\n\t}\n\n\tprotected draw(): void {\n\t\tconst surface = this.surface();\n\n\t\tthis.#placeReadout();\n\t\tthis.showCurrentMarker(this.playback());\n\t\tif (surface && this.#stripRegions(surface.size.width).key !== this.#paintedKey) {\n\t\t\tsurface.requestFrame();\n\t\t}\n\t}\n\n\tprotected isEmpty(): boolean {\n\t\treturn !this.#peaks || this.#peaks.length === 0;\n\t}\n\n\tprotected paint(context: CanvasRenderingContext2D, frame: SurfaceFrame<Colour, Length>): void {\n\t\tconst { look, size } = frame;\n\t\tconst strip = this.#stripRegions(size.width);\n\t\tconst bars = this.#barsFor(frame);\n\n\t\tthis.#paintedKey = strip.key;\n\t\tcontext.clearRect(0, 0, size.width, size.height);\n\t\tif (!bars) return;\n\n\t\tcontext.fillStyle = look.colours.wave;\n\t\tcontext.fill(bars);\n\t\tfor (const { from, kind, to } of strip.regions) {\n\t\t\tfillRegion(context, bars, [from, to, look.colours[kind]]);\n\t\t}\n\t}\n\n\tprotected override renderMarkers(): void {\n\t\tthis.#markerBand.render(this.markerList(), this.markerProportions());\n\t}\n\n\tprotected override scrubsKeyRepeat(): boolean {\n\t\treturn true;\n\t}\n\n\t#axis(): ValueAxis & { startPx: number; topPx: number } {\n\t\tconst box = this.canvas.getBoundingClientRect();\n\n\t\treturn {\n\t\t\tposition: (event: PointerEvent) => event.clientX,\n\t\t\tstartPx: box.left,\n\t\t\ttopPx: box.top,\n\t\t\ttravelPx: Math.max(1, box.width),\n\t\t};\n\t}\n\n\t#barsFor({ isDirty, look, size }: SurfaceFrame<Colour, Length>): Path2D | undefined {\n\t\tif (!isDirty && this.#bars) return this.#bars;\n\n\t\tconst peaks = this.#peaks;\n\t\tconst groove = stripGroove(size, look.lengths);\n\t\tconst drawn = groove ? [groove] : [];\n\t\tconst bars = peaks && peaks.length > 0 ? stripBars(peaks, size, look.lengths) : drawn;\n\n\t\tthis.#bars = undefined;\n\t\tif (bars.length === 0) return undefined;\n\n\t\tconst path = new Path2D();\n\n\t\tfor (const { height, radius, width, x, y } of bars) {\n\t\t\tpath.roundRect(x, y, width, height, radius);\n\t\t}\n\t\tthis.#bars = path;\n\n\t\treturn path;\n\t}\n\n\t#grab(event: PointerEvent): ValueAxis {\n\t\tconst axis = this.#axis();\n\t\tconst at = (event.clientX - axis.startPx) / axis.travelPx;\n\t\tconst outside = this.#outside();\n\t\tconst grabbed = {\n\t\t\tfromProportion: clampProportion(at),\n\t\t\tposition: axis.position,\n\t\t\ttravelPx: axis.travelPx,\n\t\t};\n\n\t\tthis.input(this.#markerAt(event, axis) ?? this.mapping().valueAt(at));\n\n\t\treturn outside ? { ...grabbed, outside } : grabbed;\n\t}\n\n\t#markerAt(\n\t\tevent: PointerEvent,\n\t\t{ startPx, topPx, travelPx }: { startPx: number; topPx: number; travelPx: number },\n\t): number | undefined {\n\t\tconst index = this.#markerBand.at(\n\t\t\t{ x: event.clientX - startPx, y: event.clientY - topPx },\n\t\t\ttravelPx,\n\t\t);\n\n\t\treturn index === undefined ? undefined : this.markerList()[index]?.start;\n\t}\n\n\t#outside(): ((event: PointerEvent) => boolean) | undefined {\n\t\tif (!this.cancellable) return undefined;\n\n\t\tconst box = this.control.getBoundingClientRect();\n\t\tconst zone = readPxProperty(\n\t\t\tgetComputedStyle(this.control),\n\t\t\t'--_sonic-cancel-zone',\n\t\t\tcancelZonePx,\n\t\t);\n\n\t\treturn (event) => event.clientY < box.top - zone || event.clientY > box.bottom + zone;\n\t}\n\n\t#placeReadout(): void {\n\t\tconst at = this.mapping().proportionOf(this.readoutValue());\n\n\t\tthis.control.style.setProperty('--_sonic-wavestrip-readout-at', String(at));\n\t}\n\n\t#stripRegions(width: number): StripRegions {\n\t\tconst { proportionOf } = this.mapping();\n\t\tconst isScrubbing = this.heldFrom() !== undefined && this.isRevealed();\n\n\t\treturn stripRegions(\n\t\t\t{\n\t\t\t\tbuffered: this.#buffered.map(([start, end]) => [proportionOf(start), proportionOf(end)]),\n\t\t\t\tplayed: proportionOf(this.playback()),\n\t\t\t\t...(isScrubbing ? { scrub: proportionOf(this.value) } : {}),\n\t\t\t},\n\t\t\twidth,\n\t\t);\n\t}\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"wavestrip.js","names":[],"sources":["../../../src/elements/wavestrip.ts"],"sourcesContent":["import type { ValueAxis } from '#elements/value-gestures.ts';\nimport type { SurfaceFrame } from '#lib/canvas-surface.ts';\nimport type { StripRegions } from '#lib/strip-scene.ts';\nimport type { TimeRegions } from '#lib/time-regions.ts';\n\nimport { SonicWaveElement } from '#elements/wave-element.ts';\nimport { createMarkerBand } from '#lib/marker-band.ts';\nimport { clampProportion } from '#lib/math.ts';\nimport { readPxProperty } from '#lib/read-px-property.ts';\nimport { requireChild, template } from '#lib/render.ts';\nimport { stripBars, stripGroove, stripRegions } from '#lib/strip-scene.ts';\nimport { sortedRegions } from '#lib/time-regions.ts';\n\ndeclare global {\n\tinterface HTMLElementTagNameMap {\n\t\t'sonic-wavestrip': SonicWavestrip;\n\t}\n}\n\ntype Colour = keyof typeof colours;\n\ntype Length = keyof typeof lengths;\n\nconst colours = {\n\tbuffered: '--_sonic-wavestrip-buffered',\n\tplayed: '--_sonic-wavestrip-played',\n\tscrub: '--_sonic-wavestrip-scrub',\n\twave: '--_sonic-wavestrip-wave',\n} as const;\n\nconst lengths = {\n\tgapRatio: '--_sonic-wavestrip-bar-gap-ratio',\n\tgrooveRatio: '--_sonic-wavestrip-groove-ratio',\n\tgrooveSize: '--_sonic-wavestrip-groove-size',\n\tpitch: '--_sonic-wavestrip-bar-pitch',\n\tradiusRatio: '--_sonic-wavestrip-bar-radius-ratio',\n} as const;\n\nconst cancelZonePx = 48;\n\nconst renderWavestrip = template(\n\t/* HTML */ `\n\t\t<div class=\"sonic-wavestrip\" role=\"slider\" tabindex=\"0\">\n\t\t\t<canvas class=\"sonic-wavestrip-canvas\" aria-hidden=\"true\"></canvas>\n\t\t\t<div class=\"sonic-wavestrip-markers\" aria-hidden=\"true\"></div>\n\t\t\t<div class=\"sonic-wavestrip-readout\" popover=\"manual\">\n\t\t\t\t<span></span>\n\t\t\t\t<input\n\t\t\t\t\tautocomplete=\"off\"\n\t\t\t\t\tclass=\"sonic-wavestrip-entry\"\n\t\t\t\t\tenterkeyhint=\"done\"\n\t\t\t\t\thidden\n\t\t\t\t\tspellcheck=\"false\"\n\t\t\t\t\ttype=\"text\"\n\t\t\t\t/>\n\t\t\t</div>\n\t\t</div>\n\t`,\n\tHTMLDivElement,\n);\n\nfunction fillRegion(\n\tcontext: CanvasRenderingContext2D,\n\tpath: Path2D,\n\t[from, to, style]: [number, number, string],\n): void {\n\tcontext.save();\n\tcontext.beginPath();\n\tcontext.rect(from, 0, to - from, context.canvas.height);\n\tcontext.clip();\n\tcontext.fillStyle = style;\n\tcontext.fill(path);\n\tcontext.restore();\n}\n\nexport class SonicWavestrip extends SonicWaveElement<Colour, Length> {\n\t// fallow-ignore-next-line code-duplication -- one accessor pair per property\n\tget buffered(): Array<[number, number]> {\n\t\treturn this.#buffered.map(([start, end]) => [start, end]);\n\t}\n\n\tset buffered(regions: TimeRegions | undefined) {\n\t\tthis.#buffered = sortedRegions(regions);\n\t\tthis.render();\n\t}\n\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 peaks(): ArrayLike<number> | undefined {\n\t\treturn this.#peaks;\n\t}\n\n\tset peaks(peaks: ArrayLike<number> | undefined) {\n\t\tthis.#peaks = peaks;\n\t\tthis.renderEmpty();\n\t\tthis.surface()?.invalidate();\n\t}\n\n\tprotected readonly control = renderWavestrip();\n\n\tprotected readonly canvas = requireChild(\n\t\tthis.control,\n\t\t'.sonic-wavestrip-canvas',\n\t\tHTMLCanvasElement,\n\t);\n\n\tprotected readonly colours = colours;\n\n\tprotected readonly lengths = lengths;\n\n\tprotected readonly sheet = 'wavestrip.css';\n\n\tprotected readonly sizeProperty = '--_sonic-wavestrip-size';\n\n\t#bars: Path2D | undefined;\n\n\t#buffered: Array<[number, number]> = [];\n\n\treadonly #markerBand = createMarkerBand(\n\t\trequireChild(this.control, '.sonic-wavestrip-markers', HTMLDivElement),\n\t);\n\n\t#paintedKey = '';\n\n\t#peaks: ArrayLike<number> | undefined;\n\n\tclientXOf(value: number): number {\n\t\tconst { startPx, travelPx } = this.#axis();\n\n\t\treturn startPx + this.mapping().proportionOf(value) * travelPx;\n\t}\n\n\toverride connectedCallback(): void {\n\t\tthis.upgradeProperties('buffered', 'cancellable', 'peaks');\n\t\tsuper.connectedCallback();\n\t}\n\n\tprotected override connect(signal: AbortSignal): void {\n\t\tconst { control } = this;\n\n\t\tsuper.connect(signal);\n\t\tthis.bindHover(control, signal, {\n\t\t\tplace: () => {\n\t\t\t\tthis.#placeReadout();\n\t\t\t},\n\t\t\tvalueAt: (event) => {\n\t\t\t\tconst axis = this.#axis();\n\t\t\t\tconst at = clampProportion((event.clientX - axis.startPx) / axis.travelPx);\n\t\t\t\tconst mapping = this.mapping();\n\n\t\t\t\treturn this.#markerAt(event, axis) ?? mapping.snap(mapping.valueAt(at));\n\t\t\t},\n\t\t});\n\t\tthis.bindGestures(control, signal, (event) => this.#grab(event));\n\t}\n\n\tprotected draw(): void {\n\t\tconst surface = this.surface();\n\n\t\tthis.#placeReadout();\n\t\tthis.showCurrentMarker(this.playback());\n\t\tif (surface && this.#stripRegions(surface.size.width).key !== this.#paintedKey) {\n\t\t\tsurface.requestFrame();\n\t\t}\n\t}\n\n\tprotected isEmpty(): boolean {\n\t\treturn !this.#peaks || this.#peaks.length === 0;\n\t}\n\n\tprotected paint(context: CanvasRenderingContext2D, frame: SurfaceFrame<Colour, Length>): void {\n\t\tconst { look, size } = frame;\n\t\tconst strip = this.#stripRegions(size.width);\n\t\tconst bars = this.#barsFor(frame);\n\n\t\tthis.#paintedKey = strip.key;\n\t\tcontext.clearRect(0, 0, size.width, size.height);\n\t\tif (!bars) return;\n\n\t\tcontext.fillStyle = look.colours.wave;\n\t\tcontext.fill(bars);\n\t\tfor (const { from, kind, to } of strip.regions) {\n\t\t\tfillRegion(context, bars, [from, to, look.colours[kind]]);\n\t\t}\n\t}\n\n\tprotected override renderMarkers(): void {\n\t\tthis.#markerBand.render(this.markerList(), this.markerProportions());\n\t}\n\n\tprotected override scrubsKeyRepeat(): boolean {\n\t\treturn true;\n\t}\n\n\t#axis(): ValueAxis & { startPx: number; topPx: number } {\n\t\tconst box = this.canvas.getBoundingClientRect();\n\n\t\treturn {\n\t\t\tposition: (event: PointerEvent) => event.clientX,\n\t\t\tstartPx: box.left,\n\t\t\ttopPx: box.top,\n\t\t\ttravelPx: Math.max(1, box.width),\n\t\t};\n\t}\n\n\t#barsFor({ isDirty, look, size }: SurfaceFrame<Colour, Length>): Path2D | undefined {\n\t\tif (!isDirty && this.#bars) return this.#bars;\n\n\t\tconst peaks = this.#peaks;\n\t\tconst groove = stripGroove(size, look.lengths);\n\t\tconst drawn = groove ? [groove] : [];\n\t\tconst bars = peaks && peaks.length > 0 ? stripBars(peaks, size, look.lengths) : drawn;\n\n\t\tthis.#bars = undefined;\n\t\tif (bars.length === 0) return undefined;\n\n\t\tconst path = new Path2D();\n\n\t\tfor (const { height, radius, width, x, y } of bars) {\n\t\t\tpath.roundRect(x, y, width, height, radius);\n\t\t}\n\t\tthis.#bars = path;\n\n\t\treturn path;\n\t}\n\n\t#grab(event: PointerEvent): ValueAxis {\n\t\tconst axis = this.#axis();\n\t\tconst at = (event.clientX - axis.startPx) / axis.travelPx;\n\t\tconst outside = this.#outside();\n\t\tconst grabbed = {\n\t\t\tfromProportion: clampProportion(at),\n\t\t\tposition: axis.position,\n\t\t\ttravelPx: axis.travelPx,\n\t\t};\n\n\t\tthis.input(this.#markerAt(event, axis) ?? this.mapping().valueAt(at));\n\n\t\treturn outside ? { ...grabbed, outside } : grabbed;\n\t}\n\n\t#markerAt(\n\t\tevent: PointerEvent,\n\t\t{ startPx, topPx, travelPx }: { startPx: number; topPx: number; travelPx: number },\n\t): number | undefined {\n\t\tconst index = this.#markerBand.at(\n\t\t\t{ x: event.clientX - startPx, y: event.clientY - topPx },\n\t\t\ttravelPx,\n\t\t);\n\n\t\treturn index === undefined ? undefined : this.markerList()[index]?.start;\n\t}\n\n\t#outside(): ((event: PointerEvent) => boolean) | undefined {\n\t\tif (!this.cancellable) return undefined;\n\n\t\tconst box = this.control.getBoundingClientRect();\n\t\tconst zone = readPxProperty(\n\t\t\tgetComputedStyle(this.control),\n\t\t\t'--_sonic-cancel-zone',\n\t\t\tcancelZonePx,\n\t\t);\n\n\t\treturn (event) => event.clientY < box.top - zone || event.clientY > box.bottom + zone;\n\t}\n\n\t#placeReadout(): void {\n\t\tconst at = this.mapping().proportionOf(this.readoutValue());\n\n\t\tthis.control.style.setProperty('--_sonic-wavestrip-readout-at', String(at));\n\t}\n\n\t#stripRegions(width: number): StripRegions {\n\t\tconst { proportionOf } = this.mapping();\n\t\tconst isScrubbing = this.heldFrom() !== undefined && this.isRevealed();\n\n\t\treturn stripRegions(\n\t\t\t{\n\t\t\t\tbuffered: this.#buffered.map(([start, end]) => [proportionOf(start), proportionOf(end)]),\n\t\t\t\tplayed: proportionOf(this.playback()),\n\t\t\t\t...(isScrubbing ? { scrub: proportionOf(this.value) } : {}),\n\t\t\t},\n\t\t\twidth,\n\t\t);\n\t}\n}\n"],"mappings":"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"}
|
package/dist/dev/index.d.ts
CHANGED
|
@@ -10,11 +10,12 @@ import { SonicSlider } from "./elements/slider.js";
|
|
|
10
10
|
import { SonicSpectrum } from "./elements/spectrum.js";
|
|
11
11
|
import { SonicSplit } from "./elements/split.js";
|
|
12
12
|
import { SonicSwitch } from "./elements/switch.js";
|
|
13
|
+
import { SonicToggle } from "./elements/toggle.js";
|
|
13
14
|
import { WaveMarker } from "./elements/wave-element.js";
|
|
14
|
-
import { SonicWaveform } from "./elements/waveform.js";
|
|
15
|
+
import { PeaksRequest, SonicWaveform } from "./elements/waveform.js";
|
|
15
16
|
import { SonicWavestrip } from "./elements/wavestrip.js";
|
|
16
17
|
import { SonicXy } from "./elements/xy.js";
|
|
17
18
|
import { crossfadeGains } from "./lib/crossfade.js";
|
|
18
19
|
import { envelopeCurve } from "./lib/envelope-curve.js";
|
|
19
20
|
import { formatPercent, parsePercent } from "./lib/percent.js";
|
|
20
|
-
export { SonicButton, SonicDial, SonicEnvelope, SonicMeter, SonicNumber, SonicSegmented, SonicSlider, SonicSpectrum, SonicSplit, SonicSwitch, SonicWaveform, SonicWavestrip, SonicXy, type TimeRegions, type WaveMarker, type WaveformPeaks, crossfadeGains, envelopeCurve, formatPercent, parsePercent };
|
|
21
|
+
export { type PeaksRequest, SonicButton, SonicDial, SonicEnvelope, SonicMeter, SonicNumber, SonicSegmented, SonicSlider, SonicSpectrum, SonicSplit, SonicSwitch, SonicToggle, SonicWaveform, SonicWavestrip, SonicXy, type TimeRegions, type WaveMarker, type WaveformPeaks, crossfadeGains, envelopeCurve, formatPercent, parsePercent };
|
package/dist/dev/index.js
CHANGED
|
@@ -9,10 +9,11 @@ import { SonicSlider } from "./elements/slider.js";
|
|
|
9
9
|
import { SonicSpectrum } from "./elements/spectrum.js";
|
|
10
10
|
import { SonicSplit } from "./elements/split.js";
|
|
11
11
|
import { SonicSwitch } from "./elements/switch.js";
|
|
12
|
+
import { SonicToggle } from "./elements/toggle.js";
|
|
12
13
|
import { SonicWaveform } from "./elements/waveform.js";
|
|
13
14
|
import { SonicWavestrip } from "./elements/wavestrip.js";
|
|
14
15
|
import { SonicXy } from "./elements/xy.js";
|
|
15
16
|
import { crossfadeGains } from "./lib/crossfade.js";
|
|
16
17
|
import { formatPercent, parsePercent } from "./lib/percent.js";
|
|
17
18
|
|
|
18
|
-
export { SonicButton, SonicDial, SonicEnvelope, SonicMeter, SonicNumber, SonicSegmented, SonicSlider, SonicSpectrum, SonicSplit, SonicSwitch, SonicWaveform, SonicWavestrip, SonicXy, crossfadeGains, envelopeCurve, formatPercent, parsePercent };
|
|
19
|
+
export { SonicButton, SonicDial, SonicEnvelope, SonicMeter, SonicNumber, SonicSegmented, SonicSlider, SonicSpectrum, SonicSplit, SonicSwitch, SonicToggle, SonicWaveform, SonicWavestrip, SonicXy, crossfadeGains, envelopeCurve, formatPercent, parsePercent };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
//#region src/lib/key-press.ts
|
|
2
|
+
function bindKeyPress(root, signal) {
|
|
3
|
+
let pressed;
|
|
4
|
+
const release = () => {
|
|
5
|
+
if (!pressed) return;
|
|
6
|
+
delete pressed.dataset.sonicPressed;
|
|
7
|
+
pressed = void 0;
|
|
8
|
+
};
|
|
9
|
+
root.addEventListener("keydown", (event) => {
|
|
10
|
+
if (event.key !== " " || event.repeat || event.defaultPrevented) return;
|
|
11
|
+
const part = event.target;
|
|
12
|
+
if (!(part instanceof HTMLButtonElement) || part.disabled) return;
|
|
13
|
+
release();
|
|
14
|
+
pressed = part;
|
|
15
|
+
part.dataset.sonicPressed = "";
|
|
16
|
+
}, { signal });
|
|
17
|
+
root.addEventListener("keyup", (event) => {
|
|
18
|
+
if (event.key === " " || event.key === "Meta") release();
|
|
19
|
+
}, { signal });
|
|
20
|
+
root.addEventListener("focusout", (event) => {
|
|
21
|
+
if (event.target === pressed) release();
|
|
22
|
+
}, { signal });
|
|
23
|
+
signal.addEventListener("abort", release, { once: true });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
//#endregion
|
|
27
|
+
export { bindKeyPress };
|
|
28
|
+
//# sourceMappingURL=key-press.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"key-press.js","names":[],"sources":["../../../src/lib/key-press.ts"],"sourcesContent":["// Firefox never matches `:active` on a button that a key holds\nexport function bindKeyPress(root: HTMLElement, signal: AbortSignal): void {\n\tlet pressed: HTMLButtonElement | undefined;\n\tconst release = (): void => {\n\t\tif (!pressed) return;\n\n\t\tdelete pressed.dataset.sonicPressed;\n\t\tpressed = undefined;\n\t};\n\n\troot.addEventListener(\n\t\t'keydown',\n\t\t(event) => {\n\t\t\tif (event.key !== ' ' || event.repeat || event.defaultPrevented) return;\n\n\t\t\tconst part = event.target;\n\t\t\tif (!(part instanceof HTMLButtonElement) || part.disabled) return;\n\n\t\t\trelease();\n\t\t\tpressed = part;\n\t\t\tpart.dataset.sonicPressed = '';\n\t\t},\n\t\t{ signal },\n\t);\n\troot.addEventListener(\n\t\t'keyup',\n\t\t(event) => {\n\t\t\t// macOS sends no `keyup` for a key let go while Cmd is down\n\t\t\tif (event.key === ' ' || event.key === 'Meta') release();\n\t\t},\n\t\t{ signal },\n\t);\n\troot.addEventListener(\n\t\t'focusout',\n\t\t(event) => {\n\t\t\tif (event.target === pressed) release();\n\t\t},\n\t\t{ signal },\n\t);\n\tsignal.addEventListener('abort', release, { once: true });\n}\n"],"mappings":";AACA,SAAgB,aAAa,MAAmB,QAA2B;CAC1E,IAAI;CACJ,MAAM,gBAAsB;EAC3B,IAAI,CAAC,SAAS;EAEd,OAAO,QAAQ,QAAQ;EACvB,UAAU;CACX;CAEA,KAAK,iBACJ,YACC,UAAU;EACV,IAAI,MAAM,QAAQ,OAAO,MAAM,UAAU,MAAM,kBAAkB;EAEjE,MAAM,OAAO,MAAM;EACnB,IAAI,EAAE,gBAAgB,sBAAsB,KAAK,UAAU;EAE3D,QAAQ;EACR,UAAU;EACV,KAAK,QAAQ,eAAe;CAC7B,GACA,EAAE,OAAO,CACV;CACA,KAAK,iBACJ,UACC,UAAU;EAEV,IAAI,MAAM,QAAQ,OAAO,MAAM,QAAQ,QAAQ,QAAQ;CACxD,GACA,EAAE,OAAO,CACV;CACA,KAAK,iBACJ,aACC,UAAU;EACV,IAAI,MAAM,WAAW,SAAS,QAAQ;CACvC,GACA,EAAE,OAAO,CACV;CACA,OAAO,iBAAiB,SAAS,SAAS,EAAE,MAAM,KAAK,CAAC;AACzD"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { clamp } from "./math.js";
|
|
2
|
+
|
|
3
|
+
//#region src/lib/toggle-travel.ts
|
|
4
|
+
function travelFrom({ at, count, first, isOnCap, last, pointer, well }) {
|
|
5
|
+
const step = count < 2 ? 0 : (well.length - well.thickness) / (count - 1);
|
|
6
|
+
return {
|
|
7
|
+
first,
|
|
8
|
+
last,
|
|
9
|
+
origin: isOnCap ? pointer - at * step : well.start + well.thickness / 2,
|
|
10
|
+
step
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
function positionAt({ first, last, origin, step }, pointer) {
|
|
14
|
+
if (step <= 0) return first;
|
|
15
|
+
return clamp((pointer - origin) / step, first, last);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
//#endregion
|
|
19
|
+
export { positionAt, travelFrom };
|
|
20
|
+
//# sourceMappingURL=toggle-travel.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"toggle-travel.js","names":[],"sources":["../../../src/lib/toggle-travel.ts"],"sourcesContent":["import { clamp } from '#lib/math.ts';\n\ninterface Well {\n\tlength: number;\n\tstart: number;\n\tthickness: number;\n}\n\ninterface Grab {\n\tat: number;\n\tcount: number;\n\tfirst: number;\n\tisOnCap: boolean;\n\tlast: number;\n\tpointer: number;\n\twell: Well;\n}\n\nexport interface ToggleTravel {\n\tfirst: number;\n\tlast: number;\n\torigin: number;\n\tstep: number;\n}\n\nexport function travelFrom({ at, count, first, isOnCap, last, pointer, well }: Grab): ToggleTravel {\n\tconst step = count < 2 ? 0 : (well.length - well.thickness) / (count - 1);\n\tconst origin = isOnCap ? pointer - at * step : well.start + well.thickness / 2;\n\n\treturn { first, last, origin, step };\n}\n\nexport function positionAt({ first, last, origin, step }: ToggleTravel, pointer: number): number {\n\tif (step <= 0) return first;\n\n\treturn clamp((pointer - origin) / step, first, last);\n}\n"],"mappings":";;;AAyBA,SAAgB,WAAW,EAAE,IAAI,OAAO,OAAO,SAAS,MAAM,SAAS,QAA4B;CAClG,MAAM,OAAO,QAAQ,IAAI,KAAK,KAAK,SAAS,KAAK,cAAc,QAAQ;CAGvE,OAAO;EAAE;EAAO;EAAM,QAFP,UAAU,UAAU,KAAK,OAAO,KAAK,QAAQ,KAAK,YAAY;EAE/C;CAAK;AACpC;AAEA,SAAgB,WAAW,EAAE,OAAO,MAAM,QAAQ,QAAsB,SAAyB;CAChG,IAAI,QAAQ,GAAG,OAAO;CAEtB,OAAO,OAAO,UAAU,UAAU,MAAM,OAAO,IAAI;AACpD"}
|