@xsynaptic/sonic-ui 0.22.0 → 0.24.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 +1 -0
- package/dist/default/elements/wave-element.js +1 -0
- package/dist/default/elements/waveform.js +48 -4
- package/dist/default/elements/wavestrip.js +1 -0
- package/dist/default/lib/waveform-bands.js +98 -0
- package/dist/default/lib/waveform-scene.js +63 -16
- package/dist/dev/elements/wave-element.d.ts +3 -2
- package/dist/dev/elements/wave-element.js +1 -0
- package/dist/dev/elements/wave-element.js.map +1 -1
- package/dist/dev/elements/waveform.d.ts +16 -4
- package/dist/dev/elements/waveform.js +48 -4
- package/dist/dev/elements/waveform.js.map +1 -1
- package/dist/dev/elements/wavestrip.d.ts +1 -0
- package/dist/dev/elements/wavestrip.js +1 -0
- package/dist/dev/elements/wavestrip.js.map +1 -1
- package/dist/dev/index.d.ts +2 -1
- package/dist/dev/lib/waveform-bands.d.ts +13 -0
- package/dist/dev/lib/waveform-bands.js +99 -0
- package/dist/dev/lib/waveform-bands.js.map +1 -0
- package/dist/dev/lib/waveform-scene.d.ts +9 -1
- package/dist/dev/lib/waveform-scene.js +63 -16
- package/dist/dev/lib/waveform-scene.js.map +1 -1
- package/dist/skins/amber.css +1 -0
- package/dist/skins/flat.css +0 -1
- package/dist/skins/ivory.css +1 -0
- package/dist/skins/lime.css +1 -0
- package/dist/skins/slate.css +1 -0
- package/dist/styles/waveform.css +50 -11
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# @xsynaptic/sonic-ui
|
|
2
2
|
|
|
3
|
+
## 0.24.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Waveform: three number tokens shape the band mix, each watched, so a change repaints with no `repaint()`. Unset, the tint is unchanged.
|
|
8
|
+
|
|
9
|
+
- `--sonic-waveform-band-normalize`, 0 to 1 (default 1): at 0 the mix is the weighted average of the band colours and stays inside the palette
|
|
10
|
+
- `--sonic-waveform-band-contrast` (default 1): the exponent on each band's weight
|
|
11
|
+
- `--sonic-waveform-band-tilt` (default 0): decibels of gain per band above the lowest
|
|
12
|
+
|
|
13
|
+
## 0.23.0
|
|
14
|
+
|
|
15
|
+
### Minor Changes
|
|
16
|
+
|
|
17
|
+
- - Waveform: `bands` (`WaveformBands`) tints the wave from levels per frequency band, coloured by `--sonic-waveform-band-1` upward and scaled by `--sonic-waveform-band-opacity`. Call `repaint()` after changing a band token.
|
|
18
|
+
- Breaking: `--sonic-waveform-edge` is replaced by `--sonic-waveform-edge-shade`, 0 to 1, how far the wave's edges mix towards `--sonic-shade`. Unset, the wave is flat; the skins with relief set 0.55.
|
|
19
|
+
|
|
3
20
|
## 0.22.0
|
|
4
21
|
|
|
5
22
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -48,6 +48,7 @@ import '@xsynaptic/sonic-ui/define';
|
|
|
48
48
|
- Events bubble and carry no detail: read the property when one fires. A control knows nothing about playback, so wire its events to your own state and write the property back.
|
|
49
49
|
- A value control fires `input` as its value moves and `change` once when a gesture ends. A drag owns the value: a write to `value` during one is dropped.
|
|
50
50
|
- `formatValue` and `parseValue` are properties holding functions. Once `parseValue` is set it has to read what `formatValue` writes, or typed entry opens on text it cannot take.
|
|
51
|
+
- To show a control without taking input, set `inert` on the host. It leaves the tab order and the accessibility tree and ignores presses, and it keeps its colours and keeps drawing; `disabled` dims it and is announced as disabled.
|
|
51
52
|
- A display takes its signal as data, and nothing in the library fetches, decodes or opens an `AudioContext`.
|
|
52
53
|
- States a control sets itself are custom states on the host, selected with `:state()`.
|
|
53
54
|
- The LED, the panel, the ring and the screen have no behaviour, so they are classes on your own elements (`sonic-led`, `sonic-panel`, `sonic-ring`, `sonic-screen`). A control draws no glass of its own: wrap one in a screen for the encased look.
|
|
@@ -7,7 +7,8 @@ import { isKind, writeKind } from "../lib/marker-band.js";
|
|
|
7
7
|
import { createLabelRider } from "../lib/marker-rider.js";
|
|
8
8
|
import { createPeaksRequest } from "../lib/peaks-request.js";
|
|
9
9
|
import { createTrackingClock } from "../lib/tracking-clock.js";
|
|
10
|
-
import {
|
|
10
|
+
import { bandTable } from "../lib/waveform-bands.js";
|
|
11
|
+
import { bandChannels, paintWaveform, waveformColours, waveformNumbers } from "../lib/waveform-scene.js";
|
|
11
12
|
import { bindZoom, bindZoomKeys } from "../lib/zoom-gesture.js";
|
|
12
13
|
|
|
13
14
|
//#region src/elements/waveform.ts
|
|
@@ -54,6 +55,14 @@ var SonicWaveform = class extends SonicWaveElement {
|
|
|
54
55
|
"zoom-min",
|
|
55
56
|
"zoomable"
|
|
56
57
|
];
|
|
58
|
+
get bands() {
|
|
59
|
+
return this.#bands;
|
|
60
|
+
}
|
|
61
|
+
set bands(bands) {
|
|
62
|
+
this.#bands = bands;
|
|
63
|
+
this.#bandTable = bandTable(bands);
|
|
64
|
+
this.#peaksRequest.changed();
|
|
65
|
+
}
|
|
57
66
|
get peaks() {
|
|
58
67
|
return this.#peaks;
|
|
59
68
|
}
|
|
@@ -157,8 +166,12 @@ var SonicWaveform = class extends SonicWaveElement {
|
|
|
157
166
|
canvas = requireChild(this.control, ".sonic-waveform-canvas", HTMLCanvasElement);
|
|
158
167
|
colours = waveformColours;
|
|
159
168
|
lengths = {};
|
|
169
|
+
numbers = waveformNumbers;
|
|
160
170
|
sheet = "waveform.css";
|
|
161
171
|
sizeProperty = "--_sonic-waveform-size";
|
|
172
|
+
#bandColours = [];
|
|
173
|
+
#bands;
|
|
174
|
+
#bandTable;
|
|
162
175
|
#clock = createTrackingClock();
|
|
163
176
|
#drawn;
|
|
164
177
|
#ghost = requireChild(this.control, ".sonic-waveform-ghost", HTMLDivElement);
|
|
@@ -182,13 +195,14 @@ var SonicWaveform = class extends SonicWaveElement {
|
|
|
182
195
|
insetProperty: "--_sonic-waveform-label-inset",
|
|
183
196
|
parkProperty: "--_sonic-waveform-label-park"
|
|
184
197
|
});
|
|
198
|
+
#strip;
|
|
185
199
|
#zoomGesture;
|
|
186
200
|
attributeChangedCallback(name) {
|
|
187
201
|
super.attributeChangedCallback(name);
|
|
188
202
|
if (name === "zoomable") this.#zoomGesture?.sync();
|
|
189
203
|
}
|
|
190
204
|
connectedCallback() {
|
|
191
|
-
this.upgradeProperties("peaks", "pending", "pendingDelay", "playing", "playbackRate", "readTime", "reducedMotion", "renderLabel", "requestPeaks", "zoom", "zoomable", "zoomMax", "zoomMin");
|
|
205
|
+
this.upgradeProperties("bands", "peaks", "pending", "pendingDelay", "playing", "playbackRate", "readTime", "reducedMotion", "renderLabel", "requestPeaks", "zoom", "zoomable", "zoomMax", "zoomMin");
|
|
192
206
|
super.connectedCallback();
|
|
193
207
|
}
|
|
194
208
|
connect(signal) {
|
|
@@ -262,8 +276,10 @@ var SonicWaveform = class extends SonicWaveElement {
|
|
|
262
276
|
if (isRebuilt) this.#kindColours = this.#readKindColours();
|
|
263
277
|
paintWaveform(context, {
|
|
264
278
|
...view,
|
|
265
|
-
colours: look.colours
|
|
279
|
+
colours: look.colours,
|
|
280
|
+
numbers: look.numbers
|
|
266
281
|
}, {
|
|
282
|
+
bands: this.#tint(isRebuilt),
|
|
267
283
|
kindColours: this.#kindColours,
|
|
268
284
|
markers: this.markerList(),
|
|
269
285
|
peaks: this.#peaks,
|
|
@@ -328,6 +344,14 @@ var SonicWaveform = class extends SonicWaveElement {
|
|
|
328
344
|
startSeconds: this.value
|
|
329
345
|
};
|
|
330
346
|
}
|
|
347
|
+
#readBandColours(strip, bandCount) {
|
|
348
|
+
const probes = Array.from({ length: bandCount }, (_probe, band) => {
|
|
349
|
+
const probe = document.createElement("div");
|
|
350
|
+
probe.style.setProperty("color", `var(--sonic-waveform-band-${String(band + 1)}, transparent)`);
|
|
351
|
+
return probe;
|
|
352
|
+
});
|
|
353
|
+
return this.#readProbes(probes).map((colour) => bandChannels(strip, colour));
|
|
354
|
+
}
|
|
331
355
|
#readKindColours() {
|
|
332
356
|
const kinds = [...new Set(this.markers.map(({ kind }) => kind).filter((kind) => isKind(kind)))];
|
|
333
357
|
const probes = kinds.map((kind) => {
|
|
@@ -336,14 +360,34 @@ var SonicWaveform = class extends SonicWaveElement {
|
|
|
336
360
|
writeKind(probe, kind, "--_sonic-waveform-marker");
|
|
337
361
|
return probe;
|
|
338
362
|
});
|
|
363
|
+
const read = this.#readProbes(probes);
|
|
364
|
+
return new Map(kinds.map((kind, index) => [kind, read[index] ?? ""]));
|
|
365
|
+
}
|
|
366
|
+
#readProbes(probes) {
|
|
339
367
|
this.control.append(...probes);
|
|
340
368
|
const read = probes.map((probe) => getComputedStyle(probe).color);
|
|
341
369
|
for (const probe of probes) probe.remove();
|
|
342
|
-
return
|
|
370
|
+
return read;
|
|
343
371
|
}
|
|
344
372
|
#sourceSeconds() {
|
|
345
373
|
return this.#readTime?.() ?? this.scrubState().played;
|
|
346
374
|
}
|
|
375
|
+
#tint(isRebuilt) {
|
|
376
|
+
const table = this.#bandTable;
|
|
377
|
+
if (!table) return void 0;
|
|
378
|
+
const strip = this.#strip ?? document.createElement("canvas").getContext("2d", { willReadFrequently: true }) ?? void 0;
|
|
379
|
+
if (!strip) return void 0;
|
|
380
|
+
this.#strip = strip;
|
|
381
|
+
if (isRebuilt || this.#bandColours.length !== table.bandCount) {
|
|
382
|
+
strip.canvas.height = 1;
|
|
383
|
+
this.#bandColours = this.#readBandColours(strip, table.bandCount);
|
|
384
|
+
}
|
|
385
|
+
return {
|
|
386
|
+
colours: this.#bandColours,
|
|
387
|
+
strip,
|
|
388
|
+
table
|
|
389
|
+
};
|
|
390
|
+
}
|
|
347
391
|
#zoomTo(target) {
|
|
348
392
|
const next = roundTo(clamp(target, this.zoomMin, this.zoomMax), 2);
|
|
349
393
|
if (next === this.zoom) return;
|
|
@@ -76,6 +76,7 @@ var SonicWavestrip = class extends SonicWaveElement {
|
|
|
76
76
|
canvas = requireChild(this.control, ".sonic-wavestrip-canvas", HTMLCanvasElement);
|
|
77
77
|
colours = colours;
|
|
78
78
|
lengths = lengths;
|
|
79
|
+
numbers = {};
|
|
79
80
|
sheet = "wavestrip.css";
|
|
80
81
|
sizeProperty = "--_sonic-wavestrip-size";
|
|
81
82
|
#bars;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { clampProportion } from "./math.js";
|
|
2
|
+
|
|
3
|
+
//#region src/lib/waveform-bands.ts
|
|
4
|
+
function fullScaleOf({ fullScale, levels }) {
|
|
5
|
+
if (fullScale !== void 0) return fullScale;
|
|
6
|
+
if (levels instanceof Uint8Array) return 255;
|
|
7
|
+
return levels instanceof Uint16Array ? 65535 : 1;
|
|
8
|
+
}
|
|
9
|
+
function amplitudeReader(bands) {
|
|
10
|
+
const fullScale = fullScaleOf(bands);
|
|
11
|
+
const minDecibels = Math.min(0, bands.minDecibels ?? 0);
|
|
12
|
+
return (level) => {
|
|
13
|
+
const proportion = Math.min(1, level / fullScale);
|
|
14
|
+
if (!(proportion > 0)) return 0;
|
|
15
|
+
return minDecibels < 0 ? 10 ** (minDecibels * (1 - proportion) / 20) : proportion;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function bandTable(bands) {
|
|
19
|
+
if (!bands) return void 0;
|
|
20
|
+
const { bandCount, framesPerSecond, levels } = bands;
|
|
21
|
+
if (!Number.isSafeInteger(bandCount) || bandCount < 1 || !(framesPerSecond > 0)) return void 0;
|
|
22
|
+
const amplitudeOf = amplitudeReader(bands);
|
|
23
|
+
if (levels instanceof Float32Array) return {
|
|
24
|
+
bandCount,
|
|
25
|
+
framesPerSecond,
|
|
26
|
+
levels,
|
|
27
|
+
weightOf: amplitudeOf
|
|
28
|
+
};
|
|
29
|
+
const weights = Float32Array.from({ length: levels instanceof Uint8Array ? 256 : 65536 }, (_weight, level) => amplitudeOf(level));
|
|
30
|
+
return {
|
|
31
|
+
bandCount,
|
|
32
|
+
framesPerSecond,
|
|
33
|
+
levels,
|
|
34
|
+
weightOf: (level) => weights[level] ?? 0
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function groupMixer({ bandCount, levels, weightOf }, colours, { contrast, normalize, tilt }) {
|
|
38
|
+
const gains = Array.from({ length: bandCount }, (_gain, band) => 10 ** (tilt * band / 20));
|
|
39
|
+
const amount = clampProportion(normalize);
|
|
40
|
+
return (fromFrame, toFrame) => {
|
|
41
|
+
let red = 0;
|
|
42
|
+
let green = 0;
|
|
43
|
+
let blue = 0;
|
|
44
|
+
let total = 0;
|
|
45
|
+
for (let band = 0; band < bandCount; band += 1) {
|
|
46
|
+
const colour = colours[band];
|
|
47
|
+
if (!colour) continue;
|
|
48
|
+
let weight = 0;
|
|
49
|
+
for (let bandFrame = fromFrame; bandFrame < toFrame; bandFrame += 1) weight += weightOf(levels[bandFrame * bandCount + band] ?? 0);
|
|
50
|
+
weight *= gains[band] ?? 1;
|
|
51
|
+
if (contrast !== 1) weight **= contrast;
|
|
52
|
+
red += weight * colour[0];
|
|
53
|
+
green += weight * colour[1];
|
|
54
|
+
blue += weight * colour[2];
|
|
55
|
+
total += weight;
|
|
56
|
+
}
|
|
57
|
+
if (total === 0) return;
|
|
58
|
+
const scale = 255 / Math.max(red, green, blue, Number.MIN_VALUE) * amount + (1 - amount) / total;
|
|
59
|
+
return [
|
|
60
|
+
red * scale,
|
|
61
|
+
green * scale,
|
|
62
|
+
blue * scale,
|
|
63
|
+
255
|
|
64
|
+
];
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function bandStrip(table, colours, view) {
|
|
68
|
+
if (view.pixelsPerSecond <= 0 || !colours.some(Boolean)) return void 0;
|
|
69
|
+
const { bandCount, framesPerSecond, levels } = table;
|
|
70
|
+
const mixGroup = groupMixer(table, colours, view.mix ?? {
|
|
71
|
+
contrast: 1,
|
|
72
|
+
normalize: 1,
|
|
73
|
+
tilt: 0
|
|
74
|
+
});
|
|
75
|
+
const pxPerFrame = view.pixelsPerSecond / framesPerSecond;
|
|
76
|
+
const frames = Math.max(1, Math.round(1 / pxPerFrame));
|
|
77
|
+
const groupWidth = frames * pxPerFrame;
|
|
78
|
+
const openingFrame = view.startSeconds * framesPerSecond;
|
|
79
|
+
const groupsTotal = Math.ceil(levels.length / bandCount / frames);
|
|
80
|
+
const first = Math.max(0, Math.floor(openingFrame / frames) - 1);
|
|
81
|
+
const count = Math.min(groupsTotal, Math.ceil((openingFrame + view.width / pxPerFrame) / frames) + 1) - first;
|
|
82
|
+
if (count < 1) return void 0;
|
|
83
|
+
const pixels = new Uint8ClampedArray(count * 4);
|
|
84
|
+
for (let index = 0; index < count; index += 1) {
|
|
85
|
+
const fromFrame = (first + index) * frames;
|
|
86
|
+
const mixed = mixGroup(fromFrame, fromFrame + frames);
|
|
87
|
+
if (mixed) pixels.set(mixed, index * 4);
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
count,
|
|
91
|
+
groupWidth,
|
|
92
|
+
pixels,
|
|
93
|
+
x: (first * frames - openingFrame) * pxPerFrame
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
//#endregion
|
|
98
|
+
export { bandStrip, bandTable };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { clamp } from "./math.js";
|
|
2
2
|
import { isKind } from "./marker-band.js";
|
|
3
|
+
import { bandStrip } from "./waveform-bands.js";
|
|
3
4
|
import { waveformBuckets } from "./waveform-buckets.js";
|
|
4
5
|
|
|
5
6
|
//#region src/lib/waveform-scene.ts
|
|
@@ -8,8 +9,16 @@ const waveformColours = {
|
|
|
8
9
|
grid: "--_sonic-waveform-grid",
|
|
9
10
|
marker: "--_sonic-waveform-marker",
|
|
10
11
|
placeholder: "--_sonic-waveform-placeholder",
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
shade: "--_sonic-waveform-shade",
|
|
13
|
+
shadeClear: "--_sonic-waveform-shade-clear",
|
|
14
|
+
wave: "--_sonic-waveform-wave"
|
|
15
|
+
};
|
|
16
|
+
const waveformNumbers = {
|
|
17
|
+
bandContrast: "--_sonic-waveform-band-contrast",
|
|
18
|
+
bandNormalize: "--_sonic-waveform-band-normalize",
|
|
19
|
+
bandOpacity: "--_sonic-waveform-band-opacity",
|
|
20
|
+
bandTilt: "--_sonic-waveform-band-tilt",
|
|
21
|
+
edgeShade: "--_sonic-waveform-edge-shade"
|
|
13
22
|
};
|
|
14
23
|
const amplitudeMargin = .94;
|
|
15
24
|
const hatchPitchPx = 8;
|
|
@@ -53,27 +62,63 @@ function paintPlaceholders(context, scene) {
|
|
|
53
62
|
context.strokeStyle = scene.colours.placeholder;
|
|
54
63
|
context.stroke();
|
|
55
64
|
}
|
|
56
|
-
function
|
|
57
|
-
|
|
65
|
+
function bandChannels(strip, colour) {
|
|
66
|
+
strip.clearRect(0, 0, 1, 1);
|
|
67
|
+
strip.fillStyle = colour;
|
|
68
|
+
strip.fillRect(0, 0, 1, 1);
|
|
69
|
+
const [red = 0, green = 0, blue = 0, alpha = 0] = strip.getImageData(0, 0, 1, 1).data;
|
|
70
|
+
return alpha === 0 ? void 0 : [
|
|
71
|
+
red,
|
|
72
|
+
green,
|
|
73
|
+
blue
|
|
74
|
+
];
|
|
75
|
+
}
|
|
76
|
+
function paintTint(context, scene, tint) {
|
|
77
|
+
const { bandContrast, bandNormalize, bandTilt } = scene.numbers;
|
|
78
|
+
const strip = bandStrip(tint.table, tint.colours, {
|
|
79
|
+
...scene,
|
|
80
|
+
mix: {
|
|
81
|
+
contrast: bandContrast,
|
|
82
|
+
normalize: bandNormalize,
|
|
83
|
+
tilt: bandTilt
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
if (!strip) return;
|
|
87
|
+
const { canvas } = tint.strip;
|
|
88
|
+
const { count } = strip;
|
|
89
|
+
if (canvas.width < count) canvas.width = count;
|
|
90
|
+
tint.strip.putImageData(new ImageData(strip.pixels, count, 1), 0, 0);
|
|
91
|
+
context.globalAlpha = scene.numbers.bandOpacity;
|
|
92
|
+
context.drawImage(canvas, 0, 0, count, 1, strip.x, 0, count * strip.groupWidth, scene.height);
|
|
93
|
+
}
|
|
94
|
+
function paintEdgeShade(context, scene) {
|
|
95
|
+
const { shade, shadeClear } = scene.colours;
|
|
58
96
|
const gradient = context.createLinearGradient(0, 0, 0, scene.height);
|
|
59
|
-
gradient.addColorStop(0,
|
|
60
|
-
gradient.addColorStop(.5,
|
|
61
|
-
gradient.addColorStop(1,
|
|
62
|
-
|
|
97
|
+
gradient.addColorStop(0, shade);
|
|
98
|
+
gradient.addColorStop(.5, shadeClear);
|
|
99
|
+
gradient.addColorStop(1, shade);
|
|
100
|
+
context.globalAlpha = scene.numbers.edgeShade;
|
|
101
|
+
context.fillStyle = gradient;
|
|
102
|
+
context.fillRect(0, 0, scene.width, scene.height);
|
|
63
103
|
}
|
|
64
|
-
function paintWave(context, scene,
|
|
65
|
-
const buckets = waveformBuckets(peaks, scene);
|
|
104
|
+
function paintWave(context, scene, drawn) {
|
|
105
|
+
const buckets = drawn.peaks ? waveformBuckets(drawn.peaks, scene) : [];
|
|
66
106
|
const [first] = buckets;
|
|
67
107
|
if (!first) return;
|
|
68
108
|
const center = scene.height / 2;
|
|
69
109
|
const amplitudePx = center * amplitudeMargin;
|
|
70
|
-
|
|
110
|
+
const { bandOpacity, edgeShade } = scene.numbers;
|
|
111
|
+
context.fillStyle = scene.colours.wave;
|
|
71
112
|
context.beginPath();
|
|
72
113
|
context.moveTo(first.x, center);
|
|
73
114
|
for (const bucket of buckets) context.lineTo(bucket.x, center - bucket.high * amplitudePx);
|
|
74
115
|
for (const bucket of buckets.toReversed()) context.lineTo(bucket.x, center - bucket.low * amplitudePx);
|
|
75
116
|
context.closePath();
|
|
76
117
|
context.fill();
|
|
118
|
+
context.globalCompositeOperation = "source-atop";
|
|
119
|
+
if (drawn.bands && bandOpacity > 0) paintTint(context, scene, drawn.bands);
|
|
120
|
+
if (edgeShade > 0) paintEdgeShade(context, scene);
|
|
121
|
+
context.globalAlpha = 1;
|
|
77
122
|
}
|
|
78
123
|
function paintMarkers(context, scene, drawn) {
|
|
79
124
|
const line = Math.max(1, Math.round(scene.dpr));
|
|
@@ -93,16 +138,18 @@ function paintWaveform(context, scene, drawn) {
|
|
|
93
138
|
const [opening, closing] = [x(drawn.range[0]), x(drawn.range[1])];
|
|
94
139
|
const [from, to] = [clamp(opening, 0, width), clamp(closing, 0, width)];
|
|
95
140
|
context.clearRect(0, 0, width, height);
|
|
96
|
-
|
|
97
|
-
|
|
141
|
+
paintWave(context, scene, drawn);
|
|
142
|
+
context.globalCompositeOperation = "destination-over";
|
|
143
|
+
paintPlaceholders(context, scene);
|
|
98
144
|
context.fillStyle = grid;
|
|
99
145
|
if (to > from) context.fillRect(from, Math.round((height - line) / 2), to - from, line);
|
|
100
|
-
|
|
101
|
-
|
|
146
|
+
paintHatch(context, scene, [0, from]);
|
|
147
|
+
paintHatch(context, scene, [to, width]);
|
|
148
|
+
context.globalCompositeOperation = "source-over";
|
|
102
149
|
context.fillStyle = ends;
|
|
103
150
|
for (const edge of [opening, closing]) context.fillRect(edge - line / 2, 0, line, height);
|
|
104
151
|
paintMarkers(context, scene, drawn);
|
|
105
152
|
}
|
|
106
153
|
|
|
107
154
|
//#endregion
|
|
108
|
-
export { paintWaveform, waveformColours };
|
|
155
|
+
export { bandChannels, paintWaveform, waveformColours, waveformNumbers };
|
|
@@ -17,7 +17,7 @@ export interface WaveMarker {
|
|
|
17
17
|
label?: string;
|
|
18
18
|
start: Seconds;
|
|
19
19
|
}
|
|
20
|
-
export declare abstract class SonicWaveElement<Colour extends string, Length extends string = never> extends SonicValueElement {
|
|
20
|
+
export declare abstract class SonicWaveElement<Colour extends string, Length extends string = never, Numeric extends string = never> extends SonicValueElement {
|
|
21
21
|
#private;
|
|
22
22
|
static readonly observedAttributes: string[];
|
|
23
23
|
get currentMarker(): undefined | WaveMarker;
|
|
@@ -30,6 +30,7 @@ export declare abstract class SonicWaveElement<Colour extends string, Length ext
|
|
|
30
30
|
protected abstract readonly control: HTMLElement;
|
|
31
31
|
protected abstract readonly lengths: Record<Length, `--_sonic-${string}`>;
|
|
32
32
|
protected readonly notation: ValueNotation;
|
|
33
|
+
protected abstract readonly numbers: Record<Numeric, `--_sonic-${string}`>;
|
|
33
34
|
protected abstract readonly sheet: string;
|
|
34
35
|
protected abstract readonly sizeProperty: `--_sonic-${string}`;
|
|
35
36
|
attributeChangedCallback(name: string): void;
|
|
@@ -40,7 +41,7 @@ export declare abstract class SonicWaveElement<Colour extends string, Length ext
|
|
|
40
41
|
protected abstract isEmpty(): boolean;
|
|
41
42
|
protected markerList(): ReadonlyArray<WaveMarker>;
|
|
42
43
|
protected markerProportions(): Array<[number, number]>;
|
|
43
|
-
protected abstract paint(context: CanvasRenderingContext2D, frame: SurfaceFrame<Colour, Length>): void;
|
|
44
|
+
protected abstract paint(context: CanvasRenderingContext2D, frame: SurfaceFrame<Colour, Length, Numeric>): void;
|
|
44
45
|
protected proportionsChanged(): void;
|
|
45
46
|
protected renderEmpty(): void;
|
|
46
47
|
protected abstract renderMarkers(): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wave-element.js","names":[],"sources":["../../../src/elements/wave-element.ts"],"sourcesContent":["import type { ValueNotation } from '#elements/value-element.ts';\nimport type { Surface, SurfaceFrame } from '#lib/canvas-surface.ts';\nimport type { Seconds } from '#lib/units.ts';\n\nimport { SonicValueElement } from '#elements/value-element.ts';\nimport { bindSurface } from '#lib/canvas-surface.ts';\nimport { formatClock, parseClock } from '#lib/clock.ts';\nimport { spokenDuration } from '#lib/spoken-duration.ts';\n\ndeclare const __DEV__: boolean;\n\ndeclare global {\n\tinterface HTMLElementEventMap {\n\t\t'sonic-marker': Event;\n\t}\n}\n\nexport interface WaveMarker {\n\t// Open to a consumer's own keys, which `renderLabel` draws from\n\t[key: string]: unknown;\n\tdimmed?: boolean;\n\t/** Makes the marker a region */\n\tend?: Seconds;\n\t/** Lowercase letters, digits and hyphens; colours the marker through `--sonic-marker-<kind>` */\n\tkind?: string;\n\tlabel?: string;\n\tstart: Seconds;\n}\n\nfunction markerKey(marker: undefined | WaveMarker): string {\n\treturn marker ? [marker.start, marker.end, marker.kind, marker.label].join('|') : '';\n}\n\nexport abstract class SonicWaveElement<\n\tColour extends string,\n\tLength extends string = never,\n> extends SonicValueElement {\n\tstatic override readonly observedAttributes = [...SonicValueElement.observedAttributes, 'fill'];\n\n\tget currentMarker(): undefined | WaveMarker {\n\t\treturn this.#current ? { ...this.#current } : undefined;\n\t}\n\n\tget fill(): boolean {\n\t\treturn this.hasAttribute('fill');\n\t}\n\n\tset fill(isFilling: boolean) {\n\t\tthis.reflect('fill', isFilling);\n\t}\n\n\tget markers(): Array<WaveMarker> {\n\t\treturn this.#markers.map((marker) => ({ ...marker }));\n\t}\n\n\tset markers(list: Iterable<WaveMarker> | undefined) {\n\t\tthis.#markers = [...(list ?? [])]\n\t\t\t.filter((marker) => Number.isFinite(marker.start))\n\t\t\t.map((marker) => ({ ...marker }))\n\t\t\t.toSorted((first, second) => first.start - second.start);\n\t\tif (!this.isBound()) return;\n\n\t\tthis.renderMarkers();\n\t\tthis.render();\n\t}\n\n\tprotected abstract readonly canvas: HTMLCanvasElement;\n\n\tprotected abstract readonly colours: Record<Colour, `--_sonic-${string}`>;\n\n\tprotected abstract readonly control: HTMLElement;\n\n\tprotected abstract readonly lengths: Record<Length, `--_sonic-${string}`>;\n\n\tprotected override readonly notation: ValueNotation = {\n\t\tformat: formatClock,\n\t\tparse: parseClock,\n\t\tspeak: (seconds) => spokenDuration(seconds, this.closest<HTMLElement>('[lang]')?.lang),\n\t};\n\n\tprotected abstract readonly sheet: string;\n\n\tprotected abstract readonly sizeProperty: `--_sonic-${string}`;\n\n\t#current: undefined | WaveMarker;\n\n\t#markers: Array<WaveMarker> = [];\n\n\t#surface: Surface | undefined;\n\n\toverride attributeChangedCallback(name: string): void {\n\t\tsuper.attributeChangedCallback(name);\n\t\tif (name === 'fill') this.#surface?.setFill(this.fill);\n\t}\n\n\toverride connectedCallback(): void {\n\t\tthis.upgradeProperties('fill', 'markers');\n\t\tsuper.connectedCallback();\n\t}\n\n\trepaint(): void {\n\t\tthis.#surface?.rebuild();\n\t}\n\n\tprotected connect(signal: AbortSignal): void {\n\t\tconst control = this.control;\n\n\t\tthis.keepControl(control, signal);\n\t\tthis.renderEmpty();\n\t\tthis.#surface = bindSurface({\n\t\t\tcanvas: this.canvas,\n\t\t\tcolours: this.colours,\n\t\t\tfill: { control, sizeProperty: this.sizeProperty },\n\t\t\tlengths: this.lengths,\n\t\t\tpaint: (context, frame) => {\n\t\t\t\tthis.paint(context, frame);\n\t\t\t},\n\t\t\tresize: () => {\n\t\t\t\tthis.renderMarkers();\n\t\t\t},\n\t\t\tsignal,\n\t\t});\n\t\tthis.render();\n\t\tif (__DEV__)\n\t\t\tthis.checkStyles(control, this.sheet, {\n\t\t\t\tproperty: 'margin-bottom',\n\t\t\t\tselector: '[popover]',\n\t\t\t});\n\t\tthis.#surface.setFill(this.fill);\n\t}\n\n\tprotected override focusTarget(): HTMLElement {\n\t\treturn this.control;\n\t}\n\n\tprotected abstract isEmpty(): boolean;\n\n\tprotected markerList(): ReadonlyArray<WaveMarker> {\n\t\treturn this.#markers;\n\t}\n\n\tprotected markerProportions(): Array<[number, number]> {\n\t\tconst mapping = this.mapping();\n\n\t\treturn this.#markers.map(({ end, start }) => {\n\t\t\tconst at = mapping.proportionOf(start);\n\t\t\tconst to = end === undefined ? at : mapping.proportionOf(end);\n\n\t\t\treturn [Math.min(at, to), Math.max(at, to)];\n\t\t});\n\t}\n\n\tprotected abstract paint(\n\t\tcontext: CanvasRenderingContext2D,\n\t\tframe: SurfaceFrame<Colour, Length>,\n\t): void;\n\n\tprotected override proportionsChanged(): void {\n\t\tthis.renderMarkers();\n\t}\n\n\tprotected renderEmpty(): void {\n\t\tthis.toggleState('empty', this.isEmpty());\n\t}\n\n\tprotected abstract renderMarkers(): void;\n\n\tprotected override scrubChanged(): void {\n\t\tthis.render();\n\t}\n\n\tprotected showCurrentMarker(seconds: number): void {\n\t\tconst current = this.#markers.findLast((marker) => marker.start <= seconds);\n\t\tif (markerKey(current) === markerKey(this.#current)) return;\n\n\t\tthis.#current = current;\n\t\tthis.dispatchEvent(new Event('sonic-marker', { bubbles: true }));\n\t}\n\n\tprotected surface(): Surface | undefined {\n\t\treturn this.#surface;\n\t}\n}\n"],"mappings":";;;;;;AA6BA,SAAS,UAAU,QAAwC;CAC1D,OAAO,SAAS;EAAC,OAAO;EAAO,OAAO;EAAK,OAAO;EAAM,OAAO;CAAK,CAAC,CAAC,KAAK,GAAG,IAAI;AACnF;AAEA,IAAsB,mBAAtB,
|
|
1
|
+
{"version":3,"file":"wave-element.js","names":[],"sources":["../../../src/elements/wave-element.ts"],"sourcesContent":["import type { ValueNotation } from '#elements/value-element.ts';\nimport type { Surface, SurfaceFrame } from '#lib/canvas-surface.ts';\nimport type { Seconds } from '#lib/units.ts';\n\nimport { SonicValueElement } from '#elements/value-element.ts';\nimport { bindSurface } from '#lib/canvas-surface.ts';\nimport { formatClock, parseClock } from '#lib/clock.ts';\nimport { spokenDuration } from '#lib/spoken-duration.ts';\n\ndeclare const __DEV__: boolean;\n\ndeclare global {\n\tinterface HTMLElementEventMap {\n\t\t'sonic-marker': Event;\n\t}\n}\n\nexport interface WaveMarker {\n\t// Open to a consumer's own keys, which `renderLabel` draws from\n\t[key: string]: unknown;\n\tdimmed?: boolean;\n\t/** Makes the marker a region */\n\tend?: Seconds;\n\t/** Lowercase letters, digits and hyphens; colours the marker through `--sonic-marker-<kind>` */\n\tkind?: string;\n\tlabel?: string;\n\tstart: Seconds;\n}\n\nfunction markerKey(marker: undefined | WaveMarker): string {\n\treturn marker ? [marker.start, marker.end, marker.kind, marker.label].join('|') : '';\n}\n\nexport abstract class SonicWaveElement<\n\tColour extends string,\n\tLength extends string = never,\n\tNumeric extends string = never,\n> extends SonicValueElement {\n\tstatic override readonly observedAttributes = [...SonicValueElement.observedAttributes, 'fill'];\n\n\tget currentMarker(): undefined | WaveMarker {\n\t\treturn this.#current ? { ...this.#current } : undefined;\n\t}\n\n\tget fill(): boolean {\n\t\treturn this.hasAttribute('fill');\n\t}\n\n\tset fill(isFilling: boolean) {\n\t\tthis.reflect('fill', isFilling);\n\t}\n\n\tget markers(): Array<WaveMarker> {\n\t\treturn this.#markers.map((marker) => ({ ...marker }));\n\t}\n\n\tset markers(list: Iterable<WaveMarker> | undefined) {\n\t\tthis.#markers = [...(list ?? [])]\n\t\t\t.filter((marker) => Number.isFinite(marker.start))\n\t\t\t.map((marker) => ({ ...marker }))\n\t\t\t.toSorted((first, second) => first.start - second.start);\n\t\tif (!this.isBound()) return;\n\n\t\tthis.renderMarkers();\n\t\tthis.render();\n\t}\n\n\tprotected abstract readonly canvas: HTMLCanvasElement;\n\n\tprotected abstract readonly colours: Record<Colour, `--_sonic-${string}`>;\n\n\tprotected abstract readonly control: HTMLElement;\n\n\tprotected abstract readonly lengths: Record<Length, `--_sonic-${string}`>;\n\n\tprotected override readonly notation: ValueNotation = {\n\t\tformat: formatClock,\n\t\tparse: parseClock,\n\t\tspeak: (seconds) => spokenDuration(seconds, this.closest<HTMLElement>('[lang]')?.lang),\n\t};\n\n\tprotected abstract readonly numbers: Record<Numeric, `--_sonic-${string}`>;\n\n\tprotected abstract readonly sheet: string;\n\n\tprotected abstract readonly sizeProperty: `--_sonic-${string}`;\n\n\t#current: undefined | WaveMarker;\n\n\t#markers: Array<WaveMarker> = [];\n\n\t#surface: Surface | undefined;\n\n\toverride attributeChangedCallback(name: string): void {\n\t\tsuper.attributeChangedCallback(name);\n\t\tif (name === 'fill') this.#surface?.setFill(this.fill);\n\t}\n\n\toverride connectedCallback(): void {\n\t\tthis.upgradeProperties('fill', 'markers');\n\t\tsuper.connectedCallback();\n\t}\n\n\trepaint(): void {\n\t\tthis.#surface?.rebuild();\n\t}\n\n\tprotected connect(signal: AbortSignal): void {\n\t\tconst control = this.control;\n\n\t\tthis.keepControl(control, signal);\n\t\tthis.renderEmpty();\n\t\tthis.#surface = bindSurface({\n\t\t\tcanvas: this.canvas,\n\t\t\tcolours: this.colours,\n\t\t\tfill: { control, sizeProperty: this.sizeProperty },\n\t\t\tlengths: this.lengths,\n\t\t\tnumbers: this.numbers,\n\t\t\tpaint: (context, frame) => {\n\t\t\t\tthis.paint(context, frame);\n\t\t\t},\n\t\t\tresize: () => {\n\t\t\t\tthis.renderMarkers();\n\t\t\t},\n\t\t\tsignal,\n\t\t});\n\t\tthis.render();\n\t\tif (__DEV__)\n\t\t\tthis.checkStyles(control, this.sheet, {\n\t\t\t\tproperty: 'margin-bottom',\n\t\t\t\tselector: '[popover]',\n\t\t\t});\n\t\tthis.#surface.setFill(this.fill);\n\t}\n\n\tprotected override focusTarget(): HTMLElement {\n\t\treturn this.control;\n\t}\n\n\tprotected abstract isEmpty(): boolean;\n\n\tprotected markerList(): ReadonlyArray<WaveMarker> {\n\t\treturn this.#markers;\n\t}\n\n\tprotected markerProportions(): Array<[number, number]> {\n\t\tconst mapping = this.mapping();\n\n\t\treturn this.#markers.map(({ end, start }) => {\n\t\t\tconst at = mapping.proportionOf(start);\n\t\t\tconst to = end === undefined ? at : mapping.proportionOf(end);\n\n\t\t\treturn [Math.min(at, to), Math.max(at, to)];\n\t\t});\n\t}\n\n\tprotected abstract paint(\n\t\tcontext: CanvasRenderingContext2D,\n\t\tframe: SurfaceFrame<Colour, Length, Numeric>,\n\t): void;\n\n\tprotected override proportionsChanged(): void {\n\t\tthis.renderMarkers();\n\t}\n\n\tprotected renderEmpty(): void {\n\t\tthis.toggleState('empty', this.isEmpty());\n\t}\n\n\tprotected abstract renderMarkers(): void;\n\n\tprotected override scrubChanged(): void {\n\t\tthis.render();\n\t}\n\n\tprotected showCurrentMarker(seconds: number): void {\n\t\tconst current = this.#markers.findLast((marker) => marker.start <= seconds);\n\t\tif (markerKey(current) === markerKey(this.#current)) return;\n\n\t\tthis.#current = current;\n\t\tthis.dispatchEvent(new Event('sonic-marker', { bubbles: true }));\n\t}\n\n\tprotected surface(): Surface | undefined {\n\t\treturn this.#surface;\n\t}\n}\n"],"mappings":"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"}
|
|
@@ -3,8 +3,9 @@ import { TimeRegions } from "../lib/time-regions.js";
|
|
|
3
3
|
import { SurfaceFrame } from "../lib/canvas-surface.js";
|
|
4
4
|
import { SonicWaveElement, WaveMarker } from "./wave-element.js";
|
|
5
5
|
import { PeaksRequest } from "../lib/peaks-request.js";
|
|
6
|
+
import { WaveformBands } from "../lib/waveform-bands.js";
|
|
6
7
|
import { WaveformPeaks } from "../lib/waveform-buckets.js";
|
|
7
|
-
import { waveformColours } from "../lib/waveform-scene.js";
|
|
8
|
+
import { waveformColours, waveformNumbers } from "../lib/waveform-scene.js";
|
|
8
9
|
//#region src/elements/waveform.d.ts
|
|
9
10
|
declare global {
|
|
10
11
|
interface HTMLElementEventMap {
|
|
@@ -15,10 +16,13 @@ declare global {
|
|
|
15
16
|
}
|
|
16
17
|
}
|
|
17
18
|
type Colour = keyof typeof waveformColours;
|
|
19
|
+
type Numeric = keyof typeof waveformNumbers;
|
|
18
20
|
export type LabelRender = (marker: WaveMarker, element: HTMLElement) => void;
|
|
19
|
-
export declare class SonicWaveform extends SonicWaveElement<Colour> {
|
|
21
|
+
export declare class SonicWaveform extends SonicWaveElement<Colour, never, Numeric> {
|
|
20
22
|
#private;
|
|
21
23
|
static readonly observedAttributes: string[];
|
|
24
|
+
get bands(): undefined | WaveformBands;
|
|
25
|
+
set bands(bands: undefined | WaveformBands);
|
|
22
26
|
get peaks(): undefined | WaveformPeaks;
|
|
23
27
|
set peaks(peaks: undefined | WaveformPeaks);
|
|
24
28
|
/** Spans being fetched; drawn as placeholders until cleared */
|
|
@@ -55,10 +59,18 @@ export declare class SonicWaveform extends SonicWaveElement<Colour> {
|
|
|
55
59
|
readonly grid: "--_sonic-waveform-grid";
|
|
56
60
|
readonly marker: "--_sonic-waveform-marker";
|
|
57
61
|
readonly placeholder: "--_sonic-waveform-placeholder";
|
|
62
|
+
readonly shade: "--_sonic-waveform-shade";
|
|
63
|
+
readonly shadeClear: "--_sonic-waveform-shade-clear";
|
|
58
64
|
readonly wave: "--_sonic-waveform-wave";
|
|
59
|
-
readonly waveEdge: "--_sonic-waveform-wave-edge";
|
|
60
65
|
};
|
|
61
66
|
protected readonly lengths: {};
|
|
67
|
+
protected readonly numbers: {
|
|
68
|
+
readonly bandContrast: "--_sonic-waveform-band-contrast";
|
|
69
|
+
readonly bandNormalize: "--_sonic-waveform-band-normalize";
|
|
70
|
+
readonly bandOpacity: "--_sonic-waveform-band-opacity";
|
|
71
|
+
readonly bandTilt: "--_sonic-waveform-band-tilt";
|
|
72
|
+
readonly edgeShade: "--_sonic-waveform-edge-shade";
|
|
73
|
+
};
|
|
62
74
|
protected readonly sheet = "waveform.css";
|
|
63
75
|
protected readonly sizeProperty = "--_sonic-waveform-size";
|
|
64
76
|
attributeChangedCallback(name: string): void;
|
|
@@ -66,7 +78,7 @@ export declare class SonicWaveform extends SonicWaveElement<Colour> {
|
|
|
66
78
|
protected connect(signal: AbortSignal): void;
|
|
67
79
|
protected draw(): void;
|
|
68
80
|
protected isEmpty(): boolean;
|
|
69
|
-
protected paint(context: CanvasRenderingContext2D, { frameMs, isDirty, isRebuilt, look, size }: SurfaceFrame<Colour>): void;
|
|
81
|
+
protected paint(context: CanvasRenderingContext2D, { frameMs, isDirty, isRebuilt, look, size }: SurfaceFrame<Colour, never, Numeric>): void;
|
|
70
82
|
protected renderMarkers(): void;
|
|
71
83
|
protected scrubChanged(): void;
|
|
72
84
|
protected scrubsKeyRepeat(): boolean;
|
|
@@ -7,7 +7,8 @@ import { isKind, writeKind } from "../lib/marker-band.js";
|
|
|
7
7
|
import { createLabelRider } from "../lib/marker-rider.js";
|
|
8
8
|
import { createPeaksRequest } from "../lib/peaks-request.js";
|
|
9
9
|
import { createTrackingClock } from "../lib/tracking-clock.js";
|
|
10
|
-
import {
|
|
10
|
+
import { bandTable } from "../lib/waveform-bands.js";
|
|
11
|
+
import { bandChannels, paintWaveform, waveformColours, waveformNumbers } from "../lib/waveform-scene.js";
|
|
11
12
|
import { bindZoom, bindZoomKeys } from "../lib/zoom-gesture.js";
|
|
12
13
|
|
|
13
14
|
//#region src/elements/waveform.ts
|
|
@@ -54,6 +55,14 @@ var SonicWaveform = class extends SonicWaveElement {
|
|
|
54
55
|
"zoom-min",
|
|
55
56
|
"zoomable"
|
|
56
57
|
];
|
|
58
|
+
get bands() {
|
|
59
|
+
return this.#bands;
|
|
60
|
+
}
|
|
61
|
+
set bands(bands) {
|
|
62
|
+
this.#bands = bands;
|
|
63
|
+
this.#bandTable = bandTable(bands);
|
|
64
|
+
this.#peaksRequest.changed();
|
|
65
|
+
}
|
|
57
66
|
get peaks() {
|
|
58
67
|
return this.#peaks;
|
|
59
68
|
}
|
|
@@ -157,8 +166,12 @@ var SonicWaveform = class extends SonicWaveElement {
|
|
|
157
166
|
canvas = requireChild(this.control, ".sonic-waveform-canvas", HTMLCanvasElement);
|
|
158
167
|
colours = waveformColours;
|
|
159
168
|
lengths = {};
|
|
169
|
+
numbers = waveformNumbers;
|
|
160
170
|
sheet = "waveform.css";
|
|
161
171
|
sizeProperty = "--_sonic-waveform-size";
|
|
172
|
+
#bandColours = [];
|
|
173
|
+
#bands;
|
|
174
|
+
#bandTable;
|
|
162
175
|
#clock = createTrackingClock();
|
|
163
176
|
#drawn;
|
|
164
177
|
#ghost = requireChild(this.control, ".sonic-waveform-ghost", HTMLDivElement);
|
|
@@ -182,13 +195,14 @@ var SonicWaveform = class extends SonicWaveElement {
|
|
|
182
195
|
insetProperty: "--_sonic-waveform-label-inset",
|
|
183
196
|
parkProperty: "--_sonic-waveform-label-park"
|
|
184
197
|
});
|
|
198
|
+
#strip;
|
|
185
199
|
#zoomGesture;
|
|
186
200
|
attributeChangedCallback(name) {
|
|
187
201
|
super.attributeChangedCallback(name);
|
|
188
202
|
if (name === "zoomable") this.#zoomGesture?.sync();
|
|
189
203
|
}
|
|
190
204
|
connectedCallback() {
|
|
191
|
-
this.upgradeProperties("peaks", "pending", "pendingDelay", "playing", "playbackRate", "readTime", "reducedMotion", "renderLabel", "requestPeaks", "zoom", "zoomable", "zoomMax", "zoomMin");
|
|
205
|
+
this.upgradeProperties("bands", "peaks", "pending", "pendingDelay", "playing", "playbackRate", "readTime", "reducedMotion", "renderLabel", "requestPeaks", "zoom", "zoomable", "zoomMax", "zoomMin");
|
|
192
206
|
super.connectedCallback();
|
|
193
207
|
}
|
|
194
208
|
connect(signal) {
|
|
@@ -262,8 +276,10 @@ var SonicWaveform = class extends SonicWaveElement {
|
|
|
262
276
|
if (isRebuilt) this.#kindColours = this.#readKindColours();
|
|
263
277
|
paintWaveform(context, {
|
|
264
278
|
...view,
|
|
265
|
-
colours: look.colours
|
|
279
|
+
colours: look.colours,
|
|
280
|
+
numbers: look.numbers
|
|
266
281
|
}, {
|
|
282
|
+
bands: this.#tint(isRebuilt),
|
|
267
283
|
kindColours: this.#kindColours,
|
|
268
284
|
markers: this.markerList(),
|
|
269
285
|
peaks: this.#peaks,
|
|
@@ -328,6 +344,14 @@ var SonicWaveform = class extends SonicWaveElement {
|
|
|
328
344
|
startSeconds: this.value
|
|
329
345
|
};
|
|
330
346
|
}
|
|
347
|
+
#readBandColours(strip, bandCount) {
|
|
348
|
+
const probes = Array.from({ length: bandCount }, (_probe, band) => {
|
|
349
|
+
const probe = document.createElement("div");
|
|
350
|
+
probe.style.setProperty("color", `var(--sonic-waveform-band-${String(band + 1)}, transparent)`);
|
|
351
|
+
return probe;
|
|
352
|
+
});
|
|
353
|
+
return this.#readProbes(probes).map((colour) => bandChannels(strip, colour));
|
|
354
|
+
}
|
|
331
355
|
#readKindColours() {
|
|
332
356
|
const kinds = [...new Set(this.markers.map(({ kind }) => kind).filter((kind) => isKind(kind)))];
|
|
333
357
|
const probes = kinds.map((kind) => {
|
|
@@ -336,14 +360,34 @@ var SonicWaveform = class extends SonicWaveElement {
|
|
|
336
360
|
writeKind(probe, kind, "--_sonic-waveform-marker");
|
|
337
361
|
return probe;
|
|
338
362
|
});
|
|
363
|
+
const read = this.#readProbes(probes);
|
|
364
|
+
return new Map(kinds.map((kind, index) => [kind, read[index] ?? ""]));
|
|
365
|
+
}
|
|
366
|
+
#readProbes(probes) {
|
|
339
367
|
this.control.append(...probes);
|
|
340
368
|
const read = probes.map((probe) => getComputedStyle(probe).color);
|
|
341
369
|
for (const probe of probes) probe.remove();
|
|
342
|
-
return
|
|
370
|
+
return read;
|
|
343
371
|
}
|
|
344
372
|
#sourceSeconds() {
|
|
345
373
|
return this.#readTime?.() ?? this.scrubState().played;
|
|
346
374
|
}
|
|
375
|
+
#tint(isRebuilt) {
|
|
376
|
+
const table = this.#bandTable;
|
|
377
|
+
if (!table) return void 0;
|
|
378
|
+
const strip = this.#strip ?? document.createElement("canvas").getContext("2d", { willReadFrequently: true }) ?? void 0;
|
|
379
|
+
if (!strip) return void 0;
|
|
380
|
+
this.#strip = strip;
|
|
381
|
+
if (isRebuilt || this.#bandColours.length !== table.bandCount) {
|
|
382
|
+
strip.canvas.height = 1;
|
|
383
|
+
this.#bandColours = this.#readBandColours(strip, table.bandCount);
|
|
384
|
+
}
|
|
385
|
+
return {
|
|
386
|
+
colours: this.#bandColours,
|
|
387
|
+
strip,
|
|
388
|
+
table
|
|
389
|
+
};
|
|
390
|
+
}
|
|
347
391
|
#zoomTo(target) {
|
|
348
392
|
const next = roundTo(clamp(target, this.zoomMin, this.zoomMax), 2);
|
|
349
393
|
if (next === this.zoom) return;
|