@xsynaptic/sonic-ui 0.6.1 → 0.8.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/README.md +4 -0
- package/dist/default/define/spectrum.js +7 -0
- package/dist/default/define.js +1 -0
- package/dist/default/elements/button.js +30 -2
- package/dist/default/elements/display-element.js +77 -0
- package/dist/default/elements/envelope.js +1 -1
- package/dist/default/elements/slider.js +67 -1
- package/dist/default/elements/spectrum.js +230 -0
- package/dist/default/elements/value-element.js +49 -11
- package/dist/default/elements/wave-element.js +59 -9
- package/dist/default/elements/waveform.js +56 -10
- package/dist/default/elements/wavestrip.js +31 -45
- package/dist/default/elements/xy.js +1 -1
- package/dist/default/index.js +2 -1
- package/dist/default/lib/canvas-surface.js +7 -4
- package/dist/default/lib/frame-timeline.js +2 -2
- package/dist/default/lib/hover-preview.js +46 -0
- package/dist/default/lib/marker-rider.js +7 -4
- package/dist/default/lib/pointer-drag.js +7 -2
- package/dist/default/lib/spectrum-columns.js +74 -0
- package/dist/default/lib/spectrum-levels.js +55 -0
- package/dist/default/lib/strip-scene.js +12 -1
- package/dist/default/lib/time-regions.js +5 -1
- package/dist/dev/define/spectrum.d.ts +2 -0
- package/dist/dev/define/spectrum.js +8 -0
- package/dist/dev/define/spectrum.js.map +1 -0
- package/dist/dev/define.d.ts +3 -1
- package/dist/dev/define.js +1 -0
- package/dist/dev/elements/button.d.ts +2 -0
- package/dist/dev/elements/button.js +30 -2
- package/dist/dev/elements/button.js.map +1 -1
- package/dist/dev/elements/display-element.d.ts +29 -0
- package/dist/dev/elements/display-element.js +78 -0
- package/dist/dev/elements/display-element.js.map +1 -0
- package/dist/dev/elements/envelope.js +1 -1
- package/dist/dev/elements/slider.d.ts +6 -0
- package/dist/dev/elements/slider.js +67 -1
- package/dist/dev/elements/slider.js.map +1 -1
- package/dist/dev/elements/spectrum.d.ts +59 -0
- package/dist/dev/elements/spectrum.js +231 -0
- package/dist/dev/elements/spectrum.js.map +1 -0
- package/dist/dev/elements/value-element.d.ts +5 -1
- package/dist/dev/elements/value-element.js +49 -11
- package/dist/dev/elements/value-element.js.map +1 -1
- package/dist/dev/elements/wave-element.d.ts +8 -2
- package/dist/dev/elements/wave-element.js +59 -9
- package/dist/dev/elements/wave-element.js.map +1 -1
- package/dist/dev/elements/waveform.d.ts +12 -4
- package/dist/dev/elements/waveform.js +56 -10
- package/dist/dev/elements/waveform.js.map +1 -1
- package/dist/dev/elements/wavestrip.d.ts +3 -0
- package/dist/dev/elements/wavestrip.js +31 -45
- package/dist/dev/elements/wavestrip.js.map +1 -1
- package/dist/dev/elements/xy.js +1 -1
- package/dist/dev/index.d.ts +4 -1
- package/dist/dev/index.js +2 -1
- package/dist/dev/lib/canvas-surface.d.ts +4 -3
- package/dist/dev/lib/canvas-surface.js +7 -4
- package/dist/dev/lib/canvas-surface.js.map +1 -1
- package/dist/dev/lib/frame-timeline.js +2 -2
- package/dist/dev/lib/frame-timeline.js.map +1 -1
- package/dist/dev/lib/hover-preview.js +47 -0
- package/dist/dev/lib/hover-preview.js.map +1 -0
- package/dist/dev/lib/marker-rider.js +7 -4
- package/dist/dev/lib/marker-rider.js.map +1 -1
- package/dist/dev/lib/pointer-drag.js +7 -2
- package/dist/dev/lib/pointer-drag.js.map +1 -1
- package/dist/dev/lib/spectrum-columns.js +75 -0
- package/dist/dev/lib/spectrum-columns.js.map +1 -0
- package/dist/dev/lib/spectrum-levels.js +56 -0
- package/dist/dev/lib/spectrum-levels.js.map +1 -0
- package/dist/dev/lib/strip-scene.js +12 -1
- package/dist/dev/lib/strip-scene.js.map +1 -1
- package/dist/dev/lib/time-regions.js +5 -1
- package/dist/dev/lib/time-regions.js.map +1 -1
- package/dist/styles/button.css +7 -1
- package/dist/styles/controls.css +1 -0
- package/dist/styles/material.css +18 -3
- package/dist/styles/meter.css +22 -18
- package/dist/styles/slider.css +42 -5
- package/dist/styles/spectrum.css +105 -0
- package/dist/styles/waveform.css +38 -16
- package/dist/styles/wavestrip.css +22 -4
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -23,6 +23,10 @@ import '@xsynaptic/sonic-ui/define';
|
|
|
23
23
|
<sonic-dial aria-label="Cutoff" value="40"></sonic-dial>
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
+
A `-size` token takes any CSS length and scales its control as one piece; a `-length` token also takes a percentage of the parent.
|
|
27
|
+
|
|
28
|
+
A display takes its signal as data: `spectrum.analyser = node`, or `spectrum.push(frame)`.
|
|
29
|
+
|
|
26
30
|
Under a hydrating framework, register the definitions once hydration is done (in React, from an effect); a control owns one child of its host and uses only the children it documents.
|
|
27
31
|
|
|
28
32
|
Targets current Chrome and Edge, Safari 26 and Firefox 147. MIT licensed.
|
package/dist/default/define.js
CHANGED
|
@@ -4,16 +4,25 @@ import { copyNode } from "../lib/copy-node.js";
|
|
|
4
4
|
import { bindHold } from "../lib/hold.js";
|
|
5
5
|
import { placeChildren, requireChild, template } from "../lib/render.js";
|
|
6
6
|
import { mirrorChildren } from "../lib/mirror-children.js";
|
|
7
|
+
import { capturePointer } from "../lib/pointer-drag.js";
|
|
7
8
|
|
|
8
9
|
//#region src/elements/button.ts
|
|
9
10
|
const renderButton = template(`
|
|
10
11
|
<button class="sonic-button" type="button"><span class="sonic-button-cap"></span></button>
|
|
11
12
|
`, HTMLButtonElement);
|
|
12
13
|
const holdKeys = /* @__PURE__ */ new Set([" ", "Enter"]);
|
|
14
|
+
const popupAttributes = {
|
|
15
|
+
controls: "aria-controls",
|
|
16
|
+
expanded: "aria-expanded",
|
|
17
|
+
popup: "aria-haspopup"
|
|
18
|
+
};
|
|
19
|
+
const pressWhens = /* @__PURE__ */ new Set(["pressed", "released"]);
|
|
13
20
|
var SonicButton = class extends SonicFormElement {
|
|
14
21
|
static observedAttributes = [
|
|
15
22
|
...SonicFormElement.observedAttributes,
|
|
23
|
+
...Object.keys(popupAttributes),
|
|
16
24
|
"busy",
|
|
25
|
+
"legend",
|
|
17
26
|
"momentary",
|
|
18
27
|
"pressed",
|
|
19
28
|
"soft-disabled",
|
|
@@ -44,6 +53,12 @@ var SonicButton = class extends SonicFormElement {
|
|
|
44
53
|
set latching(isLatching) {
|
|
45
54
|
this.reflect("latching", isLatching);
|
|
46
55
|
}
|
|
56
|
+
get legend() {
|
|
57
|
+
return this.getAttribute("legend") ?? void 0;
|
|
58
|
+
}
|
|
59
|
+
set legend(name) {
|
|
60
|
+
this.reflect("legend", name);
|
|
61
|
+
}
|
|
47
62
|
get momentary() {
|
|
48
63
|
return this.hasAttribute("momentary");
|
|
49
64
|
}
|
|
@@ -88,12 +103,13 @@ var SonicButton = class extends SonicFormElement {
|
|
|
88
103
|
connect(signal) {
|
|
89
104
|
const button = this.#button;
|
|
90
105
|
const cap = requireChild(button, ".sonic-button-cap", HTMLSpanElement);
|
|
91
|
-
this.upgradeProperties("armed", "busy", "defaultPressed", "momentary", "softDisabled", "latching", "pressed", "value");
|
|
106
|
+
this.upgradeProperties("armed", "busy", "defaultPressed", "legend", "momentary", "softDisabled", "latching", "pressed", "value");
|
|
92
107
|
mirrorChildren(this, {
|
|
93
108
|
control: button,
|
|
94
109
|
copy: copyNode,
|
|
95
110
|
place: (copies) => {
|
|
96
111
|
placeChildren(cap, copies);
|
|
112
|
+
this.#showLegend();
|
|
97
113
|
}
|
|
98
114
|
}, signal);
|
|
99
115
|
this.render();
|
|
@@ -122,6 +138,8 @@ var SonicButton = class extends SonicFormElement {
|
|
|
122
138
|
writeAttribute(button, "aria-pressed", this.latching ? String(this.pressed) : void 0);
|
|
123
139
|
this.toggleState("pressed", this.pressed);
|
|
124
140
|
this.forwardNaming(button, true);
|
|
141
|
+
this.#forwardPopup(button);
|
|
142
|
+
this.#showLegend();
|
|
125
143
|
const submitted = this.latching && this.pressed ? this.value : null;
|
|
126
144
|
this.writeFormValue(submitted, String(this.pressed));
|
|
127
145
|
}
|
|
@@ -142,7 +160,7 @@ var SonicButton = class extends SonicFormElement {
|
|
|
142
160
|
this.#holding = holding;
|
|
143
161
|
button.addEventListener("pointerdown", (event) => {
|
|
144
162
|
if (event.button !== 0 || !this.#isMomentary()) return;
|
|
145
|
-
button
|
|
163
|
+
capturePointer(button, event.pointerId);
|
|
146
164
|
holding.hold(event.pointerId);
|
|
147
165
|
}, { signal });
|
|
148
166
|
button.addEventListener("keydown", (event) => {
|
|
@@ -150,6 +168,9 @@ var SonicButton = class extends SonicFormElement {
|
|
|
150
168
|
holding.hold(event.key);
|
|
151
169
|
}, { signal });
|
|
152
170
|
}
|
|
171
|
+
#forwardPopup(button) {
|
|
172
|
+
for (const [name, aria] of Object.entries(popupAttributes)) writeAttribute(button, aria, this.getAttribute(name) ?? void 0);
|
|
173
|
+
}
|
|
153
174
|
#isMomentary() {
|
|
154
175
|
return this.momentary && !this.latching;
|
|
155
176
|
}
|
|
@@ -160,6 +181,13 @@ var SonicButton = class extends SonicFormElement {
|
|
|
160
181
|
this.pressed = isPressed;
|
|
161
182
|
this.dispatchEvent(new Event("change", { bubbles: true }));
|
|
162
183
|
}
|
|
184
|
+
#showLegend() {
|
|
185
|
+
const legend = this.legend;
|
|
186
|
+
for (const part of this.#button.querySelectorAll(":scope > .sonic-button-cap > [data-sonic-when]")) {
|
|
187
|
+
const when = part.dataset.sonicWhen ?? "";
|
|
188
|
+
part.toggleAttribute("data-sonic-hidden", !pressWhens.has(when) && when !== legend);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
163
191
|
};
|
|
164
192
|
|
|
165
193
|
//#endregion
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { SonicElement } from "./sonic-element.js";
|
|
2
|
+
import { bindSurface } from "../lib/canvas-surface.js";
|
|
3
|
+
|
|
4
|
+
//#region src/elements/display-element.ts
|
|
5
|
+
var SonicDisplayElement = class extends SonicElement {
|
|
6
|
+
static observedAttributes = [
|
|
7
|
+
...SonicElement.observedAttributes,
|
|
8
|
+
"max",
|
|
9
|
+
"min"
|
|
10
|
+
];
|
|
11
|
+
get analyser() {
|
|
12
|
+
return this.#analyser;
|
|
13
|
+
}
|
|
14
|
+
set analyser(node) {
|
|
15
|
+
this.#analyser = node ?? void 0;
|
|
16
|
+
this.#surface?.requestFrame();
|
|
17
|
+
}
|
|
18
|
+
get max() {
|
|
19
|
+
return this.numberAttribute("max", 0);
|
|
20
|
+
}
|
|
21
|
+
set max(decibels) {
|
|
22
|
+
this.reflect("max", decibels);
|
|
23
|
+
}
|
|
24
|
+
get min() {
|
|
25
|
+
return this.numberAttribute("min", -90);
|
|
26
|
+
}
|
|
27
|
+
set min(decibels) {
|
|
28
|
+
this.reflect("min", decibels);
|
|
29
|
+
}
|
|
30
|
+
#analyser;
|
|
31
|
+
#buffer = /* @__PURE__ */ new Float32Array(0);
|
|
32
|
+
#surface;
|
|
33
|
+
attributeChangedCallback() {
|
|
34
|
+
this.#surface?.invalidate();
|
|
35
|
+
}
|
|
36
|
+
connectedCallback() {
|
|
37
|
+
this.upgradeProperties("analyser", "max", "min");
|
|
38
|
+
super.connectedCallback();
|
|
39
|
+
}
|
|
40
|
+
push(frame) {
|
|
41
|
+
this.receive(frame);
|
|
42
|
+
this.#surface?.requestFrame();
|
|
43
|
+
}
|
|
44
|
+
repaint() {
|
|
45
|
+
this.#surface?.rebuild();
|
|
46
|
+
}
|
|
47
|
+
connect(signal) {
|
|
48
|
+
const control = this.control;
|
|
49
|
+
this.keepControl(control, signal);
|
|
50
|
+
this.#surface = bindSurface({
|
|
51
|
+
canvas: this.canvas,
|
|
52
|
+
colours: this.colours,
|
|
53
|
+
numbers: this.numbers,
|
|
54
|
+
paint: (context, frame) => {
|
|
55
|
+
this.#paint(context, frame);
|
|
56
|
+
},
|
|
57
|
+
signal
|
|
58
|
+
});
|
|
59
|
+
this.checkStyles(control, this.sheet);
|
|
60
|
+
}
|
|
61
|
+
surface() {
|
|
62
|
+
return this.#surface;
|
|
63
|
+
}
|
|
64
|
+
#paint(context, frame) {
|
|
65
|
+
const analyser = this.isDisabled() ? void 0 : this.#analyser;
|
|
66
|
+
if (analyser) {
|
|
67
|
+
if (this.#buffer.length !== analyser.frequencyBinCount) this.#buffer = new Float32Array(analyser.frequencyBinCount);
|
|
68
|
+
this.pull(analyser, this.#buffer);
|
|
69
|
+
this.receive(this.#buffer);
|
|
70
|
+
}
|
|
71
|
+
this.paint(context, frame);
|
|
72
|
+
if (analyser) this.#surface?.requestFrame();
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
//#endregion
|
|
77
|
+
export { SonicDisplayElement };
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { writeAttribute } from "../lib/write-attribute.js";
|
|
2
2
|
import { SonicElement } from "./sonic-element.js";
|
|
3
3
|
import { requireChild, template } from "../lib/render.js";
|
|
4
|
+
import { bindDrag } from "../lib/pointer-drag.js";
|
|
4
5
|
import { ReadoutClaim } from "./readout-claim.js";
|
|
5
6
|
import { Readout } from "./readout.js";
|
|
6
7
|
import { dragThresholdPx } from "../lib/drag-step.js";
|
|
7
|
-
import { bindDrag } from "../lib/pointer-drag.js";
|
|
8
8
|
import { SonicValueElement, linkValue } from "./value-element.js";
|
|
9
9
|
import { adsrPath, adsrShape, adsrStages, timeStages } from "../lib/adsr-shape.js";
|
|
10
10
|
import { pointerMove, pointerPosition, startFieldDrag, stepFieldDrag } from "../lib/field.js";
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { clampProportion } from "../lib/math.js";
|
|
1
2
|
import { requireChild, template } from "../lib/render.js";
|
|
2
3
|
import { SonicValueElement } from "./value-element.js";
|
|
3
4
|
import { bindScale } from "./scale.js";
|
|
5
|
+
import { bindHoverPreview } from "../lib/hover-preview.js";
|
|
4
6
|
import { sortedRegions } from "../lib/time-regions.js";
|
|
5
7
|
|
|
6
8
|
//#region src/elements/slider.ts
|
|
@@ -34,6 +36,7 @@ var SonicSlider = class extends SonicValueElement {
|
|
|
34
36
|
...SonicValueElement.observedAttributes,
|
|
35
37
|
"modulation",
|
|
36
38
|
"orientation",
|
|
39
|
+
"scrub",
|
|
37
40
|
"spring"
|
|
38
41
|
];
|
|
39
42
|
get buffered() {
|
|
@@ -67,6 +70,12 @@ var SonicSlider = class extends SonicValueElement {
|
|
|
67
70
|
set orientation(direction) {
|
|
68
71
|
this.reflect("orientation", direction);
|
|
69
72
|
}
|
|
73
|
+
get scrub() {
|
|
74
|
+
return this.hasAttribute("scrub");
|
|
75
|
+
}
|
|
76
|
+
set scrub(isScrubbing) {
|
|
77
|
+
this.reflect("scrub", isScrubbing);
|
|
78
|
+
}
|
|
70
79
|
get spring() {
|
|
71
80
|
return this.hasAttribute("spring");
|
|
72
81
|
}
|
|
@@ -74,9 +83,10 @@ var SonicSlider = class extends SonicValueElement {
|
|
|
74
83
|
this.reflect("spring", isSpring);
|
|
75
84
|
}
|
|
76
85
|
#buffered = [];
|
|
86
|
+
#hover;
|
|
77
87
|
#slider = renderSlider();
|
|
78
88
|
connectedCallback() {
|
|
79
|
-
this.upgradeProperties("buffered", "groovePress", "modulationValue", "modulation", "orientation", "spring");
|
|
89
|
+
this.upgradeProperties("buffered", "groovePress", "modulationValue", "modulation", "orientation", "scrub", "spring");
|
|
80
90
|
super.connectedCallback();
|
|
81
91
|
}
|
|
82
92
|
connect(signal) {
|
|
@@ -89,6 +99,23 @@ var SonicSlider = class extends SonicValueElement {
|
|
|
89
99
|
slider.addEventListener("transitionend", (event) => {
|
|
90
100
|
if (event.propertyName === "--_sonic-slider-value") this.toggleState("springing", false);
|
|
91
101
|
}, { signal });
|
|
102
|
+
this.#hover = bindHoverPreview(slider, {
|
|
103
|
+
canShow: () => this.scrub && this.readout && this.orientation === "horizontal",
|
|
104
|
+
dismiss: () => {
|
|
105
|
+
this.render();
|
|
106
|
+
},
|
|
107
|
+
isTaken: () => this.isEditing() || this.isRevealed(),
|
|
108
|
+
show: () => {
|
|
109
|
+
this.renderReadout();
|
|
110
|
+
this.#placeReadout(slider);
|
|
111
|
+
},
|
|
112
|
+
valueAt: (event) => {
|
|
113
|
+
const axis = this.#axis(slider, cap);
|
|
114
|
+
const at = clampProportion((axis.position(event) - axis.startPx) / axis.travelPx);
|
|
115
|
+
const mapping = this.mapping();
|
|
116
|
+
return mapping.snap(mapping.valueAt(at));
|
|
117
|
+
}
|
|
118
|
+
}, signal);
|
|
92
119
|
this.render();
|
|
93
120
|
this.checkStyles(slider, "slider.css");
|
|
94
121
|
this.bindGestures(slider, signal, (event) => {
|
|
@@ -103,12 +130,28 @@ var SonicSlider = class extends SonicValueElement {
|
|
|
103
130
|
return this.orientation;
|
|
104
131
|
}
|
|
105
132
|
draw() {
|
|
133
|
+
if (this.isEditing() || this.isRevealed()) this.#hover?.clear();
|
|
106
134
|
this.writeProportions(this.#slider, "slider", this.modulation);
|
|
107
135
|
this.#renderBuffered(this.#slider);
|
|
136
|
+
this.#renderScrub(this.#slider);
|
|
137
|
+
this.#placeReadout(this.#slider);
|
|
108
138
|
}
|
|
109
139
|
focusTarget() {
|
|
110
140
|
return this.#slider;
|
|
111
141
|
}
|
|
142
|
+
heldWrite(next) {
|
|
143
|
+
super.heldWrite(next);
|
|
144
|
+
if (this.scrub) this.render();
|
|
145
|
+
}
|
|
146
|
+
holdChanged() {
|
|
147
|
+
if (this.scrub) this.render();
|
|
148
|
+
}
|
|
149
|
+
previewValue() {
|
|
150
|
+
return this.#hover?.value();
|
|
151
|
+
}
|
|
152
|
+
scrubsKeyRepeat() {
|
|
153
|
+
return this.scrub;
|
|
154
|
+
}
|
|
112
155
|
springTarget() {
|
|
113
156
|
return this.spring ? this.originValue() : void 0;
|
|
114
157
|
}
|
|
@@ -125,6 +168,13 @@ var SonicSlider = class extends SonicValueElement {
|
|
|
125
168
|
travelPx: Math.max(1, box.width - capBox.width)
|
|
126
169
|
};
|
|
127
170
|
}
|
|
171
|
+
#placeReadout(slider) {
|
|
172
|
+
if (!this.scrub) {
|
|
173
|
+
slider.style.removeProperty("--_sonic-slider-readout-value");
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
slider.style.setProperty("--_sonic-slider-readout-value", String(this.mapping().proportionOf(this.readoutValue())));
|
|
177
|
+
}
|
|
128
178
|
#renderBuffered(slider) {
|
|
129
179
|
const buffered = this.#buffered;
|
|
130
180
|
if (buffered.length === 0) {
|
|
@@ -143,6 +193,22 @@ var SonicSlider = class extends SonicValueElement {
|
|
|
143
193
|
});
|
|
144
194
|
slider.style.setProperty("--_sonic-slider-buffered-regions", `linear-gradient(var(--_sonic-slider-toward), ${stops.join(", ")})`);
|
|
145
195
|
}
|
|
196
|
+
#renderScrub(slider) {
|
|
197
|
+
const { style } = slider;
|
|
198
|
+
const { proportionOf } = this.mapping();
|
|
199
|
+
const isHeld = this.scrub && this.heldFrom() !== void 0;
|
|
200
|
+
const played = proportionOf(this.playback());
|
|
201
|
+
const scrubbed = proportionOf(this.value);
|
|
202
|
+
if (isHeld) style.setProperty("--_sonic-slider-played", String(played));
|
|
203
|
+
else style.removeProperty("--_sonic-slider-played");
|
|
204
|
+
if (played === scrubbed || !isHeld || !this.isRevealed()) {
|
|
205
|
+
style.removeProperty("--_sonic-slider-scrub-region");
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const from = grooveStop(Math.min(played, scrubbed));
|
|
209
|
+
const to = grooveStop(Math.max(played, scrubbed));
|
|
210
|
+
style.setProperty("--_sonic-slider-scrub-region", `linear-gradient(var(--_sonic-slider-toward), transparent ${from}, var(--_sonic-slider-scrub) ${from} ${to}, transparent ${to})`);
|
|
211
|
+
}
|
|
146
212
|
};
|
|
147
213
|
|
|
148
214
|
//#endregion
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { requireChild, template } from "../lib/render.js";
|
|
2
|
+
import { SonicDisplayElement } from "./display-element.js";
|
|
3
|
+
import { columnEdges, columnLevels, gridColumns, gridRows, levelRows, loudestBin, zoneRows } from "../lib/spectrum-columns.js";
|
|
4
|
+
import { areSettled, createLevels, receiveLevels, stepLevels } from "../lib/spectrum-levels.js";
|
|
5
|
+
|
|
6
|
+
//#region src/elements/spectrum.ts
|
|
7
|
+
const colours = {
|
|
8
|
+
clip: "--_sonic-spectrum-clip",
|
|
9
|
+
grid: "--_sonic-spectrum-grid",
|
|
10
|
+
hot: "--_sonic-spectrum-hot",
|
|
11
|
+
lit: "--_sonic-spectrum-lit"
|
|
12
|
+
};
|
|
13
|
+
const numbers = {
|
|
14
|
+
clipFrom: "--_sonic-spectrum-clip-from",
|
|
15
|
+
hotFrom: "--_sonic-spectrum-hot-from"
|
|
16
|
+
};
|
|
17
|
+
const defaultFrequencies = [20, 2e4];
|
|
18
|
+
const defaultSampleRate = 48e3;
|
|
19
|
+
const renderSpectrum = template(`
|
|
20
|
+
<div aria-hidden="true" class="sonic-spectrum">
|
|
21
|
+
<canvas class="sonic-spectrum-canvas"></canvas>
|
|
22
|
+
</div>
|
|
23
|
+
`, HTMLDivElement);
|
|
24
|
+
function paintGrid(context, { dpr, height, look, range, rowOf, width }, [frequencyMin, frequencyMax]) {
|
|
25
|
+
const line = Math.max(1, Math.round(dpr));
|
|
26
|
+
const columns = gridColumns({
|
|
27
|
+
frequencyMax,
|
|
28
|
+
frequencyMin
|
|
29
|
+
}, width);
|
|
30
|
+
const rows = gridRows(range, rowOf);
|
|
31
|
+
context.beginPath();
|
|
32
|
+
for (const column of columns) context.rect(column, 0, line, height);
|
|
33
|
+
for (const row of rows) context.rect(0, row, width, line);
|
|
34
|
+
context.fillStyle = look.colours.grid;
|
|
35
|
+
context.fill();
|
|
36
|
+
}
|
|
37
|
+
function zoneFill(context, { height, look, rowOf }) {
|
|
38
|
+
const gradient = context.createLinearGradient(0, 0, 0, height);
|
|
39
|
+
const { clip, hot } = zoneRows(look.numbers, rowOf);
|
|
40
|
+
gradient.addColorStop(clip / height, look.colours.clip);
|
|
41
|
+
gradient.addColorStop(clip / height, look.colours.hot);
|
|
42
|
+
gradient.addColorStop(hot / height, look.colours.hot);
|
|
43
|
+
gradient.addColorStop(hot / height, look.colours.lit);
|
|
44
|
+
return gradient;
|
|
45
|
+
}
|
|
46
|
+
function paintBars(context, scene, bars) {
|
|
47
|
+
const { height, rowOf } = scene;
|
|
48
|
+
let previous = height;
|
|
49
|
+
context.beginPath();
|
|
50
|
+
context.moveTo(0, height);
|
|
51
|
+
for (const [column, level] of bars.entries()) {
|
|
52
|
+
const row = rowOf(level);
|
|
53
|
+
bars[column] = row;
|
|
54
|
+
if (row === previous) continue;
|
|
55
|
+
context.lineTo(column, previous);
|
|
56
|
+
context.lineTo(column, row);
|
|
57
|
+
previous = row;
|
|
58
|
+
}
|
|
59
|
+
context.lineTo(bars.length, previous);
|
|
60
|
+
context.lineTo(bars.length, height);
|
|
61
|
+
context.closePath();
|
|
62
|
+
context.fillStyle = zoneFill(context, scene);
|
|
63
|
+
context.fill();
|
|
64
|
+
}
|
|
65
|
+
function paintHolds(context, { look, rowOf }, { bars, holds }) {
|
|
66
|
+
context.beginPath();
|
|
67
|
+
for (const [column, level] of holds.entries()) {
|
|
68
|
+
const row = rowOf(level);
|
|
69
|
+
if (row < (bars[column] ?? 0)) context.rect(column, row, 1, 1);
|
|
70
|
+
}
|
|
71
|
+
context.fillStyle = look.colours.lit;
|
|
72
|
+
context.fill();
|
|
73
|
+
}
|
|
74
|
+
var SonicSpectrum = class extends SonicDisplayElement {
|
|
75
|
+
static observedAttributes = [
|
|
76
|
+
...SonicDisplayElement.observedAttributes,
|
|
77
|
+
"frequency-max",
|
|
78
|
+
"frequency-min",
|
|
79
|
+
"grid",
|
|
80
|
+
"peak-hold",
|
|
81
|
+
"sample-rate"
|
|
82
|
+
];
|
|
83
|
+
get frequencyMax() {
|
|
84
|
+
return this.numberAttribute("frequency-max", defaultFrequencies[1]);
|
|
85
|
+
}
|
|
86
|
+
set frequencyMax(hertz) {
|
|
87
|
+
this.reflect("frequency-max", hertz);
|
|
88
|
+
}
|
|
89
|
+
get frequencyMin() {
|
|
90
|
+
return this.numberAttribute("frequency-min", defaultFrequencies[0]);
|
|
91
|
+
}
|
|
92
|
+
set frequencyMin(hertz) {
|
|
93
|
+
this.reflect("frequency-min", hertz);
|
|
94
|
+
}
|
|
95
|
+
get grid() {
|
|
96
|
+
return this.getAttribute("grid") === "none" ? "none" : "on";
|
|
97
|
+
}
|
|
98
|
+
set grid(grid) {
|
|
99
|
+
this.reflect("grid", grid);
|
|
100
|
+
}
|
|
101
|
+
get peak() {
|
|
102
|
+
return this.#peak && { ...this.#peak };
|
|
103
|
+
}
|
|
104
|
+
get peakHold() {
|
|
105
|
+
return this.hasAttribute("peak-hold");
|
|
106
|
+
}
|
|
107
|
+
set peakHold(isHeld) {
|
|
108
|
+
this.reflect("peak-hold", isHeld);
|
|
109
|
+
}
|
|
110
|
+
get sampleRate() {
|
|
111
|
+
const sampleRate = this.numberAttribute("sample-rate", defaultSampleRate);
|
|
112
|
+
return sampleRate > 0 ? sampleRate : defaultSampleRate;
|
|
113
|
+
}
|
|
114
|
+
set sampleRate(hertz) {
|
|
115
|
+
this.reflect("sample-rate", hertz);
|
|
116
|
+
}
|
|
117
|
+
control = renderSpectrum();
|
|
118
|
+
canvas = requireChild(this.control, ".sonic-spectrum-canvas", HTMLCanvasElement);
|
|
119
|
+
colours = colours;
|
|
120
|
+
numbers = numbers;
|
|
121
|
+
sheet = "spectrum.css";
|
|
122
|
+
#columns;
|
|
123
|
+
#isSettled = true;
|
|
124
|
+
#lastFrameMs;
|
|
125
|
+
#levels = createLevels(0);
|
|
126
|
+
#peak;
|
|
127
|
+
#receivedRate;
|
|
128
|
+
connectedCallback() {
|
|
129
|
+
this.upgradeProperties("frequencyMax", "frequencyMin", "grid", "peakHold", "sampleRate");
|
|
130
|
+
super.connectedCallback();
|
|
131
|
+
}
|
|
132
|
+
resetPeak() {
|
|
133
|
+
this.#peak = void 0;
|
|
134
|
+
}
|
|
135
|
+
paint(context, { frameMs, isDirty, look, size }) {
|
|
136
|
+
if (this.isDisabled()) {
|
|
137
|
+
if (isDirty) this.#draw(context, look, size);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const wasSettled = this.#isSettled;
|
|
141
|
+
this.#step(frameMs, look.isReducedMotion);
|
|
142
|
+
if (isDirty || !wasSettled || !this.#isSettled) this.#draw(context, look, size);
|
|
143
|
+
if (!this.#isSettled) this.surface()?.requestFrame();
|
|
144
|
+
}
|
|
145
|
+
pull(analyser, buffer) {
|
|
146
|
+
analyser.getFloatFrequencyData(buffer);
|
|
147
|
+
}
|
|
148
|
+
receive(frame) {
|
|
149
|
+
if (frame.length !== this.#levels.bars.length) this.#levels = createLevels(frame.length);
|
|
150
|
+
this.#receivedRate = this.analyser?.context.sampleRate ?? this.sampleRate;
|
|
151
|
+
receiveLevels(this.#levels, frame, this.min);
|
|
152
|
+
const loudest = loudestBin(frame, this.#axis());
|
|
153
|
+
if (loudest && loudest.decibels > (this.#peak?.decibels ?? -Infinity)) this.#peak = loudest;
|
|
154
|
+
}
|
|
155
|
+
#axis() {
|
|
156
|
+
const [frequencyMin, frequencyMax] = this.#frequencies();
|
|
157
|
+
return {
|
|
158
|
+
binCount: this.#levels.bars.length,
|
|
159
|
+
frequencyMax,
|
|
160
|
+
frequencyMin,
|
|
161
|
+
sampleRate: this.#receivedRate ?? this.sampleRate
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
#columnsFor(width) {
|
|
165
|
+
const axis = this.#axis();
|
|
166
|
+
const key = [
|
|
167
|
+
width,
|
|
168
|
+
axis.binCount,
|
|
169
|
+
axis.sampleRate,
|
|
170
|
+
axis.frequencyMin,
|
|
171
|
+
axis.frequencyMax
|
|
172
|
+
].join(" ");
|
|
173
|
+
if (this.#columns?.key !== key) this.#columns = {
|
|
174
|
+
bars: new Float64Array(width),
|
|
175
|
+
edges: columnEdges(axis, width),
|
|
176
|
+
holds: new Float64Array(width),
|
|
177
|
+
key
|
|
178
|
+
};
|
|
179
|
+
return this.#columns;
|
|
180
|
+
}
|
|
181
|
+
#draw(context, look, size) {
|
|
182
|
+
const scene = {
|
|
183
|
+
...size,
|
|
184
|
+
look,
|
|
185
|
+
range: {
|
|
186
|
+
max: this.max,
|
|
187
|
+
min: this.min
|
|
188
|
+
},
|
|
189
|
+
rowOf: levelRows({
|
|
190
|
+
max: this.max,
|
|
191
|
+
min: this.min
|
|
192
|
+
}, size.height)
|
|
193
|
+
};
|
|
194
|
+
context.clearRect(0, 0, size.width, size.height);
|
|
195
|
+
if (this.grid === "on") paintGrid(context, scene, this.#frequencies());
|
|
196
|
+
if (this.isDisabled()) return;
|
|
197
|
+
const { bars, edges, holds } = this.#columnsFor(size.width);
|
|
198
|
+
const spread = {
|
|
199
|
+
edges,
|
|
200
|
+
floor: scene.range.min
|
|
201
|
+
};
|
|
202
|
+
columnLevels(bars, this.#levels.bars, spread);
|
|
203
|
+
paintBars(context, scene, bars);
|
|
204
|
+
if (!this.peakHold) return;
|
|
205
|
+
columnLevels(holds, this.#levels.holds, spread);
|
|
206
|
+
paintHolds(context, scene, {
|
|
207
|
+
bars,
|
|
208
|
+
holds
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
#frequencies() {
|
|
212
|
+
const { frequencyMax, frequencyMin } = this;
|
|
213
|
+
return frequencyMin > 0 && frequencyMax > frequencyMin ? [frequencyMin, frequencyMax] : defaultFrequencies;
|
|
214
|
+
}
|
|
215
|
+
#step(frameMs, isReducedMotion) {
|
|
216
|
+
const floor = this.min;
|
|
217
|
+
stepLevels(this.#levels, {
|
|
218
|
+
elapsedMs: Math.max(0, frameMs - (this.#lastFrameMs ?? frameMs)),
|
|
219
|
+
floor,
|
|
220
|
+
isHeld: this.peakHold,
|
|
221
|
+
isHoldStepped: isReducedMotion,
|
|
222
|
+
nowMs: frameMs
|
|
223
|
+
});
|
|
224
|
+
this.#isSettled = areSettled(this.#levels, floor);
|
|
225
|
+
this.#lastFrameMs = this.#isSettled ? void 0 : frameMs;
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
//#endregion
|
|
230
|
+
export { SonicSpectrum };
|