@xsynaptic/sonic-ui 0.17.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +112 -19
  3. package/dist/default/dat.js +1 -0
  4. package/dist/default/elements/button.js +2 -0
  5. package/dist/default/elements/dial.js +2 -0
  6. package/dist/default/elements/display-element.js +9 -7
  7. package/dist/default/elements/envelope.js +31 -72
  8. package/dist/default/elements/field-gesture.js +81 -0
  9. package/dist/default/elements/meter.js +4 -0
  10. package/dist/default/elements/position-group.js +42 -26
  11. package/dist/default/elements/radio-group.js +59 -56
  12. package/dist/default/elements/scale.js +3 -3
  13. package/dist/default/elements/slider.js +11 -13
  14. package/dist/default/elements/split.js +14 -13
  15. package/dist/default/elements/switch.js +31 -46
  16. package/dist/default/elements/toggle.js +20 -44
  17. package/dist/default/elements/value-element.js +65 -91
  18. package/dist/default/elements/value-gestures.js +22 -22
  19. package/dist/default/elements/wave-element.js +9 -12
  20. package/dist/default/elements/waveform.js +42 -241
  21. package/dist/default/elements/wavestrip.js +10 -5
  22. package/dist/default/elements/xy.js +67 -112
  23. package/dist/default/lib/canvas-surface.js +25 -0
  24. package/dist/default/lib/crossfade.js +1 -0
  25. package/dist/default/lib/envelope-curve.js +1 -0
  26. package/dist/default/lib/peaks-request.js +44 -0
  27. package/dist/default/lib/positions.js +58 -0
  28. package/dist/default/lib/scrub.js +12 -0
  29. package/dist/default/lib/value-model.js +62 -0
  30. package/dist/default/lib/waveform-scene.js +108 -0
  31. package/dist/default/lib/zoom-gesture.js +97 -1
  32. package/dist/dev/dat.d.ts +1 -0
  33. package/dist/dev/dat.js +1 -0
  34. package/dist/dev/dat.js.map +1 -1
  35. package/dist/dev/elements/button.d.ts +2 -0
  36. package/dist/dev/elements/button.js +2 -0
  37. package/dist/dev/elements/button.js.map +1 -1
  38. package/dist/dev/elements/dial.d.ts +2 -0
  39. package/dist/dev/elements/dial.js +2 -0
  40. package/dist/dev/elements/dial.js.map +1 -1
  41. package/dist/dev/elements/display-element.d.ts +5 -3
  42. package/dist/dev/elements/display-element.js +9 -7
  43. package/dist/dev/elements/display-element.js.map +1 -1
  44. package/dist/dev/elements/envelope.js +31 -72
  45. package/dist/dev/elements/envelope.js.map +1 -1
  46. package/dist/dev/elements/field-gesture.js +82 -0
  47. package/dist/dev/elements/field-gesture.js.map +1 -0
  48. package/dist/dev/elements/meter.d.ts +10 -5
  49. package/dist/dev/elements/meter.js +4 -0
  50. package/dist/dev/elements/meter.js.map +1 -1
  51. package/dist/dev/elements/position-group.d.ts +12 -6
  52. package/dist/dev/elements/position-group.js +42 -26
  53. package/dist/dev/elements/position-group.js.map +1 -1
  54. package/dist/dev/elements/radio-group.d.ts +11 -7
  55. package/dist/dev/elements/radio-group.js +59 -56
  56. package/dist/dev/elements/radio-group.js.map +1 -1
  57. package/dist/dev/elements/scale.js +3 -3
  58. package/dist/dev/elements/scale.js.map +1 -1
  59. package/dist/dev/elements/slider.d.ts +4 -2
  60. package/dist/dev/elements/slider.js +11 -13
  61. package/dist/dev/elements/slider.js.map +1 -1
  62. package/dist/dev/elements/split.d.ts +1 -0
  63. package/dist/dev/elements/split.js +14 -13
  64. package/dist/dev/elements/split.js.map +1 -1
  65. package/dist/dev/elements/switch.d.ts +2 -2
  66. package/dist/dev/elements/switch.js +31 -46
  67. package/dist/dev/elements/switch.js.map +1 -1
  68. package/dist/dev/elements/toggle.js +20 -44
  69. package/dist/dev/elements/toggle.js.map +1 -1
  70. package/dist/dev/elements/value-element.d.ts +5 -5
  71. package/dist/dev/elements/value-element.js +65 -91
  72. package/dist/dev/elements/value-element.js.map +1 -1
  73. package/dist/dev/elements/value-gestures.js +22 -22
  74. package/dist/dev/elements/value-gestures.js.map +1 -1
  75. package/dist/dev/elements/wave-element.d.ts +6 -4
  76. package/dist/dev/elements/wave-element.js +9 -12
  77. package/dist/dev/elements/wave-element.js.map +1 -1
  78. package/dist/dev/elements/waveform.d.ts +19 -21
  79. package/dist/dev/elements/waveform.js +42 -241
  80. package/dist/dev/elements/waveform.js.map +1 -1
  81. package/dist/dev/elements/wavestrip.d.ts +5 -1
  82. package/dist/dev/elements/wavestrip.js +10 -5
  83. package/dist/dev/elements/wavestrip.js.map +1 -1
  84. package/dist/dev/elements/xy.js +67 -112
  85. package/dist/dev/elements/xy.js.map +1 -1
  86. package/dist/dev/index.d.ts +2 -1
  87. package/dist/dev/lib/canvas-surface.d.ts +1 -0
  88. package/dist/dev/lib/canvas-surface.js +25 -0
  89. package/dist/dev/lib/canvas-surface.js.map +1 -1
  90. package/dist/dev/lib/crossfade.d.ts +1 -0
  91. package/dist/dev/lib/crossfade.js +1 -0
  92. package/dist/dev/lib/crossfade.js.map +1 -1
  93. package/dist/dev/lib/envelope-curve.d.ts +1 -0
  94. package/dist/dev/lib/envelope-curve.js +1 -0
  95. package/dist/dev/lib/envelope-curve.js.map +1 -1
  96. package/dist/dev/lib/peaks-request.d.ts +5 -0
  97. package/dist/dev/lib/peaks-request.js +45 -0
  98. package/dist/dev/lib/peaks-request.js.map +1 -0
  99. package/dist/dev/lib/positions.d.ts +7 -0
  100. package/dist/dev/lib/positions.js +59 -0
  101. package/dist/dev/lib/positions.js.map +1 -0
  102. package/dist/dev/lib/scrub.d.ts +8 -0
  103. package/dist/dev/lib/scrub.js +13 -0
  104. package/dist/dev/lib/scrub.js.map +1 -0
  105. package/dist/dev/lib/time-regions.d.ts +1 -0
  106. package/dist/dev/lib/time-regions.js.map +1 -1
  107. package/dist/dev/lib/units.d.ts +8 -0
  108. package/dist/dev/lib/value-model.js +63 -0
  109. package/dist/dev/lib/value-model.js.map +1 -0
  110. package/dist/dev/lib/waveform-buckets.d.ts +1 -0
  111. package/dist/dev/lib/waveform-buckets.js.map +1 -1
  112. package/dist/dev/lib/waveform-scene.d.ts +11 -0
  113. package/dist/dev/lib/waveform-scene.js +109 -0
  114. package/dist/dev/lib/waveform-scene.js.map +1 -0
  115. package/dist/dev/lib/zoom-gesture.js +97 -1
  116. package/dist/dev/lib/zoom-gesture.js.map +1 -1
  117. package/dist/styles/button.css +16 -6
  118. package/dist/styles/waveform.css +2 -1
  119. package/package.json +1 -1
  120. package/dist/default/lib/asked-value.js +0 -21
  121. package/dist/default/lib/fill-watch.js +0 -29
  122. package/dist/dev/lib/asked-value.js +0 -22
  123. package/dist/dev/lib/asked-value.js.map +0 -1
  124. package/dist/dev/lib/fill-watch.js +0 -30
  125. package/dist/dev/lib/fill-watch.js.map +0 -1
