@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.
Files changed (84) hide show
  1. package/README.md +4 -0
  2. package/dist/default/define/spectrum.js +7 -0
  3. package/dist/default/define.js +1 -0
  4. package/dist/default/elements/button.js +30 -2
  5. package/dist/default/elements/display-element.js +77 -0
  6. package/dist/default/elements/envelope.js +1 -1
  7. package/dist/default/elements/slider.js +67 -1
  8. package/dist/default/elements/spectrum.js +230 -0
  9. package/dist/default/elements/value-element.js +49 -11
  10. package/dist/default/elements/wave-element.js +59 -9
  11. package/dist/default/elements/waveform.js +56 -10
  12. package/dist/default/elements/wavestrip.js +31 -45
  13. package/dist/default/elements/xy.js +1 -1
  14. package/dist/default/index.js +2 -1
  15. package/dist/default/lib/canvas-surface.js +7 -4
  16. package/dist/default/lib/frame-timeline.js +2 -2
  17. package/dist/default/lib/hover-preview.js +46 -0
  18. package/dist/default/lib/marker-rider.js +7 -4
  19. package/dist/default/lib/pointer-drag.js +7 -2
  20. package/dist/default/lib/spectrum-columns.js +74 -0
  21. package/dist/default/lib/spectrum-levels.js +55 -0
  22. package/dist/default/lib/strip-scene.js +12 -1
  23. package/dist/default/lib/time-regions.js +5 -1
  24. package/dist/dev/define/spectrum.d.ts +2 -0
  25. package/dist/dev/define/spectrum.js +8 -0
  26. package/dist/dev/define/spectrum.js.map +1 -0
  27. package/dist/dev/define.d.ts +3 -1
  28. package/dist/dev/define.js +1 -0
  29. package/dist/dev/elements/button.d.ts +2 -0
  30. package/dist/dev/elements/button.js +30 -2
  31. package/dist/dev/elements/button.js.map +1 -1
  32. package/dist/dev/elements/display-element.d.ts +29 -0
  33. package/dist/dev/elements/display-element.js +78 -0
  34. package/dist/dev/elements/display-element.js.map +1 -0
  35. package/dist/dev/elements/envelope.js +1 -1
  36. package/dist/dev/elements/slider.d.ts +6 -0
  37. package/dist/dev/elements/slider.js +67 -1
  38. package/dist/dev/elements/slider.js.map +1 -1
  39. package/dist/dev/elements/spectrum.d.ts +59 -0
  40. package/dist/dev/elements/spectrum.js +231 -0
  41. package/dist/dev/elements/spectrum.js.map +1 -0
  42. package/dist/dev/elements/value-element.d.ts +5 -1
  43. package/dist/dev/elements/value-element.js +49 -11
  44. package/dist/dev/elements/value-element.js.map +1 -1
  45. package/dist/dev/elements/wave-element.d.ts +8 -2
  46. package/dist/dev/elements/wave-element.js +59 -9
  47. package/dist/dev/elements/wave-element.js.map +1 -1
  48. package/dist/dev/elements/waveform.d.ts +12 -4
  49. package/dist/dev/elements/waveform.js +56 -10
  50. package/dist/dev/elements/waveform.js.map +1 -1
  51. package/dist/dev/elements/wavestrip.d.ts +3 -0
  52. package/dist/dev/elements/wavestrip.js +31 -45
  53. package/dist/dev/elements/wavestrip.js.map +1 -1
  54. package/dist/dev/elements/xy.js +1 -1
  55. package/dist/dev/index.d.ts +4 -1
  56. package/dist/dev/index.js +2 -1
  57. package/dist/dev/lib/canvas-surface.d.ts +4 -3
  58. package/dist/dev/lib/canvas-surface.js +7 -4
  59. package/dist/dev/lib/canvas-surface.js.map +1 -1
  60. package/dist/dev/lib/frame-timeline.js +2 -2
  61. package/dist/dev/lib/frame-timeline.js.map +1 -1
  62. package/dist/dev/lib/hover-preview.js +47 -0
  63. package/dist/dev/lib/hover-preview.js.map +1 -0
  64. package/dist/dev/lib/marker-rider.js +7 -4
  65. package/dist/dev/lib/marker-rider.js.map +1 -1
  66. package/dist/dev/lib/pointer-drag.js +7 -2
  67. package/dist/dev/lib/pointer-drag.js.map +1 -1
  68. package/dist/dev/lib/spectrum-columns.js +75 -0
  69. package/dist/dev/lib/spectrum-columns.js.map +1 -0
  70. package/dist/dev/lib/spectrum-levels.js +56 -0
  71. package/dist/dev/lib/spectrum-levels.js.map +1 -0
  72. package/dist/dev/lib/strip-scene.js +12 -1
  73. package/dist/dev/lib/strip-scene.js.map +1 -1
  74. package/dist/dev/lib/time-regions.js +5 -1
  75. package/dist/dev/lib/time-regions.js.map +1 -1
  76. package/dist/styles/button.css +7 -1
  77. package/dist/styles/controls.css +1 -0
  78. package/dist/styles/material.css +18 -3
  79. package/dist/styles/meter.css +22 -18
  80. package/dist/styles/slider.css +42 -5
  81. package/dist/styles/spectrum.css +105 -0
  82. package/dist/styles/waveform.css +38 -16
  83. package/dist/styles/wavestrip.css +22 -4
  84. package/package.json +2 -1
