@xsynaptic/sonic-ui 0.17.0 → 0.19.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 (178) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +122 -19
  3. package/dist/default/elements/button.js +2 -5
  4. package/dist/default/elements/dial.js +2 -7
  5. package/dist/default/elements/display-element.js +9 -12
  6. package/dist/default/elements/envelope.js +31 -78
  7. package/dist/default/elements/field-gesture.js +81 -0
  8. package/dist/default/elements/meter.js +4 -1
  9. package/dist/default/elements/number.js +0 -7
  10. package/dist/default/elements/position-group.js +42 -26
  11. package/dist/default/elements/radio-group.js +59 -56
  12. package/dist/default/elements/scale.js +3 -3
  13. package/dist/default/elements/segmented.js +0 -5
  14. package/dist/default/elements/slider.js +11 -20
  15. package/dist/default/elements/sonic-element.js +3 -1
  16. package/dist/default/elements/split.js +14 -13
  17. package/dist/default/elements/switch.js +31 -47
  18. package/dist/default/elements/toggle.js +20 -49
  19. package/dist/default/elements/value-element.js +72 -94
  20. package/dist/default/elements/value-gestures.js +22 -22
  21. package/dist/default/elements/wave-element.js +16 -20
  22. package/dist/default/elements/waveform.js +42 -241
  23. package/dist/default/elements/wavestrip.js +10 -5
  24. package/dist/default/elements/xy.js +67 -119
  25. package/dist/default/index.js +2 -1
  26. package/dist/default/lib/canvas-surface.js +25 -0
  27. package/dist/default/lib/clock.js +23 -0
  28. package/dist/default/lib/crossfade.js +1 -0
  29. package/dist/default/lib/envelope-curve.js +1 -0
  30. package/dist/default/lib/peaks-request.js +44 -0
  31. package/dist/default/lib/positions.js +58 -0
  32. package/dist/default/lib/scrub.js +12 -0
  33. package/dist/default/lib/spoken-duration.js +30 -0
  34. package/dist/default/lib/value-model.js +62 -0
  35. package/dist/default/lib/waveform-buckets.js +7 -1
  36. package/dist/default/lib/waveform-scene.js +108 -0
  37. package/dist/default/lib/zoom-gesture.js +97 -1
  38. package/dist/dev/elements/button.d.ts +2 -0
  39. package/dist/dev/elements/button.js +14 -5
  40. package/dist/dev/elements/button.js.map +1 -1
  41. package/dist/dev/elements/dial.d.ts +2 -0
  42. package/dist/dev/elements/dial.js +16 -7
  43. package/dist/dev/elements/dial.js.map +1 -1
  44. package/dist/dev/elements/display-element.d.ts +5 -3
  45. package/dist/dev/elements/display-element.js +11 -9
  46. package/dist/dev/elements/display-element.js.map +1 -1
  47. package/dist/dev/elements/envelope.js +48 -78
  48. package/dist/dev/elements/envelope.js.map +1 -1
  49. package/dist/dev/elements/field-gesture.js +82 -0
  50. package/dist/dev/elements/field-gesture.js.map +1 -0
  51. package/dist/dev/elements/meter.d.ts +10 -5
  52. package/dist/dev/elements/meter.js +5 -1
  53. package/dist/dev/elements/meter.js.map +1 -1
  54. package/dist/dev/elements/number.js +13 -7
  55. package/dist/dev/elements/number.js.map +1 -1
  56. package/dist/dev/elements/position-group.d.ts +12 -6
  57. package/dist/dev/elements/position-group.js +42 -26
  58. package/dist/dev/elements/position-group.js.map +1 -1
  59. package/dist/dev/elements/radio-group.d.ts +11 -7
  60. package/dist/dev/elements/radio-group.js +59 -56
  61. package/dist/dev/elements/radio-group.js.map +1 -1
  62. package/dist/dev/elements/scale.js +3 -3
  63. package/dist/dev/elements/scale.js.map +1 -1
  64. package/dist/dev/elements/segmented.js +12 -5
  65. package/dist/dev/elements/segmented.js.map +1 -1
  66. package/dist/dev/elements/slider.d.ts +4 -2
  67. package/dist/dev/elements/slider.js +25 -20
  68. package/dist/dev/elements/slider.js.map +1 -1
  69. package/dist/dev/elements/sonic-element.d.ts +13 -2
  70. package/dist/dev/elements/sonic-element.js +63 -11
  71. package/dist/dev/elements/sonic-element.js.map +1 -1
  72. package/dist/dev/elements/split.d.ts +1 -0
  73. package/dist/dev/elements/split.js +14 -13
  74. package/dist/dev/elements/split.js.map +1 -1
  75. package/dist/dev/elements/switch.d.ts +2 -2
  76. package/dist/dev/elements/switch.js +38 -47
  77. package/dist/dev/elements/switch.js.map +1 -1
  78. package/dist/dev/elements/toggle.js +32 -49
  79. package/dist/dev/elements/toggle.js.map +1 -1
  80. package/dist/dev/elements/value-element.d.ts +11 -5
  81. package/dist/dev/elements/value-element.js +72 -94
  82. package/dist/dev/elements/value-element.js.map +1 -1
  83. package/dist/dev/elements/value-gestures.js +22 -22
  84. package/dist/dev/elements/value-gestures.js.map +1 -1
  85. package/dist/dev/elements/wave-element.d.ts +8 -5
  86. package/dist/dev/elements/wave-element.js +30 -20
  87. package/dist/dev/elements/wave-element.js.map +1 -1
  88. package/dist/dev/elements/waveform.d.ts +20 -22
  89. package/dist/dev/elements/waveform.js +42 -241
  90. package/dist/dev/elements/waveform.js.map +1 -1
  91. package/dist/dev/elements/wavestrip.d.ts +5 -1
  92. package/dist/dev/elements/wavestrip.js +10 -5
  93. package/dist/dev/elements/wavestrip.js.map +1 -1
  94. package/dist/dev/elements/xy.js +81 -119
  95. package/dist/dev/elements/xy.js.map +1 -1
  96. package/dist/dev/index.d.ts +5 -3
  97. package/dist/dev/index.js +2 -1
  98. package/dist/dev/lib/canvas-surface.d.ts +1 -0
  99. package/dist/dev/lib/canvas-surface.js +25 -0
  100. package/dist/dev/lib/canvas-surface.js.map +1 -1
  101. package/dist/dev/lib/clock.d.ts +5 -0
  102. package/dist/dev/lib/clock.js +24 -0
  103. package/dist/dev/lib/clock.js.map +1 -0
  104. package/dist/dev/lib/crossfade.d.ts +1 -0
  105. package/dist/dev/lib/crossfade.js +1 -0
  106. package/dist/dev/lib/crossfade.js.map +1 -1
  107. package/dist/dev/lib/envelope-curve.d.ts +1 -0
  108. package/dist/dev/lib/envelope-curve.js +1 -0
  109. package/dist/dev/lib/envelope-curve.js.map +1 -1
  110. package/dist/dev/lib/mirror-children.js.map +1 -1
  111. package/dist/dev/lib/owned-control.js.map +1 -1
  112. package/dist/dev/lib/peaks-request.d.ts +5 -0
  113. package/dist/dev/lib/peaks-request.js +45 -0
  114. package/dist/dev/lib/peaks-request.js.map +1 -0
  115. package/dist/dev/lib/positions.d.ts +7 -0
  116. package/dist/dev/lib/positions.js +59 -0
  117. package/dist/dev/lib/positions.js.map +1 -0
  118. package/dist/dev/lib/scrub.d.ts +8 -0
  119. package/dist/dev/lib/scrub.js +13 -0
  120. package/dist/dev/lib/scrub.js.map +1 -0
  121. package/dist/dev/lib/spoken-duration.js +31 -0
  122. package/dist/dev/lib/spoken-duration.js.map +1 -0
  123. package/dist/dev/lib/time-regions.d.ts +1 -0
  124. package/dist/dev/lib/time-regions.js.map +1 -1
  125. package/dist/dev/lib/units.d.ts +8 -0
  126. package/dist/dev/lib/value-model.js +63 -0
  127. package/dist/dev/lib/value-model.js.map +1 -0
  128. package/dist/dev/lib/waveform-buckets.d.ts +2 -1
  129. package/dist/dev/lib/waveform-buckets.js +7 -1
  130. package/dist/dev/lib/waveform-buckets.js.map +1 -1
  131. package/dist/dev/lib/waveform-scene.d.ts +11 -0
  132. package/dist/dev/lib/waveform-scene.js +109 -0
  133. package/dist/dev/lib/waveform-scene.js.map +1 -0
  134. package/dist/dev/lib/zoom-gesture.js +97 -1
  135. package/dist/dev/lib/zoom-gesture.js.map +1 -1
  136. package/dist/skins/amber.css +1 -0
  137. package/dist/skins/ivory.css +1 -0
  138. package/dist/skins/lime.css +1 -0
  139. package/dist/skins/slate.css +1 -0
  140. package/dist/styles/button.css +45 -11
  141. package/dist/styles/dial.css +10 -9
  142. package/dist/styles/envelope.css +10 -4
  143. package/dist/styles/led.css +5 -1
  144. package/dist/styles/material/arc.css +3 -2
  145. package/dist/styles/material/bracket.css +2 -2
  146. package/dist/styles/material/cap.css +33 -12
  147. package/dist/styles/material/core.css +4 -0
  148. package/dist/styles/material/glass.css +1 -1
  149. package/dist/styles/material/groove.css +4 -3
  150. package/dist/styles/material/keycap.css +3 -2
  151. package/dist/styles/material/lens.css +1 -1
  152. package/dist/styles/material/pane.css +5 -4
  153. package/dist/styles/material/readout.css +6 -6
  154. package/dist/styles/material/well.css +2 -2
  155. package/dist/styles/meter.css +2 -7
  156. package/dist/styles/number.css +5 -1
  157. package/dist/styles/panel.css +4 -3
  158. package/dist/styles/ring.css +1 -1
  159. package/dist/styles/screen.css +5 -1
  160. package/dist/styles/segmented.css +17 -4
  161. package/dist/styles/slider.css +9 -3
  162. package/dist/styles/spectrum.css +16 -3
  163. package/dist/styles/switch.css +35 -10
  164. package/dist/styles/toggle.css +14 -7
  165. package/dist/styles/waveform.css +17 -5
  166. package/dist/styles/wavestrip.css +11 -3
  167. package/dist/styles/xy.css +9 -5
  168. package/package.json +1 -6
  169. package/dist/default/dat.js +0 -51
  170. package/dist/default/lib/asked-value.js +0 -21
  171. package/dist/default/lib/fill-watch.js +0 -29
  172. package/dist/dev/dat.d.ts +0 -19
  173. package/dist/dev/dat.js +0 -52
  174. package/dist/dev/dat.js.map +0 -1
  175. package/dist/dev/lib/asked-value.js +0 -22
  176. package/dist/dev/lib/asked-value.js.map +0 -1
  177. package/dist/dev/lib/fill-watch.js +0 -30
  178. package/dist/dev/lib/fill-watch.js.map +0 -1
