@xsynaptic/sonic-ui 0.24.1 → 0.25.1

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 (104) hide show
  1. package/CHANGELOG.md +28 -2
  2. package/README.md +5 -3
  3. package/dist/default/define/region.js +7 -0
  4. package/dist/default/define.js +1 -0
  5. package/dist/default/elements/envelope.js +11 -11
  6. package/dist/default/elements/form-element.js +10 -0
  7. package/dist/default/elements/readout.js +1 -1
  8. package/dist/default/elements/region-layer.js +251 -0
  9. package/dist/default/elements/region.js +157 -0
  10. package/dist/default/elements/sonic-element.js +3 -0
  11. package/dist/default/elements/split.js +2 -2
  12. package/dist/default/elements/toggle.js +2 -2
  13. package/dist/default/elements/value-element.js +7 -3
  14. package/dist/default/elements/value-gestures.js +7 -5
  15. package/dist/default/elements/wave-element.js +12 -3
  16. package/dist/default/elements/waveform.js +38 -2
  17. package/dist/default/elements/wavestrip.js +90 -17
  18. package/dist/default/elements/xy.js +3 -11
  19. package/dist/default/index.js +2 -1
  20. package/dist/default/lib/clock.js +1 -0
  21. package/dist/default/lib/drag-step.js +1 -1
  22. package/dist/default/lib/marker-band.js +40 -29
  23. package/dist/default/lib/region-drag.js +48 -0
  24. package/dist/dev/define/region.d.ts +2 -0
  25. package/dist/dev/define/region.js +8 -0
  26. package/dist/dev/define/region.js.map +1 -0
  27. package/dist/dev/define.d.ts +3 -1
  28. package/dist/dev/define.js +1 -0
  29. package/dist/dev/elements/envelope.js +11 -11
  30. package/dist/dev/elements/envelope.js.map +1 -1
  31. package/dist/dev/elements/form-element.d.ts +1 -0
  32. package/dist/dev/elements/form-element.js +10 -0
  33. package/dist/dev/elements/form-element.js.map +1 -1
  34. package/dist/dev/elements/readout.js +1 -1
  35. package/dist/dev/elements/readout.js.map +1 -1
  36. package/dist/dev/elements/region-layer.js +252 -0
  37. package/dist/dev/elements/region-layer.js.map +1 -0
  38. package/dist/dev/elements/region.d.ts +29 -0
  39. package/dist/dev/elements/region.js +165 -0
  40. package/dist/dev/elements/region.js.map +1 -0
  41. package/dist/dev/elements/sonic-element.d.ts +1 -0
  42. package/dist/dev/elements/sonic-element.js +4 -1
  43. package/dist/dev/elements/sonic-element.js.map +1 -1
  44. package/dist/dev/elements/split.js +2 -2
  45. package/dist/dev/elements/split.js.map +1 -1
  46. package/dist/dev/elements/toggle.js +2 -2
  47. package/dist/dev/elements/toggle.js.map +1 -1
  48. package/dist/dev/elements/value-element.d.ts +3 -1
  49. package/dist/dev/elements/value-element.js +7 -3
  50. package/dist/dev/elements/value-element.js.map +1 -1
  51. package/dist/dev/elements/value-gestures.js +7 -5
  52. package/dist/dev/elements/value-gestures.js.map +1 -1
  53. package/dist/dev/elements/wave-element.d.ts +7 -1
  54. package/dist/dev/elements/wave-element.js +12 -3
  55. package/dist/dev/elements/wave-element.js.map +1 -1
  56. package/dist/dev/elements/waveform.d.ts +7 -0
  57. package/dist/dev/elements/waveform.js +38 -2
  58. package/dist/dev/elements/waveform.js.map +1 -1
  59. package/dist/dev/elements/wavestrip.d.ts +17 -1
  60. package/dist/dev/elements/wavestrip.js +90 -17
  61. package/dist/dev/elements/wavestrip.js.map +1 -1
  62. package/dist/dev/elements/xy.js +3 -11
  63. package/dist/dev/elements/xy.js.map +1 -1
  64. package/dist/dev/index.d.ts +3 -2
  65. package/dist/dev/index.js +2 -1
  66. package/dist/dev/lib/clock.js +1 -0
  67. package/dist/dev/lib/clock.js.map +1 -1
  68. package/dist/dev/lib/drag-step.js +1 -1
  69. package/dist/dev/lib/drag-step.js.map +1 -1
  70. package/dist/dev/lib/marker-band.d.ts +7 -0
  71. package/dist/dev/lib/marker-band.js +40 -29
  72. package/dist/dev/lib/marker-band.js.map +1 -1
  73. package/dist/dev/lib/region-drag.js +49 -0
  74. package/dist/dev/lib/region-drag.js.map +1 -0
  75. package/dist/styles/button.css +12 -4
  76. package/dist/styles/controls.css +1 -0
  77. package/dist/styles/dial.css +35 -18
  78. package/dist/styles/envelope.css +6 -5
  79. package/dist/styles/material/arc.css +2 -2
  80. package/dist/styles/material/cap.css +21 -11
  81. package/dist/styles/material/glass.css +1 -1
  82. package/dist/styles/material/groove.css +3 -3
  83. package/dist/styles/material/keycap.css +2 -2
  84. package/dist/styles/material/lens.css +1 -1
  85. package/dist/styles/material/pane.css +13 -5
  86. package/dist/styles/material/readout.css +4 -3
  87. package/dist/styles/material/{bracket.css → reticle.css} +15 -8
  88. package/dist/styles/material/well.css +2 -2
  89. package/dist/styles/material.css +1 -1
  90. package/dist/styles/meter.css +18 -5
  91. package/dist/styles/number.css +2 -1
  92. package/dist/styles/panel.css +3 -3
  93. package/dist/styles/region.css +88 -0
  94. package/dist/styles/ring.css +2 -1
  95. package/dist/styles/screen.css +7 -5
  96. package/dist/styles/segmented.css +2 -1
  97. package/dist/styles/slider.css +12 -3
  98. package/dist/styles/spectrum.css +9 -1
  99. package/dist/styles/switch.css +3 -2
  100. package/dist/styles/toggle.css +2 -1
  101. package/dist/styles/waveform.css +3 -2
  102. package/dist/styles/wavestrip.css +11 -8
  103. package/dist/styles/xy.css +9 -8
  104. package/package.json +3 -2
