@xsynaptic/sonic-ui 0.16.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 (128) hide show
  1. package/CHANGELOG.md +20 -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 +64 -249
  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/marker-rider.js +37 -14
  27. package/dist/default/lib/peaks-request.js +44 -0
  28. package/dist/default/lib/positions.js +58 -0
  29. package/dist/default/lib/scrub.js +12 -0
  30. package/dist/default/lib/value-model.js +62 -0
  31. package/dist/default/lib/waveform-scene.js +108 -0
  32. package/dist/default/lib/zoom-gesture.js +97 -1
  33. package/dist/dev/dat.d.ts +1 -0
  34. package/dist/dev/dat.js +1 -0
  35. package/dist/dev/dat.js.map +1 -1
  36. package/dist/dev/elements/button.d.ts +2 -0
  37. package/dist/dev/elements/button.js +2 -0
  38. package/dist/dev/elements/button.js.map +1 -1
  39. package/dist/dev/elements/dial.d.ts +2 -0
  40. package/dist/dev/elements/dial.js +2 -0
  41. package/dist/dev/elements/dial.js.map +1 -1
  42. package/dist/dev/elements/display-element.d.ts +5 -3
  43. package/dist/dev/elements/display-element.js +9 -7
  44. package/dist/dev/elements/display-element.js.map +1 -1
  45. package/dist/dev/elements/envelope.js +31 -72
  46. package/dist/dev/elements/envelope.js.map +1 -1
  47. package/dist/dev/elements/field-gesture.js +82 -0
  48. package/dist/dev/elements/field-gesture.js.map +1 -0
  49. package/dist/dev/elements/meter.d.ts +10 -5
  50. package/dist/dev/elements/meter.js +4 -0
  51. package/dist/dev/elements/meter.js.map +1 -1
  52. package/dist/dev/elements/position-group.d.ts +12 -6
  53. package/dist/dev/elements/position-group.js +42 -26
  54. package/dist/dev/elements/position-group.js.map +1 -1
  55. package/dist/dev/elements/radio-group.d.ts +11 -7
  56. package/dist/dev/elements/radio-group.js +59 -56
  57. package/dist/dev/elements/radio-group.js.map +1 -1
  58. package/dist/dev/elements/scale.js +3 -3
  59. package/dist/dev/elements/scale.js.map +1 -1
  60. package/dist/dev/elements/slider.d.ts +4 -2
  61. package/dist/dev/elements/slider.js +11 -13
  62. package/dist/dev/elements/slider.js.map +1 -1
  63. package/dist/dev/elements/split.d.ts +1 -0
  64. package/dist/dev/elements/split.js +14 -13
  65. package/dist/dev/elements/split.js.map +1 -1
  66. package/dist/dev/elements/switch.d.ts +2 -2
  67. package/dist/dev/elements/switch.js +31 -46
  68. package/dist/dev/elements/switch.js.map +1 -1
  69. package/dist/dev/elements/toggle.js +20 -44
  70. package/dist/dev/elements/toggle.js.map +1 -1
  71. package/dist/dev/elements/value-element.d.ts +5 -5
  72. package/dist/dev/elements/value-element.js +65 -91
  73. package/dist/dev/elements/value-element.js.map +1 -1
  74. package/dist/dev/elements/value-gestures.js +22 -22
  75. package/dist/dev/elements/value-gestures.js.map +1 -1
  76. package/dist/dev/elements/wave-element.d.ts +7 -4
  77. package/dist/dev/elements/wave-element.js +9 -12
  78. package/dist/dev/elements/wave-element.js.map +1 -1
  79. package/dist/dev/elements/waveform.d.ts +23 -22
  80. package/dist/dev/elements/waveform.js +64 -249
  81. package/dist/dev/elements/waveform.js.map +1 -1
  82. package/dist/dev/elements/wavestrip.d.ts +5 -1
  83. package/dist/dev/elements/wavestrip.js +10 -5
  84. package/dist/dev/elements/wavestrip.js.map +1 -1
  85. package/dist/dev/elements/xy.js +67 -112
  86. package/dist/dev/elements/xy.js.map +1 -1
  87. package/dist/dev/index.d.ts +3 -2
  88. package/dist/dev/lib/canvas-surface.d.ts +1 -0
  89. package/dist/dev/lib/canvas-surface.js +25 -0
  90. package/dist/dev/lib/canvas-surface.js.map +1 -1
  91. package/dist/dev/lib/crossfade.d.ts +1 -0
  92. package/dist/dev/lib/crossfade.js +1 -0
  93. package/dist/dev/lib/crossfade.js.map +1 -1
  94. package/dist/dev/lib/envelope-curve.d.ts +1 -0
  95. package/dist/dev/lib/envelope-curve.js +1 -0
  96. package/dist/dev/lib/envelope-curve.js.map +1 -1
  97. package/dist/dev/lib/marker-rider.js +37 -14
  98. package/dist/dev/lib/marker-rider.js.map +1 -1
  99. package/dist/dev/lib/peaks-request.d.ts +5 -0
  100. package/dist/dev/lib/peaks-request.js +45 -0
  101. package/dist/dev/lib/peaks-request.js.map +1 -0
  102. package/dist/dev/lib/positions.d.ts +7 -0
  103. package/dist/dev/lib/positions.js +59 -0
  104. package/dist/dev/lib/positions.js.map +1 -0
  105. package/dist/dev/lib/scrub.d.ts +8 -0
  106. package/dist/dev/lib/scrub.js +13 -0
  107. package/dist/dev/lib/scrub.js.map +1 -0
  108. package/dist/dev/lib/time-regions.d.ts +1 -0
  109. package/dist/dev/lib/time-regions.js.map +1 -1
  110. package/dist/dev/lib/units.d.ts +8 -0
  111. package/dist/dev/lib/value-model.js +63 -0
  112. package/dist/dev/lib/value-model.js.map +1 -0
  113. package/dist/dev/lib/waveform-buckets.d.ts +1 -0
  114. package/dist/dev/lib/waveform-buckets.js.map +1 -1
  115. package/dist/dev/lib/waveform-scene.d.ts +11 -0
  116. package/dist/dev/lib/waveform-scene.js +109 -0
  117. package/dist/dev/lib/waveform-scene.js.map +1 -0
  118. package/dist/dev/lib/zoom-gesture.js +97 -1
  119. package/dist/dev/lib/zoom-gesture.js.map +1 -1
  120. package/dist/styles/button.css +16 -6
  121. package/dist/styles/waveform.css +12 -4
  122. package/package.json +1 -1
  123. package/dist/default/lib/asked-value.js +0 -21
  124. package/dist/default/lib/fill-watch.js +0 -29
  125. package/dist/dev/lib/asked-value.js +0 -22
  126. package/dist/dev/lib/asked-value.js.map +0 -1
  127. package/dist/dev/lib/fill-watch.js +0 -30
  128. 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
  }