@@ -14,7 +14,7 @@ const detentZonePx = 8;
14
14
  var ValueGestures = class {
15
15
  #claim = new ReadoutClaim(() => {
16
16
  this.#showRevealed();
17
- this.#host.holdChanged();
17
+ this.#host.scrubChanged();
18
18
  });
19
19
  #control;
20
20
  #entry;
@@ -53,7 +53,7 @@ var ValueGestures = class {
53
53
  this.#pointerType = void 0;
54
54
  return;
55
55
  }
56
- const fromValue = host.value();
56
+ const fromValue = host.model.value;
57
57
  host.toggleState("dragging", true);
58
58
  const axis = grab(event);
59
59
  if (!axis) {
@@ -103,9 +103,6 @@ var ValueGestures = class {
103
103
  this.#pointerDrag?.end();
104
104
  this.#endKeyScrub();
105
105
  }
106
- heldFrom() {
107
- return this.#dragging()?.fromValue ?? this.#keyScrub?.fromValue;
108
- }
109
106
  isEditing() {
110
107
  return this.#entry?.isOpen === true;
111
108
  }
@@ -118,6 +115,9 @@ var ValueGestures = class {
118
115
  pointerType() {
119
116
  return this.#pointerType;
120
117
  }
118
+ scrubFrom() {
119
+ return this.#dragging()?.fromValue ?? this.#keyScrub?.fromValue;
120
+ }
121
121
  #bindEntry(signal) {
122
122
  const control = this.#control;
123
123
  const host = this.#host;
@@ -156,13 +156,13 @@ var ValueGestures = class {
156
156
  this.#claim.reveal("keys");
157
157
  this.#showRevealed();
158
158
  this.#keyTo(event, next);
159
- host.holdChanged();
159
+ host.scrubChanged();
160
160
  }, { signal });
161
161
  control.addEventListener("keyup", (event) => {
162
162
  const isMeta = event.key === "Meta";
163
163
  if (isMeta || event.key === this.#keyScrub?.key) this.#endKeyScrub();
164
164
  if (isMeta && this.#dragging()) return;
165
- if (isMeta || host.mapping().keyTarget(event.key, host.value()) !== void 0) this.#springBack();
165
+ if (isMeta || host.model.mapping().keyTarget(event.key, host.model.value) !== void 0) this.#springBack();
166
166
  }, { signal });
167
167
  control.addEventListener("blur", () => {
168
168
  this.#endKeyScrub();
@@ -172,13 +172,13 @@ var ValueGestures = class {
172
172
  }
173
173
  #commit(next, direction = 0) {
174
174
  const host = this.#host;
175
- const landed = Number.isFinite(next) ? host.land(next, direction) : next;
175
+ const landed = Number.isFinite(next) ? host.model.land(next, direction) : next;
176
176
  if (host.input(landed)) host.dispatch("change");
177
177
  }
178
178
  #concealKeyReveal() {
179
179
  if (!this.#claim.conceal("keys")) return;
180
180
  this.#showRevealed();
181
- this.#host.holdChanged();
181
+ this.#host.scrubChanged();
182
182
  }
183
183
  #crossCancelZone(drag, value) {
184
184
  const host = this.#host;
@@ -196,7 +196,7 @@ var ValueGestures = class {
196
196
  const host = this.#host;
197
197
  const isOutside = drag.outside?.(event) === true;
198
198
  const wasEngaged = drag.state.isEngaged;
199
- const mapping = host.mapping();
199
+ const mapping = host.model.mapping();
200
200
  const { state, value } = stepDrag(mapping, drag.state, {
201
201
  isFine: event.shiftKey,
202
202
  isOutside,
@@ -213,13 +213,13 @@ var ValueGestures = class {
213
213
  const scrub = this.#keyScrub;
214
214
  if (!scrub) return;
215
215
  this.#keyScrub = void 0;
216
- host.holdChanged();
217
- if (host.value() !== scrub.fromValue) host.dispatch("change");
216
+ host.scrubChanged();
217
+ if (host.model.value !== scrub.fromValue) host.dispatch("change");
218
218
  }
219
219
  #keyTarget(key) {
220
220
  const host = this.#host;
221
221
  if (resetKeys.has(key)) return host.default();
222
- return host.mapping().keyTarget(key, host.value(), host.keyStep());
222
+ return host.model.mapping().keyTarget(key, host.model.value, host.keyStep());
223
223
  }
224
224
  #keyTo(event, next) {
225
225
  const host = this.#host;
@@ -228,7 +228,7 @@ var ValueGestures = class {
228
228
  return;
229
229
  }
230
230
  this.#endKeyScrub();
231
- this.#commit(next, next > host.value() ? 1 : -1);
231
+ this.#commit(next, next > host.model.value ? 1 : -1);
232
232
  }
233
233
  #lift(drag, event, entry) {
234
234
  const host = this.#host;
@@ -248,16 +248,16 @@ var ValueGestures = class {
248
248
  clearTimeout(drag.revealTimer);
249
249
  this.#claim.conceal("drag");
250
250
  host.toggleState("cancelling", false);
251
- host.holdChanged();
251
+ host.scrubChanged();
252
252
  this.#showRevealed();
253
- if (host.value() !== drag.fromValue) host.dispatch("change");
253
+ if (host.model.value !== drag.fromValue) host.dispatch("change");
254
254
  this.#springBack();
255
255
  this.#pointerType = void 0;
256
256
  }
257
257
  #renderDragReveal(drag) {
258
258
  if (drag.isRevealed && !drag.isOutside) this.#claim.reveal("drag");
259
259
  else this.#claim.conceal("drag");
260
- this.#host.holdChanged();
260
+ this.#host.scrubChanged();
261
261
  this.#showRevealed();
262
262
  }
263
263
  #reset() {
@@ -273,7 +273,7 @@ var ValueGestures = class {
273
273
  if (this.#keyScrub?.key !== key) {
274
274
  this.#endKeyScrub();
275
275
  this.#keyScrub = {
276
- fromValue: host.value(),
276
+ fromValue: host.model.value,
277
277
  key
278
278
  };
279
279
  }
@@ -290,14 +290,14 @@ var ValueGestures = class {
290
290
  const host = this.#host;
291
291
  const target = host.springTarget();
292
292
  if (target === void 0) return;
293
- if (host.mapping().snap(target) !== host.value()) host.toggleState("springing", true);
293
+ if (host.model.mapping().snap(target) !== host.model.value) host.toggleState("springing", true);
294
294
  this.#commit(target);
295
295
  }
296
296
  #startDrag(axis, event) {
297
297
  const host = this.#host;
298
- const mapping = host.mapping();
298
+ const mapping = host.model.mapping();
299
299
  const detent = host.detent();
300
- const value = host.value();
300
+ const value = host.model.value;
301
301
  const start = {
302
302
  from: value,
303
303
  position: axis.position(event),
@@ -316,7 +316,7 @@ var ValueGestures = class {
316
316
  });
317
317
  }
318
318
  #withinLimit(mapping, state) {
319
- const limit = this.#host.limit();
319
+ const limit = this.#host.model.limit();
320
320
  if (!limit || mapping.isWrapping) return state;
321
321
  const [low, high] = limit;
322
322
  return {
@@ -1,6 +1,7 @@
1
1
  import { SonicValueElement } from "./value-element.js";
2
2
  import { bindSurface } from "../lib/canvas-surface.js";
3
- import { bindFill } from "../lib/fill-watch.js";
3
+ import { formatClock, parseClock } from "../lib/clock.js";
4
+ import { spokenDuration } from "../lib/spoken-duration.js";
4
5
 
5
6
  //#region src/elements/wave-element.ts
6
7
  function markerKey(marker) {
@@ -31,13 +32,17 @@ var SonicWaveElement = class extends SonicValueElement {
31
32
  this.renderMarkers();
32
33
  this.render();
33
34
  }
35
+ notation = {
36
+ format: formatClock,
37
+ parse: parseClock,
38
+ speak: (seconds) => spokenDuration(seconds, this.closest("[lang]")?.lang)
39
+ };
34
40
  #current;
35
41
  #markers = [];
36
42
  #surface;
37
- #syncFill;
38
43
  attributeChangedCallback(name) {
39
44
  super.attributeChangedCallback(name);
40
- if (name === "fill") this.#syncFill?.(this.fill);
45
+ if (name === "fill") this.#surface?.setFill(this.fill);
41
46
  }
42
47
  connectedCallback() {
43
48
  this.upgradeProperties("fill", "markers");
@@ -53,6 +58,10 @@ var SonicWaveElement = class extends SonicValueElement {
53
58
  this.#surface = bindSurface({
54
59
  canvas: this.canvas,
55
60
  colours: this.colours,
61
+ fill: {
62
+ control,
63
+ sizeProperty: this.sizeProperty
64
+ },
56
65
  lengths: this.lengths,
57
66
  paint: (context, frame) => {
58
67
  this.paint(context, frame);
@@ -63,27 +72,11 @@ var SonicWaveElement = class extends SonicValueElement {
63
72
  signal
64
73
  });
65
74
  this.render();
66
- this.checkStyles(control, this.sheet, [
67
- "cap",
68
- "glass",
69
- "pane",
70
- "well",
71
- "readout",
72
- "value"
73
- ]);
74
- this.#syncFill = bindFill(control, this.sizeProperty, signal);
75
- this.#syncFill(this.fill);
75
+ this.#surface.setFill(this.fill);
76
76
  }
77
77
  focusTarget() {
78
78
  return this.control;
79
79
  }
80
- heldWrite(next) {
81
- super.heldWrite(next);
82
- this.render();
83
- }
84
- holdChanged() {
85
- this.render();
86
- }
87
80
  markerList() {
88
81
  return this.#markers;
89
82
  }
@@ -101,6 +94,9 @@ var SonicWaveElement = class extends SonicValueElement {
101
94
  renderEmpty() {
102
95
  this.toggleState("empty", this.isEmpty());
103
96
  }
97
+ scrubChanged() {
98
+ this.render();
99
+ }
104
100
  showCurrentMarker(seconds) {
105
101
  const current = this.#markers.findLast((marker) => marker.start <= seconds);
106
102
  if (markerKey(current) === markerKey(this.#current)) return;
@@ -1,36 +1,19 @@
1
1
  import { clamp, roundTo } from "../lib/math.js";
2
2
  import { requireChild, template } from "../lib/render.js";
3
- import { capturePointer } from "../lib/pointer-drag.js";
4
3
  import { dueRegions, readRegions } from "../lib/time-regions.js";
5
4
  import { SonicWaveElement } from "./wave-element.js";
6
5
  import { frameTimeline } from "../lib/frame-timeline.js";
7
6
  import { isKind, writeKind } from "../lib/marker-band.js";
8
7
  import { createLabelRider } from "../lib/marker-rider.js";
8
+ import { createPeaksRequest } from "../lib/peaks-request.js";
9
9
  import { createTrackingClock } from "../lib/tracking-clock.js";
10
- import { waveformBuckets } from "../lib/waveform-buckets.js";
11
- import { pinchFactor, wheelFactor, zoomKeyFactor } from "../lib/zoom-gesture.js";
10
+ import { paintWaveform, waveformColours } from "../lib/waveform-scene.js";
11
+ import { bindZoom, bindZoomKeys } from "../lib/zoom-gesture.js";
12
12
 
13
13
  //#region src/elements/waveform.ts
14
- const colours = {
15
- ends: "--_sonic-waveform-ends",
16
- grid: "--_sonic-waveform-grid",
17
- marker: "--_sonic-waveform-marker",
18
- placeholder: "--_sonic-waveform-placeholder",
19
- wave: "--_sonic-waveform-wave",
20
- waveEdge: "--_sonic-waveform-wave-edge"
21
- };
22
14
  const defaultZoom = 70;
23
15
  const defaultZoomMin = 20;
24
16
  const defaultZoomMax = 280;
25
- const zoomKeys = /* @__PURE__ */ new Map([
26
- ["+", 1],
27
- ["-", -1],
28
- ["=", 1]
29
- ]);
30
- const amplitudeMargin = .94;
31
- const hatchPitchPx = 8;
32
- const placeholderWavelengthPx = 48;
33
- const placeholderAmplitude = .2;
34
17
  const renderWaveform = template(`
35
18
  <div class="sonic-waveform" role="slider" tabindex="0">
36
19
  <canvas class="sonic-waveform-canvas" aria-hidden="true"></canvas>
@@ -59,66 +42,6 @@ function placeLine(line, at) {
59
42
  const translate = `${String(at * 100)}cqi`;
60
43
  if (line.style.getPropertyValue("translate") !== translate) line.style.setProperty("translate", translate);
61
44
  }
62
- function paintHatch(context, scene, [from, to]) {
63
- if (to <= from) return;
64
- const { dpr, height } = scene;
65
- const pitch = Math.max(2, Math.round(hatchPitchPx * dpr));
66
- const offset = scene.startSeconds * scene.pixelsPerSecond;
67
- context.save();
68
- context.beginPath();
69
- context.rect(from, 0, to - from, height);
70
- context.clip();
71
- context.beginPath();
72
- for (let x = Math.floor((from - height + offset) / pitch) * pitch - offset; x < to; x += pitch) {
73
- context.moveTo(x, height);
74
- context.lineTo(x + height, 0);
75
- }
76
- context.lineWidth = Math.max(1, Math.round(dpr));
77
- context.strokeStyle = scene.colours.ends;
78
- context.stroke();
79
- context.restore();
80
- }
81
- function paintPlaceholders(context, scene) {
82
- if (scene.pending.length === 0) return;
83
- const { dpr, height, pixelsPerSecond, startSeconds } = scene;
84
- const wavelength = placeholderWavelengthPx * dpr;
85
- const center = height / 2;
86
- const origin = startSeconds * pixelsPerSecond;
87
- const waveY = (x) => center - Math.sin(((x + origin) / wavelength + scene.phase) * 2 * Math.PI) * center * placeholderAmplitude;
88
- context.beginPath();
89
- for (const [from, to] of scene.pending) {
90
- const opening = (from - startSeconds) * pixelsPerSecond;
91
- const closing = (to - startSeconds) * pixelsPerSecond;
92
- context.moveTo(opening, waveY(opening));
93
- for (let x = opening + dpr; x < closing; x += dpr) context.lineTo(x, waveY(x));
94
- context.lineTo(closing, waveY(closing));
95
- }
96
- context.lineWidth = Math.max(1, Math.round(dpr));
97
- context.strokeStyle = scene.colours.placeholder;
98
- context.stroke();
99
- }
100
- function waveFill(context, scene) {
101
- const { wave, waveEdge } = scene.colours;
102
- const gradient = context.createLinearGradient(0, 0, 0, scene.height);
103
- gradient.addColorStop(0, waveEdge);
104
- gradient.addColorStop(.5, wave);
105
- gradient.addColorStop(1, waveEdge);
106
- return gradient;
107
- }
108
- function paintWave(context, scene, peaks) {
109
- const buckets = waveformBuckets(peaks, scene);
110
- const [first] = buckets;
111
- if (!first) return;
112
- const center = scene.height / 2;
113
- const amplitudePx = center * amplitudeMargin;
114
- context.fillStyle = waveFill(context, scene);
115
- context.beginPath();
116
- context.moveTo(first.x, center);
117
- for (const bucket of buckets) context.lineTo(bucket.x, center - bucket.high * amplitudePx);
118
- for (const bucket of buckets.toReversed()) context.lineTo(bucket.x, center - bucket.low * amplitudePx);
119
- context.closePath();
120
- context.fill();
121
- }
122
45
  var SonicWaveform = class extends SonicWaveElement {
123
46
  static observedAttributes = [
124
47
  ...SonicWaveElement.observedAttributes,
@@ -137,8 +60,9 @@ var SonicWaveform = class extends SonicWaveElement {
137
60
  set peaks(peaks) {
138
61
  this.#peaks = peaks;
139
62
  this.renderEmpty();
140
- this.#repaint();
63
+ this.#peaksRequest.changed();
141
64
  }
65
+ /** Spans being fetched; drawn as placeholders until cleared */
142
66
  get pending() {
143
67
  return this.#pending.map(([start, end]) => [start, end]);
144
68
  }
@@ -152,8 +76,9 @@ var SonicWaveform = class extends SonicWaveElement {
152
76
  const key = region.join(":");
153
77
  return [key, listedMs.get(key) ?? nowMs];
154
78
  }));
155
- this.#repaint();
79
+ this.#peaksRequest.changed();
156
80
  }
81
+ /** How long a span stays listed in `pending` before its placeholder draws */
157
82
  get pendingDelay() {
158
83
  return Math.max(0, this.numberAttribute("pending-delay", 0));
159
84
  }
@@ -173,6 +98,7 @@ var SonicWaveform = class extends SonicWaveElement {
173
98
  set playing(isPlaying) {
174
99
  this.reflect("playing", isPlaying);
175
100
  }
101
+ /** Read every frame for the playback time, so the waveform scrolls smoother than writes to `value` arrive */
176
102
  get readTime() {
177
103
  return this.#readTime;
178
104
  }
@@ -198,8 +124,7 @@ var SonicWaveform = class extends SonicWaveElement {
198
124
  }
199
125
  set requestPeaks(request) {
200
126
  this.#requestPeaks = request;
201
- this.#askedKey = void 0;
202
- this.surface()?.invalidate();
127
+ this.#peaksRequest.reset();
203
128
  }
204
129
  get zoom() {
205
130
  const zoom = this.numberAttribute("zoom", defaultZoom);
@@ -230,25 +155,22 @@ var SonicWaveform = class extends SonicWaveElement {
230
155
  }
231
156
  control = renderWaveform();
232
157
  canvas = requireChild(this.control, ".sonic-waveform-canvas", HTMLCanvasElement);
233
- colours = colours;
158
+ colours = waveformColours;
234
159
  lengths = {};
235
160
  sheet = "waveform.css";
236
161
  sizeProperty = "--_sonic-waveform-size";
237
- #asked = /* @__PURE__ */ new WeakSet();
238
- #askedKey;
239
162
  #clock = createTrackingClock();
240
- #connection;
241
163
  #drawn;
242
164
  #ghost = requireChild(this.control, ".sonic-waveform-ghost", HTMLDivElement);
243
165
  #grabbed;
244
- #isAsking = false;
245
- #isPinchSpent = false;
246
166
  #kindColours = /* @__PURE__ */ new Map();
247
167
  #paintedKey = "";
248
168
  #peaks;
169
+ #peaksRequest = createPeaksRequest(() => {
170
+ this.surface()?.invalidate();
171
+ });
249
172
  #pending = [];
250
173
  #pendingListedMs = /* @__PURE__ */ new Map();
251
- #pinch;
252
174
  #playhead = requireChild(this.control, ".sonic-waveform-playhead", HTMLDivElement);
253
175
  #readTime;
254
176
  #renderLabel;
@@ -260,11 +182,10 @@ var SonicWaveform = class extends SonicWaveElement {
260
182
  insetProperty: "--_sonic-waveform-label-inset",
261
183
  parkProperty: "--_sonic-waveform-label-park"
262
184
  });
263
- #touches = /* @__PURE__ */ new Map();
264
- #wheel;
185
+ #zoomGesture;
265
186
  attributeChangedCallback(name) {
266
187
  super.attributeChangedCallback(name);
267
- if (name === "zoomable") this.#bindWheel();
188
+ if (name === "zoomable") this.#zoomGesture?.sync();
268
189
  }
269
190
  connectedCallback() {
270
191
  this.upgradeProperties("peaks", "pending", "pendingDelay", "playing", "playbackRate", "readTime", "reducedMotion", "renderLabel", "requestPeaks", "zoom", "zoomable", "zoomMax", "zoomMin");
@@ -272,15 +193,20 @@ var SonicWaveform = class extends SonicWaveElement {
272
193
  }
273
194
  connect(signal) {
274
195
  super.connect(signal);
275
- this.#connection = signal;
276
- signal.addEventListener("abort", () => {
277
- this.#connection = void 0;
278
- this.#bindWheel();
279
- }, { once: true });
280
- this.#bindWheel();
281
- this.#bindPinch(signal);
196
+ const zoomTarget = {
197
+ claim: () => {
198
+ this.abandonHold();
199
+ },
200
+ isDisabled: () => this.isDisabled(),
201
+ isZoomable: () => this.zoomable,
202
+ zoom: () => this.zoom,
203
+ zoomTo: (target) => {
204
+ this.#zoomTo(target);
205
+ }
206
+ };
207
+ this.#zoomGesture = bindZoom(this.control, zoomTarget, signal);
282
208
  this.bindGestures(this.control, signal, () => this.#grab());
283
- this.#bindZoomKeys(signal);
209
+ bindZoomKeys(this.control, zoomTarget, signal);
284
210
  if (!("fonts" in document)) return;
285
211
  document.fonts.addEventListener("loadingdone", () => {
286
212
  this.renderMarkers();
@@ -298,6 +224,7 @@ var SonicWaveform = class extends SonicWaveElement {
298
224
  const held = this.#held();
299
225
  const isPaged = look.isReducedMotion && this.reducedMotion === "page";
300
226
  const pending = this.#duePending();
227
+ const range = [this.min, Math.max(this.min, this.max)];
301
228
  const timeline = frameTimeline({
302
229
  clockSeconds: this.#clock.read(frameMs, {
303
230
  isPlaying: this.playing,
@@ -310,7 +237,7 @@ var SonicWaveform = class extends SonicWaveElement {
310
237
  isPlaying: this.playing,
311
238
  isStill: look.isReducedMotion,
312
239
  pending,
313
- range: [this.min, Math.max(this.min, this.max)],
240
+ range,
314
241
  size,
315
242
  zoom: this.zoom
316
243
  });
@@ -332,11 +259,17 @@ var SonicWaveform = class extends SonicWaveElement {
332
259
  });
333
260
  if (isDirty || timeline.paintKey !== this.#paintedKey) {
334
261
  this.#paintedKey = timeline.paintKey;
335
- this.#paintScene(context, {
262
+ if (isRebuilt) this.#kindColours = this.#readKindColours();
263
+ paintWaveform(context, {
336
264
  ...view,
337
265
  colours: look.colours
338
- }, isRebuilt);
339
- if (timeline.wanted) this.#ask(timeline.wanted);
266
+ }, {
267
+ kindColours: this.#kindColours,
268
+ markers: this.markerList(),
269
+ peaks: this.#peaks,
270
+ range
271
+ });
272
+ this.#peaksRequest.ask(this.#requestPeaks, timeline.wanted);
340
273
  }
341
274
  if (timeline.isMoving) this.surface()?.requestFrame();
342
275
  }
@@ -354,95 +287,13 @@ var SonicWaveform = class extends SonicWaveElement {
354
287
  }]));
355
288
  this.surface()?.rebuild();
356
289
  }
357
- #ask(wanted) {
358
- const key = wanted.join(":");
359
- if (key === this.#askedKey) return;
360
- let asked;
361
- this.#askedKey = key;
362
- this.#isAsking = true;
363
- try {
364
- asked = this.#requestPeaks?.(...wanted);
365
- } finally {
366
- this.#isAsking = false;
367
- }
368
- const promises = asked instanceof Promise ? [asked] : asked ?? [];
369
- for (const promise of promises) {
370
- if (this.#asked.has(promise)) continue;
371
- this.#asked.add(promise);
372
- this.#repaintAfter(promise);
373
- }
374
- }
375
- #bindPinch(signal) {
376
- const control = this.control;
377
- this.#touches.clear();
378
- this.#pinch = void 0;
379
- this.#isPinchSpent = false;
380
- control.addEventListener("pointerdown", (event) => {
381
- if (event.pointerType !== "touch" || !this.zoomable || this.isDisabled()) return;
382
- this.#touches.set(event.pointerId, event.clientX);
383
- if (this.#pinch || this.#isPinchSpent) return;
384
- const pointerIds = this.#touchPair();
385
- if (!pointerIds) return;
386
- this.abandonHold();
387
- capturePointer(control, event.pointerId);
388
- this.#pinch = {
389
- pointerIds,
390
- startSpanPx: this.#spanPx(pointerIds),
391
- startZoom: this.zoom
392
- };
393
- }, { signal });
394
- control.addEventListener("pointermove", (event) => {
395
- if (!this.#touches.has(event.pointerId)) return;
396
- this.#touches.set(event.pointerId, event.clientX);
397
- const pinch = this.#pinch;
398
- if (!pinch?.pointerIds.includes(event.pointerId)) return;
399
- this.#zoomTo(pinch.startZoom * pinchFactor(pinch.startSpanPx, this.#spanPx(pinch.pointerIds)));
400
- }, { signal });
401
- for (const type of ["pointerup", "pointercancel"]) control.addEventListener(type, (event) => {
402
- if (!this.#touches.delete(event.pointerId)) return;
403
- if (this.#pinch?.pointerIds.includes(event.pointerId)) {
404
- this.#pinch = void 0;
405
- this.#isPinchSpent = true;
406
- }
407
- if (this.#touches.size === 0) this.#isPinchSpent = false;
408
- }, { signal });
409
- }
410
- #bindWheel() {
411
- this.#wheel?.abort();
412
- this.#wheel = void 0;
413
- if (!this.zoomable || !this.#connection) return;
414
- this.#wheel = new AbortController();
415
- this.control.addEventListener("wheel", (event) => {
416
- if (!event.ctrlKey && !event.metaKey || this.isDisabled()) return;
417
- event.preventDefault();
418
- this.#zoomTo(this.zoom * wheelFactor(event.deltaY, event.deltaMode));
419
- }, {
420
- passive: false,
421
- signal: this.#wheel.signal
422
- });
423
- let startZoom = this.zoom;
424
- for (const type of ["gesturestart", "gesturechange"]) this.control.addEventListener(type, (event) => {
425
- if (this.isDisabled() || this.#touches.size > 0) return;
426
- event.preventDefault();
427
- if (type === "gesturestart") startZoom = this.zoom;
428
- else if ("scale" in event && typeof event.scale === "number") this.#zoomTo(startZoom * event.scale);
429
- }, { signal: this.#wheel.signal });
430
- }
431
- #bindZoomKeys(signal) {
432
- this.control.addEventListener("keydown", (event) => {
433
- const direction = this.#zoomKey(event);
434
- if (direction === void 0) return;
435
- event.preventDefault();
436
- this.#zoomTo(this.zoom * zoomKeyFactor ** direction);
437
- }, { signal });
438
- }
439
290
  #duePending() {
440
291
  const due = dueRegions(this.#pending, this.#pendingListedMs, [performance.now(), this.pendingDelay]);
441
292
  if (due.length < this.#pending.length) this.surface()?.requestFrame();
442
293
  return due;
443
294
  }
444
295
  #grab() {
445
- if (this.#pinch || this.#isPinchSpent) return void 0;
296
+ if (this.#zoomGesture?.isPinching() === true) return void 0;
446
297
  const drawn = this.#drawn ?? {
447
298
  seconds: this.value,
448
299
  startSeconds: this.value
@@ -459,42 +310,13 @@ var SonicWaveform = class extends SonicWaveElement {
459
310
  }
460
311
  #held() {
461
312
  const grabbed = this.#grabbed;
462
- const from = this.heldFrom();
313
+ const { from } = this.scrubState();
463
314
  if (!grabbed || from === void 0) return void 0;
464
315
  return {
465
316
  grabbed,
466
317
  seconds: this.value === from ? grabbed.seconds : this.value
467
318
  };
468
319
  }
469
- #paintMarkers(context, scene, isRebuilt) {
470
- if (isRebuilt) this.#kindColours = this.#readKindColours();
471
- const line = Math.max(1, Math.round(scene.dpr));
472
- for (const { dimmed, kind, start } of this.markers) {
473
- const x = (start - scene.startSeconds) * scene.pixelsPerSecond;
474
- context.fillStyle = (isKind(kind) ? this.#kindColours.get(kind) : void 0) ?? scene.colours.marker;
475
- context.globalAlpha = dimmed === true ? .5 : 1;
476
- context.fillRect(x - line / 2, 0, line, scene.height);
477
- }
478
- context.globalAlpha = 1;
479
- }
480
- #paintScene(context, scene, isRebuilt) {
481
- const { dpr, height, pixelsPerSecond, startSeconds, width } = scene;
482
- const x = (seconds) => (seconds - startSeconds) * pixelsPerSecond;
483
- const { ends, grid } = scene.colours;
484
- const line = Math.max(1, Math.round(dpr));
485
- const [opening, closing] = [x(this.min), x(Math.max(this.min, this.max))];
486
- const [from, to] = [clamp(opening, 0, width), clamp(closing, 0, width)];
487
- context.clearRect(0, 0, width, height);
488
- paintHatch(context, scene, [0, from]);
489
- paintHatch(context, scene, [to, width]);
490
- context.fillStyle = grid;
491
- if (to > from) context.fillRect(from, Math.round((height - line) / 2), to - from, line);
492
- paintPlaceholders(context, scene);
493
- if (this.#peaks) paintWave(context, scene, this.#peaks);
494
- context.fillStyle = ends;
495
- for (const edge of [opening, closing]) context.fillRect(edge - line / 2, 0, line, height);
496
- this.#paintMarkers(context, scene, isRebuilt);
497
- }
498
320
  #readKindColours() {
499
321
  const kinds = [...new Set(this.markers.map(({ kind }) => kind).filter((kind) => isKind(kind)))];
500
322
  const probes = kinds.map((kind) => {
@@ -508,29 +330,8 @@ var SonicWaveform = class extends SonicWaveElement {
508
330
  for (const probe of probes) probe.remove();
509
331
  return new Map(kinds.map((kind, index) => [kind, read[index] ?? ""]));
510
332
  }
511
- #repaint() {
512
- if (!this.#isAsking) this.#askedKey = void 0;
513
- this.surface()?.invalidate();
514
- }
515
- async #repaintAfter(asked) {
516
- await Promise.allSettled([asked]);
517
- this.#repaint();
518
- }
519
333
  #sourceSeconds() {
520
- return this.#readTime?.() ?? this.playback();
521
- }
522
- #spanPx([first, second]) {
523
- return (this.#touches.get(second) ?? 0) - (this.#touches.get(first) ?? 0);
524
- }
525
- #touchPair() {
526
- const [first, second, third] = this.#touches.keys();
527
- if (first === void 0 || second === void 0 || third !== void 0) return void 0;
528
- return [first, second];
529
- }
530
- #zoomKey(event) {
531
- if (!this.zoomable || this.isDisabled() || event.target !== this.control) return void 0;
532
- if (event.ctrlKey || event.metaKey || event.altKey) return void 0;
533
- return zoomKeys.get(event.key);
334
+ return this.#readTime?.() ?? this.scrubState().played;
534
335
  }
535
336
  #zoomTo(target) {
536
337
  const next = roundTo(clamp(target, this.zoomMin, this.zoomMax), 2);