@xsynaptic/sonic-ui 0.17.0 → 0.18.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 (125) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +112 -19
  3. package/dist/default/dat.js +1 -0
  4. package/dist/default/elements/button.js +2 -0
  5. package/dist/default/elements/dial.js +2 -0
  6. package/dist/default/elements/display-element.js +9 -7
  7. package/dist/default/elements/envelope.js +31 -72
  8. package/dist/default/elements/field-gesture.js +81 -0
  9. package/dist/default/elements/meter.js +4 -0
  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/slider.js +11 -13
  14. package/dist/default/elements/split.js +14 -13
  15. package/dist/default/elements/switch.js +31 -46
  16. package/dist/default/elements/toggle.js +20 -44
  17. package/dist/default/elements/value-element.js +65 -91
  18. package/dist/default/elements/value-gestures.js +22 -22
  19. package/dist/default/elements/wave-element.js +9 -12
  20. package/dist/default/elements/waveform.js +42 -241
  21. package/dist/default/elements/wavestrip.js +10 -5
  22. package/dist/default/elements/xy.js +67 -112
  23. package/dist/default/lib/canvas-surface.js +25 -0
  24. package/dist/default/lib/crossfade.js +1 -0
  25. package/dist/default/lib/envelope-curve.js +1 -0
  26. package/dist/default/lib/peaks-request.js +44 -0
  27. package/dist/default/lib/positions.js +58 -0
  28. package/dist/default/lib/scrub.js +12 -0
  29. package/dist/default/lib/value-model.js +62 -0
  30. package/dist/default/lib/waveform-scene.js +108 -0
  31. package/dist/default/lib/zoom-gesture.js +97 -1
  32. package/dist/dev/dat.d.ts +1 -0
  33. package/dist/dev/dat.js +1 -0
  34. package/dist/dev/dat.js.map +1 -1
  35. package/dist/dev/elements/button.d.ts +2 -0
  36. package/dist/dev/elements/button.js +2 -0
  37. package/dist/dev/elements/button.js.map +1 -1
  38. package/dist/dev/elements/dial.d.ts +2 -0
  39. package/dist/dev/elements/dial.js +2 -0
  40. package/dist/dev/elements/dial.js.map +1 -1
  41. package/dist/dev/elements/display-element.d.ts +5 -3
  42. package/dist/dev/elements/display-element.js +9 -7
  43. package/dist/dev/elements/display-element.js.map +1 -1
  44. package/dist/dev/elements/envelope.js +31 -72
  45. package/dist/dev/elements/envelope.js.map +1 -1
  46. package/dist/dev/elements/field-gesture.js +82 -0
  47. package/dist/dev/elements/field-gesture.js.map +1 -0
  48. package/dist/dev/elements/meter.d.ts +10 -5
  49. package/dist/dev/elements/meter.js +4 -0
  50. package/dist/dev/elements/meter.js.map +1 -1
  51. package/dist/dev/elements/position-group.d.ts +12 -6
  52. package/dist/dev/elements/position-group.js +42 -26
  53. package/dist/dev/elements/position-group.js.map +1 -1
  54. package/dist/dev/elements/radio-group.d.ts +11 -7
  55. package/dist/dev/elements/radio-group.js +59 -56
  56. package/dist/dev/elements/radio-group.js.map +1 -1
  57. package/dist/dev/elements/scale.js +3 -3
  58. package/dist/dev/elements/scale.js.map +1 -1
  59. package/dist/dev/elements/slider.d.ts +4 -2
  60. package/dist/dev/elements/slider.js +11 -13
  61. package/dist/dev/elements/slider.js.map +1 -1
  62. package/dist/dev/elements/split.d.ts +1 -0
  63. package/dist/dev/elements/split.js +14 -13
  64. package/dist/dev/elements/split.js.map +1 -1
  65. package/dist/dev/elements/switch.d.ts +2 -2
  66. package/dist/dev/elements/switch.js +31 -46
  67. package/dist/dev/elements/switch.js.map +1 -1
  68. package/dist/dev/elements/toggle.js +20 -44
  69. package/dist/dev/elements/toggle.js.map +1 -1
  70. package/dist/dev/elements/value-element.d.ts +5 -5
  71. package/dist/dev/elements/value-element.js +65 -91
  72. package/dist/dev/elements/value-element.js.map +1 -1
  73. package/dist/dev/elements/value-gestures.js +22 -22
  74. package/dist/dev/elements/value-gestures.js.map +1 -1
  75. package/dist/dev/elements/wave-element.d.ts +6 -4
  76. package/dist/dev/elements/wave-element.js +9 -12
  77. package/dist/dev/elements/wave-element.js.map +1 -1
  78. package/dist/dev/elements/waveform.d.ts +19 -21
  79. package/dist/dev/elements/waveform.js +42 -241
  80. package/dist/dev/elements/waveform.js.map +1 -1
  81. package/dist/dev/elements/wavestrip.d.ts +5 -1
  82. package/dist/dev/elements/wavestrip.js +10 -5
  83. package/dist/dev/elements/wavestrip.js.map +1 -1
  84. package/dist/dev/elements/xy.js +67 -112
  85. package/dist/dev/elements/xy.js.map +1 -1
  86. package/dist/dev/index.d.ts +2 -1
  87. package/dist/dev/lib/canvas-surface.d.ts +1 -0
  88. package/dist/dev/lib/canvas-surface.js +25 -0
  89. package/dist/dev/lib/canvas-surface.js.map +1 -1
  90. package/dist/dev/lib/crossfade.d.ts +1 -0
  91. package/dist/dev/lib/crossfade.js +1 -0
  92. package/dist/dev/lib/crossfade.js.map +1 -1
  93. package/dist/dev/lib/envelope-curve.d.ts +1 -0
  94. package/dist/dev/lib/envelope-curve.js +1 -0
  95. package/dist/dev/lib/envelope-curve.js.map +1 -1
  96. package/dist/dev/lib/peaks-request.d.ts +5 -0
  97. package/dist/dev/lib/peaks-request.js +45 -0
  98. package/dist/dev/lib/peaks-request.js.map +1 -0
  99. package/dist/dev/lib/positions.d.ts +7 -0
  100. package/dist/dev/lib/positions.js +59 -0
  101. package/dist/dev/lib/positions.js.map +1 -0
  102. package/dist/dev/lib/scrub.d.ts +8 -0
  103. package/dist/dev/lib/scrub.js +13 -0
  104. package/dist/dev/lib/scrub.js.map +1 -0
  105. package/dist/dev/lib/time-regions.d.ts +1 -0
  106. package/dist/dev/lib/time-regions.js.map +1 -1
  107. package/dist/dev/lib/units.d.ts +8 -0
  108. package/dist/dev/lib/value-model.js +63 -0
  109. package/dist/dev/lib/value-model.js.map +1 -0
  110. package/dist/dev/lib/waveform-buckets.d.ts +1 -0
  111. package/dist/dev/lib/waveform-buckets.js.map +1 -1
  112. package/dist/dev/lib/waveform-scene.d.ts +11 -0
  113. package/dist/dev/lib/waveform-scene.js +109 -0
  114. package/dist/dev/lib/waveform-scene.js.map +1 -0
  115. package/dist/dev/lib/zoom-gesture.js +97 -1
  116. package/dist/dev/lib/zoom-gesture.js.map +1 -1
  117. package/dist/styles/button.css +16 -6
  118. package/dist/styles/waveform.css +2 -1
  119. package/package.json +1 -1
  120. package/dist/default/lib/asked-value.js +0 -21
  121. package/dist/default/lib/fill-watch.js +0 -29
  122. package/dist/dev/lib/asked-value.js +0 -22
  123. package/dist/dev/lib/asked-value.js.map +0 -1
  124. package/dist/dev/lib/fill-watch.js +0 -30
  125. package/dist/dev/lib/fill-watch.js.map +0 -1