@@ -186,13 +112,19 @@ var SonicWaveform = class extends SonicWaveElement {
186
112
  set reducedMotion(motion) {
187
113
  this.reflect("reduced-motion", motion);
188
114
  }
115
+ get renderLabel() {
116
+ return this.#renderLabel;
117
+ }
118
+ set renderLabel(render) {
119
+ this.#renderLabel = render;
120
+ if (this.isBound()) this.renderMarkers();
121
+ }
189
122
  get requestPeaks() {
190
123
  return this.#requestPeaks;
191
124
  }
192
125
  set requestPeaks(request) {
193
126
  this.#requestPeaks = request;
194
- this.#askedKey = void 0;
195
- this.surface()?.invalidate();
127
+ this.#peaksRequest.reset();
196
128
  }
197
129
  get zoom() {
198
130
  const zoom = this.numberAttribute("zoom", defaultZoom);
@@ -223,56 +155,58 @@ var SonicWaveform = class extends SonicWaveElement {
223
155
  }
224
156
  control = renderWaveform();
225
157
  canvas = requireChild(this.control, ".sonic-waveform-canvas", HTMLCanvasElement);
226
- colours = colours;
158
+ colours = waveformColours;
227
159
  lengths = {};
228
160
  sheet = "waveform.css";
229
161
  sizeProperty = "--_sonic-waveform-size";
230
- #asked = /* @__PURE__ */ new WeakSet();
231
- #askedKey;
232
162
  #clock = createTrackingClock();
233
- #connection;
234
163
  #drawn;
235
164
  #ghost = requireChild(this.control, ".sonic-waveform-ghost", HTMLDivElement);
236
165
  #grabbed;
237
- #isAsking = false;
238
- #isPinchSpent = false;
239
166
  #kindColours = /* @__PURE__ */ new Map();
240
167
  #paintedKey = "";
241
168
  #peaks;
169
+ #peaksRequest = createPeaksRequest(() => {
170
+ this.surface()?.invalidate();
171
+ });
242
172
  #pending = [];
243
173
  #pendingListedMs = /* @__PURE__ */ new Map();
244
- #pinch;
245
174
  #playhead = requireChild(this.control, ".sonic-waveform-playhead", HTMLDivElement);