@@ -286,16 +286,17 @@ var SonicValueElement = class extends SonicFormElement {
286
286
  toggleState: (state, isOn) => {
287
287
  this.toggleState(state, isOn);
288
288
  }
289
- }, control);
289
+ }, control, this.focusTarget());
290
290
  this.#gestures.bind(signal, grab);
291
291
  control.addEventListener("pointerup", (event) => {
292
292
  this.#hover?.lift(event);
293
293
  }, { signal });
294
294
  }
295
- bindHover(control, signal, { canShow = () => true, place, valueAt }) {
295
+ bindHover(control, signal, { canShow = () => true, changed, place, valueAt }) {
296
296
  this.#hover = bindHoverPreview(control, {
297
297
  canShow,
298
298
  changed: () => {
299
+ changed?.();
299
300
  this.dispatchEvent(new Event("sonic-hover", { bubbles: true }));
300
301
  },
301
302
  dismiss: () => {
@@ -367,6 +368,9 @@ var SonicValueElement = class extends SonicFormElement {
367
368
  played: from === void 0 ? this.#model.value : this.#heldAt ?? from
368
369
  };
369
370
  }
371
+ spokenText(value) {
372
+ return (this.#formatSpokenValue ?? this.#formatValue ?? this.notation.speak)?.(value);
373
+ }
370
374
  springTarget() {}
371
375
  tapTarget() {}
372
376
  travelPx(control, property) {
@@ -401,7 +405,7 @@ var SonicValueElement = class extends SonicFormElement {
401
405
  writeAttribute(control, "aria-valuemax", String(high));
402
406
  writeAttribute(control, "aria-valuenow", String(this.#spokenNow()));
403
407
  if (orientation) writeAttribute(control, "aria-orientation", orientation);
404
- writeAttribute(control, "aria-valuetext", (this.#formatSpokenValue ?? this.#formatValue ?? this.notation.speak)?.(this.#spokenNow()));
408
+ writeAttribute(control, "aria-valuetext", this.spokenText(this.#spokenNow()));
405
409
  this.forwardNaming(control, true);
406
410
  this.#renderDisabled(control);
407
411
  this.renderReadout();
@@ -18,14 +18,16 @@ var ValueGestures = class {
18
18
  });
19
19
  #control;
20
20
  #entry;
21
+ #focus;
21
22
  #host;
22
23
  #isRevealed = false;
23
24
  #keyScrub;
24
25
  #pointerDrag;
25
26
  #pointerType;
26
- constructor(host, control) {
27
+ constructor(host, control, focus) {
27
28
  this.#host = host;
28
29
  this.#control = control;
30
+ this.#focus = focus;
29
31
  }
30
32
  abandon() {
31
33
  const drag = this.#dragging();
@@ -46,7 +48,7 @@ var ValueGestures = class {
46
48
  if (isMenuPress(event) || host.isDisabled() || entry.isOpen) return;
47
49
  host.toggleState("springing", false);
48
50
  if (event.pointerType === "touch") event.preventDefault();
49
- focusByPointer(control);
51
+ focusByPointer(this.#focus);
50
52
  this.#pointerType = event.pointerType;
51
53
  if (isResetPress(event)) {
52
54
  this.#reset();
@@ -135,14 +137,14 @@ var ValueGestures = class {
135
137
  }
136
138
  });
137
139
  this.#entry = entry;
138
- entry.bind(control, signal);
140
+ entry.bind(this.#focus, signal);
139
141
  return entry;
140
142
  }
141
143
  #bindKeys(entry, signal) {
142
144
  const control = this.#control;
143
145
  const host = this.#host;
144
146
  control.addEventListener("keydown", (event) => {
145
- if (host.isDisabled() || event.defaultPrevented || event.target !== control) return;
147
+ if (host.isDisabled() || event.defaultPrevented || event.target !== this.#focus) return;
146
148
  host.toggleState("springing", false);
147
149
  if (event.key === "Enter") {
148
150
  if (host.springTarget() !== void 0 || !host.hasEntry()) return;
@@ -164,7 +166,7 @@ var ValueGestures = class {
164
166
  if (isMeta && this.#dragging()) return;
165
167
  if (isMeta || host.model.mapping().keyTarget(event.key, host.model.value) !== void 0) this.#springBack();
166
168
  }, { signal });
167
- control.addEventListener("blur", () => {
169
+ this.#focus.addEventListener("blur", () => {
168
170
  this.#endKeyScrub();
169
171
  this.#concealKeyReveal();
170
172
  this.#springBack();
@@ -48,6 +48,18 @@ var SonicWaveElement = class extends SonicValueElement {
48
48
  this.upgradeProperties("fill", "markers");
49
49
  super.connectedCallback();
50
50
  }
51
+ /** Copies; none outside the canvas */
52
+ markersFromPoint(clientX, clientY) {
53
+ const box = this.canvas.getBoundingClientRect();
54
+ if (!(clientX >= box.left && clientX <= box.right && clientY >= box.top && clientY <= box.bottom)) return [];
55
+ return this.markersInReach({
56
+ x: clientX - box.left,
57
+ y: clientY - box.top
58
+ }, box.width).flatMap((index) => {
59
+ const marker = this.#markers[index];
60
+ return marker ? [{ ...marker }] : [];
61
+ });
62
+ }
51
63
  repaint() {
52
64
  this.#surface?.rebuild();
53
65
  }
@@ -75,9 +87,6 @@ var SonicWaveElement = class extends SonicValueElement {
75
87
  this.render();
76
88
  this.#surface.setFill(this.fill);
77
89
  }
78
- focusTarget() {
79
- return this.control;
80
- }
81
90
  markerList() {
82
91
  return this.#markers;
83
92
  }
@@ -15,6 +15,7 @@ import { bindZoom, bindZoomKeys } from "../lib/zoom-gesture.js";
15
15
  const defaultZoom = 70;
16
16
  const defaultZoomMin = 20;
17
17
  const defaultZoomMax = 280;
18
+ const lineReachPx = 4;
18
19
  const renderWaveform = template(`
19
20
  <div class="sonic-waveform" role="slider" tabindex="0">
20
21
  <canvas class="sonic-waveform-canvas" aria-hidden="true"></canvas>
@@ -201,10 +202,21 @@ var SonicWaveform = class extends SonicWaveElement {
201
202
  super.attributeChangedCallback(name);
202
203
  if (name === "zoomable") this.#zoomGesture?.sync();
203
204
  }
205
+ /** Viewport x of a time as last drawn; not clamped to the window */
206
+ clientXOf(value) {
207
+ const { left, width } = this.canvas.getBoundingClientRect();
208
+ return left + this.#drawnX(value, width);
209
+ }
204
210
  connectedCallback() {
205
211
  this.upgradeProperties("bands", "peaks", "pending", "pendingDelay", "playing", "playbackRate", "readTime", "reducedMotion", "renderLabel", "requestPeaks", "zoom", "zoomable", "zoomMax", "zoomMin");
206
212
  super.connectedCallback();
207
213
  }
214
+ /** The time at a viewport x as last drawn */
215
+ valueFromPoint(clientX, _clientY) {
216
+ const { left, width } = this.canvas.getBoundingClientRect();
217
+ const { startSeconds, zoom } = this.#drawnWindow(width);
218
+ return clamp(startSeconds + (clientX - left) / zoom, ...this.mapping().bounds);
219
+ }
208
220
  connect(signal) {
209
221
  super.connect(signal);
210
222
  const zoomTarget = {
@@ -231,9 +243,18 @@ var SonicWaveform = class extends SonicWaveElement {
231
243
  surface?.requestFrame();
232
244
  if (surface?.isVisible === false || !this.#drawn) this.showCurrentMarker(this.#sourceSeconds());
233
245
  }
246
+ focusTarget() {
247
+ return this.control;
248
+ }
234
249
  isEmpty() {
235
250
  return !this.#peaks || this.#peaks.samples.length === 0;
236
251
  }
252
+ markersInReach({ x }, widthPx) {
253
+ return this.markerList().map(({ start }, index) => ({
254
+ distance: Math.abs(this.#drawnX(start, widthPx) - x),
255
+ index
256
+ })).filter(({ distance }) => distance <= lineReachPx).toSorted((first, second) => first.distance - second.distance).map(({ index }) => index);
257
+ }
237
258
  paint(context, { frameMs, isDirty, isRebuilt, look, size }) {
238
259
  const held = this.#held();
239
260
  const isPaged = look.isReducedMotion && this.reducedMotion === "page";
@@ -258,7 +279,8 @@ var SonicWaveform = class extends SonicWaveElement {
258
279
  const { seconds, view } = timeline;
259
280
  this.#drawn = {
260
281
  seconds,
261
- startSeconds: view.startSeconds
282
+ startSeconds: view.startSeconds,
283
+ zoom: this.zoom
262
284
  };
263
285
  placeLine(this.#playhead, timeline.playheadAt);
264
286
  placeLine(this.#ghost, timeline.ghostAt);
@@ -281,7 +303,10 @@ var SonicWaveform = class extends SonicWaveElement {
281
303
  }, {
282
304
  bands: this.#tint(isRebuilt),
283
305
  kindColours: this.#kindColours,
284
- markers: this.markerList(),
306
+ markers: this.markerList().map((marker) => ({
307
+ ...marker,
308
+ start: clamp(marker.start, ...range)
309
+ })),
285
310
  peaks: this.#peaks,
286
311
  range
287
312
  });
@@ -311,6 +336,17 @@ var SonicWaveform = class extends SonicWaveElement {
311
336
  scrubsKeyRepeat() {
312
337
  return true;
313
338
  }
339
+ #drawnWindow(widthPx) {
340
+ const zoom = this.#drawn?.zoom ?? this.zoom;
341
+ return {
342
+ startSeconds: this.#drawn?.startSeconds ?? this.value - widthPx / zoom / 2,
343
+ zoom
344
+ };
345
+ }
346
+ #drawnX(value, widthPx) {
347
+ const { startSeconds, zoom } = this.#drawnWindow(widthPx);
348
+ return (clamp(value, ...this.mapping().bounds) - startSeconds) * zoom;
349
+ }
314
350
  #duePending() {
315
351
  const due = dueRegions(this.#pending, this.#pendingListedMs, [performance.now(), this.pendingDelay]);
316
352
  if (due.length < this.#pending.length) this.surface()?.requestFrame();
@@ -1,10 +1,11 @@
1
- import { clampProportion } from "../lib/math.js";
1
+ import { clamp, 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
4
  import { scrubRegions } from "../lib/scrub.js";
5
5
  import { sortedRegions } from "../lib/time-regions.js";
6
6
  import { SonicWaveElement } from "./wave-element.js";
7
7
  import { createMarkerBand } from "../lib/marker-band.js";
8
+ import { RegionLayer } from "./region-layer.js";
8
9
  import { stripBars, stripGroove, stripRegions } from "../lib/strip-scene.js";
9
10
 
10
11
  //#region src/elements/wavestrip.ts
@@ -23,9 +24,10 @@ const lengths = {
23
24
  };
24
25
  const cancelZonePx = 48;
25
26
  const renderWavestrip = template(`
26
- <div class="sonic-wavestrip" role="slider" tabindex="0">
27
- <canvas class="sonic-wavestrip-canvas" aria-hidden="true"></canvas>
27
+ <div class="sonic-wavestrip">
28
+ <canvas class="sonic-wavestrip-canvas" role="slider" tabindex="0"></canvas>
28
29
  <div class="sonic-wavestrip-markers" aria-hidden="true"></div>
30
+ <div class="sonic-wavestrip-regions"></div>
29
31
  <div class="sonic-wavestrip-readout" popover="manual">
30
32
  <span></span>
31
33
  <input
@@ -63,6 +65,14 @@ var SonicWavestrip = class extends SonicWaveElement {
63
65
  set cancellable(isCancellable) {
64
66
  this.reflect("cancellable", isCancellable);
65
67
  }
68
+ /** Words a region's `aria-valuetext`; unset, a region speaks its start as the strip speaks a time */
69
+ get formatSpokenRegion() {
70
+ return this.#formatSpokenRegion;
71
+ }
72
+ set formatSpokenRegion(format) {
73
+ this.#formatSpokenRegion = format;
74
+ this.#regions?.render();
75
+ }
66
76
  /** One amplitude from 0 to 1 per slice of the whole track, any length; resampled to the bars and never normalised */
67
77
  get peaks() {
68
78
  return this.#peaks;
@@ -72,6 +82,25 @@ var SonicWavestrip = class extends SonicWaveElement {
72
82
  this.renderEmpty();
73
83
  this.surface()?.invalidate();
74
84
  }
85
+ /** A time a release will seek to, drawn as a scrub's edge; a hold or a hover of the strip's own is drawn instead */
86
+ get preview() {
87
+ return this.#preview;
88
+ }
89
+ set preview(seconds) {
90
+ this.#preview = Number.isFinite(seconds) ? seconds ?? void 0 : void 0;
91
+ this.render();
92
+ }
93
+ /** Runs once per point marker on each `markers` or `renderMarker` write; the element lasts until the next */
94
+ get renderMarker() {
95
+ return this.#renderMarker;
96
+ }
97
+ set renderMarker(render) {
98
+ this.#renderMarker = render;
99
+ this.#drawMarker = render && ((marker, element) => {
100
+ render({ ...marker }, element);
101
+ });
102
+ if (this.isBound()) this.renderMarkers();
103
+ }
75
104
  control = renderWavestrip();
76
105
  canvas = requireChild(this.control, ".sonic-wavestrip-canvas", HTMLCanvasElement);
77
106
  colours = colours;
@@ -81,43 +110,75 @@ var SonicWavestrip = class extends SonicWaveElement {
81
110
  sizeProperty = "--_sonic-wavestrip-size";
82
111
  #bars;
83
112
  #buffered = [];
113
+ #drawMarker;
114
+ #formatSpokenRegion;
84
115
  #markerBand = createMarkerBand(requireChild(this.control, ".sonic-wavestrip-markers", HTMLDivElement));
85
116
  #paintedKey = "";
86
117
  #peaks;
118
+ #preview;
119
+ #regions;
120
+ #renderMarker;
87
121
  /** Viewport x of a time, clamped to the strip */
88
122
  clientXOf(value) {
89
123
  const { startPx, travelPx } = this.#axis();
90
124
  return startPx + this.mapping().proportionOf(value) * travelPx;
91
125
  }
92
126
  connectedCallback() {
93
- this.upgradeProperties("buffered", "cancellable", "peaks");
127
+ this.upgradeProperties("buffered", "cancellable", "formatSpokenRegion", "peaks", "preview", "renderMarker");
94
128
  super.connectedCallback();
95
129
  }
130
+ valueFromPoint(clientX, _clientY) {
131
+ const { startPx, travelPx } = this.#axis();
132
+ const mapping = this.mapping();
133
+ return clamp(mapping.valueAt(clampProportion((clientX - startPx) / travelPx)), ...mapping.bounds);
134
+ }
96
135
  connect(signal) {
97
136
  const { control } = this;
137
+ this.#regions = new RegionLayer({
138
+ axis: () => this.#axis(),
139
+ control,
140
+ element: this,
141
+ isDisabled: () => this.isDisabled(),
142
+ layer: requireChild(control, ".sonic-wavestrip-regions", HTMLDivElement),
143
+ mapping: () => this.mapping(),
144
+ outside: () => this.#outside(),
145
+ seek: (event) => {
146
+ if (this.input(this.valueFromPoint(event.clientX, event.clientY))) this.dispatchEvent(new Event("change", { bubbles: true }));
147
+ },
148
+ speak: ({ end, start }) => this.#formatSpokenRegion?.(start, end) ?? this.spokenText(start) ?? this.notation.format(start)
149
+ }, signal);
98
150
  super.connect(signal);
99
151
  this.bindHover(control, signal, {
152
+ changed: () => {
153
+ if (this.#preview !== void 0) this.render();
154
+ },
100
155
  place: () => {
101
156
  this.#placeReadout();
102
157
  },
103
- valueAt: (event) => {
104
- const axis = this.#axis();
105
- const at = clampProportion((event.clientX - axis.startPx) / axis.travelPx);
106
- const mapping = this.mapping();
107
- return this.#markerAt(event, axis) ?? mapping.snap(mapping.valueAt(at));
108
- }
158
+ valueAt: (event) => this.#markerStartAt(event, this.#axis()) ?? this.mapping().snap(this.valueFromPoint(event.clientX, event.clientY))
109
159
  });
110
160
  this.bindGestures(control, signal, (event) => this.#grab(event));
111
161
  }
112
162
  draw() {
113
163
  const surface = this.surface();
114
164
  this.#placeReadout();
165
+ this.#regions?.follow();
115
166
  this.showCurrentMarker(this.scrubState().played);
116
167
  if (surface && this.#stripRegions(surface.size.width).key !== this.#paintedKey) surface.requestFrame();
117
168
  }
169
+ focusTarget() {
170
+ return this.canvas;
171
+ }
118
172
  isEmpty() {
119
173
  return !this.#peaks || this.#peaks.length === 0;
120
174
  }
175
+ /** The first point marker is the one a press there snaps to */
176
+ markersInReach(point, widthPx) {
177
+ const regions = this.markerProportions().flatMap(([from, to], index) => {
178
+ return this.markerList()[index]?.end !== void 0 && point.x >= from * widthPx && point.x <= to * widthPx ? [index] : [];
179
+ });
180
+ return [...this.#markerBand.inReach(point, widthPx), ...regions];
181
+ }
121
182
  paint(context, frame) {
122
183
  const { look, size } = frame;
123
184
  const strip = this.#stripRegions(size.width);
@@ -134,11 +195,14 @@ var SonicWavestrip = class extends SonicWaveElement {
134
195
  ]);
135
196
  }
136
197
  renderMarkers() {
137
- this.#markerBand.render(this.markerList(), this.markerProportions());
198
+ this.#markerBand.render(this.markerList(), this.markerProportions(), this.#drawMarker);
138
199
  }
139
200
  scrubsKeyRepeat() {
140
201
  return true;
141
202
  }
203
+ usesChild(child) {
204
+ return child instanceof Element && child.localName === "sonic-region";
205
+ }
142
206
  #axis() {
143
207
  const box = this.canvas.getBoundingClientRect();
144
208
  return {
@@ -163,6 +227,13 @@ var SonicWavestrip = class extends SonicWaveElement {
163
227
  }
164
228
  #grab(event) {
165
229
  const axis = this.#axis();
230
+ const marker = this.#markerStartAt(event, axis);
231
+ const regions = this.#regions;
232
+ if (regions?.isHeld() === true) {
233
+ regions.abandon();
234
+ return;
235
+ }
236
+ if (marker === void 0 && regions?.take(event) === true) return void 0;
166
237
  const at = (event.clientX - axis.startPx) / axis.travelPx;
167
238
  const outside = this.#outside();
168
239
  const grabbed = {
@@ -170,16 +241,16 @@ var SonicWavestrip = class extends SonicWaveElement {
170
241
  position: axis.position,
171
242
  travelPx: axis.travelPx
172
243
  };
173
- this.input(this.#markerAt(event, axis) ?? this.mapping().valueAt(at));
244
+ this.input(marker ?? this.valueFromPoint(event.clientX, event.clientY));
174
245
  return outside ? {
175
246
  ...grabbed,
176
247
  outside
177
248
  } : grabbed;
178
249
  }
179
- #markerAt(event, { startPx, topPx, travelPx }) {
180
- const index = this.#markerBand.at({
181
- x: event.clientX - startPx,
182
- y: event.clientY - topPx
250
+ #markerStartAt({ clientX, clientY }, { startPx, topPx, travelPx }) {
251
+ const [index] = this.#markerBand.inReach({
252
+ x: clientX - startPx,
253
+ y: clientY - topPx
183
254
  }, travelPx);
184
255
  return index === void 0 ? void 0 : this.markerList()[index]?.start;
185
256
  }
@@ -196,7 +267,9 @@ var SonicWavestrip = class extends SonicWaveElement {
196
267
  #stripRegions(width) {
197
268
  const mapping = this.mapping();
198
269
  const { proportionOf } = mapping;
199
- const { played, scrub } = scrubRegions(this.scrubState(), this.value, mapping);
270
+ const state = this.scrubState();
271
+ const preview = state.from !== void 0 || this.hoverValue !== void 0 || this.#preview === void 0 ? void 0 : proportionOf(this.#preview);
272
+ const { played, scrub = preview } = scrubRegions(state, this.value, mapping);
200
273
  return stripRegions({
201
274
  buffered: this.#buffered.map(([start, end]) => [proportionOf(start), proportionOf(end)]),
202
275
  played,
@@ -30,7 +30,7 @@ const orientations = {
30
30
  };
31
31
  const renderXy = template(`
32
32
  <div class="sonic-xy">
33
- <div class="sonic-xy-bracket" aria-hidden="true"></div>
33
+ <div class="sonic-xy-reticle" aria-hidden="true"></div>
34
34
  <div class="sonic-xy-field" aria-hidden="true"></div>
35
35
  <div class="sonic-xy-puck">
36
36
  <div class="sonic-xy-axis" data-sonic-axis="x" role="slider" tabindex="0"></div>
@@ -404,16 +404,8 @@ var SonicXy = class extends SonicFormElement {
404
404
  }
405
405
  }
406
406
  #renderForm() {
407
- const { name, x, y } = this;
408
- const state = `${String(x)},${String(y)}`;
409
- if (name === "") {
410
- this.writeFormValue(null, state);
411
- return;
412
- }
413
- const fields = new FormData();
414
- fields.set(`${name}.x`, String(x));
415
- fields.set(`${name}.y`, String(y));
416
- this.writeFormValue(fields, state);
407
+ const { x, y } = this;
408
+ this.writeFormFields([["x", x], ["y", y]], `${String(x)},${String(y)}`);
417
409
  }
418
410
  #renderReadout() {
419
411
  const { isRevealed } = this.#claim;
@@ -4,6 +4,7 @@ import { envelopeCurve } from "./lib/envelope-curve.js";
4
4
  import { SonicEnvelope } from "./elements/envelope.js";
5
5
  import { SonicMeter } from "./elements/meter.js";
6
6
  import { SonicNumber } from "./elements/number.js";
7
+ import { SonicRegion } from "./elements/region.js";
7
8
  import { SonicSegmented } from "./elements/segmented.js";
8
9
  import { SonicSlider } from "./elements/slider.js";
9
10
  import { SonicSpectrum } from "./elements/spectrum.js";
@@ -17,4 +18,4 @@ import { SonicXy } from "./elements/xy.js";
17
18
  import { crossfadeGains } from "./lib/crossfade.js";
18
19
  import { formatPercent, parsePercent } from "./lib/percent.js";
19
20
 
20
- export { SonicButton, SonicDial, SonicEnvelope, SonicMeter, SonicNumber, SonicSegmented, SonicSlider, SonicSpectrum, SonicSplit, SonicSwitch, SonicToggle, SonicWaveform, SonicWavestrip, SonicXy, crossfadeGains, envelopeCurve, formatClock, formatPercent, parseClock, parsePercent };
21
+ export { SonicButton, SonicDial, SonicEnvelope, SonicMeter, SonicNumber, SonicRegion, SonicSegmented, SonicSlider, SonicSpectrum, SonicSplit, SonicSwitch, SonicToggle, SonicWaveform, SonicWavestrip, SonicXy, crossfadeGains, envelopeCurve, formatClock, formatPercent, parseClock, parsePercent };
@@ -1,6 +1,7 @@
1
1
  //#region src/lib/clock.ts
2
2
  const sign = /^[-−]/;
3
3
  function formatClock(seconds) {
4
+ if (!Number.isFinite(seconds)) return "";
4
5
  const total = Math.floor(Math.abs(seconds));
5
6
  const hours = Math.floor(total / 3600);
6
7
  const minutes = Math.floor(total / 60) % 60;
@@ -96,4 +96,4 @@ function stepDrag(mapping, state, move) {
96
96
  }
97
97
 
98
98
  //#endregion
99
- export { dragThresholdPx, startDrag, stepDrag };
99
+ export { dragThresholdPx, fineFactor, startDrag, stepDrag };
@@ -2,7 +2,7 @@ import { readPxProperty } from "./read-px-property.js";
2
2
 
3
3
  //#region src/lib/marker-band.ts
4
4
  const kindPattern = /^[a-z][a-z0-9-]*$/;
5
- const snapSlopPx = 4;
5
+ const snapMarginPx = 4;
6
6
  function isKind(kind) {
7
7
  return kind !== void 0 && kindPattern.test(kind);
8
8
  }
@@ -13,45 +13,61 @@ function writeKind(part, kind, fallback) {
13
13
  }
14
14
  part.style.setProperty("--_sonic-marker", `var(--sonic-marker-${kind}, var(${fallback}))`);
15
15
  }
16
- function renderMarker(marker, [from, to], lane) {
16
+ function createPart(marker) {
17
17
  const part = document.createElement("div");
18
- part.className = lane === void 0 ? "sonic-wavestrip-region" : "sonic-wavestrip-marker";
19
- if (lane !== void 0) part.style.setProperty("--_sonic-marker-lane", String(lane));
20
- part.style.setProperty("--_sonic-marker-from", String(from));
21
- part.style.setProperty("--_sonic-marker-to", String(to));
18
+ part.className = marker.end === void 0 ? "sonic-wavestrip-marker" : "sonic-wavestrip-region";
22
19
  writeKind(part, marker.kind, "--_sonic-marker-default");
23
20
  if (marker.dimmed === true) part.dataset.sonicDimmed = "";
24
21
  return part;
25
22
  }
26
23
  function createMarkerBand(band) {
27
- let dots = [];
24
+ let built;
25
+ let placed = [];
28
26
  const px = (property) => readPxProperty(getComputedStyle(band), property, 0);
27
+ function build(markers, draw) {
28
+ const parts = markers.map((marker) => {
29
+ const part = createPart(marker);
30
+ if (draw && marker.end === void 0) {
31
+ part.style.setProperty("--_sonic-marker-paint", "transparent");
32
+ draw(marker, part);
33
+ }
34
+ return part;
35
+ });
36
+ band.replaceChildren(...parts);
37
+ built = {
38
+ draw,
39
+ markers,
40
+ parts
41
+ };
42
+ return parts;
43
+ }
29
44
  return {
30
- at: ({ x, y }, widthPx) => {
45
+ inReach: (point, widthPx) => {
31
46
  const size = px("--_sonic-marker-size");
32
47
  const step = px("--_sonic-marker-step");
33
- const nearest = nearestDot({
34
- x,
35
- y
36
- }, dots.map(({ at, lane }) => ({
48
+ return centersInReach(point, placed.map(({ at, lane }) => ({
37
49
  x: at * widthPx,
38
50
  y: size / 2 + lane * step
39
- })), size / 2 + snapSlopPx);
40
- return nearest === void 0 ? void 0 : dots[nearest]?.index;
51
+ })), size / 2 + snapMarginPx).flatMap((order) => placed[order]?.index ?? []);
41
52
  },
42
- render: (markers, proportions) => {
53
+ render: (markers, proportions, draw) => {
43
54
  const widthPx = band.getBoundingClientRect().width;
44
55
  const points = markers.flatMap((marker, index) => marker.end === void 0 ? [{
45
56
  at: proportions[index]?.[0] ?? 0,
46
57
  index
47
58
  }] : []);
48
59
  const lanes = markerLanes(points.map(({ at }) => at * widthPx), px("--_sonic-marker-step"));
49
- dots = points.map((point, order) => ({
60
+ const parts = built?.markers === markers && built.draw === draw ? built.parts : build(markers, draw);
61
+ placed = points.map((point, order) => ({
50
62
  ...point,
51
63
  lane: lanes[order] ?? 0
52
64
  }));
53
- const laneOf = new Map(dots.map(({ index, lane }) => [index, lane]));
54
- band.replaceChildren(...markers.map((marker, index) => renderMarker(marker, proportions[index] ?? [0, 0], laneOf.get(index))));
65
+ for (const [index, part] of parts.entries()) {
66
+ const [from, to] = proportions[index] ?? [0, 0];
67
+ part.style.setProperty("--_sonic-marker-from", String(from));
68
+ part.style.setProperty("--_sonic-marker-to", String(to));
69
+ }
70
+ for (const { index, lane } of placed) parts[index]?.style.setProperty("--_sonic-marker-lane", String(lane));
55
71
  }
56
72
  };
57
73
  }
@@ -64,17 +80,12 @@ function markerLanes(positions, minDistance) {
64
80
  return lane;
65
81
  });
66
82
  }
67
- function nearestDot(point, dots, reach) {
68
- let nearest;
69
- let nearestDistance = reach * reach;
70
- for (const [index, dot] of dots.entries()) {
71
- const distance = (dot.x - point.x) ** 2 + (dot.y - point.y) ** 2;
72
- if (distance > nearestDistance) continue;
73
- nearest = index;
74
- nearestDistance = distance;
75
- }
76
- return nearest;
83
+ function centersInReach(point, centers, reach) {
84
+ return centers.map((center, index) => ({
85
+ distance: (center.x - point.x) ** 2 + (center.y - point.y) ** 2,
86
+ index
87
+ })).filter(({ distance }) => distance <= reach * reach).toSorted((first, second) => first.distance - second.distance || second.index - first.index).map(({ index }) => index);
77
88
  }
78
89
 
79
90
  //#endregion
80
- export { createMarkerBand, isKind, markerLanes, nearestDot, writeKind };
91
+ export { centersInReach, createMarkerBand, isKind, markerLanes, writeKind };
@@ -0,0 +1,48 @@
1
+ import { clamp, clampProportion, trimFloat } from "./math.js";
2
+ import { fineFactor } from "./drag-step.js";
3
+
4
+ //#region src/lib/region-drag.ts
5
+ function regionAt(bodies, x) {
6
+ const index = bodies.findLastIndex((body) => body !== void 0 && x >= body[0] && x <= body[1]);
7
+ return index === -1 ? void 0 : index;
8
+ }
9
+ function regionTravel({ end, start }, [low, high]) {
10
+ return [low, Math.max(low, trimFloat(high - (end - start)))];
11
+ }
12
+ function moveRegionTo(mapping, span, start) {
13
+ const moved = clamp(mapping.snap(start), ...regionTravel(span, mapping.bounds));
14
+ return {
15
+ end: trimFloat(moved + (span.end - span.start)),
16
+ start: moved
17
+ };
18
+ }
19
+ function startRegionDrag({ position, ...fixed }) {
20
+ return {
21
+ ...fixed,
22
+ isEngaged: false,
23
+ lastPosition: position,
24
+ startPosition: position,
25
+ travelPx: 0
26
+ };
27
+ }
28
+ function stepRegionDrag(mapping, state, move) {
29
+ if (!(state.isEngaged || Math.abs(move.position - state.startPosition) >= state.thresholdPx)) return {
30
+ span: void 0,
31
+ state
32
+ };
33
+ const pace = move.isFine ? fineFactor : 1;
34
+ const travelPx = state.travelPx + (move.position - state.lastPosition) * pace;
35
+ const at = mapping.proportionOf(state.from.start) + travelPx / state.widthPx;
36
+ return {
37
+ span: moveRegionTo(mapping, state.from, mapping.valueAt(clampProportion(at))),
38
+ state: {
39
+ ...state,
40
+ isEngaged: true,
41
+ lastPosition: move.position,
42
+ travelPx
43
+ }
44
+ };
45
+ }
46
+
47
+ //#endregion
48
+ export { moveRegionTo, regionAt, regionTravel, startRegionDrag, stepRegionDrag };
@@ -0,0 +1,2 @@
1
+ import { SonicRegion } from "../elements/region.js";
2
+ export type { SonicRegion };
@@ -0,0 +1,8 @@
1
+ import { defineOnce } from "../elements/define-once.js";
2
+ import { SonicRegion } from "../elements/region.js";
3
+
4
+ //#region src/define/region.ts
5
+ defineOnce("sonic-region", SonicRegion);
6
+
7
+ //#endregion
8
+ //# sourceMappingURL=region.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"region.js","names":[],"sources":["../../../src/define/region.ts"],"sourcesContent":["import { defineOnce } from '#elements/define-once.ts';\nimport { SonicRegion } from '#elements/region.ts';\n\ndefineOnce('sonic-region', SonicRegion);\n\nexport type { SonicRegion } from '#elements/region.ts';\n"],"mappings":";;;;AAGA,WAAW,gBAAgB,WAAW"}
@@ -8,6 +8,8 @@ import { SonicMeter } from "./elements/meter.js";
8
8
  import "./define/meter.js";
9
9
  import { SonicNumber } from "./elements/number.js";
10
10
  import "./define/number.js";
11
+ import { SonicRegion } from "./elements/region.js";
12
+ import "./define/region.js";
11
13
  import { SonicSegmented } from "./elements/segmented.js";
12
14
  import "./define/segmented.js";
13
15
  import { SonicSlider } from "./elements/slider.js";
@@ -26,4 +28,4 @@ import { SonicWavestrip } from "./elements/wavestrip.js";
26
28
  import "./define/wavestrip.js";
27
29
  import { SonicXy } from "./elements/xy.js";
28
30
  import "./define/xy.js";
29
- export type { SonicButton, SonicDial, SonicEnvelope, SonicMeter, SonicNumber, SonicSegmented, SonicSlider, SonicSpectrum, SonicSplit, SonicSwitch, SonicToggle, SonicWaveform, SonicWavestrip, SonicXy };
31
+ export type { SonicButton, SonicDial, SonicEnvelope, SonicMeter, SonicNumber, SonicRegion, SonicSegmented, SonicSlider, SonicSpectrum, SonicSplit, SonicSwitch, SonicToggle, SonicWaveform, SonicWavestrip, SonicXy };