@xsynaptic/sonic-ui 0.17.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +122 -19
  3. package/dist/default/elements/button.js +2 -5
  4. package/dist/default/elements/dial.js +2 -7
  5. package/dist/default/elements/display-element.js +9 -12
  6. package/dist/default/elements/envelope.js +31 -78
  7. package/dist/default/elements/field-gesture.js +81 -0
  8. package/dist/default/elements/meter.js +4 -1
  9. package/dist/default/elements/number.js +0 -7
  10. package/dist/default/elements/position-group.js +42 -26
  11. package/dist/default/elements/radio-group.js +59 -56
  12. package/dist/default/elements/scale.js +3 -3
  13. package/dist/default/elements/segmented.js +0 -5
  14. package/dist/default/elements/slider.js +11 -20
  15. package/dist/default/elements/sonic-element.js +3 -1
  16. package/dist/default/elements/split.js +14 -13
  17. package/dist/default/elements/switch.js +31 -47
  18. package/dist/default/elements/toggle.js +20 -49
  19. package/dist/default/elements/value-element.js +72 -94
  20. package/dist/default/elements/value-gestures.js +22 -22
  21. package/dist/default/elements/wave-element.js +16 -20
  22. package/dist/default/elements/waveform.js +42 -241
  23. package/dist/default/elements/wavestrip.js +10 -5
  24. package/dist/default/elements/xy.js +67 -119
  25. package/dist/default/index.js +2 -1
  26. package/dist/default/lib/canvas-surface.js +25 -0
  27. package/dist/default/lib/clock.js +23 -0
  28. package/dist/default/lib/crossfade.js +1 -0
  29. package/dist/default/lib/envelope-curve.js +1 -0
  30. package/dist/default/lib/peaks-request.js +44 -0
  31. package/dist/default/lib/positions.js +58 -0
  32. package/dist/default/lib/scrub.js +12 -0
  33. package/dist/default/lib/spoken-duration.js +30 -0
  34. package/dist/default/lib/value-model.js +62 -0
  35. package/dist/default/lib/waveform-buckets.js +7 -1
  36. package/dist/default/lib/waveform-scene.js +108 -0
  37. package/dist/default/lib/zoom-gesture.js +97 -1
  38. package/dist/dev/elements/button.d.ts +2 -0
  39. package/dist/dev/elements/button.js +14 -5
  40. package/dist/dev/elements/button.js.map +1 -1
  41. package/dist/dev/elements/dial.d.ts +2 -0
  42. package/dist/dev/elements/dial.js +16 -7
  43. package/dist/dev/elements/dial.js.map +1 -1
  44. package/dist/dev/elements/display-element.d.ts +5 -3
  45. package/dist/dev/elements/display-element.js +11 -9
  46. package/dist/dev/elements/display-element.js.map +1 -1
  47. package/dist/dev/elements/envelope.js +48 -78
  48. package/dist/dev/elements/envelope.js.map +1 -1
  49. package/dist/dev/elements/field-gesture.js +82 -0
  50. package/dist/dev/elements/field-gesture.js.map +1 -0
  51. package/dist/dev/elements/meter.d.ts +10 -5
  52. package/dist/dev/elements/meter.js +5 -1
  53. package/dist/dev/elements/meter.js.map +1 -1
  54. package/dist/dev/elements/number.js +13 -7
  55. package/dist/dev/elements/number.js.map +1 -1
  56. package/dist/dev/elements/position-group.d.ts +12 -6
  57. package/dist/dev/elements/position-group.js +42 -26
  58. package/dist/dev/elements/position-group.js.map +1 -1
  59. package/dist/dev/elements/radio-group.d.ts +11 -7
  60. package/dist/dev/elements/radio-group.js +59 -56
  61. package/dist/dev/elements/radio-group.js.map +1 -1
  62. package/dist/dev/elements/scale.js +3 -3
  63. package/dist/dev/elements/scale.js.map +1 -1
  64. package/dist/dev/elements/segmented.js +12 -5
  65. package/dist/dev/elements/segmented.js.map +1 -1
  66. package/dist/dev/elements/slider.d.ts +4 -2
  67. package/dist/dev/elements/slider.js +25 -20
  68. package/dist/dev/elements/slider.js.map +1 -1
  69. package/dist/dev/elements/sonic-element.d.ts +13 -2
  70. package/dist/dev/elements/sonic-element.js +63 -11
  71. package/dist/dev/elements/sonic-element.js.map +1 -1
  72. package/dist/dev/elements/split.d.ts +1 -0
  73. package/dist/dev/elements/split.js +14 -13
  74. package/dist/dev/elements/split.js.map +1 -1
  75. package/dist/dev/elements/switch.d.ts +2 -2
  76. package/dist/dev/elements/switch.js +38 -47
  77. package/dist/dev/elements/switch.js.map +1 -1
  78. package/dist/dev/elements/toggle.js +32 -49
  79. package/dist/dev/elements/toggle.js.map +1 -1
  80. package/dist/dev/elements/value-element.d.ts +11 -5
  81. package/dist/dev/elements/value-element.js +72 -94
  82. package/dist/dev/elements/value-element.js.map +1 -1
  83. package/dist/dev/elements/value-gestures.js +22 -22
  84. package/dist/dev/elements/value-gestures.js.map +1 -1
  85. package/dist/dev/elements/wave-element.d.ts +8 -5
  86. package/dist/dev/elements/wave-element.js +30 -20
  87. package/dist/dev/elements/wave-element.js.map +1 -1
  88. package/dist/dev/elements/waveform.d.ts +20 -22
  89. package/dist/dev/elements/waveform.js +42 -241
  90. package/dist/dev/elements/waveform.js.map +1 -1
  91. package/dist/dev/elements/wavestrip.d.ts +5 -1
  92. package/dist/dev/elements/wavestrip.js +10 -5
  93. package/dist/dev/elements/wavestrip.js.map +1 -1
  94. package/dist/dev/elements/xy.js +81 -119
  95. package/dist/dev/elements/xy.js.map +1 -1
  96. package/dist/dev/index.d.ts +5 -3
  97. package/dist/dev/index.js +2 -1
  98. package/dist/dev/lib/canvas-surface.d.ts +1 -0
  99. package/dist/dev/lib/canvas-surface.js +25 -0
  100. package/dist/dev/lib/canvas-surface.js.map +1 -1
  101. package/dist/dev/lib/clock.d.ts +5 -0
  102. package/dist/dev/lib/clock.js +24 -0
  103. package/dist/dev/lib/clock.js.map +1 -0
  104. package/dist/dev/lib/crossfade.d.ts +1 -0
  105. package/dist/dev/lib/crossfade.js +1 -0
  106. package/dist/dev/lib/crossfade.js.map +1 -1
  107. package/dist/dev/lib/envelope-curve.d.ts +1 -0
  108. package/dist/dev/lib/envelope-curve.js +1 -0
  109. package/dist/dev/lib/envelope-curve.js.map +1 -1
  110. package/dist/dev/lib/mirror-children.js.map +1 -1
  111. package/dist/dev/lib/owned-control.js.map +1 -1
  112. package/dist/dev/lib/peaks-request.d.ts +5 -0
  113. package/dist/dev/lib/peaks-request.js +45 -0
  114. package/dist/dev/lib/peaks-request.js.map +1 -0
  115. package/dist/dev/lib/positions.d.ts +7 -0
  116. package/dist/dev/lib/positions.js +59 -0
  117. package/dist/dev/lib/positions.js.map +1 -0
  118. package/dist/dev/lib/scrub.d.ts +8 -0
  119. package/dist/dev/lib/scrub.js +13 -0
  120. package/dist/dev/lib/scrub.js.map +1 -0
  121. package/dist/dev/lib/spoken-duration.js +31 -0
  122. package/dist/dev/lib/spoken-duration.js.map +1 -0
  123. package/dist/dev/lib/time-regions.d.ts +1 -0
  124. package/dist/dev/lib/time-regions.js.map +1 -1
  125. package/dist/dev/lib/units.d.ts +8 -0
  126. package/dist/dev/lib/value-model.js +63 -0
  127. package/dist/dev/lib/value-model.js.map +1 -0
  128. package/dist/dev/lib/waveform-buckets.d.ts +2 -1
  129. package/dist/dev/lib/waveform-buckets.js +7 -1
  130. package/dist/dev/lib/waveform-buckets.js.map +1 -1
  131. package/dist/dev/lib/waveform-scene.d.ts +11 -0
  132. package/dist/dev/lib/waveform-scene.js +109 -0
  133. package/dist/dev/lib/waveform-scene.js.map +1 -0
  134. package/dist/dev/lib/zoom-gesture.js +97 -1
  135. package/dist/dev/lib/zoom-gesture.js.map +1 -1
  136. package/dist/skins/amber.css +1 -0
  137. package/dist/skins/ivory.css +1 -0
  138. package/dist/skins/lime.css +1 -0
  139. package/dist/skins/slate.css +1 -0
  140. package/dist/styles/button.css +45 -11
  141. package/dist/styles/dial.css +10 -9
  142. package/dist/styles/envelope.css +10 -4
  143. package/dist/styles/led.css +5 -1
  144. package/dist/styles/material/arc.css +3 -2
  145. package/dist/styles/material/bracket.css +2 -2
  146. package/dist/styles/material/cap.css +33 -12
  147. package/dist/styles/material/core.css +4 -0
  148. package/dist/styles/material/glass.css +1 -1
  149. package/dist/styles/material/groove.css +4 -3
  150. package/dist/styles/material/keycap.css +3 -2
  151. package/dist/styles/material/lens.css +1 -1
  152. package/dist/styles/material/pane.css +5 -4
  153. package/dist/styles/material/readout.css +6 -6
  154. package/dist/styles/material/well.css +2 -2
  155. package/dist/styles/meter.css +2 -7
  156. package/dist/styles/number.css +5 -1
  157. package/dist/styles/panel.css +4 -3
  158. package/dist/styles/ring.css +1 -1
  159. package/dist/styles/screen.css +5 -1
  160. package/dist/styles/segmented.css +17 -4
  161. package/dist/styles/slider.css +9 -3
  162. package/dist/styles/spectrum.css +16 -3
  163. package/dist/styles/switch.css +35 -10
  164. package/dist/styles/toggle.css +14 -7
  165. package/dist/styles/waveform.css +17 -5
  166. package/dist/styles/wavestrip.css +11 -3
  167. package/dist/styles/xy.css +9 -5
  168. package/package.json +1 -6
  169. package/dist/default/dat.js +0 -51
  170. package/dist/default/lib/asked-value.js +0 -21
  171. package/dist/default/lib/fill-watch.js +0 -29
  172. package/dist/dev/dat.d.ts +0 -19
  173. package/dist/dev/dat.js +0 -52
  174. package/dist/dev/dat.js.map +0 -1
  175. package/dist/dev/lib/asked-value.js +0 -22
  176. package/dist/dev/lib/asked-value.js.map +0 -1
  177. package/dist/dev/lib/fill-watch.js +0 -30
  178. package/dist/dev/lib/fill-watch.js.map +0 -1