@@ -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,41 +197,28 @@ 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;
@@ -254,11 +240,11 @@ var SonicXy = class extends SonicFormElement {
254
240
  render() {
255
241
  if (!this.isBound()) return;
256
242
  const { style } = this.#xy;
257
- const mappings = this.#mappings();
258
243
  for (const axis of axes) {
259
- const mapping = mappings[axis];
244
+ const model = this.#models[axis];
245
+ const mapping = model.mapping();
260
246
  const origin = this.optionalNumberAttribute(`${axis}-origin`) ?? mapping.bounds[0];
261
- style.setProperty(`--_sonic-xy-${axis}`, String(mapping.proportionOf(this.#values[axis].value)));
247
+ style.setProperty(`--_sonic-xy-${axis}`, String(mapping.proportionOf(model.value)));
262
248
  style.setProperty(`--_sonic-xy-${axis}-origin`, String(mapping.proportionOf(origin)));
263
249
  }
264
250
  this.#renderAria();
@@ -271,7 +257,7 @@ var SonicXy = class extends SonicFormElement {
271
257
  this.y = y;
272
258
  }
273
259
  #axisText(axis) {
274
- const { value } = this.#values[axis];
260
+ const { value } = this.#models[axis];
275
261
  return `${this.getAttribute(`${axis}-label`) ?? axis.toUpperCase()} ${this.#formatValue?.(value, axis) ?? String(value)}`;
276
262
  }
277
263
  #bindKeys(xy, signal) {
@@ -297,32 +283,22 @@ var SonicXy = class extends SonicFormElement {
297
283
  xy.addEventListener("mousedown", (event) => {
298
284
  event.preventDefault();
299
285
  }, { signal });
300
- this.#pointerDrag = bindDrag(xy, {
301
- cancel: (drag) => {
302
- this.#input(drag.from);
303
- },
286
+ this.#gesture = bindFieldGesture(xy, {
287
+ claim: this.#claim,
304
288
  grab: (event) => {
305
289
  if (this.isDisabled() || isMenuPress(event)) return;
306
290
  focusByPointer(this.focusTarget());
307
- this.#pointerType = event.pointerType;
308
291
  if (!isResetPress(event)) {
309
292
  this.toggleState("dragging", true);
310
293
  return this.#grab(event);
311
294
  }
312
295
  this.#reset();
313
- this.#pointerType = void 0;
314
296
  },
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();
297
+ input: (_hold, next) => {
298
+ if (!this.#input(next)) this.#renderReadout();
320
299
  },
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;
300
+ release: (_hold, moved) => {
301
+ if (moved.length > 0) this.dispatchEvent(new Event("change", { bubbles: true }));
326
302
  },
327
303
  toggle: (isDragging) => {
328
304
  this.toggleState("dragging", isDragging);
@@ -333,13 +309,13 @@ var SonicXy = class extends SonicFormElement {
333
309
  if (this.#input(next)) this.dispatchEvent(new Event("change", { bubbles: true }));
334
310
  }
335
311
  #grab(event) {
336
- const from = {
337
- x: this.x,
338
- y: this.y
339
- };
340
312
  const field = this.#field.getBoundingClientRect();
341
313
  const puck = this.#puck.getBoundingClientRect();
342
- const mappings = this.#mappings();
314
+ const { x, y } = this.#models;
315
+ const from = {
316
+ x: x.value,
317
+ y: y.value
318
+ };
343
319
  const travel = {
344
320
  x: Math.max(1, field.width - puck.width),
345
321
  y: Math.max(1, field.height - puck.height)
@@ -351,32 +327,26 @@ var SonicXy = class extends SonicFormElement {
351
327
  };
352
328
  const isOnPuck = event.target instanceof Node && this.#puck.contains(event.target);
353
329
  if (!isOnPuck) this.#input({
354
- x: mappings.x.valueAt(pressed.x),
355
- y: mappings.y.valueAt(pressed.y)
330
+ x: x.mapping().valueAt(pressed.x),
331
+ y: y.mapping().valueAt(pressed.y)
356
332
  });
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]),
333
+ const hold = (axis) => ({
334
+ from: from[axis],
335
+ model: this.#models[axis],
336
+ ...isOnPuck ? {} : { proportion: clampProportion(pressed[axis]) },
362
337
  travelPx: travel[axis]
363
338
  });
364
- return {
365
- from,
366
- state: startFieldDrag({
367
- position,
368
- thresholdPx: dragThresholdPx(event.pointerType),
369
- x: start("x"),
370
- y: start("y")
371
- })
372
- };
339
+ return { axes: {
340
+ x: hold("x"),
341
+ y: hold("y")
342
+ } };
373
343
  }
374
344
  #hasDefault() {
375
345
  return this.xDefault !== void 0 || this.yDefault !== void 0;
376
346
  }
377
347
  #input(next) {
378
348
  let hasMoved = false;
379
- for (const axis of axes) if (moveTo(this.#values[axis], this.#mappings()[axis], next[axis] ?? NaN)) hasMoved = true;
349
+ for (const axis of axes) if (this.#models[axis].input(next[axis] ?? NaN)) hasMoved = true;
380
350
  if (!hasMoved) return false;
381
351
  this.render();
382
352
  this.dispatchEvent(new Event("input", {
@@ -386,9 +356,8 @@ var SonicXy = class extends SonicFormElement {
386
356
  return true;
387
357
  }
388
358
  #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);
359
+ const model = this.#models[axis];
360
+ const hasMoved = model.input(model.mapping().keyTarget(key, model.value) ?? NaN);
392
361
  this.#currentAxis = axis;
393
362
  this.render();
394
363
  this.#parts[axis].focus();
@@ -399,46 +368,18 @@ var SonicXy = class extends SonicFormElement {
399
368
  }));
400
369
  this.dispatchEvent(new Event("change", { bubbles: true }));
401
370
  }
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
371
  #renderAria() {
431
- const mappings = this.#mappings();
432
372
  const isDisabled = this.isDisabled();
433
373
  for (const axis of axes) {
434
374
  const part = this.#parts[axis];
435
- const [low, high] = mappings[axis].bounds;
375
+ const model = this.#models[axis];
376
+ const [low, high] = model.mapping().bounds;
436
377
  const isCurrent = axis === this.#currentAxis;
437
378
  const stop = isCurrent ? "0" : "-1";
438
379
  writeAttribute(part, "aria-orientation", orientations[axis]);
439
380
  writeAttribute(part, "aria-valuemin", String(low));
440
381
  writeAttribute(part, "aria-valuemax", String(high));
441
- writeAttribute(part, "aria-valuenow", String(this.#values[axis].value));
382
+ writeAttribute(part, "aria-valuenow", String(model.value));
442
383
  writeAttribute(part, "aria-valuetext", this.#valueText(axis));
443
384
  this.forwardNaming(part, true);
444
385
  writeAttribute(part, "aria-hidden", isCurrent ? void 0 : "true");
@@ -471,6 +412,22 @@ var SonicXy = class extends SonicFormElement {
471
412
  y: this.yDefault
472
413
  });
473
414
  }
415
+ #respec(...forgotten) {
416
+ for (const axis of axes) this.#models[axis].respec(this.numberAttribute(axis, this.numberAttribute(`${axis}-min`, 0)), { forgetAsk: forgotten.includes(axis) });
417
+ this.render();
418
+ }
419
+ #spec(axis) {
420
+ const midpoint = this.optionalNumberAttribute(`${axis}-midpoint`);
421
+ return {
422
+ max: this.numberAttribute(`${axis}-max`, 100),
423
+ ...midpoint === void 0 ? {} : { midpoint },
424
+ isNotched: false,
425
+ isWrapping: false,
426
+ min: this.numberAttribute(`${axis}-min`, 0),
427
+ step: this.numberAttribute(`${axis}-step`, 1),
428
+ taper: this.#taper(axis)
429
+ };
430
+ }
474
431
  #taper(axis) {
475
432
  return this.getAttribute(`${axis}-taper`) === "log" ? "log" : "linear";
476
433
  }
@@ -478,14 +435,12 @@ var SonicXy = class extends SonicFormElement {
478
435
  return `${this.#axisText(axis)}, ${this.#axisText(axis === "x" ? "y" : "x")}`;
479
436
  }
480
437
  #write(axis, next) {
481
- if (this.#pointerDrag?.current()) return;
482
- const axisValue = this.#values[axis];
438
+ if (this.#gesture?.current()) return;
483
439
  if (next === void 0 || next === null) {
484
- axisValue.asked = void 0;
485
- this.#refresh();
440
+ this.#respec(axis);
486
441
  return;
487
442
  }
488
- if (setAsked(axisValue, this.#mappings()[axis], toNumber(next))) this.render();
443
+ if (this.#models[axis].write(toNumber(next))) this.render();
489
444
  }
490
445
  };
491
446
 
@@ -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", () => {
@@ -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,62 @@
1
+ import { clamp } from "./math.js";
2
+ import { valueMapping } from "./value-mapping.js";
3
+
4
+ //#region src/lib/value-model.ts
5
+ function createValueModel(readSpec) {
6
+ const listeners = /* @__PURE__ */ new Set();
7
+ let asked;
8
+ let bounds;
9
+ let landing;
10
+ let mapped;
11
+ let value = 0;
12
+ function mapping() {
13
+ if (mapped) return mapped;
14
+ mapped = valueMapping(readSpec());
15
+ return mapped;
16
+ }
17
+ function moveTo(next) {
18
+ if (!Number.isFinite(next)) return false;
19
+ const snapped = mapping().snap(next);
20
+ asked = snapped;
21
+ if (snapped === value) return false;
22
+ value = snapped;
23
+ return true;
24
+ }
25
+ return {
26
+ input: (next) => moveTo(bounds ? clamp(next, ...bounds) : next),
27
+ land: (target, direction) => landing?.(target, direction) ?? target,
28
+ limit: () => bounds,
29
+ mapping,
30
+ notify: () => {
31
+ for (const listener of listeners) listener();
32
+ },
33
+ respec: (fallback, options) => {
34
+ mapped = void 0;
35
+ if (options?.forgetAsk === true) asked = void 0;
36
+ value = mapping().snap(asked ?? fallback);
37
+ },
38
+ setLanding: (resolve) => {
39
+ landing = resolve;
40
+ },
41
+ setLimit: (next) => {
42
+ bounds = next;
43
+ },
44
+ get value() {
45
+ return value;
46
+ },
47
+ watch: (listener) => {
48
+ listeners.add(listener);
49
+ return () => {
50
+ listeners.delete(listener);
51
+ };
52
+ },
53
+ write: (next) => {
54
+ const hasMoved = moveTo(next);
55
+ if (Number.isFinite(next)) asked = next;
56
+ return hasMoved;
57
+ }
58
+ };
59
+ }
60
+
61
+ //#endregion
62
+ export { createValueModel };
@@ -0,0 +1,108 @@
1
+ import { clamp } from "./math.js";
2
+ import { isKind } from "./marker-band.js";
3
+ import { waveformBuckets } from "./waveform-buckets.js";
4
+
5
+ //#region src/lib/waveform-scene.ts
6
+ const waveformColours = {
7
+ ends: "--_sonic-waveform-ends",
8
+ grid: "--_sonic-waveform-grid",
9
+ marker: "--_sonic-waveform-marker",
10
+ placeholder: "--_sonic-waveform-placeholder",
11
+ wave: "--_sonic-waveform-wave",
12
+ waveEdge: "--_sonic-waveform-wave-edge"
13
+ };
14
+ const amplitudeMargin = .94;
15
+ const hatchPitchPx = 8;
16
+ const placeholderWavelengthPx = 48;
17
+ const placeholderAmplitude = .2;
18
+ function paintHatch(context, scene, [from, to]) {
19
+ if (to <= from) return;
20
+ const { dpr, height } = scene;
21
+ const pitch = Math.max(2, Math.round(hatchPitchPx * dpr));
22
+ const offset = scene.startSeconds * scene.pixelsPerSecond;
23
+ context.save();
24
+ context.beginPath();
25
+ context.rect(from, 0, to - from, height);
26
+ context.clip();
27
+ context.beginPath();
28
+ for (let x = Math.floor((from - height + offset) / pitch) * pitch - offset; x < to; x += pitch) {
29
+ context.moveTo(x, height);
30
+ context.lineTo(x + height, 0);
31
+ }
32
+ context.lineWidth = Math.max(1, Math.round(dpr));
33
+ context.strokeStyle = scene.colours.ends;
34
+ context.stroke();
35
+ context.restore();
36
+ }
37
+ function paintPlaceholders(context, scene) {
38
+ if (scene.pending.length === 0) return;
39
+ const { dpr, height, pixelsPerSecond, startSeconds } = scene;
40
+ const wavelength = placeholderWavelengthPx * dpr;
41
+ const center = height / 2;
42
+ const origin = startSeconds * pixelsPerSecond;
43
+ const waveY = (x) => center - Math.sin(((x + origin) / wavelength + scene.phase) * 2 * Math.PI) * center * placeholderAmplitude;
44
+ context.beginPath();
45
+ for (const [from, to] of scene.pending) {
46
+ const opening = (from - startSeconds) * pixelsPerSecond;
47
+ const closing = (to - startSeconds) * pixelsPerSecond;
48
+ context.moveTo(opening, waveY(opening));
49
+ for (let x = opening + dpr; x < closing; x += dpr) context.lineTo(x, waveY(x));
50
+ context.lineTo(closing, waveY(closing));
51
+ }
52
+ context.lineWidth = Math.max(1, Math.round(dpr));
53
+ context.strokeStyle = scene.colours.placeholder;
54
+ context.stroke();
55
+ }
56
+ function waveFill(context, scene) {
57
+ const { wave, waveEdge } = scene.colours;
58
+ const gradient = context.createLinearGradient(0, 0, 0, scene.height);
59
+ gradient.addColorStop(0, waveEdge);
60
+ gradient.addColorStop(.5, wave);
61
+ gradient.addColorStop(1, waveEdge);
62
+ return gradient;
63
+ }
64
+ function paintWave(context, scene, peaks) {
65
+ const buckets = waveformBuckets(peaks, scene);
66
+ const [first] = buckets;
67
+ if (!first) return;
68
+ const center = scene.height / 2;
69
+ const amplitudePx = center * amplitudeMargin;
70
+ context.fillStyle = waveFill(context, scene);
71
+ context.beginPath();
72
+ context.moveTo(first.x, center);
73
+ for (const bucket of buckets) context.lineTo(bucket.x, center - bucket.high * amplitudePx);
74
+ for (const bucket of buckets.toReversed()) context.lineTo(bucket.x, center - bucket.low * amplitudePx);
75
+ context.closePath();
76
+ context.fill();
77
+ }
78
+ function paintMarkers(context, scene, drawn) {
79
+ const line = Math.max(1, Math.round(scene.dpr));
80
+ for (const { dimmed, kind, start } of drawn.markers) {
81
+ const x = (start - scene.startSeconds) * scene.pixelsPerSecond;
82
+ context.fillStyle = (isKind(kind) ? drawn.kindColours.get(kind) : void 0) ?? scene.colours.marker;
83
+ context.globalAlpha = dimmed === true ? .5 : 1;
84
+ context.fillRect(x - line / 2, 0, line, scene.height);
85
+ }
86
+ context.globalAlpha = 1;
87
+ }
88
+ function paintWaveform(context, scene, drawn) {
89
+ const { dpr, height, pixelsPerSecond, startSeconds, width } = scene;
90
+ const x = (seconds) => (seconds - startSeconds) * pixelsPerSecond;
91
+ const { ends, grid } = scene.colours;
92
+ const line = Math.max(1, Math.round(dpr));
93
+ const [opening, closing] = [x(drawn.range[0]), x(drawn.range[1])];
94
+ const [from, to] = [clamp(opening, 0, width), clamp(closing, 0, width)];
95
+ context.clearRect(0, 0, width, height);
96
+ paintHatch(context, scene, [0, from]);
97
+ paintHatch(context, scene, [to, width]);
98
+ context.fillStyle = grid;
99
+ if (to > from) context.fillRect(from, Math.round((height - line) / 2), to - from, line);
100
+ paintPlaceholders(context, scene);
101
+ if (drawn.peaks) paintWave(context, scene, drawn.peaks);
102
+ context.fillStyle = ends;
103
+ for (const edge of [opening, closing]) context.fillRect(edge - line / 2, 0, line, height);
104
+ paintMarkers(context, scene, drawn);
105
+ }
106
+
107
+ //#endregion
108
+ export { paintWaveform, waveformColours };