@xsynaptic/sonic-ui 0.4.0 → 0.5.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 (240) hide show
  1. package/README.md +2 -21
  2. package/dist/default/dat.js +50 -0
  3. package/dist/default/define/button.js +7 -0
  4. package/dist/default/define/envelope.js +7 -0
  5. package/dist/default/define/split.js +7 -0
  6. package/dist/default/define/switch.js +7 -0
  7. package/dist/default/define/waveform.js +7 -0
  8. package/dist/default/define/wavestrip.js +7 -0
  9. package/dist/default/define/xy.js +7 -0
  10. package/dist/default/define.js +7 -1
  11. package/dist/default/elements/button.js +165 -0
  12. package/dist/default/elements/dial.js +16 -22
  13. package/dist/default/elements/envelope.js +367 -0
  14. package/dist/default/elements/meter.js +149 -61
  15. package/dist/default/elements/number.js +21 -8
  16. package/dist/default/elements/radio-group.js +185 -0
  17. package/dist/default/elements/readout-claim.js +81 -0
  18. package/dist/default/elements/readout.js +35 -0
  19. package/dist/default/elements/segmented.js +9 -159
  20. package/dist/default/elements/slider.js +42 -51
  21. package/dist/default/elements/sonic-element.js +24 -13
  22. package/dist/default/elements/split.js +249 -0
  23. package/dist/default/elements/switch.js +279 -0
  24. package/dist/default/elements/value-element.js +691 -0
  25. package/dist/default/elements/{range-entry.js → value-entry.js} +3 -3
  26. package/dist/default/elements/wave-element.js +77 -0
  27. package/dist/default/elements/waveform.js +305 -0
  28. package/dist/default/elements/wavestrip.js +182 -0
  29. package/dist/default/elements/xy.js +473 -0
  30. package/dist/default/index.js +10 -2
  31. package/dist/default/lib/adsr-shape.js +108 -0
  32. package/dist/default/lib/ballistics.js +55 -0
  33. package/dist/default/lib/canvas-surface.js +192 -0
  34. package/dist/default/lib/copy-node.js +32 -2
  35. package/dist/default/lib/crossfade.js +22 -0
  36. package/dist/default/lib/distribute.js +215 -0
  37. package/dist/default/lib/drag-step.js +99 -0
  38. package/dist/default/lib/envelope-curve.js +9 -0
  39. package/dist/default/lib/field.js +94 -0
  40. package/dist/default/lib/focus-by-pointer.js +28 -0
  41. package/dist/default/lib/frame-timeline.js +62 -0
  42. package/dist/default/lib/hold.js +38 -0
  43. package/dist/default/lib/marker-band.js +36 -0
  44. package/dist/default/lib/marker-rider.js +114 -0
  45. package/dist/default/lib/math.js +12 -3
  46. package/dist/default/lib/modifier-press.js +11 -0
  47. package/dist/default/lib/number-list.js +6 -6
  48. package/dist/default/lib/pointer-drag.js +43 -0
  49. package/dist/default/lib/render.js +20 -5
  50. package/dist/default/lib/resample-peaks.js +36 -0
  51. package/dist/default/lib/strip-scene.js +48 -0
  52. package/dist/default/lib/taper.js +23 -17
  53. package/dist/default/lib/time-regions.js +8 -0
  54. package/dist/default/lib/tracking-clock.js +35 -0
  55. package/dist/default/lib/value-mapping.js +115 -0
  56. package/dist/default/lib/waveform-buckets.js +41 -0
  57. package/dist/dev/dat.d.ts +19 -0
  58. package/dist/dev/dat.js +51 -0
  59. package/dist/dev/dat.js.map +1 -0
  60. package/dist/dev/define/button.d.ts +2 -0
  61. package/dist/dev/define/button.js +8 -0
  62. package/dist/dev/define/button.js.map +1 -0
  63. package/dist/dev/define/dial.js.map +1 -1
  64. package/dist/dev/define/envelope.d.ts +2 -0
  65. package/dist/dev/define/envelope.js +8 -0
  66. package/dist/dev/define/envelope.js.map +1 -0
  67. package/dist/dev/define/meter.js.map +1 -1
  68. package/dist/dev/define/number.js.map +1 -1
  69. package/dist/dev/define/segmented.js.map +1 -1
  70. package/dist/dev/define/slider.js.map +1 -1
  71. package/dist/dev/define/split.d.ts +2 -0
  72. package/dist/dev/define/split.js +8 -0
  73. package/dist/dev/define/split.js.map +1 -0
  74. package/dist/dev/define/switch.d.ts +2 -0
  75. package/dist/dev/define/switch.js +8 -0
  76. package/dist/dev/define/switch.js.map +1 -0
  77. package/dist/dev/define/waveform.d.ts +2 -0
  78. package/dist/dev/define/waveform.js +8 -0
  79. package/dist/dev/define/waveform.js.map +1 -0
  80. package/dist/dev/define/wavestrip.d.ts +2 -0
  81. package/dist/dev/define/wavestrip.js +8 -0
  82. package/dist/dev/define/wavestrip.js.map +1 -0
  83. package/dist/dev/define/xy.d.ts +2 -0
  84. package/dist/dev/define/xy.js +8 -0
  85. package/dist/dev/define/xy.js.map +1 -0
  86. package/dist/dev/define.d.ts +15 -3
  87. package/dist/dev/define.js +7 -1
  88. package/dist/dev/elements/{key.d.ts → button.d.ts} +8 -6
  89. package/dist/dev/elements/button.js +166 -0
  90. package/dist/dev/elements/button.js.map +1 -0
  91. package/dist/dev/elements/dial.d.ts +6 -4
  92. package/dist/dev/elements/dial.js +16 -22
  93. package/dist/dev/elements/dial.js.map +1 -1
  94. package/dist/dev/elements/envelope.d.ts +36 -0
  95. package/dist/dev/elements/envelope.js +372 -0
  96. package/dist/dev/elements/envelope.js.map +1 -0
  97. package/dist/dev/elements/form-element.d.ts +1 -1
  98. package/dist/dev/elements/form-element.js.map +1 -1
  99. package/dist/dev/elements/meter.d.ts +10 -5
  100. package/dist/dev/elements/meter.js +149 -61
  101. package/dist/dev/elements/meter.js.map +1 -1
  102. package/dist/dev/elements/number.d.ts +7 -3
  103. package/dist/dev/elements/number.js +21 -8
  104. package/dist/dev/elements/number.js.map +1 -1
  105. package/dist/dev/elements/radio-group.d.ts +25 -0
  106. package/dist/dev/elements/radio-group.js +186 -0
  107. package/dist/dev/elements/radio-group.js.map +1 -0
  108. package/dist/dev/elements/readout-claim.js +82 -0
  109. package/dist/dev/elements/readout-claim.js.map +1 -0
  110. package/dist/dev/elements/readout.js +36 -0
  111. package/dist/dev/elements/readout.js.map +1 -0
  112. package/dist/dev/elements/segmented.d.ts +4 -10
  113. package/dist/dev/elements/segmented.js +9 -159
  114. package/dist/dev/elements/segmented.js.map +1 -1
  115. package/dist/dev/elements/slider.d.ts +6 -6
  116. package/dist/dev/elements/slider.js +42 -51
  117. package/dist/dev/elements/slider.js.map +1 -1
  118. package/dist/dev/elements/sonic-element.d.ts +4 -3
  119. package/dist/dev/elements/sonic-element.js +51 -22
  120. package/dist/dev/elements/sonic-element.js.map +1 -1
  121. package/dist/dev/elements/split.d.ts +21 -0
  122. package/dist/dev/elements/split.js +258 -0
  123. package/dist/dev/elements/split.js.map +1 -0
  124. package/dist/dev/elements/switch.d.ts +35 -0
  125. package/dist/dev/elements/switch.js +280 -0
  126. package/dist/dev/elements/switch.js.map +1 -0
  127. package/dist/dev/elements/value-element.d.ts +84 -0
  128. package/dist/dev/elements/value-element.js +692 -0
  129. package/dist/dev/elements/value-element.js.map +1 -0
  130. package/dist/dev/elements/{range-entry.js → value-entry.js} +4 -4
  131. package/dist/dev/elements/value-entry.js.map +1 -0
  132. package/dist/dev/elements/wave-element.d.ts +36 -0
  133. package/dist/dev/elements/wave-element.js +78 -0
  134. package/dist/dev/elements/wave-element.js.map +1 -0
  135. package/dist/dev/elements/waveform.d.ts +55 -0
  136. package/dist/dev/elements/waveform.js +306 -0
  137. package/dist/dev/elements/waveform.js.map +1 -0
  138. package/dist/dev/elements/wavestrip.d.ts +51 -0
  139. package/dist/dev/elements/wavestrip.js +183 -0
  140. package/dist/dev/elements/wavestrip.js.map +1 -0
  141. package/dist/dev/elements/xy.d.ts +62 -0
  142. package/dist/dev/elements/xy.js +474 -0
  143. package/dist/dev/elements/xy.js.map +1 -0
  144. package/dist/dev/index.d.ts +11 -2
  145. package/dist/dev/index.js +10 -2
  146. package/dist/dev/lib/adsr-shape.js +109 -0
  147. package/dist/dev/lib/adsr-shape.js.map +1 -0
  148. package/dist/dev/lib/ballistics.js +56 -0
  149. package/dist/dev/lib/ballistics.js.map +1 -0
  150. package/dist/dev/lib/canvas-surface.d.ts +25 -0
  151. package/dist/dev/lib/canvas-surface.js +193 -0
  152. package/dist/dev/lib/canvas-surface.js.map +1 -0
  153. package/dist/dev/lib/copy-node.js +32 -2
  154. package/dist/dev/lib/copy-node.js.map +1 -1
  155. package/dist/dev/lib/crossfade.d.ts +12 -0
  156. package/dist/dev/lib/crossfade.js +23 -0
  157. package/dist/dev/lib/crossfade.js.map +1 -0
  158. package/dist/dev/lib/distribute.d.ts +4 -0
  159. package/dist/dev/lib/distribute.js +216 -0
  160. package/dist/dev/lib/distribute.js.map +1 -0
  161. package/dist/dev/lib/drag-step.js +100 -0
  162. package/dist/dev/lib/drag-step.js.map +1 -0
  163. package/dist/dev/lib/envelope-curve.d.ts +4 -0
  164. package/dist/dev/lib/envelope-curve.js +10 -0
  165. package/dist/dev/lib/envelope-curve.js.map +1 -0
  166. package/dist/dev/lib/field.d.ts +4 -0
  167. package/dist/dev/lib/field.js +95 -0
  168. package/dist/dev/lib/field.js.map +1 -0
  169. package/dist/dev/lib/focus-by-pointer.js +29 -0
  170. package/dist/dev/lib/focus-by-pointer.js.map +1 -0
  171. package/dist/dev/lib/frame-timeline.js +63 -0
  172. package/dist/dev/lib/frame-timeline.js.map +1 -0
  173. package/dist/dev/lib/hold.js +39 -0
  174. package/dist/dev/lib/hold.js.map +1 -0
  175. package/dist/dev/lib/marker-band.js +37 -0
  176. package/dist/dev/lib/marker-band.js.map +1 -0
  177. package/dist/dev/lib/marker-rider.js +115 -0
  178. package/dist/dev/lib/marker-rider.js.map +1 -0
  179. package/dist/dev/lib/math.js +12 -3
  180. package/dist/dev/lib/math.js.map +1 -1
  181. package/dist/dev/lib/modifier-press.js +12 -0
  182. package/dist/dev/lib/modifier-press.js.map +1 -0
  183. package/dist/dev/lib/number-list.js +6 -6
  184. package/dist/dev/lib/number-list.js.map +1 -1
  185. package/dist/dev/lib/pointer-drag.js +44 -0
  186. package/dist/dev/lib/pointer-drag.js.map +1 -0
  187. package/dist/dev/lib/read-px-property.js.map +1 -1
  188. package/dist/dev/lib/render.js +20 -5
  189. package/dist/dev/lib/render.js.map +1 -1
  190. package/dist/dev/lib/resample-peaks.js +37 -0
  191. package/dist/dev/lib/resample-peaks.js.map +1 -0
  192. package/dist/dev/lib/strip-scene.js +49 -0
  193. package/dist/dev/lib/strip-scene.js.map +1 -0
  194. package/dist/dev/lib/taper.js +23 -17
  195. package/dist/dev/lib/taper.js.map +1 -1
  196. package/dist/dev/lib/time-regions.d.ts +4 -0
  197. package/dist/dev/lib/time-regions.js +9 -0
  198. package/dist/dev/lib/time-regions.js.map +1 -0
  199. package/dist/dev/lib/tracking-clock.js +36 -0
  200. package/dist/dev/lib/tracking-clock.js.map +1 -0
  201. package/dist/dev/lib/value-mapping.d.ts +23 -0
  202. package/dist/dev/lib/value-mapping.js +116 -0
  203. package/dist/dev/lib/value-mapping.js.map +1 -0
  204. package/dist/dev/lib/waveform-buckets.d.ts +9 -0
  205. package/dist/dev/lib/waveform-buckets.js +42 -0
  206. package/dist/dev/lib/waveform-buckets.js.map +1 -0
  207. package/dist/skins/ivory.css +1 -2
  208. package/dist/styles/button.css +212 -0
  209. package/dist/styles/controls.css +8 -1
  210. package/dist/styles/dial.css +88 -42
  211. package/dist/styles/envelope.css +159 -0
  212. package/dist/styles/led.css +36 -5
  213. package/dist/styles/material.css +277 -76
  214. package/dist/styles/meter.css +33 -48
  215. package/dist/styles/number.css +5 -1
  216. package/dist/styles/panel.css +59 -0
  217. package/dist/styles/ring.css +74 -0
  218. package/dist/styles/segmented.css +21 -48
  219. package/dist/styles/slider.css +175 -39
  220. package/dist/styles/switch.css +347 -0
  221. package/dist/styles/waveform.css +173 -0
  222. package/dist/styles/wavestrip.css +215 -0
  223. package/dist/styles/xy.css +204 -0
  224. package/package.json +14 -2
  225. package/dist/default/define/key.js +0 -7
  226. package/dist/default/elements/key.js +0 -169
  227. package/dist/default/elements/range-element.js +0 -546
  228. package/dist/default/lib/detent.js +0 -17
  229. package/dist/dev/define/key.d.ts +0 -2
  230. package/dist/dev/define/key.js +0 -8
  231. package/dist/dev/define/key.js.map +0 -1
  232. package/dist/dev/elements/key.js +0 -170
  233. package/dist/dev/elements/key.js.map +0 -1
  234. package/dist/dev/elements/range-element.d.ts +0 -64
  235. package/dist/dev/elements/range-element.js +0 -547
  236. package/dist/dev/elements/range-element.js.map +0 -1
  237. package/dist/dev/elements/range-entry.js.map +0 -1
  238. package/dist/dev/lib/detent.js +0 -18
  239. package/dist/dev/lib/detent.js.map +0 -1
  240. package/dist/styles/key.css +0 -242