246
175
  #readTime;
176
+ #renderLabel;
247
177
  #requestPeaks;
248
178
  #rider = createLabelRider(this.control, {
249
179
  className: "sonic-waveform-label",
250
180
  colourProperty: "--_sonic-waveform-marker",
251
- fadeProperty: "--_sonic-waveform-label-fade",
181
+ fadeProperties: ["--_sonic-waveform-label-fade-start", "--_sonic-waveform-label-fade-end"],
252
182
  insetProperty: "--_sonic-waveform-label-inset",
253
183
  parkProperty: "--_sonic-waveform-label-park"
254
184
  });
255
- #touches = /* @__PURE__ */ new Map();
256
- #wheel;
185
+ #zoomGesture;
257
186
  attributeChangedCallback(name) {
258
187
  super.attributeChangedCallback(name);
259
- if (name === "zoomable") this.#bindWheel();
188
+ if (name === "zoomable") this.#zoomGesture?.sync();
260
189
  }
261
190
  connectedCallback() {
262
- this.upgradeProperties("peaks", "pending", "pendingDelay", "playing", "playbackRate", "readTime", "reducedMotion", "requestPeaks", "zoom", "zoomable", "zoomMax", "zoomMin");
191
+ this.upgradeProperties("peaks", "pending", "pendingDelay", "playing", "playbackRate", "readTime", "reducedMotion", "renderLabel", "requestPeaks", "zoom", "zoomable", "zoomMax", "zoomMin");
263
192
  super.connectedCallback();
264
193
  }
265
194
  connect(signal) {
266
195
  super.connect(signal);
267
- this.#connection = signal;
268
- signal.addEventListener("abort", () => {
269
- this.#connection = void 0;
270
- this.#bindWheel();
271
- }, { once: true });
272
- this.#bindWheel();
273
- 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);
274
208
  this.bindGestures(this.control, signal, () => this.#grab());
275
- this.#bindZoomKeys(signal);
209
+ bindZoomKeys(this.control, zoomTarget, signal);
276
210
  if (!("fonts" in document)) return;
277
211
  document.fonts.addEventListener("loadingdone", () => {
278
212
  this.renderMarkers();
@@ -288,7 +222,9 @@ var SonicWaveform = class extends SonicWaveElement {
288
222
  }
289
223
  paint(context, { frameMs, isDirty, isRebuilt, look, size }) {
290
224
  const held = this.#held();
225
+ const isPaged = look.isReducedMotion && this.reducedMotion === "page";
291
226
  const pending = this.#duePending();
227
+ const range = [this.min, Math.max(this.min, this.max)];
292
228
  const timeline = frameTimeline({
293
229
  clockSeconds: this.#clock.read(frameMs, {
294
230
  isPlaying: this.playing,
@@ -297,11 +233,11 @@ var SonicWaveform = class extends SonicWaveElement {
297
233
  }),
298
234
  frameMs,
299
235
  ...held ? { held } : {},
300
- isPaged: look.isReducedMotion && this.reducedMotion === "page",
236
+ isPaged,
301
237
  isPlaying: this.playing,
302
238
  isStill: look.isReducedMotion,
303
239
  pending,
304
- range: [this.min, Math.max(this.min, this.max)],
240
+ range,
305
241
  size,
306
242
  zoom: this.zoom
307
243
  });
@@ -315,6 +251,7 @@ var SonicWaveform = class extends SonicWaveElement {
315
251
  this.showCurrentMarker(seconds);
316
252
  this.toggleState("pending", view.pending.length > 0);
317
253
  this.#rider.place({
254
+ isPaged,
318
255
  playheadSeconds: seconds,
319
256
  startSeconds: view.startSeconds,
320
257
  widthPx: view.width / view.dpr,
@@ -322,113 +259,41 @@ var SonicWaveform = class extends SonicWaveElement {
322
259
  });
323
260
  if (isDirty || timeline.paintKey !== this.#paintedKey) {
324
261
  this.#paintedKey = timeline.paintKey;
325
- this.#paintScene(context, {
262
+ if (isRebuilt) this.#kindColours = this.#readKindColours();
263
+ paintWaveform(context, {
326
264
  ...view,
327
265
  colours: look.colours
328
- }, isRebuilt);
329
- 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);
330
273
  }
331
274
  if (timeline.isMoving) this.surface()?.requestFrame();
332
275
  }
333
276
  renderMarkers() {
334
277
  const bounds = this.mapping().bounds;
335
- this.#rider.measure(this.markers.flatMap(({ dimmed, kind, label, start }) => label === void 0 || label === "" ? [] : [{
336
- isDimmed: dimmed === true,
337
- ...kind === void 0 ? {} : { kind },
338
- start: clamp(start, ...bounds),
339
- text: label
278
+ const render = this.#renderLabel;
279
+ this.#rider.measure(this.markers.flatMap((marker) => marker.label === void 0 || marker.label === "" ? [] : [{
280
+ isDimmed: marker.dimmed === true,
281
+ ...marker.kind === void 0 ? {} : { kind: marker.kind },
282
+ start: clamp(marker.start, ...bounds),
283
+ text: marker.label,
284
+ ...render ? { write: (element) => {
285
+ render(marker, element);
286
+ } } : {}
340
287
  }]));
