@xsynaptic/sonic-ui 0.5.0 → 0.6.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 (138) hide show
  1. package/README.md +3 -1
  2. package/dist/default/dat.js +5 -4
  3. package/dist/default/define/button.js +1 -1
  4. package/dist/default/define.js +2 -2
  5. package/dist/default/elements/button.js +6 -5
  6. package/dist/default/elements/dial.js +5 -1
  7. package/dist/default/elements/envelope.js +46 -18
  8. package/dist/default/elements/meter.js +12 -8
  9. package/dist/default/elements/number.js +1 -1
  10. package/dist/default/elements/radio-group.js +46 -47
  11. package/dist/default/elements/readout-claim.js +0 -29
  12. package/dist/default/elements/readout.js +1 -1
  13. package/dist/default/elements/scale.js +43 -0
  14. package/dist/default/elements/slider.js +16 -3
  15. package/dist/default/elements/sonic-element.js +17 -55
  16. package/dist/default/elements/switch.js +33 -39
  17. package/dist/default/elements/value-element.js +142 -187
  18. package/dist/default/elements/wave-element.js +5 -2
  19. package/dist/default/elements/waveform.js +35 -12
  20. package/dist/default/elements/wavestrip.js +59 -18
  21. package/dist/default/elements/xy.js +34 -19
  22. package/dist/default/index.js +2 -2
  23. package/dist/default/lib/adsr-shape.js +5 -0
  24. package/dist/default/lib/ballistics.js +2 -2
  25. package/dist/default/lib/canvas-surface.js +13 -2
  26. package/dist/default/lib/distribute.js +1 -1
  27. package/dist/default/lib/drag-step.js +4 -4
  28. package/dist/default/lib/frame-timeline.js +2 -1
  29. package/dist/default/lib/marker-band.js +54 -10
  30. package/dist/default/lib/marker-rider.js +10 -4
  31. package/dist/default/lib/math.js +6 -1
  32. package/dist/default/lib/mirror-children.js +64 -0
  33. package/dist/default/lib/owned-control.js +10 -0
  34. package/dist/default/lib/pointer-drag.js +8 -5
  35. package/dist/default/lib/resample-peaks.js +11 -1
  36. package/dist/default/lib/time-regions.js +4 -1
  37. package/dist/default/lib/write-attribute.js +1 -0
  38. package/dist/dev/dat.d.ts +2 -2
  39. package/dist/dev/dat.js +5 -4
  40. package/dist/dev/dat.js.map +1 -1
  41. package/dist/dev/define/button.js +1 -1
  42. package/dist/dev/define.d.ts +4 -4
  43. package/dist/dev/define.js +2 -2
  44. package/dist/dev/elements/button.js +6 -5
  45. package/dist/dev/elements/button.js.map +1 -1
  46. package/dist/dev/elements/dial.d.ts +1 -1
  47. package/dist/dev/elements/dial.js +5 -1
  48. package/dist/dev/elements/dial.js.map +1 -1
  49. package/dist/dev/elements/envelope.js +46 -18
  50. package/dist/dev/elements/envelope.js.map +1 -1
  51. package/dist/dev/elements/meter.d.ts +1 -1
  52. package/dist/dev/elements/meter.js +12 -8
  53. package/dist/dev/elements/meter.js.map +1 -1
  54. package/dist/dev/elements/number.js +1 -1
  55. package/dist/dev/elements/number.js.map +1 -1
  56. package/dist/dev/elements/radio-group.js +46 -47
  57. package/dist/dev/elements/radio-group.js.map +1 -1
  58. package/dist/dev/elements/readout-claim.js +0 -29
  59. package/dist/dev/elements/readout-claim.js.map +1 -1
  60. package/dist/dev/elements/readout.js +1 -1
  61. package/dist/dev/elements/readout.js.map +1 -1
  62. package/dist/dev/elements/scale.js +44 -0
  63. package/dist/dev/elements/scale.js.map +1 -0
  64. package/dist/dev/elements/slider.d.ts +4 -1
  65. package/dist/dev/elements/slider.js +16 -3
  66. package/dist/dev/elements/slider.js.map +1 -1
  67. package/dist/dev/elements/sonic-element.d.ts +2 -8
  68. package/dist/dev/elements/sonic-element.js +18 -55
  69. package/dist/dev/elements/sonic-element.js.map +1 -1
  70. package/dist/dev/elements/switch.js +33 -39
  71. package/dist/dev/elements/switch.js.map +1 -1
  72. package/dist/dev/elements/value-element.d.ts +6 -8
  73. package/dist/dev/elements/value-element.js +142 -187
  74. package/dist/dev/elements/value-element.js.map +1 -1
  75. package/dist/dev/elements/wave-element.d.ts +1 -0
  76. package/dist/dev/elements/wave-element.js +5 -2
  77. package/dist/dev/elements/wave-element.js.map +1 -1
  78. package/dist/dev/elements/waveform.d.ts +1 -1
  79. package/dist/dev/elements/waveform.js +35 -12
  80. package/dist/dev/elements/waveform.js.map +1 -1
  81. package/dist/dev/elements/wavestrip.d.ts +4 -0
  82. package/dist/dev/elements/wavestrip.js +59 -18
  83. package/dist/dev/elements/wavestrip.js.map +1 -1
  84. package/dist/dev/elements/xy.d.ts +3 -3
  85. package/dist/dev/elements/xy.js +34 -19
  86. package/dist/dev/elements/xy.js.map +1 -1
  87. package/dist/dev/index.d.ts +2 -2
  88. package/dist/dev/index.js +2 -2
  89. package/dist/dev/lib/adsr-shape.js +5 -0
  90. package/dist/dev/lib/adsr-shape.js.map +1 -1
  91. package/dist/dev/lib/ballistics.js +2 -2
  92. package/dist/dev/lib/ballistics.js.map +1 -1
  93. package/dist/dev/lib/canvas-surface.d.ts +1 -0
  94. package/dist/dev/lib/canvas-surface.js +13 -2
  95. package/dist/dev/lib/canvas-surface.js.map +1 -1
  96. package/dist/dev/lib/distribute.js +1 -1
  97. package/dist/dev/lib/distribute.js.map +1 -1
  98. package/dist/dev/lib/drag-step.js +4 -4
  99. package/dist/dev/lib/drag-step.js.map +1 -1
  100. package/dist/dev/lib/frame-timeline.js +2 -1
  101. package/dist/dev/lib/frame-timeline.js.map +1 -1
  102. package/dist/dev/lib/marker-band.js +54 -10
  103. package/dist/dev/lib/marker-band.js.map +1 -1
  104. package/dist/dev/lib/marker-rider.js +10 -4
  105. package/dist/dev/lib/marker-rider.js.map +1 -1
  106. package/dist/dev/lib/math.js +6 -1
  107. package/dist/dev/lib/math.js.map +1 -1
  108. package/dist/dev/lib/mirror-children.js +80 -0
  109. package/dist/dev/lib/mirror-children.js.map +1 -0
  110. package/dist/dev/lib/owned-control.js +23 -0
  111. package/dist/dev/lib/owned-control.js.map +1 -0
  112. package/dist/dev/lib/pointer-drag.js +8 -5
  113. package/dist/dev/lib/pointer-drag.js.map +1 -1
  114. package/dist/dev/lib/resample-peaks.js +11 -1
  115. package/dist/dev/lib/resample-peaks.js.map +1 -1
  116. package/dist/dev/lib/time-regions.js +4 -1
  117. package/dist/dev/lib/time-regions.js.map +1 -1
  118. package/dist/dev/lib/write-attribute.js +1 -0
  119. package/dist/dev/lib/write-attribute.js.map +1 -1
  120. package/dist/skins/amber.css +8 -0
  121. package/dist/skins/flat.css +26 -0
  122. package/dist/skins/ivory.css +1 -0
  123. package/dist/skins/lime.css +2 -0
  124. package/dist/skins/slate.css +2 -0
  125. package/dist/styles/button.css +59 -10
  126. package/dist/styles/dial.css +24 -16
  127. package/dist/styles/envelope.css +58 -13
  128. package/dist/styles/led.css +20 -4
  129. package/dist/styles/material.css +143 -55
  130. package/dist/styles/meter.css +8 -4
  131. package/dist/styles/panel.css +7 -4
  132. package/dist/styles/segmented.css +27 -5
  133. package/dist/styles/slider.css +2 -1
  134. package/dist/styles/switch.css +65 -23
  135. package/dist/styles/waveform.css +28 -5
  136. package/dist/styles/wavestrip.css +23 -6
  137. package/dist/styles/xy.css +48 -27
  138. package/package.json +2 -5