@@ -1,7 +1,8 @@
1
- import { clamp, toNumber } from "../lib/math.js";
1
+ import { clamp, toNumber, trimFloat } from "../lib/math.js";
2
2
  import { writeAttribute } from "../lib/write-attribute.js";
3
3
  import { SonicFormElement } from "./form-element.js";
4
4
  import { requireChild } from "../lib/render.js";
5
+ import { bindDrag } from "../lib/pointer-drag.js";
5
6
  import { ReadoutClaim } from "./readout-claim.js";
6
7
  import { Readout } from "./readout.js";
7
8
  import { ValueEntry } from "./value-entry.js";
@@ -9,7 +10,6 @@ import { dragThresholdPx, startDrag, stepDrag } from "../lib/drag-step.js";
9
10
  import { focusByPointer } from "../lib/focus-by-pointer.js";
10
11
  import { isMenuPress, isResetPress } from "../lib/modifier-press.js";
11
12
  import { parseNumberList } from "../lib/number-list.js";
12
- import { bindDrag } from "../lib/pointer-drag.js";
13
13
  import { readPxProperty } from "../lib/read-px-property.js";
14
14
  import { resetKeys, valueMapping } from "../lib/value-mapping.js";
15
15
 
@@ -49,6 +49,7 @@ var SonicValueElement = class extends SonicFormElement {
49
49
  "notched",
50
50
  "origin",
51
51
  "readout",
52
+ "spoken-step",
52
53
  "step",
53
54
  "tabindex",
54
55
  "taper",
@@ -156,6 +157,9 @@ var SonicValueElement = class extends SonicFormElement {
156
157
  set parseValue(parse) {
157
158
  this.#parseValue = parse;
158
159
  }
160
+ get pointerType() {
161
+ return this.#dragging()?.pointerType;
162
+ }
159
163
  get positions() {
160
164
  return parseNumberList(this.getAttribute("positions"));
161
165
  }
@@ -168,6 +172,12 @@ var SonicValueElement = class extends SonicFormElement {
168
172
  set readout(isShown) {
169
173
  this.reflect("readout", isShown);
170
174
  }
175
+ get spokenStep() {
176
+ return this.optionalNumberAttribute("spoken-step");
177
+ }
178
+ set spokenStep(value) {
179
+ this.reflect("spoken-step", value);
180
+ }
171
181
  get step() {
172
182
  return this.numberAttribute("step", 1);
173
183
  }
@@ -206,8 +216,10 @@ var SonicValueElement = class extends SonicFormElement {
206
216
  #entry;
207
217
  #formatSpokenValue;
208
218
  #formatValue;
219
+ #heldAt;
209
220
  #instance = String(instanceCount += 1);
210
221
  #isModulated = false;
222
+ #isRevealed = false;
211
223
  #keyScrub;
212
224
  #land;
213
225
  #limit;
@@ -235,7 +247,7 @@ var SonicValueElement = class extends SonicFormElement {
235
247
  this.render();
236
248
  }
237
249
  connectedCallback() {
238
- this.upgradeProperties("default", "dimmed", "doublePress", "max", "midpoint", "min", "notched", "origin", "readout", "step", "taper", "positions", "detent", "formatValue", "formatSpokenValue", "keyStep", "parseValue", "value");
250
+ this.upgradeProperties("default", "dimmed", "doublePress", "max", "midpoint", "min", "notched", "origin", "readout", "spokenStep", "step", "taper", "positions", "detent", "formatValue", "formatSpokenValue", "keyStep", "parseValue", "value");
239
251
  super.connectedCallback();
240
252
  }
241
253
  bindGestures(control, signal, grab) {
@@ -261,6 +273,7 @@ var SonicValueElement = class extends SonicFormElement {
261
273
  fromValue,
262
274
  isOutside: false,
263
275
  isRevealed: false,
276
+ pointerType: event.pointerType,
264
277
  revealTimer: void 0,
265
278
  state: this.#startDrag(control, axis, event)
266
279
  };
@@ -306,7 +319,9 @@ var SonicValueElement = class extends SonicFormElement {
306
319
  heldFrom() {
307
320
  return this.#dragging()?.fromValue ?? this.#keyScrub?.fromValue;
308
321
  }
309
- heldWrite(_next) {}
322
+ heldWrite(next) {
323
+ this.#heldAt = next;
324
+ }
310
325
  holdChanged() {}
311
326
  input(next) {
312
327
  const previous = this.#value;
@@ -346,6 +361,10 @@ var SonicValueElement = class extends SonicFormElement {
346
361
  originValue() {
347
362
  return this.origin ?? this.mapping().bounds[0];
348
363
  }
364
+ playback() {
365
+ const from = this.heldFrom();
366
+ return from === void 0 ? this.#value : this.#heldAt ?? from;
367
+ }
349
368
  previewValue() {}
350
369
  proportionsChanged() {}
351
370
  readModulationValue() {
@@ -362,10 +381,11 @@ var SonicValueElement = class extends SonicFormElement {
362
381
  const readout = this.#readout;
363
382
  if (!readout) return;
364
383
  const { isOpen, value } = this.#shown();
384
+ const text = value === void 0 ? void 0 : this.#textFor(value);
365
385
  readout.bubble.show({
366
386
  anchor: readout.anchor,
367
- isOpen,
368
- text: value === void 0 ? void 0 : this.#textFor(value)
387
+ isOpen: isOpen && text !== "",
388
+ text
369
389
  });
370
390
  }
371
391
  restoreState(state) {
@@ -473,10 +493,13 @@ var SonicValueElement = class extends SonicFormElement {
473
493
  drag.state = this.#withinLimit(mapping, state);
474
494
  if (value === void 0) return;
475
495
  if (!wasEngaged) this.#reveal(drag);
496
+ const isCrossing = isOutside !== drag.isOutside;
497
+ if (isCrossing) {
498
+ drag.isOutside = isOutside;
499
+ this.toggleState("cancelling", isOutside);
500
+ }
476
501
  this.input(isOutside ? drag.fromValue : value);
477
- if (isOutside === drag.isOutside) return;
478
- drag.isOutside = isOutside;
479
- this.#renderDragReveal(drag);
502
+ if (isCrossing) this.#renderDragReveal(drag);
480
503
  }
481
504
  #endHolds() {
482
505
  this.#entry?.close(false);
@@ -508,7 +531,9 @@ var SonicValueElement = class extends SonicFormElement {
508
531
  #releaseDrag(drag) {
509
532
  clearTimeout(drag.revealTimer);
510
533
  this.#claim.conceal("drag");
534
+ this.toggleState("cancelling", false);
511
535
  this.#renderHold();
536
+ this.#showRevealed(false);
512
537
  if (this.#value !== drag.fromValue) this.dispatchEvent(new Event("change", { bubbles: true }));
513
538
  this.#springBack();
514
539
  }
@@ -524,7 +549,7 @@ var SonicValueElement = class extends SonicFormElement {
524
549
  writeAttribute(control, "role", this.controlRole());
525
550
  writeAttribute(control, "aria-valuemin", String(low));
526
551
  writeAttribute(control, "aria-valuemax", String(high));
527
- writeAttribute(control, "aria-valuenow", String(this.#value));
552
+ writeAttribute(control, "aria-valuenow", String(this.#spokenNow()));
528
553
  if (orientation) writeAttribute(control, "aria-orientation", orientation);
529
554
  writeAttribute(control, "aria-valuetext", (this.#formatSpokenValue ?? this.#formatValue)?.(this.#value));
530
555
  this.forwardNaming(control, true);
@@ -543,11 +568,14 @@ var SonicValueElement = class extends SonicFormElement {
543
568
  writeAttribute(control, "tabindex", isDisabled ? void 0 : stop);
544
569
  }
545
570
  #renderDragReveal(drag) {
546
- if (drag.isRevealed && !drag.isOutside) this.#claim.reveal("drag");
571
+ const isRevealed = drag.isRevealed && !drag.isOutside;
572
+ if (isRevealed) this.#claim.reveal("drag");
547
573
  else this.#claim.conceal("drag");
548
574
  this.#renderHold();
575
+ this.#showRevealed(isRevealed);
549
576
  }
550
577
  #renderHold() {
578
+ if (this.heldFrom() === void 0) this.#heldAt = void 0;
551
579
  this.renderReadout();
552
580
  this.holdChanged();
553
581
  }
@@ -581,6 +609,16 @@ var SonicValueElement = class extends SonicFormElement {
581
609
  value: preview ?? this.#value
582
610
  };
583
611
  }
612
+ #showRevealed(isRevealed) {
613
+ if (isRevealed === this.#isRevealed) return;
614
+ this.#isRevealed = isRevealed;
615
+ this.toggleState("revealed", isRevealed);
616
+ this.dispatchEvent(new Event("sonic-reveal", { bubbles: true }));
617
+ }
618
+ #spokenNow() {
619
+ const step = this.spokenStep;
620
+ return step !== void 0 && step > 0 ? trimFloat(Math.round(this.#value / step) * step) : this.#value;
621
+ }
584
622
  #springBack() {
585
623
  const target = this.springTarget();
586
624
  if (target === void 0) return;
@@ -2,19 +2,44 @@ import { SonicValueElement } from "./value-element.js";
2
2
  import { bindSurface } from "../lib/canvas-surface.js";
3
3
 
4
4
  //#region src/elements/wave-element.ts
5
+ function markerKey(marker) {
6
+ return marker ? [
7
+ marker.start,
8
+ marker.end,
9
+ marker.kind,
10
+ marker.label
11
+ ].join("|") : "";
12
+ }
5
13
  var SonicWaveElement = class extends SonicValueElement {
14
+ static observedAttributes = [...SonicValueElement.observedAttributes, "fill"];
15
+ get currentMarker() {
16
+ return this.#current ? { ...this.#current } : void 0;
17
+ }
18
+ get fill() {
19
+ return this.hasAttribute("fill");
20
+ }
21
+ set fill(isFilling) {
22
+ this.reflect("fill", isFilling);
23
+ }
6
24
  get markers() {
7
25
  return this.#markers.map((marker) => ({ ...marker }));
8
26
  }
9
27
  set markers(list) {
10
28
  this.#markers = [...list ?? []].filter((marker) => Number.isFinite(marker.start)).map((marker) => ({ ...marker })).toSorted((first, second) => first.start - second.start);
11
- if (this.isBound()) this.renderMarkers();
29
+ if (!this.isBound()) return;
30
+ this.renderMarkers();
31
+ this.render();
12
32
  }
13
- #heldAt;
33
+ #current;
34
+ #fillWatch;
14
35
  #markers = [];
15
36
  #surface;
37
+ attributeChangedCallback(name) {
38
+ super.attributeChangedCallback(name);
39
+ if (name === "fill") this.#watchFill();
40
+ }
16
41
  connectedCallback() {
17
- this.upgradeProperties("markers");
42
+ this.upgradeProperties("fill", "markers");
18
43
  super.connectedCallback();
19
44
  }
20
45
  repaint() {
@@ -38,16 +63,20 @@ var SonicWaveElement = class extends SonicValueElement {
38
63
  });
39
64
  this.render();
40
65
  this.checkStyles(control, this.sheet);
66
+ this.#watchFill();
67
+ signal.addEventListener("abort", () => {
68
+ this.#fillWatch?.disconnect();
69
+ this.#fillWatch = void 0;
70
+ }, { once: true });
41
71
  }
42
72
  focusTarget() {
43
73
  return this.control;
44
74
  }
45
75
  heldWrite(next) {
46
- this.#heldAt = next;
76
+ super.heldWrite(next);
47
77
  this.render();
48
78
  }
49
79
  holdChanged() {
50
- if (this.heldFrom() === void 0) this.#heldAt = void 0;
51
80
  this.render();
52
81
  }
53
82
  markerList() {
@@ -61,19 +90,40 @@ var SonicWaveElement = class extends SonicValueElement {
61
90
  return [Math.min(at, to), Math.max(at, to)];
62
91
  });
63
92
  }
64
- playback() {
65
- const from = this.heldFrom();
66
- return from === void 0 ? this.value : this.#heldAt ?? from;
67
- }
68
93
  proportionsChanged() {
69
94
  this.renderMarkers();
70
95
  }
71
96
  renderEmpty() {
72
97
  this.toggleState("empty", this.isEmpty());
73
98
  }
99
+ showCurrentMarker(seconds) {
100
+ const current = this.#markers.findLast((marker) => marker.start <= seconds);
101
+ if (markerKey(current) === markerKey(this.#current)) return;
102
+ this.#current = current;
103
+ this.dispatchEvent(new Event("sonic-marker", { bubbles: true }));
104
+ }
74
105
  surface() {
75
106
  return this.#surface;
76
107
  }
108
+ #watchFill() {
109
+ const control = this.control;
110
+ const isFilling = this.fill && this.isBound();
111
+ if (isFilling === (this.#fillWatch !== void 0)) return;
112
+ this.#fillWatch?.disconnect();
113
+ this.#fillWatch = void 0;
114
+ if (!isFilling) {
115
+ control.style.removeProperty(this.sizeProperty);
116
+ return;
117
+ }
118
+ this.#fillWatch = new ResizeObserver((entries) => {
119
+ const box = entries.at(-1)?.borderBoxSize[0];
120
+ if (!box) return;
121
+ requestAnimationFrame(() => {
122
+ if (this.#fillWatch) control.style.setProperty(this.sizeProperty, `${String(box.blockSize)}px`);
123
+ });
124
+ });
125
+ this.#fillWatch.observe(control);
126
+ }
77
127
  };
78
128
 
79
129
  //#endregion
@@ -1,6 +1,6 @@
1
1
  import { clamp } from "../lib/math.js";
2
2
  import { requireChild, template } from "../lib/render.js";
3
- import { readRegions } from "../lib/time-regions.js";
3
+ import { dueRegions, readRegions } from "../lib/time-regions.js";
4
4
  import { SonicWaveElement } from "./wave-element.js";
5
5
  import { frameTimeline } from "../lib/frame-timeline.js";
6
6
  import { isKind, writeKind } from "../lib/marker-band.js";
@@ -10,9 +10,10 @@ import { waveformBuckets } from "../lib/waveform-buckets.js";
10
10
 
11
11
  //#region src/elements/waveform.ts
12
12
  const colours = {
13
+ ends: "--_sonic-waveform-ends",
13
14
  grid: "--_sonic-waveform-grid",
14
- hatch: "--_sonic-waveform-hatch",
15
15
  marker: "--_sonic-waveform-marker",
16
+ placeholder: "--_sonic-waveform-placeholder",
16
17
  wave: "--_sonic-waveform-wave",
17
18
  waveEdge: "--_sonic-waveform-wave-edge"
18
19
  };
@@ -63,7 +64,7 @@ function paintHatch(context, scene, [from, to]) {
63
64
  context.lineTo(x + height, 0);
64
65
  }
65
66
  context.lineWidth = Math.max(1, Math.round(dpr));
66
- context.strokeStyle = scene.colours.hatch;
67
+ context.strokeStyle = scene.colours.ends;
67
68
  context.stroke();
68
69
  context.restore();
69
70
  }
@@ -83,7 +84,7 @@ function paintPlaceholders(context, scene) {
83
84
  context.lineTo(closing, waveY(closing));
84
85
  }
85
86
  context.lineWidth = Math.max(1, Math.round(dpr));
86
- context.strokeStyle = scene.colours.hatch;
87
+ context.strokeStyle = scene.colours.placeholder;
87
88
  context.stroke();
88
89
  }
89
90
  function waveFill(context, scene) {
@@ -111,8 +112,10 @@ function paintWave(context, scene, peaks) {
111
112
  var SonicWaveform = class extends SonicWaveElement {
112
113
  static observedAttributes = [
113
114
  ...SonicWaveElement.observedAttributes,
115
+ "pending-delay",
114
116
  "playing",
115
117
  "playback-rate",
118
+ "reduced-motion",
116
119
  "zoom"
117
120
  ];
118
121
  get peaks() {
@@ -127,9 +130,21 @@ var SonicWaveform = class extends SonicWaveElement {
127
130
  return this.#pending.map(([start, end]) => [start, end]);
128
131
  }
129
132
  set pending(regions) {
133
+ const listedMs = this.#pendingListedMs;
134
+ const nowMs = performance.now();
130
135
  this.#pending = regions ? readRegions(regions) : [];
136
+ this.#pendingListedMs = new Map(this.#pending.map((region) => {
137
+ const key = region.join(":");
138
+ return [key, listedMs.get(key) ?? nowMs];
139
+ }));
131
140
  this.surface()?.invalidate();
132
141
  }
142
+ get pendingDelay() {
143
+ return Math.max(0, this.numberAttribute("pending-delay", 0));
144
+ }
145
+ set pendingDelay(value) {
146
+ this.reflect("pending-delay", value);
147
+ }
133
148
  get playbackRate() {
134
149
  const playbackRate = this.numberAttribute("playback-rate", 1);
135
150
  return playbackRate > 0 ? playbackRate : 1;
@@ -150,11 +165,18 @@ var SonicWaveform = class extends SonicWaveElement {
150
165
  this.#readTime = read;
151
166
  this.render();
152
167
  }
168
+ get reducedMotion() {
169
+ return this.getAttribute("reduced-motion") === "scroll" ? "scroll" : "page";
170
+ }
171
+ set reducedMotion(motion) {
172
+ this.reflect("reduced-motion", motion);
173
+ }
153
174
  get requestPeaks() {
154
175
  return this.#requestPeaks;
155
176
  }
156
177
  set requestPeaks(request) {
157
178
  this.#requestPeaks = request;
179
+ this.#asked = void 0;
158
180
  this.surface()?.invalidate();
159
181
  }
160
182
  get zoom() {
@@ -169,6 +191,8 @@ var SonicWaveform = class extends SonicWaveElement {
169
191
  colours = colours;
170
192
  lengths = {};
171
193
  sheet = "waveform.css";
194
+ sizeProperty = "--_sonic-waveform-size";
195
+ #asked;
172
196
  #clock = createTrackingClock();
173
197
  #drawn;
174
198
  #ghost = requireChild(this.control, ".sonic-waveform-ghost", HTMLDivElement);
@@ -177,16 +201,18 @@ var SonicWaveform = class extends SonicWaveElement {
177
201
  #paintedKey = "";
178
202
  #peaks;
179
203
  #pending = [];
204
+ #pendingListedMs = /* @__PURE__ */ new Map();
180
205
  #playhead = requireChild(this.control, ".sonic-waveform-playhead", HTMLDivElement);
181
206
  #readTime;
182
207
  #requestPeaks;
183
208
  #rider = createLabelRider(this.control, {
184
209
  className: "sonic-waveform-label",
185
210
  colourProperty: "--_sonic-waveform-marker",
211
+ fadeProperty: "--_sonic-waveform-label-fade",
186
212
  insetProperty: "--_sonic-waveform-label-inset"
187
213
  });
188
214
  connectedCallback() {
189
- this.upgradeProperties("peaks", "pending", "playing", "playbackRate", "readTime", "requestPeaks", "zoom");
215
+ this.upgradeProperties("peaks", "pending", "pendingDelay", "playing", "playbackRate", "readTime", "reducedMotion", "requestPeaks", "zoom");
190
216
  super.connectedCallback();
191
217
  }
192
218
  connect(signal) {
@@ -204,6 +230,7 @@ var SonicWaveform = class extends SonicWaveElement {
204
230
  }
205
231
  paint(context, { frameMs, isDirty, isRebuilt, look, size }) {
206
232
  const held = this.#held();
233
+ const pending = this.#duePending();
207
234
  const timeline = frameTimeline({
208
235
  clockSeconds: this.#clock.read(frameMs, {
209
236
  isPlaying: this.playing,
@@ -212,9 +239,10 @@ var SonicWaveform = class extends SonicWaveElement {
212
239
  }),
213
240
  frameMs,
214
241
  ...held ? { held } : {},
215
- isPaged: look.isReducedMotion,
242
+ isPaged: look.isReducedMotion && this.reducedMotion === "page",
216
243
  isPlaying: this.playing,
217
- pending: this.#pending,
244
+ isStill: look.isReducedMotion,
245
+ pending,
218
246
  range: [this.min, Math.max(this.min, this.max)],
219
247
  size,
220
248
  zoom: this.zoom
@@ -226,6 +254,7 @@ var SonicWaveform = class extends SonicWaveElement {
226
254
  };
227
255
  placeLine(this.#playhead, timeline.playheadAt);
228
256
  placeLine(this.#ghost, timeline.ghostAt);
257
+ this.showCurrentMarker(seconds);
229
258
  this.toggleState("pending", view.pending.length > 0);
230
259
  this.#rider.place({
231
260
  playheadSeconds: seconds,
@@ -239,7 +268,7 @@ var SonicWaveform = class extends SonicWaveElement {
239
268
  ...view,
240
269
  colours: look.colours
241
270
  }, isRebuilt);
242
- if (timeline.wanted) this.#requestPeaks?.(...timeline.wanted);
271
+ if (timeline.wanted) this.#ask(timeline.wanted);
243
272
  }
244
273
  if (timeline.isMoving) this.surface()?.requestFrame();
245
274
  }
@@ -253,6 +282,17 @@ var SonicWaveform = class extends SonicWaveElement {
253
282
  }]));
254
283
  this.surface()?.rebuild();
255
284
  }
285
+ #ask(wanted) {
286
+ const asked = this.#requestPeaks?.(...wanted);
287
+ if (!(asked instanceof Promise)) return;
288
+ this.#asked = asked;
289
+ this.#repaintAfter(asked);
290
+ }
291
+ #duePending() {
292
+ const due = dueRegions(this.#pending, this.#pendingListedMs, [performance.now(), this.pendingDelay]);
293
+ if (due.length < this.#pending.length) this.surface()?.requestFrame();
294
+ return due;
295
+ }
256
296
  #grab() {
257
297
  const drawn = this.#drawn ?? {
258
298
  seconds: this.value,
@@ -291,7 +331,7 @@ var SonicWaveform = class extends SonicWaveElement {
291
331
  #paintScene(context, scene, isRebuilt) {
292
332
  const { dpr, height, pixelsPerSecond, startSeconds, width } = scene;
293
333
  const x = (seconds) => (seconds - startSeconds) * pixelsPerSecond;
294
- const { grid } = scene.colours;
334
+ const { ends, grid } = scene.colours;
295
335
  const line = Math.max(1, Math.round(dpr));
296
336
  const [opening, closing] = [x(this.min), x(Math.max(this.min, this.max))];
297
337
  const [from, to] = [clamp(opening, 0, width), clamp(closing, 0, width)];
@@ -302,7 +342,7 @@ var SonicWaveform = class extends SonicWaveElement {
302
342
  if (to > from) context.fillRect(from, Math.round((height - line) / 2), to - from, line);
303
343
  paintPlaceholders(context, scene);
304
344
  if (this.#peaks) paintWave(context, scene, this.#peaks);
305
- context.fillStyle = grid;
345
+ context.fillStyle = ends;
306
346
  for (const edge of [opening, closing]) context.fillRect(edge - line / 2, 0, line, height);
307
347
  this.#paintMarkers(context, scene, isRebuilt);
308
348
  }
@@ -319,6 +359,12 @@ var SonicWaveform = class extends SonicWaveElement {
319
359
  for (const probe of probes) probe.remove();
320
360
  return new Map(kinds.map((kind, index) => [kind, read[index] ?? ""]));
321
361
  }
362
+ async #repaintAfter(asked) {
363
+ await Promise.allSettled([asked]);
364
+ if (this.#asked !== asked) return;
365
+ this.#asked = void 0;
366
+ this.surface()?.invalidate();
367
+ }
322
368
  #sourceSeconds() {
323
369
  return this.#readTime?.() ?? this.playback();
324
370
  }
@@ -1,10 +1,11 @@
1
1
  import { clampProportion } from "../lib/math.js";
2
2
  import { requireChild, template } from "../lib/render.js";
3
3
  import { readPxProperty } from "../lib/read-px-property.js";
4
+ import { bindHoverPreview } from "../lib/hover-preview.js";
4
5
  import { sortedRegions } from "../lib/time-regions.js";
5
6
  import { SonicWaveElement } from "./wave-element.js";
6
7
  import { createMarkerBand } from "../lib/marker-band.js";
7
- import { stripBars, stripRegions } from "../lib/strip-scene.js";
8
+ import { stripBars, stripGroove, stripRegions } from "../lib/strip-scene.js";
8
9
 
9
10
  //#region src/elements/wavestrip.ts
10
11
  const colours = {
@@ -15,6 +16,7 @@ const colours = {
15
16
  };
16
17
  const lengths = {
17
18
  gapRatio: "--_sonic-wavestrip-bar-gap-ratio",
19
+ grooveRatio: "--_sonic-wavestrip-groove-ratio",
18
20
  pitch: "--_sonic-wavestrip-bar-pitch",
19
21
  radiusRatio: "--_sonic-wavestrip-bar-radius-ratio"
20
22
  };
@@ -72,11 +74,10 @@ var SonicWavestrip = class extends SonicWaveElement {
72
74
  colours = colours;
73
75
  lengths = lengths;
74
76
  sheet = "wavestrip.css";
77
+ sizeProperty = "--_sonic-wavestrip-size";
75
78
  #bars;
76
79
  #buffered = [];
77
- #escapeWatch;
78
80
  #hover;
79
- #isHoverDismissed = false;
80
81
  #markerBand = createMarkerBand(requireChild(this.control, ".sonic-wavestrip-markers", HTMLDivElement));
81
82
  #paintedKey = "";
82
83
  #peaks;
@@ -86,13 +87,30 @@ var SonicWavestrip = class extends SonicWaveElement {
86
87
  }
87
88
  connect(signal) {
88
89
  super.connect(signal);
89
- this.#bindHover(this.control, signal);
90
+ this.#hover = bindHoverPreview(this.control, {
91
+ canShow: () => this.readout,
92
+ dismiss: () => {
93
+ this.render();
94
+ },
95
+ isTaken: () => this.isEditing() || this.isRevealed(),
96
+ show: () => {
97
+ this.renderReadout();
98
+ this.#placeReadout();
99
+ },
100
+ valueAt: (event) => {
101
+ const axis = this.#axis();
102
+ const at = clampProportion((event.clientX - axis.startPx) / axis.travelPx);
103
+ const mapping = this.mapping();
104
+ return this.#markerAt(event, axis) ?? mapping.snap(mapping.valueAt(at));
105
+ }
106
+ }, signal);
90
107
  this.bindGestures(this.control, signal, (event) => this.#grab(event));
91
108
  }
92
109
  draw() {
93
110
  const surface = this.surface();
94
- if (this.isEditing() || this.isRevealed()) this.#hover = void 0;
111
+ if (this.isEditing() || this.isRevealed()) this.#hover?.clear();
95
112
  this.#placeReadout();
113
+ this.showCurrentMarker(this.playback());
96
114
  if (surface && this.#stripRegions(surface.size.width).key !== this.#paintedKey) surface.requestFrame();
97
115
  }
98
116
  isEmpty() {
@@ -114,7 +132,7 @@ var SonicWavestrip = class extends SonicWaveElement {
114
132
  ]);
115
133
  }
116
134
  previewValue() {
117
- return this.#hover;
135
+ return this.#hover?.value();
118
136
  }
119
137
  renderMarkers() {
120
138
  this.#markerBand.render(this.markerList(), this.markerProportions());
@@ -132,28 +150,18 @@ var SonicWavestrip = class extends SonicWaveElement {
132
150
  };
133
151
  }
134
152
  #barsFor({ isDirty, look, size }) {
135
- const peaks = this.#peaks;
136
- if (!peaks || peaks.length === 0) return void 0;
137
153
  if (!isDirty && this.#bars) return this.#bars;
154
+ const peaks = this.#peaks;
155
+ const groove = stripGroove(size, look.lengths.grooveRatio);
156
+ const drawn = groove ? [groove] : [];
157
+ const bars = peaks && peaks.length > 0 ? stripBars(peaks, size, look.lengths) : drawn;
158
+ this.#bars = void 0;
159
+ if (bars.length === 0) return void 0;
138
160
  const path = new Path2D();
139
- for (const { height, radius, width, x, y } of stripBars(peaks, size, look.lengths)) path.roundRect(x, y, width, height, radius);
161
+ for (const { height, radius, width, x, y } of bars) path.roundRect(x, y, width, height, radius);
140
162
  this.#bars = path;
141
163
  return path;
142
164
  }
143
- #bindHover(strip, signal) {
144
- strip.addEventListener("pointermove", (event) => {
145
- if (event.pointerType === "touch" || event.buttons !== 0) return;
146
- if (!this.readout) return;
147
- const axis = this.#axis();
148
- const at = clampProportion((event.clientX - axis.startPx) / axis.travelPx);
149
- const mapping = this.mapping();
150
- this.#hoverAt(this.#markerAt(event, axis) ?? mapping.snap(mapping.valueAt(at)));
151
- this.#placeReadout();
152
- }, { signal });
153
- for (const type of ["pointerdown", "pointerleave"]) strip.addEventListener(type, () => {
154
- this.#hoverAt(void 0);
155
- }, { signal });
156
- }
157
165
  #grab(event) {
158
166
  const axis = this.#axis();
159
167
  const at = (event.clientX - axis.startPx) / axis.travelPx;
@@ -169,12 +177,6 @@ var SonicWavestrip = class extends SonicWaveElement {
169
177
  outside
170
178
  } : grabbed;
171
179
  }
172
- #hoverAt(value) {
173
- if (value === void 0) this.#isHoverDismissed = false;
174
- if (!this.#isHoverDismissed && !this.isEditing() && !this.isRevealed()) this.#hover = value;
175
- this.renderReadout();
176
- this.#watchEscape(value !== void 0);
177
- }
178
180
  #markerAt(event, { startPx, topPx, travelPx }) {
179
181
  const index = this.#markerBand.at({
180
182
  x: event.clientX - startPx,
@@ -201,22 +203,6 @@ var SonicWavestrip = class extends SonicWaveElement {
201
203
  ...isScrubbing ? { scrub: proportionOf(this.value) } : {}
202
204
  }, width);
203
205
  }
204
- #watchEscape(isHovered) {
205
- if (!isHovered) {
206
- this.#escapeWatch?.abort();
207
- this.#escapeWatch = void 0;
208
- return;
209
- }
210
- if (this.#escapeWatch) return;
211
- const watch = new AbortController();
212
- this.#escapeWatch = watch;
213
- this.ownerDocument.addEventListener("keydown", (event) => {
214
- if (event.key !== "Escape" || this.#hover === void 0) return;
215
- this.#hover = void 0;
216
- this.#isHoverDismissed = true;
217
- this.render();
218
- }, { signal: watch.signal });
219
- }
220
206
  };
221
207
 
222
208
  //#endregion
@@ -2,12 +2,12 @@ import { clampProportion, toNumber } from "../lib/math.js";
2
2
  import { writeAttribute } from "../lib/write-attribute.js";
3
3
  import { SonicFormElement } from "./form-element.js";
4
4
  import { requireChild, template } from "../lib/render.js";
5
+ import { bindDrag } from "../lib/pointer-drag.js";
5
6
  import { ReadoutClaim } from "./readout-claim.js";
6
7
  import { Readout } from "./readout.js";
7
8
  import { dragThresholdPx } from "../lib/drag-step.js";
8
9
  import { focusByPointer } from "../lib/focus-by-pointer.js";
9
10
  import { isMenuPress, isResetPress } from "../lib/modifier-press.js";
10
- import { bindDrag } from "../lib/pointer-drag.js";
11
11
  import { resetKeys, valueMapping } from "../lib/value-mapping.js";
12
12
  import { pointerMove, pointerPosition, startFieldDrag, stepFieldDrag } from "../lib/field.js";
13
13
 
@@ -6,6 +6,7 @@ import { SonicMeter } from "./elements/meter.js";
6
6
  import { SonicNumber } from "./elements/number.js";
7
7
  import { SonicSegmented } from "./elements/segmented.js";
8
8
  import { SonicSlider } from "./elements/slider.js";
9
+ import { SonicSpectrum } from "./elements/spectrum.js";
9
10
  import { SonicSplit } from "./elements/split.js";
10
11
  import { SonicSwitch } from "./elements/switch.js";
11
12
  import { SonicWaveform } from "./elements/waveform.js";
@@ -14,4 +15,4 @@ import { SonicXy } from "./elements/xy.js";
14
15
  import { crossfadeGains } from "./lib/crossfade.js";
15
16
  import { formatPercent, parsePercent } from "./lib/percent.js";
16
17
 
17
- export { SonicButton, SonicDial, SonicEnvelope, SonicMeter, SonicNumber, SonicSegmented, SonicSlider, SonicSplit, SonicSwitch, SonicWaveform, SonicWavestrip, SonicXy, crossfadeGains, envelopeCurve, formatPercent, parsePercent };
18
+ export { SonicButton, SonicDial, SonicEnvelope, SonicMeter, SonicNumber, SonicSegmented, SonicSlider, SonicSpectrum, SonicSplit, SonicSwitch, SonicWaveform, SonicWavestrip, SonicXy, crossfadeGains, envelopeCurve, formatPercent, parsePercent };