@@ -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);
@@ -1,6 +1,7 @@
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 { scrubRegions } from "../lib/scrub.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";
@@ -48,6 +49,7 @@ function fillRegion(context, path, [from, to, style]) {
48
49
  context.restore();
49
50
  }
50
51
  var SonicWavestrip = class extends SonicWaveElement {
52
+ /** Loaded spans, in seconds */
51
53
  get buffered() {
52
54
  return this.#buffered.map(([start, end]) => [start, end]);
53
55
  }
@@ -61,6 +63,7 @@ var SonicWavestrip = class extends SonicWaveElement {
61
63
  set cancellable(isCancellable) {
62
64
  this.reflect("cancellable", isCancellable);
63
65
  }
66
+ /** One amplitude from 0 to 1 per slice of the whole track, any length; resampled to the bars and never normalised */
64
67
  get peaks() {
65
68
  return this.#peaks;
66
69
  }
@@ -80,6 +83,7 @@ var SonicWavestrip = class extends SonicWaveElement {
80
83
  #markerBand = createMarkerBand(requireChild(this.control, ".sonic-wavestrip-markers", HTMLDivElement));
81
84
  #paintedKey = "";
82
85
  #peaks;
86
+ /** Viewport x of a time, clamped to the strip */
83
87
  clientXOf(value) {
84
88
  const { startPx, travelPx } = this.#axis();
85
89
  return startPx + this.mapping().proportionOf(value) * travelPx;
@@ -107,7 +111,7 @@ var SonicWavestrip = class extends SonicWaveElement {
107
111
  draw() {
108
112
  const surface = this.surface();
109
113
  this.#placeReadout();
110
- this.showCurrentMarker(this.playback());
114
+ this.showCurrentMarker(this.scrubState().played);
111
115
  if (surface && this.#stripRegions(surface.size.width).key !== this.#paintedKey) surface.requestFrame();
112
116
  }
113
117
  isEmpty() {
@@ -189,12 +193,13 @@ var SonicWavestrip = class extends SonicWaveElement {
189
193
  this.control.style.setProperty("--_sonic-wavestrip-readout-at", String(at));
190
194
  }
191
195
  #stripRegions(width) {
192
- const { proportionOf } = this.mapping();
193
- const isScrubbing = this.heldFrom() !== void 0 && this.isRevealed();
196
+ const mapping = this.mapping();
197
+ const { proportionOf } = mapping;
198
+ const { played, scrub } = scrubRegions(this.scrubState(), this.value, mapping);
194
199
  return stripRegions({
195
200
  buffered: this.#buffered.map(([start, end]) => [proportionOf(start), proportionOf(end)]),
196
- played: proportionOf(this.playback()),
197
- ...isScrubbing ? { scrub: proportionOf(this.value) } : {}
201
+ played,
202
+ ...scrub === void 0 ? {} : { scrub }
198
203
  }, width);
199
204
  }
200
205
  };