@@ -0,0 +1,28 @@
1
+ //#region src/lib/focus-by-pointer.ts
2
+ const modifierKeys = /* @__PURE__ */ new Set([
3
+ "Alt",
4
+ "Control",
5
+ "Meta",
6
+ "Shift"
7
+ ]);
8
+ function focusByPointer(target) {
9
+ const isMarked = "sonicPointerFocus" in target.dataset;
10
+ target.dataset.sonicPointerFocus = "";
11
+ target.focus();
12
+ if (isMarked) return;
13
+ const marking = new AbortController();
14
+ const clear = () => {
15
+ delete target.dataset.sonicPointerFocus;
16
+ marking.abort();
17
+ };
18
+ target.addEventListener("blur", () => {
19
+ if (target.ownerDocument.activeElement !== target) clear();
20
+ }, { signal: marking.signal });
21
+ target.addEventListener("keydown", (event) => {
22
+ if (event.metaKey || event.ctrlKey || event.altKey || modifierKeys.has(event.key)) return;
23
+ clear();
24
+ }, { signal: marking.signal });
25
+ }
26
+
27
+ //#endregion
28
+ export { focusByPointer };
@@ -0,0 +1,62 @@
1
+ import { trimFloat } from "./math.js";
2
+
3
+ //#region src/lib/frame-timeline.ts
4
+ const placeholderPeriodMs = 2500;
5
+ function windowStart(input, seconds, windowSeconds) {
6
+ if (!input.isPaged) return seconds - windowSeconds / 2;
7
+ if (input.held) return input.held.grabbed.startSeconds + seconds - input.held.grabbed.seconds;
8
+ return Math.floor(trimFloat(seconds / windowSeconds)) * windowSeconds;
9
+ }
10
+ function overlap([from, to], [low, high]) {
11
+ const start = Math.max(from, low);
12
+ const end = Math.min(to, high);
13
+ return end > start ? [start, end] : void 0;
14
+ }
15
+ function pendingIn({ pending, range }, window) {
16
+ return pending.flatMap((region) => {
17
+ const inRange = overlap(region, range);
18
+ const shown = inRange && overlap(inRange, window);
19
+ return shown ? [shown] : [];
20
+ });
21
+ }
22
+ function placeholderPhase({ frameMs, isPaged }, pending) {
23
+ return !isPaged && pending.length > 0 ? frameMs / placeholderPeriodMs : void 0;
24
+ }
25
+ function frameTimeline(input) {
26
+ const { clockSeconds, held, range, size } = input;
27
+ const seconds = held?.seconds ?? clockSeconds;
28
+ const pixelsPerSecond = input.zoom * size.dpr;
29
+ const windowSeconds = size.width / pixelsPerSecond;
30
+ const startSeconds = windowStart(input, seconds, windowSeconds);
31
+ const pending = pendingIn(input, [startSeconds, startSeconds + windowSeconds]);
32
+ const placeholder = placeholderPhase(input, pending);
33
+ const phase = placeholder ?? 0;
34
+ const wanted = overlap([startSeconds - windowSeconds, startSeconds + 2 * windowSeconds], range);
35
+ const at = (time) => (time - startSeconds) / windowSeconds;
36
+ return {
37
+ ...held ? { ghostAt: at(clockSeconds) } : {},
38
+ isMoving: held !== void 0 || placeholder !== void 0 || input.isPlaying,
39
+ paintKey: [
40
+ startSeconds,
41
+ windowSeconds,
42
+ ...range,
43
+ phase,
44
+ pending.join(",")
45
+ ].join(":"),
46
+ playheadAt: at(seconds),
47
+ seconds,
48
+ view: {
49
+ dpr: size.dpr,
50
+ height: size.height,
51
+ pending,
52
+ phase,
53
+ pixelsPerSecond,
54
+ startSeconds,
55
+ width: size.width
56
+ },
57
+ ...wanted ? { wanted } : {}
58
+ };
59
+ }
60
+
61
+ //#endregion
62
+ export { frameTimeline };
@@ -0,0 +1,38 @@
1
+ //#region src/lib/hold.ts
2
+ function bindHold(target, options, signal) {
3
+ let holder;
4
+ const hold = {
5
+ hold(by) {
6
+ if (holder !== void 0 || !options.canHold()) return false;
7
+ holder = by;
8
+ options.onHold();
9
+ return true;
10
+ },
11
+ release() {
12
+ if (holder === void 0) return;
13
+ holder = void 0;
14
+ options.onRelease();
15
+ }
16
+ };
17
+ const releasePointer = (event) => {
18
+ if (event.pointerId === holder) hold.release();
19
+ };
20
+ target.addEventListener("lostpointercapture", releasePointer, { signal });
21
+ target.addEventListener("pointercancel", releasePointer, { signal });
22
+ target.addEventListener("pointerup", releasePointer, { signal });
23
+ target.addEventListener("keyup", (event) => {
24
+ if (event.key === "Meta" && typeof holder === "string" || event.key === holder) hold.release();
25
+ }, { signal });
26
+ target.addEventListener(options.leave, (event) => {
27
+ const next = event.relatedTarget;
28
+ const isInside = next instanceof Node && target.contains(next);
29
+ if (typeof holder === "string" && !isInside) hold.release();
30
+ }, { signal });
31
+ signal.addEventListener("abort", () => {
32
+ hold.release();
33
+ }, { once: true });
34
+ return hold;
35
+ }
36
+
37
+ //#endregion
38
+ export { bindHold };
@@ -0,0 +1,36 @@
1
+ import { readPxProperty } from "./read-px-property.js";
2
+
3
+ //#region src/lib/marker-band.ts
4
+ const kindPattern = /^[a-z][a-z0-9-]*$/;
5
+ const snapSlopPx = 4;
6
+ function renderMarker(marker, [from, to]) {
7
+ const part = document.createElement("div");
8
+ part.className = marker.end === void 0 ? "sonic-wavestrip-marker" : "sonic-wavestrip-region";
9
+ part.style.setProperty("--_sonic-marker-from", String(from));
10
+ 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))`);
12
+ if (marker.dimmed === true) part.dataset.sonicDimmed = "";
13
+ return part;
14
+ }
15
+ function createMarkerBand(band) {
16
+ return {
17
+ reachPx: () => readPxProperty(getComputedStyle(band), "--_sonic-marker-size", 0) / 2 + snapSlopPx,
18
+ render: (markers, proportions) => {
19
+ band.replaceChildren(...markers.map((marker, index) => renderMarker(marker, proportions[index] ?? [0, 0])));
20
+ }
21
+ };
22
+ }
23
+ function nearestMarker(proportion, markerProportions, reach) {
24
+ let nearest;
25
+ let nearestDistance = reach;
26
+ for (const [index, markerProportion] of markerProportions.entries()) {
27
+ const distance = Math.abs(markerProportion - proportion);
28
+ if (distance > nearestDistance) continue;
29
+ nearest = index;
30
+ nearestDistance = distance;
31
+ }
32
+ return nearest;
33
+ }
34
+
35
+ //#endregion
36
+ export { createMarkerBand, nearestMarker };
@@ -0,0 +1,114 @@
1
+ import { clamp } from "./math.js";
2
+ import { requireChild } from "./render.js";
3
+ import { readPxProperty } from "./read-px-property.js";
4
+
5
+ //#region src/lib/marker-rider.ts
6
+ const fadeShare = .25;
7
+ function lineX(seconds, view) {
8
+ return (seconds - view.startSeconds) / view.windowSeconds * view.widthPx;
9
+ }
10
+ function parkedPlacement(labels, index, [view, arrivingX]) {
11
+ const parked = labels[index];
12
+ if (!parked) return void 0;
13
+ const next = labels[index + 1];
14
+ const x = Math.max(view.insetPx, lineX(parked.start, view) + view.insetPx);
15
+ const approachPx = next ? lineX(next.start, view) - lineX(view.playheadSeconds, view) : Infinity;
16
+ return {
17
+ index,
18
+ opacity: clamp(approachPx / (view.widthPx * fadeShare), 0, 1),
19
+ shownPx: Math.min(parked.widthPx, arrivingX - x, view.widthPx - x),
20
+ x
21
+ };
22
+ }
23
+ function isDrawable(length) {
24
+ return Number.isFinite(length) && length > 0;
25
+ }
26
+ function layoutRider(labels, view) {
27
+ if (!isDrawable(view.windowSeconds) || !isDrawable(view.widthPx)) return {};
28
+ const parkedIndex = labels.findLastIndex((label) => label.start <= view.playheadSeconds);
29
+ const next = labels[parkedIndex + 1];
30
+ const arrivingX = next ? lineX(next.start, view) + view.insetPx : Infinity;
31
+ const parked = parkedPlacement(labels, parkedIndex, [view, arrivingX]);
32
+ const arriving = next && arrivingX < view.widthPx ? {
33
+ index: parkedIndex + 1,
34
+ opacity: 1,
35
+ shownPx: Math.min(next.widthPx, view.widthPx - arrivingX),
36
+ x: arrivingX
37
+ } : void 0;
38
+ return {
39
+ ...arriving ? { arriving } : {},
40
+ ...parked ? { parked } : {}
41
+ };
42
+ }
43
+ function labelSlot(element) {
44
+ return {
45
+ element,
46
+ opacity: NaN,
47
+ shownPx: NaN,
48
+ x: NaN
49
+ };
50
+ }
51
+ function writePlacement(slot, { opacity, shownPx, x }, widthPx) {
52
+ const { style } = slot.element;
53
+ if (slot.x !== x) {
54
+ slot.x = x;
55
+ style.setProperty("translate", `${x.toFixed(2)}px`);
56
+ }
57
+ if (slot.shownPx !== shownPx) {
58
+ slot.shownPx = shownPx;
59
+ style.setProperty("clip-path", shownPx < widthPx ? `inset(0 calc(100% - ${shownPx.toFixed(2)}px) 0 0)` : "");
60
+ }
61
+ if (slot.opacity !== opacity) {
62
+ slot.opacity = opacity;
63
+ style.setProperty("opacity", opacity < 1 ? opacity.toFixed(3) : "");
64
+ }
65
+ }
66
+ function writeLabel(slot, labels, placement) {
67
+ const { element } = slot;
68
+ const label = placement && placement.shownPx > 0 ? labels[placement.index] : void 0;
69
+ if (element.hidden !== !label) element.hidden = !label;
70
+ if (!label || !placement) return;
71
+ if (element.textContent !== label.text) element.textContent = label.text;
72
+ element.toggleAttribute("data-sonic-dimmed", label.isDimmed);
73
+ writePlacement(slot, placement, label.widthPx);
74
+ }
75
+ function measureWidths(control, className, texts) {
76
+ const probes = texts.map((text) => {
77
+ const probe = document.createElement("div");
78
+ probe.className = className;
79
+ probe.textContent = text;
80
+ return probe;
81
+ });
82
+ control.append(...probes);
83
+ const widths = probes.map((probe) => probe.getBoundingClientRect().width);
84
+ for (const probe of probes) probe.remove();
85
+ return widths;
86
+ }
87
+ function createLabelRider(control, options) {
88
+ const { className, insetProperty } = options;
89
+ const parked = labelSlot(requireChild(control, `.${className}`, HTMLElement));
90
+ const arriving = labelSlot(requireChild(control, `.${className} + .${className}`, HTMLElement));
91
+ let insetPx = 0;
92
+ let measured = [];
93
+ return {
94
+ measure: (labels) => {
95
+ const widths = measureWidths(control, className, labels.map(({ text }) => text));
96
+ insetPx = readPxProperty(getComputedStyle(control), insetProperty, 0);
97
+ measured = labels.map((label, index) => ({
98
+ ...label,
99
+ widthPx: widths[index] ?? 0
100
+ })).toSorted((first, second) => first.start - second.start);
101
+ },
102
+ place: (window) => {
103
+ const layout = layoutRider(measured, {
104
+ ...window,
105
+ insetPx
106
+ });
107
+ writeLabel(parked, measured, layout.parked);
108
+ writeLabel(arriving, measured, layout.arriving);
109
+ }
110
+ };
111
+ }
112
+
113
+ //#endregion
114
+ export { createLabelRider, layoutRider };
@@ -2,15 +2,24 @@
2
2
  function clamp(value, min, max) {
3
3
  return Math.min(max, Math.max(min, value));
4
4
  }
5
- function clampUnit(fraction) {
5
+ function clampProportion(fraction) {
6
6
  return clamp(fraction, 0, 1);
7
7
  }
8
- function wrapUnit(fraction) {
8
+ function wrapProportion(fraction) {
9
9
  return fraction - Math.floor(fraction);
10
10
  }
11
11
  function trimFloat(value) {
12
12
  return Number(value.toPrecision(12));
13
13
  }
14
+ function decimalPlaces(value) {
15
+ const [digits = "", exponent = "0"] = String(value).split("e", 2);
16
+ const fraction = digits.split(".", 2)[1] ?? "";
17
+ return Math.max(0, fraction.length - Number(exponent));
18
+ }
19
+ function roundTo(value, places) {
20
+ const rounded = Number(value.toFixed(Math.min(100, places)));
21
+ return rounded === 0 ? 0 : rounded;
22
+ }
14
23
 
15
24
  //#endregion
16
- export { clamp, clampUnit, trimFloat, wrapUnit };
25
+ export { clamp, clampProportion, decimalPlaces, roundTo, trimFloat, wrapProportion };
@@ -0,0 +1,11 @@
1
+ //#region src/lib/modifier-press.ts
2
+ const isApple = /^(Mac|iP)/.test(navigator.platform);
3
+ function isMenuPress(event) {
4
+ return isApple && event.ctrlKey;
5
+ }
6
+ function isResetPress(event) {
7
+ return isApple ? event.metaKey : event.ctrlKey;
8
+ }
9
+
10
+ //#endregion
11
+ export { isMenuPress, isResetPress };
@@ -1,14 +1,14 @@
1
1
  //#region src/lib/number-list.ts
2
2
  function parseNumberList(text) {
3
3
  if (text === null) return void 0;
4
- const entries = [...new Set(text.split(/[\s,]+/).filter(Boolean).map(Number).filter((entry) => Number.isFinite(entry)))].toSorted((first, second) => first - second);
5
- return entries.length >= 2 ? entries : void 0;
4
+ const numbers = [...new Set(text.split(/[\s,]+/).filter(Boolean).map(Number).filter((entry) => Number.isFinite(entry)))].toSorted((first, second) => first - second);
5
+ return numbers.length >= 2 ? numbers : void 0;
6
6
  }
7
- function nearestEntry(entries, value) {
8
- let nearest = entries[0] ?? value;
9
- for (const entry of entries) if (Math.abs(entry - value) < Math.abs(nearest - value)) nearest = entry;
7
+ function nearestPosition(positions, value) {
8
+ let nearest = positions[0] ?? value;
9
+ for (const position of positions) if (Math.abs(position - value) < Math.abs(nearest - value)) nearest = position;
10
10
  return nearest;
11
11
  }
12
12
 
13
13
  //#endregion
14
- export { nearestEntry, parseNumberList };
14
+ export { nearestPosition, parseNumberList };
@@ -0,0 +1,43 @@
1
+ //#region src/lib/pointer-drag.ts
2
+ function bindDrag(target, handlers, signal) {
3
+ let held;
4
+ const end = () => {
5
+ if (!held) return;
6
+ const { drag } = held;
7
+ held = void 0;
8
+ handlers.toggle(false);
9
+ handlers.release(drag);
10
+ };
11
+ const whileHeld = (type, handle) => {
12
+ target.addEventListener(type, (event) => {
13
+ if (held?.pointerId === event.pointerId) handle(held.drag, event);
14
+ }, { signal });
15
+ };
16
+ target.addEventListener("pointerdown", (event) => {
17
+ if (event.button !== 0 || held && held.pointerId !== event.pointerId) return;
18
+ end();
19
+ const drag = handlers.grab(event);
20
+ if (drag === void 0) return;
21
+ held = {
22
+ drag,
23
+ pointerId: event.pointerId
24
+ };
25
+ target.setPointerCapture(event.pointerId);
26
+ handlers.toggle(true);
27
+ }, { signal });
28
+ whileHeld("pointermove", handlers.move);
29
+ whileHeld("pointercancel", (drag) => {
30
+ handlers.cancel(drag);
31
+ end();
32
+ });
33
+ whileHeld("lostpointercapture", end);
34
+ whileHeld("pointerup", end);
35
+ signal.addEventListener("abort", end, { once: true });
36
+ return {
37
+ current: () => held?.drag,
38
+ end
39
+ };
40
+ }
41
+
42
+ //#endregion
43
+ export { bindDrag };
@@ -13,14 +13,29 @@ function template(html, root) {
13
13
  return clone;
14
14
  };
15
15
  }
16
- function attachSlot(host) {
17
- const slot = document.createElement("slot");
16
+ function placeChildren(parent, children) {
17
+ const dropped = [...parent.childNodes].filter((child) => !children.includes(child));
18
+ for (const child of dropped) child.remove();
19
+ for (const [index, child] of children.entries()) {
20
+ const current = parent.childNodes[index];
21
+ if (current === child) continue;
22
+ if (current) current.before(child);
23
+ else parent.append(child);
24
+ }
25
+ }
26
+ function attachSlots(host) {
27
+ const shown = document.createElement("slot");
28
+ const kept = document.createElement("slot");
29
+ kept.style.cssText = "position: absolute; display: block; inline-size: 0; block-size: 0; overflow: hidden; visibility: hidden";
18
30
  host.attachShadow({
19
31
  mode: "open",
20
32
  slotAssignment: "manual"
21
- }).append(slot);
22
- return slot;
33
+ }).append(shown, kept);
34
+ return {
35
+ kept,
36
+ shown
37
+ };
23
38
  }
24
39
 
25
40
  //#endregion
26
- export { attachSlot, requireChild, template };
41
+ export { attachSlots, placeChildren, requireChild, template };
@@ -0,0 +1,36 @@
1
+ //#region src/lib/resample-peaks.ts
2
+ function bucketBounds(sourceLength, index, count) {
3
+ const start = Math.floor(index * sourceLength / count);
4
+ return [start, Math.max(start + 1, Math.floor((index + 1) * sourceLength / count))];
5
+ }
6
+ function downsample(peaks, count) {
7
+ const resampled = [];
8
+ for (let index = 0; index < count; index += 1) {
9
+ const [start, end] = bucketBounds(peaks.length, index, count);
10
+ let total = 0;
11
+ for (let source = start; source < end; source += 1) total += peaks[source] ?? 0;
12
+ resampled.push(total / (end - start));
13
+ }
14
+ return resampled;
15
+ }
16
+ function upsample(peaks, count) {
17
+ const resampled = [];
18
+ const last = peaks.length - 1;
19
+ for (let index = 0; index < count; index += 1) {
20
+ const sourceIndex = count === 1 ? 0 : index * last / (count - 1);
21
+ const before = Math.floor(sourceIndex);
22
+ const weight = sourceIndex - before;
23
+ resampled.push((peaks[before] ?? 0) * (1 - weight) + (peaks[Math.min(last, before + 1)] ?? 0) * weight);
24
+ }
25
+ return resampled;
26
+ }
27
+ function resamplePeaks(peaks, count) {
28
+ if (count <= 0 || peaks.length === 0) return [];
29
+ const finite = Array.from(peaks, (peak) => Number.isFinite(peak) ? peak : 0);
30
+ if (count === finite.length) return finite;
31
+ if (count > finite.length) return upsample(finite, count);
32
+ return downsample(finite, count);
33
+ }
34
+
35
+ //#endregion
36
+ export { resamplePeaks };
@@ -0,0 +1,48 @@
1
+ import { clampProportion } from "./math.js";
2
+ import { resamplePeaks } from "./resample-peaks.js";
3
+
4
+ //#region src/lib/strip-scene.ts
5
+ function stripBars(peaks, size, grid) {
6
+ const pitch = Math.max(1, Math.round(grid.pitch * size.dpr));
7
+ const gap = Math.min(pitch - 1, Math.max(0, Math.round(pitch * grid.gapRatio)));
8
+ const width = pitch - gap;
9
+ const count = Math.max(0, Math.floor((size.width + gap) / pitch));
10
+ return resamplePeaks(peaks, count).map((peak, index) => {
11
+ const height = Math.max(1, Math.round(clampProportion(peak) * size.height));
12
+ return {
13
+ height,
14
+ radius: width * grid.radiusRatio,
15
+ width,
16
+ x: index * pitch,
17
+ y: Math.round((size.height - height) / 2)
18
+ };
19
+ });
20
+ }
21
+ function stripRegions(state, widthPx) {
22
+ const px = (proportion) => Math.round((Number.isFinite(proportion) ? proportion : 0) * widthPx);
23
+ const buffered = state.buffered.map(([start, end]) => [px(start), px(end)]).filter(([start, end]) => end > start);
24
+ const played = px(state.played);
25
+ const scrub = state.scrub === void 0 ? played : px(state.scrub);
26
+ const regions = buffered.map(([from, to]) => ({
27
+ from,
28
+ kind: "buffered",
29
+ to
30
+ }));
31
+ if (played > 0) regions.push({
32
+ from: 0,
33
+ kind: "played",
34
+ to: played
35
+ });
36
+ if (scrub !== played) regions.push({
37
+ from: Math.min(played, scrub),
38
+ kind: "scrub",
39
+ to: Math.max(played, scrub)
40
+ });
41
+ return {
42
+ key: `${String(played)}:${String(scrub)}:${buffered.join(",")}`,
43
+ regions
44
+ };
45
+ }
46
+
47
+ //#endregion
48
+ export { stripBars, stripRegions };
@@ -1,43 +1,49 @@
1
- import { clampUnit } from "./math.js";
1
+ import { clampProportion } from "./math.js";
2
2
 
3
3
  //#region src/lib/taper.ts
4
4
  function linearTaper(min, max) {
5
5
  const range = max - min;
6
6
  return {
7
- position: (value) => range > 0 ? clampUnit((value - min) / range) : 0,
8
- value: (position) => min + clampUnit(position) * range
7
+ proportionOf: (value) => range > 0 ? clampProportion((value - min) / range) : 0,
8
+ valueAt: (proportion) => min + clampProportion(proportion) * range
9
9
  };
10
10
  }
11
11
  function logTaper(min, max) {
12
12
  if (min <= 0 || max <= min) return void 0;
13
13
  const span = Math.log(max / min);
14
14
  return {
15
- position: (value) => clampUnit(Math.log(Math.max(min, value) / min) / span),
16
- value: (position) => min * Math.exp(clampUnit(position) * span)
15
+ proportionOf: (value) => clampProportion(Math.log(Math.max(min, value) / min) / span),
16
+ valueAt: (proportion) => {
17
+ const clamped = clampProportion(proportion);
18
+ if (clamped === 0) return min;
19
+ if (clamped === 1) return max;
20
+ return min * Math.exp(clamped * span);
21
+ }
17
22
  };
18
23
  }
19
24
  function skewTaper(min, max, midpoint) {
20
25
  const range = max - min;
21
- const proportion = (midpoint - min) / range;
22
- if (proportion === .5 || !(proportion > 0 && proportion < 1)) return void 0;
23
- const skew = Math.log(.5) / Math.log(proportion);
26
+ const midpointProportion = (midpoint - min) / range;
27
+ if (midpointProportion === .5 || !(midpointProportion > 0 && midpointProportion < 1)) return void 0;
28
+ const skew = Math.log(.5) / Math.log(midpointProportion);
24
29
  return {
25
- position: (value) => clampUnit((value - min) / range) ** skew,
26
- value: (position) => min + range * clampUnit(position) ** (1 / skew)
30
+ proportionOf: (value) => clampProportion((value - min) / range) ** skew,
31
+ valueAt: (proportion) => min + range * clampProportion(proportion) ** (1 / skew)
27
32
  };
28
33
  }
29
- function listTaper(entries) {
30
- const last = entries.length - 1;
34
+ function listTaper(positions) {
35
+ const last = positions.length - 1;
31
36
  return {
32
- position: (value) => {
33
- const above = entries.findIndex((entry) => entry >= value);
37
+ proportionOf: (value) => {
38
+ if (Number.isNaN(value)) return NaN;
39
+ const above = positions.findIndex((position) => position >= value);
34
40
  if (above === -1) return 1;
35
- const high = entries[above];
36
- const low = entries[above - 1];
41
+ const high = positions[above];
42
+ const low = positions[above - 1];
37
43
  if (high === void 0 || low === void 0) return 0;
38
44
  return (above - 1 + (value - low) / (high - low)) / last;
39
45
  },
40
- value: (position) => entries[Math.round(clampUnit(position) * last)] ?? NaN
46
+ valueAt: (proportion) => positions[Math.round(clampProportion(proportion) * last)] ?? NaN
41
47
  };
42
48
  }
43
49
 
@@ -0,0 +1,8 @@
1
+ //#region src/lib/time-regions.ts
2
+ function readRegions(regions) {
3
+ if ("start" in regions) return Array.from({ length: regions.length }, (_entry, index) => [regions.start(index), regions.end(index)]);
4
+ return [...regions].map(([start, end]) => [start, end]);
5
+ }
6
+
7
+ //#endregion
8
+ export { readRegions };
@@ -0,0 +1,35 @@
1
+ //#region src/lib/tracking-clock.ts
2
+ const seekThresholdSeconds = .5;
3
+ const jitterSeconds = .05;
4
+ function isLandingOnSource({ isPlaying, seconds }, carried, lastSourceSeconds) {
5
+ const hasSteppedBack = lastSourceSeconds !== void 0 && lastSourceSeconds - seconds > jitterSeconds;
6
+ return !isPlaying || hasSteppedBack || Math.abs(seconds - carried) > seekThresholdSeconds;
7
+ }
8
+ function createTrackingClock() {
9
+ let positionSeconds;
10
+ let lastFrameMs;
11
+ let lastSourceSeconds;
12
+ let wasPlaying = false;
13
+ return { read: (frameMs, source) => {
14
+ const { isPlaying, playbackRate, seconds } = source;
15
+ const elapsedSeconds = wasPlaying ? Math.max(0, frameMs - (lastFrameMs ?? frameMs)) / 1e3 : 0;
16
+ const carried = (positionSeconds ?? seconds) + elapsedSeconds * playbackRate;
17
+ lastFrameMs = frameMs;
18
+ wasPlaying = isPlaying;
19
+ if (!Number.isFinite(seconds)) {
20
+ if (positionSeconds !== void 0) positionSeconds = carried;
21
+ return carried;
22
+ }
23
+ const isLanding = isLandingOnSource(source, carried, lastSourceSeconds);
24
+ lastSourceSeconds = seconds;
25
+ if (positionSeconds === void 0 || isLanding) {
26
+ positionSeconds = seconds;
27
+ return positionSeconds;
28
+ }
29
+ positionSeconds = carried + (seconds - carried) * (1 - .94 ** (elapsedSeconds * 60));
30
+ return positionSeconds;
31
+ } };
32
+ }
33
+
34
+ //#endregion
35
+ export { createTrackingClock };