@@ -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
  };
@@ -2,15 +2,14 @@ import { clampProportion, toNumber } from "../lib/math.js";
2
2
  import { writeAttribute } from "../lib/write-attribute.js";
3
3
  import { SonicFormElement } from "./form-element.js";
4
4
  import { requireChild, template } from "../lib/render.js";
5
- import { bindDrag } from "../lib/pointer-drag.js";
6
5
  import { Readout } from "./readout.js";
7
- import { ReadoutClaim, revealDelay } from "./readout-claim.js";
8
- import { dragThresholdPx } from "../lib/drag-step.js";
6
+ import { ReadoutClaim } from "./readout-claim.js";
9
7
  import { focusByPointer } from "../lib/focus-by-pointer.js";
10
8
  import { isMenuPress, isResetPress } from "../lib/modifier-press.js";
11
- import { resetKeys, valueMapping } from "../lib/value-mapping.js";
12
- import { moveTo, resnap, setAsked } from "../lib/asked-value.js";
13
- import { pointerMove, pointerPosition, startFieldDrag, stepFieldDrag } from "../lib/field.js";
9
+ import { resetKeys } from "../lib/value-mapping.js";
10
+ import { createValueModel } from "../lib/value-model.js";
11
+ import { pointerPosition } from "../lib/field.js";
12
+ import { bindFieldGesture } from "./field-gesture.js";
14
13
 