@@ -1,8 +1,9 @@
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 { sortedRegions } from "../lib/time-regions.js";
4
5
  import { SonicWaveElement } from "./wave-element.js";
5
- import { createMarkerBand, nearestMarker } from "../lib/marker-band.js";
6
+ import { createMarkerBand } from "../lib/marker-band.js";
6
7
  import { stripBars, stripRegions } from "../lib/strip-scene.js";
7
8
 
8
9
  //#region src/elements/wavestrip.ts
@@ -45,6 +46,13 @@ function fillRegion(context, path, [from, to, style]) {
45
46
  context.restore();
46
47
  }
47
48
  var SonicWavestrip = class extends SonicWaveElement {
49
+ get buffered() {
50
+ return this.#buffered.map(([start, end]) => [start, end]);
51
+ }
52
+ set buffered(regions) {
53
+ this.#buffered = sortedRegions(regions);
54
+ this.render();
55
+ }
48
56
  get cancellable() {
49
57
  return this.hasAttribute("cancellable");
50
58
  }
@@ -65,11 +73,15 @@ var SonicWavestrip = class extends SonicWaveElement {
65
73
  lengths = lengths;
66
74
  sheet = "wavestrip.css";
67
75
  #bars;
76
+ #buffered = [];
77
+ #escapeWatch;
78
+ #hover;
79
+ #isHoverDismissed = false;
68
80
  #markerBand = createMarkerBand(requireChild(this.control, ".sonic-wavestrip-markers", HTMLDivElement));
69
81
  #paintedKey = "";
70
82
  #peaks;
71
83
  connectedCallback() {
72
- this.upgradeProperties("cancellable", "peaks");
84
+ this.upgradeProperties("buffered", "cancellable", "peaks");
73
85
  super.connectedCallback();
74
86
  }
75
87
  connect(signal) {
@@ -79,6 +91,7 @@ var SonicWavestrip = class extends SonicWaveElement {
79
91
  }
80
92
  draw() {
81
93
  const surface = this.surface();
94
+ if (this.isEditing() || this.isRevealed()) this.#hover = void 0;
82
95
  this.#placeReadout();
83
96
  if (surface && this.#stripRegions(surface.size.width).key !== this.#paintedKey) surface.requestFrame();
84
97
  }
@@ -100,8 +113,11 @@ var SonicWavestrip = class extends SonicWaveElement {
100
113
  look.colours[kind]
101
114
  ]);
102
115
  }
116
+ previewValue() {
117
+ return this.#hover;
118
+ }
103
119
  renderMarkers() {
104
- this.#markerBand.render(this.markers, this.markerProportions());
120
+ this.#markerBand.render(this.markerList(), this.markerProportions());
105
121
  }
106
122
  scrubsKeyRepeat() {
107
123
  return true;
@@ -111,6 +127,7 @@ var SonicWavestrip = class extends SonicWaveElement {
111
127
  return {
112
128
  position: (event) => event.clientX,
113
129
  startPx: box.left,
130
+ topPx: box.top,
114
131
  travelPx: Math.max(1, box.width)
115
132
  };
116
133
  }
@@ -126,36 +143,44 @@ var SonicWavestrip = class extends SonicWaveElement {
126
143
  #bindHover(strip, signal) {
127
144
  strip.addEventListener("pointermove", (event) => {
128
145
  if (event.pointerType === "touch" || event.buttons !== 0) return;
129
- if (!this.readout || this.isDisabled()) return;
146
+ if (!this.readout) return;
130
147
  const axis = this.#axis();
131
148
  const at = clampProportion((event.clientX - axis.startPx) / axis.travelPx);
132
- this.hoverReadout(this.#markerAt(at, axis.travelPx) ?? this.mapping().valueAt(at));
149
+ const mapping = this.mapping();
150
+ this.#hoverAt(this.#markerAt(event, axis) ?? mapping.snap(mapping.valueAt(at)));
133
151
  this.#placeReadout();
134
152
  }, { signal });
135
153
  for (const type of ["pointerdown", "pointerleave"]) strip.addEventListener(type, () => {
136
- this.hoverReadout(void 0);
154
+ this.#hoverAt(void 0);
137
155
  }, { signal });
138
156
  }
139
157
  #grab(event) {
140
- const { startPx, ...axis } = this.#axis();
141
- const at = (event.clientX - startPx) / axis.travelPx;
158
+ const axis = this.#axis();
159
+ const at = (event.clientX - axis.startPx) / axis.travelPx;
142
160
  const outside = this.#outside();
143
161
  const grabbed = {
144
- ...axis,
145
- fromProportion: clampProportion(at)
162
+ fromProportion: clampProportion(at),
163
+ position: axis.position,
164
+ travelPx: axis.travelPx
146
165
  };
147
- this.input(this.#markerAt(at, axis.travelPx) ?? this.mapping().valueAt(at));
166
+ this.input(this.#markerAt(event, axis) ?? this.mapping().valueAt(at));
148
167
  return outside ? {
149
168
  ...grabbed,
150
169
  outside
151
170
  } : grabbed;
152
171
  }
153
- #markerAt(at, travelPx) {
154
- const markers = this.markers;
155
- if (markers.length === 0) return void 0;
156
- const { proportionOf } = this.mapping();
157
- const index = nearestMarker(at, markers.map(({ start }) => proportionOf(start)), this.#markerBand.reachPx() / travelPx);
158
- return index === void 0 ? void 0 : markers[index]?.start;
172
+ #hoverAt(value) {
173
+ if (value === void 0) this.#isHoverDismissed = false;
174
+ if (!this.#isHoverDismissed && !this.isEditing() && !this.isRevealed()) this.#hover = value;
175
+ this.renderReadout();
176
+ this.#watchEscape(value !== void 0);
177
+ }
178
+ #markerAt(event, { startPx, topPx, travelPx }) {
179
+ const index = this.#markerBand.at({
180
+ x: event.clientX - startPx,
181
+ y: event.clientY - topPx
182
+ }, travelPx);
183
+ return index === void 0 ? void 0 : this.markerList()[index]?.start;
159
184
  }
160
185
  #outside() {
161
186
  if (!this.cancellable) return void 0;
@@ -171,11 +196,27 @@ var SonicWavestrip = class extends SonicWaveElement {
171
196
  const { proportionOf } = this.mapping();
172
197
  const isScrubbing = this.heldFrom() !== void 0 && this.isRevealed();
173
198
  return stripRegions({
174
- buffered: this.buffered.map(([start, end]) => [proportionOf(start), proportionOf(end)]),
199
+ buffered: this.#buffered.map(([start, end]) => [proportionOf(start), proportionOf(end)]),
175
200
  played: proportionOf(this.playback()),
176
201
  ...isScrubbing ? { scrub: proportionOf(this.value) } : {}
177
202
  }, width);
178
203
  }
204
+ #watchEscape(isHovered) {
205
+ if (!isHovered) {
206
+ this.#escapeWatch?.abort();
207
+ this.#escapeWatch = void 0;
208
+ return;
209
+ }
210
+ if (this.#escapeWatch) return;
211
+ const watch = new AbortController();
212
+ this.#escapeWatch = watch;
213
+ this.ownerDocument.addEventListener("keydown", (event) => {
214
+ if (event.key !== "Escape" || this.#hover === void 0) return;
215
+ this.#hover = void 0;
216
+ this.#isHoverDismissed = true;
217
+ this.render();
218
+ }, { signal: watch.signal });
219
+ }
179
220
  };
180
221
 
181
222
  //#endregion
@@ -1,15 +1,15 @@
1
- import { clampProportion } from "../lib/math.js";
2
- import { requireChild, template } from "../lib/render.js";
1
+ import { clampProportion, toNumber } from "../lib/math.js";
3
2
  import { writeAttribute } from "../lib/write-attribute.js";
4
3
  import { SonicFormElement } from "./form-element.js";
4
+ import { requireChild, template } from "../lib/render.js";
5
5
  import { ReadoutClaim } from "./readout-claim.js";
6
6
  import { Readout } from "./readout.js";
7
7
  import { dragThresholdPx } from "../lib/drag-step.js";
8
8
  import { focusByPointer } from "../lib/focus-by-pointer.js";
9
9
  import { isMenuPress, isResetPress } from "../lib/modifier-press.js";
10
+ import { bindDrag } from "../lib/pointer-drag.js";
10
11
  import { resetKeys, valueMapping } from "../lib/value-mapping.js";
11
12
  import { pointerMove, pointerPosition, startFieldDrag, stepFieldDrag } from "../lib/field.js";
12
- import { bindDrag } from "../lib/pointer-drag.js";
13
13
 
14
14
  //#region src/elements/xy.ts
15
15
  const axes = ["x", "y"];
@@ -29,6 +29,7 @@ const orientations = {
29
29
  };
30
30
  const renderXy = template(`
31
31
  <div class="sonic-xy">
32
+ <div class="sonic-xy-bracket" aria-hidden="true"></div>
32
33
  <div class="sonic-xy-field" aria-hidden="true"></div>
33
34
  <div class="sonic-xy-puck">
34
35
  <div class="sonic-xy-axis" data-sonic-axis="x" role="slider" tabindex="0"></div>
@@ -193,6 +194,7 @@ var SonicXy = class extends SonicFormElement {
193
194
  #xy = renderXy();
194
195
  #field = requireChild(this.#xy, ".sonic-xy-field", HTMLDivElement);
195
196
  #formatValue;
197
+ #mapped;
196
198
  #parts = {
197
199
  x: requireChild(this.#xy, "[data-sonic-axis=\"x\"]", HTMLDivElement),
198
200
  y: requireChild(this.#xy, "[data-sonic-axis=\"y\"]", HTMLDivElement)
@@ -202,18 +204,19 @@ var SonicXy = class extends SonicFormElement {
202
204
  #readout = new Readout(requireChild(this.#xy, ".sonic-xy-readout", HTMLDivElement));
203
205
  #values = {
204
206
  x: {
205
- isDirty: false,
207
+ asked: void 0,
206
208
  value: 0
207
209
  },
208
210
  y: {
209
- isDirty: false,
211
+ asked: void 0,
210
212
  value: 0
211
213
  }
212
214
  };
213
215
  attributeChangedCallback(name) {
216
+ this.#mapped = void 0;
214
217
  if (name === "x" || name === "y") {
215
218
  if (this.#pointerDrag?.current()) return;
216
- this.#values[name].isDirty = false;
219
+ this.#values[name].asked = void 0;
217
220
  }
218
221
  if (name === "disabled" && this.isDisabled()) this.#pointerDrag?.end();
219
222
  this.#refresh();
@@ -223,12 +226,12 @@ var SonicXy = class extends SonicFormElement {
223
226
  super.connectedCallback();
224
227
  }
225
228
  formResetCallback() {
226
- for (const axis of axes) this.#values[axis].isDirty = false;
229
+ for (const axis of axes) this.#values[axis].asked = void 0;
227
230
  this.#refresh();
228
231
  }
229
232
  connect(signal) {
230
233
  const xy = this.#xy;
231
- this.appendOnce(xy);
234
+ this.keepControl(xy, signal);
232
235
  this.render();
233
236
  this.checkStyles(xy, "xy.css");
234
237
  this.#bindPointer(xy, signal);
@@ -238,6 +241,7 @@ var SonicXy = class extends SonicFormElement {
238
241
  return this.#parts[this.#currentAxis];
239
242
  }
240
243
  render() {
244
+ if (!this.isBound()) return;
241
245
  const { style } = this.#xy;
242
246
  const mappings = this.#mappings();
243
247
  for (const axis of axes) {
@@ -365,7 +369,7 @@ var SonicXy = class extends SonicFormElement {
365
369
  return true;
366
370
  }
367
371
  #keyTo(axis, key) {
368
- const next = this.#mapping(axis).keyTarget(key, this.#values[axis].value);
372
+ const next = this.#mappings()[axis].keyTarget(key, this.#values[axis].value);
369
373
  const hasMoved = this.#set(axis, next ?? NaN);
370
374
  this.#currentAxis = axis;
371
375
  this.render();
@@ -390,17 +394,19 @@ var SonicXy = class extends SonicFormElement {
390
394
  });
391
395
  }
392
396
  #mappings() {
393
- return {
397
+ if (this.#mapped) return this.#mapped;
398
+ this.#mapped = {
394
399
  x: this.#mapping("x"),
395
400
  y: this.#mapping("y")
396
401
  };
402
+ return this.#mapped;
397
403
  }
398
404
  #refresh() {
399
405
  const mappings = this.#mappings();
400
406
  for (const axis of axes) {
401
407
  const axisValue = this.#values[axis];
402
408
  const mapping = mappings[axis];
403
- axisValue.value = mapping.snap(axisValue.isDirty ? axisValue.value : this.numberAttribute(axis, mapping.bounds[0]));
409
+ axisValue.value = mapping.snap(axisValue.asked ?? this.numberAttribute(axis, mapping.bounds[0]));
404
410
  }
405
411
  this.render();
406
412
  }
@@ -412,11 +418,11 @@ var SonicXy = class extends SonicFormElement {
412
418
  const [low, high] = mappings[axis].bounds;
413
419
  const isCurrent = axis === this.#currentAxis;
414
420
  const stop = isCurrent ? "0" : "-1";
415
- part.setAttribute("aria-orientation", orientations[axis]);
416
- part.setAttribute("aria-valuemin", String(low));
417
- part.setAttribute("aria-valuemax", String(high));
418
- part.setAttribute("aria-valuenow", String(this.#values[axis].value));
419
- part.setAttribute("aria-valuetext", this.#valueText(axis));
421
+ writeAttribute(part, "aria-orientation", orientations[axis]);
422
+ writeAttribute(part, "aria-valuemin", String(low));
423
+ writeAttribute(part, "aria-valuemax", String(high));
424
+ writeAttribute(part, "aria-valuenow", String(this.#values[axis].value));
425
+ writeAttribute(part, "aria-valuetext", this.#valueText(axis));
420
426
  this.forwardNaming(part, true);
421
427
  writeAttribute(part, "aria-hidden", isCurrent ? void 0 : "true");
422
428
  writeAttribute(part, "aria-disabled", isDisabled ? "true" : void 0);
@@ -451,8 +457,8 @@ var SonicXy = class extends SonicFormElement {
451
457
  #set(axis, next) {
452
458
  if (!Number.isFinite(next)) return false;
453
459
  const axisValue = this.#values[axis];
454
- const snapped = this.#mapping(axis).snap(next);
455
- axisValue.isDirty = true;
460
+ const snapped = this.#mappings()[axis].snap(next);
461
+ axisValue.asked = snapped;
456
462
  if (snapped === axisValue.value) return false;
457
463
  axisValue.value = snapped;
458
464
  return true;
@@ -465,7 +471,16 @@ var SonicXy = class extends SonicFormElement {
465
471
  }
466
472
  #write(axis, next) {
467
473
  if (this.#pointerDrag?.current()) return;
468
- if (this.#set(axis, next)) this.render();
474
+ const axisValue = this.#values[axis];
475
+ if (next === void 0 || next === null) {
476
+ axisValue.asked = void 0;
477
+ this.#refresh();
478
+ return;
479
+ }
480
+ const asked = toNumber(next);
481
+ const hasMoved = this.#set(axis, asked);
482
+ if (Number.isFinite(asked)) axisValue.asked = asked;
483
+ if (hasMoved) this.render();
469
484
  }
470
485
  };
471
486
 
@@ -1,13 +1,13 @@
1
+ import { SonicButton } from "./elements/button.js";
1
2
  import { SonicDial } from "./elements/dial.js";
2
3
  import { envelopeCurve } from "./lib/envelope-curve.js";
3
4
  import { SonicEnvelope } from "./elements/envelope.js";
4
- import { SonicButton } from "./elements/button.js";
5
- import { SonicSwitch } from "./elements/switch.js";
6
5
  import { SonicMeter } from "./elements/meter.js";
7
6
  import { SonicNumber } from "./elements/number.js";
8
7
  import { SonicSegmented } from "./elements/segmented.js";
9
8
  import { SonicSlider } from "./elements/slider.js";
10
9
  import { SonicSplit } from "./elements/split.js";
10
+ import { SonicSwitch } from "./elements/switch.js";
11
11
  import { SonicWaveform } from "./elements/waveform.js";
12
12
  import { SonicWavestrip } from "./elements/wavestrip.js";
13
13
  import { SonicXy } from "./elements/xy.js";
@@ -98,8 +98,13 @@ function adsrShape(proportions, curves = {}) {
98
98
  }
99
99
  function adsrPath({ points }) {
100
100
  const stroke = points.map(({ x, y }, index) => `${index === 0 ? "M" : "L"}${String(roundTo(x, 4))} ${String(roundTo(1 - y, 4))}`).join("");
101
+ const floor = {
102
+ from: roundTo(points[0]?.x ?? 0, 4),
103
+ to: roundTo(points.at(-1)?.x ?? 0, 4)
104
+ };
101
105
  return {
102
106
  fill: `${stroke}Z`,
107
+ floor,
103
108
  stroke
104
109
  };
105
110
  }
@@ -3,13 +3,13 @@ const burstTimeConstants = -Math.log(1 - 10 ** (-2 / 20));
3
3
  function ppmRate({ burstMs, fallDecibels, fallSeconds }) {
4
4
  return {
5
5
  fallDecibelsPerSecond: fallDecibels / fallSeconds,
6
- riseMs: burstMs / burstTimeConstants
6
+ riseTimeConstantMs: burstMs / burstTimeConstants
7
7
  };
8
8
  }
9
9
  const ballisticsRates = {
10
10
  peak: {
11
11
  fallDecibelsPerSecond: 20,
12
- riseMs: 0
12
+ riseTimeConstantMs: 0
13
13
  },
14
14
  "ppm-1": ppmRate({
15
15
  burstMs: 5,
@@ -69,12 +69,22 @@ var CanvasSurface = class {
69
69
  style.setProperty("transition-timing-function", "step-start");
70
70
  style.setProperty("transition-behavior", "allow-discrete");
71
71
  }
72
+ #current() {
73
+ if (this.#look && !this.#isRebuildWanted) return {
74
+ isRebuilt: false,
75
+ look: this.#look
76
+ };
77
+ return {
78
+ isRebuilt: true,
79
+ look: this.#read()
80
+ };
81
+ }
72
82
  #draw(frameMs) {
73
83
  this.#frame = void 0;
74
84
  const size = this.#size;
75
85
  if (size.width === 0 || size.height === 0) return;
76
86
  const { canvas, paint } = this.#options;
77
- const look = this.#isRebuildWanted || !this.#look ? this.#read() : this.#look;
87
+ const { isRebuilt, look } = this.#current();
78
88
  const isDirty = this.#isDirty;
79
89
  this.#look = look;
80
90
  this.#isDirty = false;
@@ -84,6 +94,7 @@ var CanvasSurface = class {
84
94
  if (this.#context) paint(this.#context, {
85
95
  frameMs,
86
96
  isDirty,
97
+ isRebuilt,
87
98
  look,
88
99
  size
89
100
  });
@@ -140,7 +151,7 @@ var CanvasSurface = class {
140
151
  const { canvas, colours, lengths = {} } = this.#options;
141
152
  const styles = getComputedStyle(canvas);
142
153
  return {
143
- colours: readEach(colours, (property) => styles.getPropertyValue(property).trim()),
154
+ colours: readEach(colours, (property) => styles.getPropertyValue(property).trim().replaceAll("currentcolor", () => styles.color)),
144
155
  isReducedMotion: this.#reducedMotion.matches,
145
156
  lengths: readEach(lengths, (property) => readPxProperty(styles, property, 0))
146
157
  };
@@ -77,7 +77,7 @@ function byShortfall(values, { exact, order }, direction) {
77
77
  const value = values[index] ?? NaN;
78
78
  return trimFloat(direction * ((exact.get(index) ?? value) - value));
79
79
  };
80
- return order.toReversed().sort((first, second) => shortfall(second) - shortfall(first));
80
+ return order.toReversed().toSorted((first, second) => shortfall(second) - shortfall(first));
81
81
  }
82
82
  function stepEach(members, values, pass) {
83
83
  let remainder = pass.owed;
@@ -10,18 +10,18 @@ function dragThresholdPx(pointerType) {
10
10
  function isAcross(from, to, proportion) {
11
11
  return from !== proportion && (from - proportion) * (to - proportion) <= 0;
12
12
  }
13
- function passDetent(hold, from, to) {
13
+ function passDetent(hold, [from, to], pace) {
14
14
  if (hold.slack === void 0 && !isAcross(from, to, hold.proportion)) return {
15
15
  proportion: to,
16
16
  slack: void 0
17
17
  };
18
- const slack = (hold.slack ?? 0) + to - hold.proportion;
18
+ const slack = (hold.slack ?? 0) + (to - hold.proportion) / pace;
19
19
  if (Math.abs(slack) <= hold.zone) return {
20
20
  proportion: void 0,
21
21
  slack
22
22
  };
23
23
  return {
24
- proportion: hold.proportion + slack - Math.sign(slack) * hold.zone,
24
+ proportion: hold.proportion + (slack - Math.sign(slack) * hold.zone) * pace,
25
25
  slack: void 0
26
26
  };
27
27
  }
@@ -77,7 +77,7 @@ function stepDrag(mapping, state, move) {
77
77
  };
78
78
  const { detent } = state;
79
79
  if (!detent) return land(mapping, moved, to);
80
- const passed = passDetent(mapping.isWrapping ? nearestTurn(detent, state.rawProportion) : detent, state.rawProportion, to);
80
+ const passed = passDetent(mapping.isWrapping ? nearestTurn(detent, state.rawProportion) : detent, [state.rawProportion, to], pace);
81
81
  const held = {
82
82
  ...moved,
83
83
  detent: {
@@ -2,6 +2,7 @@ import { trimFloat } from "./math.js";
2
2
 
3
3
  //#region src/lib/frame-timeline.ts
4
4
  const placeholderPeriodMs = 2500;
5
+ const lookAheadSeconds = 30;
5
6
  function windowStart(input, seconds, windowSeconds) {
6
7
  if (!input.isPaged) return seconds - windowSeconds / 2;
7
8
  if (input.held) return input.held.grabbed.startSeconds + seconds - input.held.grabbed.seconds;
@@ -31,7 +32,7 @@ function frameTimeline(input) {
31
32
  const pending = pendingIn(input, [startSeconds, startSeconds + windowSeconds]);
32
33
  const placeholder = placeholderPhase(input, pending);
33
34
  const phase = placeholder ?? 0;
34
- const wanted = overlap([startSeconds - windowSeconds, startSeconds + 2 * windowSeconds], range);
35
+ const wanted = overlap([startSeconds - windowSeconds, startSeconds + windowSeconds + Math.max(windowSeconds, lookAheadSeconds)], range);
35
36
  const at = (time) => (time - startSeconds) / windowSeconds;
36
37
  return {
37
38
  ...held ? { ghostAt: at(clockSeconds) } : {},
@@ -3,28 +3,72 @@ import { readPxProperty } from "./read-px-property.js";
3
3
  //#region src/lib/marker-band.ts
4
4
  const kindPattern = /^[a-z][a-z0-9-]*$/;
5
5
  const snapSlopPx = 4;
6
- function renderMarker(marker, [from, to]) {
6
+ function isKind(kind) {
7
+ return kind !== void 0 && kindPattern.test(kind);
8
+ }
9
+ function writeKind(part, kind, fallback) {
10
+ if (!isKind(kind)) {
11
+ part.style.removeProperty("--_sonic-marker");
12
+ return;
13
+ }
14
+ part.style.setProperty("--_sonic-marker", `var(--sonic-marker-${kind}, var(${fallback}))`);
15
+ }
16
+ function renderMarker(marker, [from, to], lane) {
7
17
  const part = document.createElement("div");
8
- part.className = marker.end === void 0 ? "sonic-wavestrip-marker" : "sonic-wavestrip-region";
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));
9
20
  part.style.setProperty("--_sonic-marker-from", String(from));
10
21
  part.style.setProperty("--_sonic-marker-to", String(to));
11
- if (marker.kind !== void 0 && kindPattern.test(marker.kind)) part.style.setProperty("--_sonic-marker", `var(--sonic-marker-${marker.kind}, var(--_sonic-marker-default))`);
22
+ writeKind(part, marker.kind, "--_sonic-marker-default");
12
23
  if (marker.dimmed === true) part.dataset.sonicDimmed = "";
13
24
  return part;
14
25
  }
15
26
  function createMarkerBand(band) {
27
+ let dots = [];
28
+ const px = (property) => readPxProperty(getComputedStyle(band), property, 0);
16
29
  return {
17
- reachPx: () => readPxProperty(getComputedStyle(band), "--_sonic-marker-size", 0) / 2 + snapSlopPx,
30
+ at: ({ x, y }, widthPx) => {
31
+ const size = px("--_sonic-marker-size");
32
+ const step = px("--_sonic-marker-step");
33
+ const nearest = nearestDot({
34
+ x,
35
+ y
36
+ }, dots.map(({ at, lane }) => ({
37
+ x: at * widthPx,
38
+ y: size / 2 + lane * step
39
+ })), size / 2 + snapSlopPx);
40
+ return nearest === void 0 ? void 0 : dots[nearest]?.index;
41
+ },
18
42
  render: (markers, proportions) => {
19
- band.replaceChildren(...markers.map((marker, index) => renderMarker(marker, proportions[index] ?? [0, 0])));
43
+ const widthPx = band.getBoundingClientRect().width;
44
+ const points = markers.flatMap((marker, index) => marker.end === void 0 ? [{
45
+ at: proportions[index]?.[0] ?? 0,
46
+ index
47
+ }] : []);
48
+ const lanes = markerLanes(points.map(({ at }) => at * widthPx), px("--_sonic-marker-step"));
49
+ dots = points.map((point, order) => ({
50
+ ...point,
51
+ lane: lanes[order] ?? 0
52
+ }));
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))));
20
55
  }
21
56
  };
22
57
  }
23
- function nearestMarker(proportion, markerProportions, reach) {
58
+ function markerLanes(positions, minDistance) {
59
+ const lastInLane = [];
60
+ return positions.map((position) => {
61
+ const free = lastInLane.findIndex((last) => position - last >= minDistance);
62
+ const lane = free === -1 ? lastInLane.length : free;
63
+ lastInLane[lane] = position;
64
+ return lane;
65
+ });
66
+ }
67
+ function nearestDot(point, dots, reach) {
24
68
  let nearest;
25
- let nearestDistance = reach;
26
- for (const [index, markerProportion] of markerProportions.entries()) {
27
- const distance = Math.abs(markerProportion - proportion);
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;
28
72
  if (distance > nearestDistance) continue;
29
73
  nearest = index;
30
74
  nearestDistance = distance;
@@ -33,4 +77,4 @@ function nearestMarker(proportion, markerProportions, reach) {
33
77
  }
34
78
 
35
79
  //#endregion
36
- export { createMarkerBand, nearestMarker };
80
+ export { createMarkerBand, isKind, markerLanes, nearestDot, writeKind };
@@ -1,6 +1,7 @@
1
1
  import { clamp } from "./math.js";
2
2
  import { requireChild } from "./render.js";
3
3
  import { readPxProperty } from "./read-px-property.js";
4
+ import { writeKind } from "./marker-band.js";
4
5
 
5
6
  //#region src/lib/marker-rider.ts
6
7
  const fadeShare = .25;
@@ -43,6 +44,7 @@ function layoutRider(labels, view) {
43
44
  function labelSlot(element) {
44
45
  return {
45
46
  element,
47
+ kind: void 0,
46
48
  opacity: NaN,
47
49
  shownPx: NaN,
48
50
  x: NaN
@@ -63,13 +65,17 @@ function writePlacement(slot, { opacity, shownPx, x }, widthPx) {
63
65
  style.setProperty("opacity", opacity < 1 ? opacity.toFixed(3) : "");
64
66
  }
65
67
  }
66
- function writeLabel(slot, labels, placement) {
68
+ function writeLabel(slot, [labels, colourProperty], placement) {
67
69
  const { element } = slot;
68
70
  const label = placement && placement.shownPx > 0 ? labels[placement.index] : void 0;
69
71
  if (element.hidden !== !label) element.hidden = !label;
70
72
  if (!label || !placement) return;
71
73
  if (element.textContent !== label.text) element.textContent = label.text;
72
74
  element.toggleAttribute("data-sonic-dimmed", label.isDimmed);
75
+ if (slot.kind !== label.kind) {
76
+ slot.kind = label.kind;
77
+ writeKind(element, label.kind, colourProperty);
78
+ }
73
79
  writePlacement(slot, placement, label.widthPx);
74
80
  }
75
81
  function measureWidths(control, className, texts) {
@@ -85,7 +91,7 @@ function measureWidths(control, className, texts) {
85
91
  return widths;
86
92
  }
87
93
  function createLabelRider(control, options) {
88
- const { className, insetProperty } = options;
94
+ const { className, colourProperty, insetProperty } = options;
89
95
  const parked = labelSlot(requireChild(control, `.${className}`, HTMLElement));
90
96
  const arriving = labelSlot(requireChild(control, `.${className} + .${className}`, HTMLElement));
91
97
  let insetPx = 0;
@@ -104,8 +110,8 @@ function createLabelRider(control, options) {
104
110
  ...window,
105
111
  insetPx
106
112
  });
107
- writeLabel(parked, measured, layout.parked);
108
- writeLabel(arriving, measured, layout.arriving);
113
+ writeLabel(parked, [measured, colourProperty], layout.parked);
114
+ writeLabel(arriving, [measured, colourProperty], layout.arriving);
109
115
  }
110
116
  };
111
117
  }
@@ -2,6 +2,11 @@
2
2
  function clamp(value, min, max) {
3
3
  return Math.min(max, Math.max(min, value));
4
4
  }
5
+ function toNumber(value) {
6
+ if (typeof value === "number") return value;
7
+ if (typeof value !== "string" || value.trim() === "") return NaN;
8
+ return Number(value);
9
+ }
5
10
  function clampProportion(fraction) {
6
11
  return clamp(fraction, 0, 1);
7
12
  }
@@ -22,4 +27,4 @@ function roundTo(value, places) {
22
27
  }
23
28
 
24
29
  //#endregion
25
- export { clamp, clampProportion, decimalPlaces, roundTo, trimFloat, wrapProportion };
30
+ export { clamp, clampProportion, decimalPlaces, roundTo, toNumber, trimFloat, wrapProportion };