341
288
  this.surface()?.rebuild();
342
289
  }
343
- #ask(wanted) {
344
- const key = wanted.join(":");
345
- if (key === this.#askedKey) return;
346
- let asked;
347
- this.#askedKey = key;
348
- this.#isAsking = true;
349
- try {
350
- asked = this.#requestPeaks?.(...wanted);
351
- } finally {
352
- this.#isAsking = false;
353
- }
354
- const promises = asked instanceof Promise ? [asked] : asked ?? [];
355
- for (const promise of promises) {
356
- if (this.#asked.has(promise)) continue;
357
- this.#asked.add(promise);
358
- this.#repaintAfter(promise);
359
- }
360
- }
361
- #bindPinch(signal) {
362
- const control = this.control;
363
- this.#touches.clear();
364
- this.#pinch = void 0;
365
- this.#isPinchSpent = false;
366
- control.addEventListener("pointerdown", (event) => {
367
- if (event.pointerType !== "touch" || !this.zoomable || this.isDisabled()) return;
368
- this.#touches.set(event.pointerId, event.clientX);
369
- if (this.#pinch || this.#isPinchSpent) return;
370
- const pointerIds = this.#touchPair();
371
- if (!pointerIds) return;
372
- this.abandonHold();
373
- capturePointer(control, event.pointerId);
374
- this.#pinch = {
375
- pointerIds,
376
- startSpanPx: this.#spanPx(pointerIds),
377
- startZoom: this.zoom
378
- };
379
- }, { signal });
380
- control.addEventListener("pointermove", (event) => {
381
- if (!this.#touches.has(event.pointerId)) return;
382
- this.#touches.set(event.pointerId, event.clientX);
383
- const pinch = this.#pinch;
384
- if (!pinch?.pointerIds.includes(event.pointerId)) return;
385
- this.#zoomTo(pinch.startZoom * pinchFactor(pinch.startSpanPx, this.#spanPx(pinch.pointerIds)));
386
- }, { signal });
387
- for (const type of ["pointerup", "pointercancel"]) control.addEventListener(type, (event) => {
388
- if (!this.#touches.delete(event.pointerId)) return;
389
- if (this.#pinch?.pointerIds.includes(event.pointerId)) {
390
- this.#pinch = void 0;
391
- this.#isPinchSpent = true;
392
- }
393
- if (this.#touches.size === 0) this.#isPinchSpent = false;
394
- }, { signal });
395
- }
396
- #bindWheel() {
397
- this.#wheel?.abort();
398
- this.#wheel = void 0;
399
- if (!this.zoomable || !this.#connection) return;
400
- this.#wheel = new AbortController();
401
- this.control.addEventListener("wheel", (event) => {
402
- if (!event.ctrlKey && !event.metaKey || this.isDisabled()) return;
403
- event.preventDefault();
404
- this.#zoomTo(this.zoom * wheelFactor(event.deltaY, event.deltaMode));
405
- }, {
406
- passive: false,
407
- signal: this.#wheel.signal
408
- });
409
- let startZoom = this.zoom;
410
- for (const type of ["gesturestart", "gesturechange"]) this.control.addEventListener(type, (event) => {
411
- if (this.isDisabled() || this.#touches.size > 0) return;
412
- event.preventDefault();
413
- if (type === "gesturestart") startZoom = this.zoom;
414
- else if ("scale" in event && typeof event.scale === "number") this.#zoomTo(startZoom * event.scale);
415
- }, { signal: this.#wheel.signal });
416
- }
417
- #bindZoomKeys(signal) {
418
- this.control.addEventListener("keydown", (event) => {
419
- const direction = this.#zoomKey(event);
420
- if (direction === void 0) return;
421
- event.preventDefault();
422
- this.#zoomTo(this.zoom * zoomKeyFactor ** direction);
423
- }, { signal });
424
- }
425
290
  #duePending() {
426
291
  const due = dueRegions(this.#pending, this.#pendingListedMs, [performance.now(), this.pendingDelay]);
427
292
  if (due.length < this.#pending.length) this.surface()?.requestFrame();
428
293
  return due;
429
294
  }
430
295
  #grab() {
431
- if (this.#pinch || this.#isPinchSpent) return void 0;
296
+ if (this.#zoomGesture?.isPinching() === true) return void 0;
432
297
  const drawn = this.#drawn ?? {
433
298
  seconds: this.value,
434
299
  startSeconds: this.value
@@ -445,42 +310,13 @@ var SonicWaveform = class extends SonicWaveElement {
445
310
  }
446
311
  #held() {
447
312
  const grabbed = this.#grabbed;
448
- const from = this.heldFrom();
313
+ const { from } = this.scrubState();
449
314
  if (!grabbed || from === void 0) return void 0;
450
315
  return {
451
316
  grabbed,
452
317
  seconds: this.value === from ? grabbed.seconds : this.value
453
318
  };
454
319
  }
455
- #paintMarkers(context, scene, isRebuilt) {
456
- if (isRebuilt) this.#kindColours = this.#readKindColours();
457
- const line = Math.max(1, Math.round(scene.dpr));
458
- for (const { dimmed, kind, start } of this.markers) {
459
- const x = (start - scene.startSeconds) * scene.pixelsPerSecond;
460
- context.fillStyle = (isKind(kind) ? this.#kindColours.get(kind) : void 0) ?? scene.colours.marker;
461
- context.globalAlpha = dimmed === true ? .5 : 1;
462
- context.fillRect(x - line / 2, 0, line, scene.height);
463
- }
464
- context.globalAlpha = 1;
465
- }
466
- #paintScene(context, scene, isRebuilt) {
467
- const { dpr, height, pixelsPerSecond, startSeconds, width } = scene;
468
- const x = (seconds) => (seconds - startSeconds) * pixelsPerSecond;
469
- const { ends, grid } = scene.colours;
470
- const line = Math.max(1, Math.round(dpr));
471
- const [opening, closing] = [x(this.min), x(Math.max(this.min, this.max))];
472
- const [from, to] = [clamp(opening, 0, width), clamp(closing, 0, width)];
473
- context.clearRect(0, 0, width, height);
474
- paintHatch(context, scene, [0, from]);
475
- paintHatch(context, scene, [to, width]);
476
- context.fillStyle = grid;
477
- if (to > from) context.fillRect(from, Math.round((height - line) / 2), to - from, line);
478
- paintPlaceholders(context, scene);
479
- if (this.#peaks) paintWave(context, scene, this.#peaks);
480
- context.fillStyle = ends;
481
- for (const edge of [opening, closing]) context.fillRect(edge - line / 2, 0, line, height);
482
- this.#paintMarkers(context, scene, isRebuilt);
483
- }
484
320
  #readKindColours() {
485
321
  const kinds = [...new Set(this.markers.map(({ kind }) => kind).filter((kind) => isKind(kind)))];
486
322
  const probes = kinds.map((kind) => {
@@ -494,29 +330,8 @@ var SonicWaveform = class extends SonicWaveElement {
494
330
  for (const probe of probes) probe.remove();
495
331
  return new Map(kinds.map((kind, index) => [kind, read[index] ?? ""]));
496
332
  }
497
- #repaint() {
498
- if (!this.#isAsking) this.#askedKey = void 0;
499
- this.surface()?.invalidate();
500
- }
501
- async #repaintAfter(asked) {
502
- await Promise.allSettled([asked]);
503
- this.#repaint();
504
- }
505
333
  #sourceSeconds() {
506
- return this.#readTime?.() ?? this.playback();
507
- }
508
- #spanPx([first, second]) {
509
- return (this.#touches.get(second) ?? 0) - (this.#touches.get(first) ?? 0);
510
- }
511
- #touchPair() {
512
- const [first, second, third] = this.#touches.keys();
513
- if (first === void 0 || second === void 0 || third !== void 0) return void 0;
514
- return [first, second];
515
- }
516
- #zoomKey(event) {
517
- if (!this.zoomable || this.isDisabled() || event.target !== this.control) return void 0;
518
- if (event.ctrlKey || event.metaKey || event.altKey) return void 0;
519
- return zoomKeys.get(event.key);
334
+ return this.#readTime?.() ?? this.scrubState().played;
520
335
  }
521
336
  #zoomTo(target) {
522
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
  };