@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 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.
@@ -63,6 +63,7 @@ var SonicWaveElement = class extends SonicValueElement {
63
63
  sizeProperty: this.sizeProperty
64
64
  },
65
65
  lengths: this.lengths,
66
+ numbers: this.numbers,
66
67
  paint: (context, frame) => {
67
68
  this.paint(context, frame);
68
69
  },
@@ -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 { paintWaveform, waveformColours } from "../lib/waveform-scene.js";
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 new Map(kinds.map((kind, index) => [kind, read[index] ?? ""]));
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
- wave: "--_sonic-waveform-wave",
12
- waveEdge: "--_sonic-waveform-wave-edge"
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 waveFill(context, scene) {
57
- const { wave, waveEdge } = scene.colours;
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, waveEdge);
60
- gradient.addColorStop(.5, wave);
61
- gradient.addColorStop(1, waveEdge);
62
- return gradient;
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, peaks) {
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
- context.fillStyle = waveFill(context, scene);
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
- paintHatch(context, scene, [0, from]);
97
- paintHatch(context, scene, [to, width]);
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
- paintPlaceholders(context, scene);
101
- if (drawn.peaks) paintWave(context, scene, drawn.peaks);
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;
@@ -63,6 +63,7 @@ var SonicWaveElement = class extends SonicValueElement {
63
63
  sizeProperty: this.sizeProperty
64
64
  },
65
65
  lengths: this.lengths,
66
+ numbers: this.numbers,
66
67
  paint: (context, frame) => {
67
68
  this.paint(context, frame);
68
69
  },
@@ -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":"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"}
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 { paintWaveform, waveformColours } from "../lib/waveform-scene.js";
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 new Map(kinds.map((kind, index) => [kind, read[index] ?? ""]));
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;