15
14
  //#region src/elements/xy.ts
16
15
  const axes = ["x", "y"];
@@ -75,7 +74,7 @@ var SonicXy = class extends SonicFormElement {
75
74
  this.render();
76
75
  }
77
76
  get pointerType() {
78
- return this.#pointerType;
77
+ return this.#gesture?.pointerType();
79
78
  }
80
79
  get readout() {
81
80
  return this.hasAttribute("readout");
@@ -84,7 +83,7 @@ var SonicXy = class extends SonicFormElement {
84
83
  this.reflect("readout", isEnabled);
85
84
  }
86
85
  get x() {
87
- return this.#values.x.value;
86
+ return this.#models.x.value;
88
87
  }
89
88
  set x(next) {
90
89
  this.#write("x", next);
@@ -138,7 +137,7 @@ var SonicXy = class extends SonicFormElement {
138
137
  this.reflect("x-taper", curve);
139
138
  }
140
139
  get y() {
141
- return this.#values.y.value;
140
+ return this.#models.y.value;
142
141
  }
143
142
  set y(next) {
144
143
  this.#write("y", next);
@@ -198,53 +197,33 @@ var SonicXy = class extends SonicFormElement {
198
197
  #xy = renderXy();
199
198
  #field = requireChild(this.#xy, ".sonic-xy-field", HTMLDivElement);
200
199
  #formatValue;
201
- #mapped;
200
+ #gesture;
201
+ #models = {
202
+ x: createValueModel(() => this.#spec("x")),
203
+ y: createValueModel(() => this.#spec("y"))
204
+ };
202
205
  #parts = {
203
206
  x: requireChild(this.#xy, "[data-sonic-axis=\"x\"]", HTMLDivElement),
204
207
  y: requireChild(this.#xy, "[data-sonic-axis=\"y\"]", HTMLDivElement)
205
208
  };
206
- #pointerDrag;
207
- #pointerType;
208
209
  #puck = requireChild(this.#xy, ".sonic-xy-puck", HTMLDivElement);
209
210
  #readout = new Readout(requireChild(this.#xy, ".sonic-xy-readout", HTMLDivElement));
210
- #values = {
211
- x: {
212
- asked: void 0,
213
- value: 0
214
- },
215
- y: {
216
- asked: void 0,
217
- value: 0
218
- }
219
- };
220
211
  attributeChangedCallback(name) {
221
- this.#mapped = void 0;
222
- if (name === "x" || name === "y") {
223
- if (this.#pointerDrag?.current()) return;
224
- this.#values[name].asked = void 0;
225
- }
226
- if (name === "disabled" && this.isDisabled()) this.#pointerDrag?.end();
227
- this.#refresh();
212
+ if ((name === "x" || name === "y") && this.#gesture?.current()) return;
213
+ if (name === "disabled" && this.isDisabled()) this.#gesture?.end();
214
+ this.#respec(name);
228
215
  }
229
216
  connectedCallback() {
230
217
  this.upgradeProperties("readout", "xDefault", "xLabel", "xMax", "xMidpoint", "xMin", "xOrigin", "xStep", "xTaper", "yDefault", "yLabel", "yMax", "yMidpoint", "yMin", "yOrigin", "yStep", "yTaper", "formatValue", "x", "y");
231
218
  super.connectedCallback();
232
219
  }
233
220
  formResetCallback() {
234
- for (const axis of axes) this.#values[axis].asked = void 0;
235
- this.#refresh();
221
+ this.#respec(...axes);
236
222
  }
237
223
  connect(signal) {
238
224
  const xy = this.#xy;
239
225
  this.keepControl(xy, signal);
240
226
  this.render();
241
- this.checkStyles(xy, "xy.css", [
242
- "cap",
243
- "glass",
244
- "pane",
245
- "well",
246
- "readout"
247
- ]);
248
227
  this.#bindPointer(xy, signal);
249
228
  this.#bindKeys(xy, signal);
250
229
  }
@@ -254,11 +233,11 @@ var SonicXy = class extends SonicFormElement {
254
233
  render() {
255
234
  if (!this.isBound()) return;
256
235
  const { style } = this.#xy;
257
- const mappings = this.#mappings();
258
236
  for (const axis of axes) {
259
- const mapping = mappings[axis];
237
+ const model = this.#models[axis];
238
+ const mapping = model.mapping();
260
239
  const origin = this.optionalNumberAttribute(`${axis}-origin`) ?? mapping.bounds[0];
261
- style.setProperty(`--_sonic-xy-${axis}`, String(mapping.proportionOf(this.#values[axis].value)));
240
+ style.setProperty(`--_sonic-xy-${axis}`, String(mapping.proportionOf(model.value)));
262
241
  style.setProperty(`--_sonic-xy-${axis}-origin`, String(mapping.proportionOf(origin)));
263
242
  }
264
243
  this.#renderAria();
@@ -271,7 +250,7 @@ var SonicXy = class extends SonicFormElement {
271
250
  this.y = y;
272
251
  }
273
252
  #axisText(axis) {
274
- const { value } = this.#values[axis];
253
+ const { value } = this.#models[axis];
275
254
  return `${this.getAttribute(`${axis}-label`) ?? axis.toUpperCase()} ${this.#formatValue?.(value, axis) ?? String(value)}`;
276
255
  }
277
256
  #bindKeys(xy, signal) {
@@ -297,32 +276,22 @@ var SonicXy = class extends SonicFormElement {
297
276
  xy.addEventListener("mousedown", (event) => {
298
277
  event.preventDefault();
299
278
  }, { signal });
300
- this.#pointerDrag = bindDrag(xy, {
301
- cancel: (drag) => {
302
- this.#input(drag.from);
303
- },
279
+ this.#gesture = bindFieldGesture(xy, {
280
+ claim: this.#claim,
304
281
  grab: (event) => {
305
282
  if (this.isDisabled() || isMenuPress(event)) return;
306
283
  focusByPointer(this.focusTarget());
307
- this.#pointerType = event.pointerType;
308
284
  if (!isResetPress(event)) {
309
285
  this.toggleState("dragging", true);
310
286
  return this.#grab(event);
311
287
  }
312
288
  this.#reset();
313
- this.#pointerType = void 0;
314
289
  },
315
- move: (drag, event) => {
316
- const step = stepFieldDrag(this.#mappings(), drag.state, pointerMove(event));
317
- drag.state = step.state;
318
- if (step.state.isEngaged) this.#claim.reveal("drag");
319
- if (!this.#input(step)) this.#renderReadout();
290
+ input: (_hold, next) => {
291
+ if (!this.#input(next)) this.#renderReadout();
320
292
  },
321
- release: (drag) => {
322
- const hasMoved = this.x !== drag.from.x || this.y !== drag.from.y;
323
- this.#claim.press(false);
324
- if (hasMoved) this.dispatchEvent(new Event("change", { bubbles: true }));
325
- this.#pointerType = void 0;
293
+ release: (_hold, moved) => {
294
+ if (moved.length > 0) this.dispatchEvent(new Event("change", { bubbles: true }));
326
295
  },
327
296
  toggle: (isDragging) => {
328
297
  this.toggleState("dragging", isDragging);
@@ -333,13 +302,13 @@ var SonicXy = class extends SonicFormElement {
333
302
  if (this.#input(next)) this.dispatchEvent(new Event("change", { bubbles: true }));
334
303
  }
335
304
  #grab(event) {
336
- const from = {
337
- x: this.x,
338
- y: this.y
339
- };
340
305
  const field = this.#field.getBoundingClientRect();
341
306
  const puck = this.#puck.getBoundingClientRect();
342
- const mappings = this.#mappings();
307
+ const { x, y } = this.#models;
308
+ const from = {
309
+ x: x.value,
310
+ y: y.value
311
+ };
343
312
  const travel = {
344
313
  x: Math.max(1, field.width - puck.width),
345
314
  y: Math.max(1, field.height - puck.height)
@@ -351,32 +320,26 @@ var SonicXy = class extends SonicFormElement {
351
320
  };
352
321
  const isOnPuck = event.target instanceof Node && this.#puck.contains(event.target);
353
322
  if (!isOnPuck) this.#input({
354
- x: mappings.x.valueAt(pressed.x),
355
- y: mappings.y.valueAt(pressed.y)
323
+ x: x.mapping().valueAt(pressed.x),
324
+ y: y.mapping().valueAt(pressed.y)
356
325
  });
357
- this.#claim.press(true, revealDelay(this.#xy));
358
- const start = (axis) => ({
359
- from: this.#values[axis].value,
360
- mapping: mappings[axis],
361
- proportion: isOnPuck ? mappings[axis].proportionOf(this.#values[axis].value) : clampProportion(pressed[axis]),
326
+ const hold = (axis) => ({
327
+ from: from[axis],
328
+ model: this.#models[axis],
329
+ ...isOnPuck ? {} : { proportion: clampProportion(pressed[axis]) },
362
330
  travelPx: travel[axis]
363
331
  });
364
- return {
365
- from,
366
- state: startFieldDrag({
367
- position,
368
- thresholdPx: dragThresholdPx(event.pointerType),
369
- x: start("x"),
370
- y: start("y")
371
- })
372
- };
332
+ return { axes: {
333
+ x: hold("x"),
334
+ y: hold("y")
335
+ } };
373
336
  }
374
337
  #hasDefault() {
375
338
  return this.xDefault !== void 0 || this.yDefault !== void 0;
376
339
  }
377
340
  #input(next) {
378
341
  let hasMoved = false;
379
- for (const axis of axes) if (moveTo(this.#values[axis], this.#mappings()[axis], next[axis] ?? NaN)) hasMoved = true;
342
+ for (const axis of axes) if (this.#models[axis].input(next[axis] ?? NaN)) hasMoved = true;
380
343
  if (!hasMoved) return false;
381
344
  this.render();
382
345
  this.dispatchEvent(new Event("input", {
@@ -386,9 +349,8 @@ var SonicXy = class extends SonicFormElement {
386
349
  return true;
387
350
  }
388
351
  #keyTo(axis, key) {
389
- const mapping = this.#mappings()[axis];
390
- const next = mapping.keyTarget(key, this.#values[axis].value);
391
- const hasMoved = moveTo(this.#values[axis], mapping, next ?? NaN);
352
+ const model = this.#models[axis];
353
+ const hasMoved = model.input(model.mapping().keyTarget(key, model.value) ?? NaN);
392
354
  this.#currentAxis = axis;
393
355
  this.render();
394
356
  this.#parts[axis].focus();
@@ -399,46 +361,18 @@ var SonicXy = class extends SonicFormElement {
399
361
  }));
400
362
  this.dispatchEvent(new Event("change", { bubbles: true }));
401
363
  }
402
- #mapping(axis) {
403
- const midpoint = this.optionalNumberAttribute(`${axis}-midpoint`);
404
- return valueMapping({
405
- max: this.numberAttribute(`${axis}-max`, 100),
406
- ...midpoint === void 0 ? {} : { midpoint },
407
- isNotched: false,
408
- isWrapping: false,
409
- min: this.numberAttribute(`${axis}-min`, 0),
410
- step: this.numberAttribute(`${axis}-step`, 1),
411
- taper: this.#taper(axis)
412
- });
413
- }
414
- #mappings() {
415
- if (this.#mapped) return this.#mapped;
416
- this.#mapped = {
417
- x: this.#mapping("x"),
418
- y: this.#mapping("y")
419
- };
420
- return this.#mapped;
421
- }
422
- #refresh() {
423
- const mappings = this.#mappings();
424
- for (const axis of axes) {
425
- const mapping = mappings[axis];
426
- resnap(this.#values[axis], mapping, this.numberAttribute(axis, mapping.bounds[0]));
427
- }
428
- this.render();
429
- }
430
364
  #renderAria() {
431
- const mappings = this.#mappings();
432
365
  const isDisabled = this.isDisabled();
433
366
  for (const axis of axes) {
434
367
  const part = this.#parts[axis];
435
- const [low, high] = mappings[axis].bounds;
368
+ const model = this.#models[axis];
369
+ const [low, high] = model.mapping().bounds;
436
370
  const isCurrent = axis === this.#currentAxis;
437
371
  const stop = isCurrent ? "0" : "-1";
438
372
  writeAttribute(part, "aria-orientation", orientations[axis]);
439
373
  writeAttribute(part, "aria-valuemin", String(low));
440
374
  writeAttribute(part, "aria-valuemax", String(high));
441
- writeAttribute(part, "aria-valuenow", String(this.#values[axis].value));
375
+ writeAttribute(part, "aria-valuenow", String(model.value));
442
376
  writeAttribute(part, "aria-valuetext", this.#valueText(axis));
443
377
  this.forwardNaming(part, true);
444
378
  writeAttribute(part, "aria-hidden", isCurrent ? void 0 : "true");
@@ -471,6 +405,22 @@ var SonicXy = class extends SonicFormElement {
471
405
  y: this.yDefault
472
406
  });
473
407
  }
408
+ #respec(...forgotten) {
409
+ for (const axis of axes) this.#models[axis].respec(this.numberAttribute(axis, this.numberAttribute(`${axis}-min`, 0)), { forgetAsk: forgotten.includes(axis) });
410
+ this.render();
411
+ }
412
+ #spec(axis) {
413
+ const midpoint = this.optionalNumberAttribute(`${axis}-midpoint`);
414
+ return {
415
+ max: this.numberAttribute(`${axis}-max`, 100),
416
+ ...midpoint === void 0 ? {} : { midpoint },
417
+ isNotched: false,
418
+ isWrapping: false,
419
+ min: this.numberAttribute(`${axis}-min`, 0),
420
+ step: this.numberAttribute(`${axis}-step`, 1),
421
+ taper: this.#taper(axis)
422
+ };
423
+ }
474
424
  #taper(axis) {
475
425
  return this.getAttribute(`${axis}-taper`) === "log" ? "log" : "linear";
476
426
  }
@@ -478,14 +428,12 @@ var SonicXy = class extends SonicFormElement {
478
428
  return `${this.#axisText(axis)}, ${this.#axisText(axis === "x" ? "y" : "x")}`;
479
429
  }
480
430
  #write(axis, next) {
481
- if (this.#pointerDrag?.current()) return;
482
- const axisValue = this.#values[axis];
431
+ if (this.#gesture?.current()) return;
483
432
  if (next === void 0 || next === null) {
484
- axisValue.asked = void 0;
485
- this.#refresh();
433
+ this.#respec(axis);
486
434
  return;
487
435
  }
488
- if (setAsked(axisValue, this.#mappings()[axis], toNumber(next))) this.render();
436
+ if (this.#models[axis].write(toNumber(next))) this.render();
489
437
  }
490
438
  };
491
439
 
@@ -10,10 +10,11 @@ import { SonicSpectrum } from "./elements/spectrum.js";
10
10
  import { SonicSplit } from "./elements/split.js";
11
11
  import { SonicSwitch } from "./elements/switch.js";
12
12
  import { SonicToggle } from "./elements/toggle.js";
13
+ import { formatClock, parseClock } from "./lib/clock.js";
13
14
  import { SonicWaveform } from "./elements/waveform.js";
14
15
  import { SonicWavestrip } from "./elements/wavestrip.js";
15
16
  import { SonicXy } from "./elements/xy.js";
16
17
  import { crossfadeGains } from "./lib/crossfade.js";
17
18
  import { formatPercent, parsePercent } from "./lib/percent.js";
18
19
 
19
- export { SonicButton, SonicDial, SonicEnvelope, SonicMeter, SonicNumber, SonicSegmented, SonicSlider, SonicSpectrum, SonicSplit, SonicSwitch, SonicToggle, SonicWaveform, SonicWavestrip, SonicXy, crossfadeGains, envelopeCurve, formatPercent, parsePercent };
20
+ export { SonicButton, SonicDial, SonicEnvelope, SonicMeter, SonicNumber, SonicSegmented, SonicSlider, SonicSpectrum, SonicSplit, SonicSwitch, SonicToggle, SonicWaveform, SonicWavestrip, SonicXy, crossfadeGains, envelopeCurve, formatClock, formatPercent, parseClock, parsePercent };
@@ -22,6 +22,7 @@ var CanvasSurface = class {
22
22
  }
23
23
  #context;
24
24
  #cssBox;
25
+ #fillWatch;
25
26
  #frame;
26
27
  #isDirty = true;
27
28
  #isRebuildWanted = true;
@@ -51,6 +52,7 @@ var CanvasSurface = class {
51
52
  signal.addEventListener("abort", () => {
52
53
  if (this.#frame !== void 0) cancelAnimationFrame(this.#frame);
53
54
  this.#frame = void 0;
55
+ this.#stopFill();
54
56
  }, { once: true });
55
57
  this.#schedule();
56
58
  }
@@ -65,6 +67,25 @@ var CanvasSurface = class {
65
67
  requestFrame() {
66
68
  this.#schedule();
67
69
  }
70
+ setFill(isFilling) {
71
+ const { fill, signal } = this.#options;
72
+ if (!fill || signal.aborted || isFilling === (this.#fillWatch !== void 0)) return;
73
+ const { style } = fill.control;
74
+ this.#stopFill();
75
+ if (!isFilling) {
76
+ style.removeProperty(fill.sizeProperty);
77
+ return;
78
+ }
79
+ const started = new ResizeObserver((entries) => {
80
+ const box = entries.at(-1)?.borderBoxSize[0];
81
+ if (!box) return;
82
+ requestAnimationFrame(() => {
83
+ if (this.#fillWatch === started) style.setProperty(fill.sizeProperty, `${String(box.blockSize)}px`);
84
+ });
85
+ });
86
+ this.#fillWatch = started;
87
+ started.observe(fill.control);
88
+ }
68
89
  #armTransitions() {
69
90
  const { canvas, colours, numbers = {} } = this.#options;
70
91
  const { style } = canvas;
@@ -190,6 +211,10 @@ var CanvasSurface = class {
190
211
  this.#draw(frameMs);
191
212
  });
192
213
  }
214
+ #stopFill() {
215
+ this.#fillWatch?.disconnect();
216
+ this.#fillWatch = void 0;
217
+ }
193
218
  #watchResolution() {
194
219
  const { signal } = this.#options;
195
220
  matchMedia(`(resolution: ${String(deviceRatio())}dppx)`).addEventListener("change", () => {
@@ -0,0 +1,23 @@
1
+ //#region src/lib/clock.ts
2
+ const sign = /^[-−]/;
3
+ function formatClock(seconds) {
4
+ const total = Math.floor(Math.abs(seconds));
5
+ const hours = Math.floor(total / 3600);
6
+ const minutes = Math.floor(total / 60) % 60;
7
+ const rest = String(total % 60).padStart(2, "0");
8
+ const prefix = seconds < 0 ? "−" : "";
9
+ if (hours === 0) return `${prefix}${String(minutes)}:${rest}`;
10
+ return `${prefix}${String(hours)}:${String(minutes).padStart(2, "0")}:${rest}`;
11
+ }
12
+ function parseClock(text) {
13
+ const clock = text.trim();
14
+ let seconds = 0;
15
+ for (const part of clock.replace(sign, "").split(":")) {
16
+ if (part.trim() === "") return NaN;
17
+ seconds = seconds * 60 + Number(part);
18
+ }
19
+ return sign.test(clock) ? -seconds : seconds;
20
+ }
21
+
22
+ //#endregion
23
+ export { formatClock, parseClock };
@@ -5,6 +5,7 @@ function progress(side, span) {
5
5
  if (span === 0) return side > 0 ? 1 : 0;
6
6
  return Math.min(1, side / span);
7
7
  }
8
+ /** `position` runs from 0, all a, to 1, all b; `sharpness` from 0 to 1 shortens the overlap */
8
9
  function crossfadeGains(position, options = {}) {
9
10
  const at = clampProportion(position);
10
11
  const span = 1 - clampProportion(options.sharpness ?? 0);
@@ -1,5 +1,6 @@
1
1
  //#region src/lib/envelope-curve.ts
2
2
  const flatCurve = .001;
3
+ /** `position` is 0 to 1 along a stage; a `curve` of 0 is a straight line */
3
4
  function envelopeCurve(position, curve) {
4
5
  if (Math.abs(curve) < flatCurve) return position;
5
6
  return Math.expm1(curve * position) / Math.expm1(curve);
@@ -0,0 +1,44 @@
1
+ //#region src/lib/peaks-request.ts
2
+ function createPeaksRequest(repaint) {
3
+ const awaited = /* @__PURE__ */ new WeakSet();
4
+ let askedKey;
5
+ let isAsking = false;
6
+ const changed = () => {
7
+ if (!isAsking) askedKey = void 0;
8
+ repaint();
9
+ };
10
+ const changedAfter = async (asked) => {
11
+ await Promise.allSettled([asked]);
12
+ changed();
13
+ };
14
+ const call = (request, wanted) => {
15
+ isAsking = true;
16
+ try {
17
+ return request?.(...wanted);
18
+ } finally {
19
+ isAsking = false;
20
+ }
21
+ };
22
+ return {
23
+ ask: (request, wanted) => {
24
+ const key = wanted?.join(":");
25
+ if (!wanted || key === askedKey) return;
26
+ askedKey = key;
27
+ const asked = call(request, wanted);
28
+ const promises = asked instanceof Promise ? [asked] : asked ?? [];
29
+ for (const promise of promises) {
30
+ if (awaited.has(promise)) continue;
31
+ awaited.add(promise);
32
+ changedAfter(promise);
33
+ }
34
+ },
35
+ changed,
36
+ reset: () => {
37
+ askedKey = void 0;
38
+ repaint();
39
+ }
40
+ };
41
+ }
42
+
43
+ //#endregion
44
+ export { createPeaksRequest };
@@ -0,0 +1,58 @@
1
+ import { clamp } from "./math.js";
2
+
3
+ //#region src/lib/positions.ts
4
+ const keySteps = /* @__PURE__ */ new Map([
5
+ ["ArrowDown", 1],
6
+ ["ArrowLeft", -1],
7
+ ["ArrowRight", 1],
8
+ ["ArrowUp", -1]
9
+ ]);
10
+ function indexOrUndefined(index) {
11
+ return index < 0 ? void 0 : index;
12
+ }
13
+ function isEnabled(position) {
14
+ return !position.isDisabled;
15
+ }
16
+ function enabledEnds(positions) {
17
+ return [positions.findIndex(isEnabled), positions.findLastIndex(isEnabled)];
18
+ }
19
+ function stepFrom(positions, from, { isWrapping, step }) {
20
+ const count = positions.length;
21
+ let index = from;
22
+ for (const _ of positions) {
23
+ const next = index + step;
24
+ index = isWrapping ? (next + count) % count : clamp(next, 0, count - 1);
25
+ if (positions[index]?.isDisabled === false) return index;
26
+ }
27
+ }
28
+ function keyTarget(positions, from, { isWrapping, key }) {
29
+ const [first, last] = enabledEnds(positions);
30
+ if (key === "Home") return indexOrUndefined(first);
31
+ if (key === "End") return indexOrUndefined(last);
32
+ const step = keySteps.get(key);
33
+ if (step === void 0) return void 0;
34
+ return stepFrom(positions, from, {
35
+ isWrapping,
36
+ step
37
+ }) ?? from;
38
+ }
39
+ function isMomentaryAt(positions, index) {
40
+ return (index === 0 || index === positions.length - 1) && positions.length > 1 && positions[index]?.isMomentary === true;
41
+ }
42
+ function pressTarget(positions, checked, pressed) {
43
+ return positions.length === 2 && checked >= 0 ? 1 - checked : pressed;
44
+ }
45
+ function soleStep(positions, from) {
46
+ const ways = [-1, 1].map((step) => stepFrom(positions, from, {
47
+ isWrapping: false,
48
+ step
49
+ })).filter((to) => to !== void 0 && to !== from);
50
+ return ways.length === 1 ? ways[0] : void 0;
51
+ }
52
+ function restOf(positions, held) {
53
+ if (!isMomentaryAt(positions, held)) return void 0;
54
+ return held === 0 ? 1 : held - 1;
55
+ }
56
+
57
+ //#endregion
58
+ export { enabledEnds, isMomentaryAt, keyTarget, pressTarget, restOf, soleStep, stepFrom };
@@ -0,0 +1,12 @@
1
+ //#region src/lib/scrub.ts
2
+ function scrubRegions(state, value, mapping) {
3
+ const played = mapping.proportionOf(state.played);
4
+ const reached = mapping.proportionOf(value);
5
+ return {
6
+ played,
7
+ scrub: state.from !== void 0 && state.isRevealed && reached !== played ? reached : void 0
8
+ };
9
+ }
10
+
11
+ //#endregion
12
+ export { scrubRegions };
@@ -0,0 +1,30 @@
1
+ //#region src/lib/spoken-duration.ts
2
+ const formatters = /* @__PURE__ */ new Map();
3
+ function formatterFor(language, isZero) {
4
+ const key = `${language}|${String(isZero)}`;
5
+ const cached = formatters.get(key);
6
+ if (cached) return cached;
7
+ const options = {
8
+ secondsDisplay: isZero ? "always" : "auto",
9
+ style: "long"
10
+ };
11
+ let formatter;
12
+ try {
13
+ formatter = new Intl.DurationFormat(language || void 0, options);
14
+ } catch {
15
+ formatter = new Intl.DurationFormat(void 0, options);
16
+ }
17
+ formatters.set(key, formatter);
18
+ return formatter;
19
+ }
20
+ function spokenDuration(seconds, language = "") {
21
+ const total = Math.trunc(seconds);
22
+ return formatterFor(language, total === 0).format({
23
+ hours: Math.trunc(total / 3600),
24
+ minutes: Math.trunc(total / 60) % 60,
25
+ seconds: total % 60
26
+ });
27
+ }
28
+
29
+ //#endregion
30
+ export { spokenDuration };