@xsynaptic/sonic-ui 0.4.0 → 0.6.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 (247) hide show
  1. package/README.md +3 -20
  2. package/dist/default/dat.js +51 -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 +383 -0
  14. package/dist/default/elements/meter.js +155 -63
  15. package/dist/default/elements/number.js +22 -9
  16. package/dist/default/elements/radio-group.js +183 -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 +69 -31
  22. package/dist/default/elements/split.js +249 -0
  23. package/dist/default/elements/switch.js +273 -0
  24. package/dist/default/elements/value-element.js +694 -0
  25. package/dist/default/elements/{range-entry.js → value-entry.js} +3 -3
  26. package/dist/default/elements/wave-element.js +80 -0
  27. package/dist/default/elements/waveform.js +328 -0
  28. package/dist/default/elements/wavestrip.js +185 -0
  29. package/dist/default/elements/xy.js +487 -0
  30. package/dist/default/index.js +10 -2
  31. package/dist/default/lib/adsr-shape.js +113 -0
  32. package/dist/default/lib/ballistics.js +55 -0
  33. package/dist/default/lib/canvas-surface.js +203 -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 +63 -0
  42. package/dist/default/lib/hold.js +38 -0
  43. package/dist/default/lib/marker-band.js +80 -0
  44. package/dist/default/lib/marker-rider.js +120 -0
  45. package/dist/default/lib/math.js +17 -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 +46 -0
  49. package/dist/default/lib/render.js +20 -5
  50. package/dist/default/lib/resample-peaks.js +46 -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/default/lib/write-attribute.js +1 -0
  58. package/dist/dev/dat.d.ts +19 -0
  59. package/dist/dev/dat.js +52 -0
  60. package/dist/dev/dat.js.map +1 -0
  61. package/dist/dev/define/button.d.ts +2 -0
  62. package/dist/dev/define/button.js +8 -0
  63. package/dist/dev/define/button.js.map +1 -0
  64. package/dist/dev/define/dial.js.map +1 -1
  65. package/dist/dev/define/envelope.d.ts +2 -0
  66. package/dist/dev/define/envelope.js +8 -0
  67. package/dist/dev/define/envelope.js.map +1 -0
  68. package/dist/dev/define/meter.js.map +1 -1
  69. package/dist/dev/define/number.js.map +1 -1
  70. package/dist/dev/define/segmented.js.map +1 -1
  71. package/dist/dev/define/slider.js.map +1 -1
  72. package/dist/dev/define/split.d.ts +2 -0
  73. package/dist/dev/define/split.js +8 -0
  74. package/dist/dev/define/split.js.map +1 -0
  75. package/dist/dev/define/switch.d.ts +2 -0
  76. package/dist/dev/define/switch.js +8 -0
  77. package/dist/dev/define/switch.js.map +1 -0
  78. package/dist/dev/define/waveform.d.ts +2 -0
  79. package/dist/dev/define/waveform.js +8 -0
  80. package/dist/dev/define/waveform.js.map +1 -0
  81. package/dist/dev/define/wavestrip.d.ts +2 -0
  82. package/dist/dev/define/wavestrip.js +8 -0
  83. package/dist/dev/define/wavestrip.js.map +1 -0
  84. package/dist/dev/define/xy.d.ts +2 -0
  85. package/dist/dev/define/xy.js +8 -0
  86. package/dist/dev/define/xy.js.map +1 -0
  87. package/dist/dev/define.d.ts +15 -3
  88. package/dist/dev/define.js +7 -1
  89. package/dist/dev/elements/{key.d.ts → button.d.ts} +8 -6
  90. package/dist/dev/elements/button.js +166 -0
  91. package/dist/dev/elements/button.js.map +1 -0
  92. package/dist/dev/elements/dial.d.ts +6 -4
  93. package/dist/dev/elements/dial.js +16 -22
  94. package/dist/dev/elements/dial.js.map +1 -1
  95. package/dist/dev/elements/envelope.d.ts +36 -0
  96. package/dist/dev/elements/envelope.js +388 -0
  97. package/dist/dev/elements/envelope.js.map +1 -0
  98. package/dist/dev/elements/form-element.d.ts +1 -1
  99. package/dist/dev/elements/form-element.js.map +1 -1
  100. package/dist/dev/elements/meter.d.ts +11 -6
  101. package/dist/dev/elements/meter.js +155 -63
  102. package/dist/dev/elements/meter.js.map +1 -1
  103. package/dist/dev/elements/number.d.ts +7 -3
  104. package/dist/dev/elements/number.js +22 -9
  105. package/dist/dev/elements/number.js.map +1 -1
  106. package/dist/dev/elements/radio-group.d.ts +25 -0
  107. package/dist/dev/elements/radio-group.js +184 -0
  108. package/dist/dev/elements/radio-group.js.map +1 -0
  109. package/dist/dev/elements/readout-claim.js +82 -0
  110. package/dist/dev/elements/readout-claim.js.map +1 -0
  111. package/dist/dev/elements/readout.js +36 -0
  112. package/dist/dev/elements/readout.js.map +1 -0
  113. package/dist/dev/elements/segmented.d.ts +4 -10
  114. package/dist/dev/elements/segmented.js +9 -159
  115. package/dist/dev/elements/segmented.js.map +1 -1
  116. package/dist/dev/elements/slider.d.ts +6 -6
  117. package/dist/dev/elements/slider.js +42 -51
  118. package/dist/dev/elements/slider.js.map +1 -1
  119. package/dist/dev/elements/sonic-element.d.ts +6 -3
  120. package/dist/dev/elements/sonic-element.js +119 -40
  121. package/dist/dev/elements/sonic-element.js.map +1 -1
  122. package/dist/dev/elements/split.d.ts +21 -0
  123. package/dist/dev/elements/split.js +258 -0
  124. package/dist/dev/elements/split.js.map +1 -0
  125. package/dist/dev/elements/switch.d.ts +35 -0
  126. package/dist/dev/elements/switch.js +274 -0
  127. package/dist/dev/elements/switch.js.map +1 -0
  128. package/dist/dev/elements/value-element.d.ts +84 -0
  129. package/dist/dev/elements/value-element.js +695 -0
  130. package/dist/dev/elements/value-element.js.map +1 -0
  131. package/dist/dev/elements/{range-entry.js → value-entry.js} +4 -4
  132. package/dist/dev/elements/value-entry.js.map +1 -0
  133. package/dist/dev/elements/wave-element.d.ts +37 -0
  134. package/dist/dev/elements/wave-element.js +81 -0
  135. package/dist/dev/elements/wave-element.js.map +1 -0
  136. package/dist/dev/elements/waveform.d.ts +55 -0
  137. package/dist/dev/elements/waveform.js +329 -0
  138. package/dist/dev/elements/waveform.js.map +1 -0
  139. package/dist/dev/elements/wavestrip.d.ts +51 -0
  140. package/dist/dev/elements/wavestrip.js +186 -0
  141. package/dist/dev/elements/wavestrip.js.map +1 -0
  142. package/dist/dev/elements/xy.d.ts +62 -0
  143. package/dist/dev/elements/xy.js +488 -0
  144. package/dist/dev/elements/xy.js.map +1 -0
  145. package/dist/dev/index.d.ts +11 -2
  146. package/dist/dev/index.js +10 -2
  147. package/dist/dev/lib/adsr-shape.js +114 -0
  148. package/dist/dev/lib/adsr-shape.js.map +1 -0
  149. package/dist/dev/lib/ballistics.js +56 -0
  150. package/dist/dev/lib/ballistics.js.map +1 -0
  151. package/dist/dev/lib/canvas-surface.d.ts +26 -0
  152. package/dist/dev/lib/canvas-surface.js +204 -0
  153. package/dist/dev/lib/canvas-surface.js.map +1 -0
  154. package/dist/dev/lib/copy-node.js +32 -2
  155. package/dist/dev/lib/copy-node.js.map +1 -1
  156. package/dist/dev/lib/crossfade.d.ts +12 -0
  157. package/dist/dev/lib/crossfade.js +23 -0
  158. package/dist/dev/lib/crossfade.js.map +1 -0
  159. package/dist/dev/lib/distribute.d.ts +4 -0
  160. package/dist/dev/lib/distribute.js +216 -0
  161. package/dist/dev/lib/distribute.js.map +1 -0
  162. package/dist/dev/lib/drag-step.js +100 -0
  163. package/dist/dev/lib/drag-step.js.map +1 -0
  164. package/dist/dev/lib/envelope-curve.d.ts +4 -0
  165. package/dist/dev/lib/envelope-curve.js +10 -0
  166. package/dist/dev/lib/envelope-curve.js.map +1 -0
  167. package/dist/dev/lib/field.d.ts +4 -0
  168. package/dist/dev/lib/field.js +95 -0
  169. package/dist/dev/lib/field.js.map +1 -0
  170. package/dist/dev/lib/focus-by-pointer.js +29 -0
  171. package/dist/dev/lib/focus-by-pointer.js.map +1 -0
  172. package/dist/dev/lib/frame-timeline.js +64 -0
  173. package/dist/dev/lib/frame-timeline.js.map +1 -0
  174. package/dist/dev/lib/hold.js +39 -0
  175. package/dist/dev/lib/hold.js.map +1 -0
  176. package/dist/dev/lib/marker-band.js +81 -0
  177. package/dist/dev/lib/marker-band.js.map +1 -0
  178. package/dist/dev/lib/marker-rider.js +121 -0
  179. package/dist/dev/lib/marker-rider.js.map +1 -0
  180. package/dist/dev/lib/math.js +17 -3
  181. package/dist/dev/lib/math.js.map +1 -1
  182. package/dist/dev/lib/modifier-press.js +12 -0
  183. package/dist/dev/lib/modifier-press.js.map +1 -0
  184. package/dist/dev/lib/number-list.js +6 -6
  185. package/dist/dev/lib/number-list.js.map +1 -1
  186. package/dist/dev/lib/pointer-drag.js +47 -0
  187. package/dist/dev/lib/pointer-drag.js.map +1 -0
  188. package/dist/dev/lib/read-px-property.js.map +1 -1
  189. package/dist/dev/lib/render.js +20 -5
  190. package/dist/dev/lib/render.js.map +1 -1
  191. package/dist/dev/lib/resample-peaks.js +47 -0
  192. package/dist/dev/lib/resample-peaks.js.map +1 -0
  193. package/dist/dev/lib/strip-scene.js +49 -0
  194. package/dist/dev/lib/strip-scene.js.map +1 -0
  195. package/dist/dev/lib/taper.js +23 -17
  196. package/dist/dev/lib/taper.js.map +1 -1
  197. package/dist/dev/lib/time-regions.d.ts +4 -0
  198. package/dist/dev/lib/time-regions.js +9 -0
  199. package/dist/dev/lib/time-regions.js.map +1 -0
  200. package/dist/dev/lib/tracking-clock.js +36 -0
  201. package/dist/dev/lib/tracking-clock.js.map +1 -0
  202. package/dist/dev/lib/value-mapping.d.ts +23 -0
  203. package/dist/dev/lib/value-mapping.js +116 -0
  204. package/dist/dev/lib/value-mapping.js.map +1 -0
  205. package/dist/dev/lib/waveform-buckets.d.ts +9 -0
  206. package/dist/dev/lib/waveform-buckets.js +42 -0
  207. package/dist/dev/lib/waveform-buckets.js.map +1 -0
  208. package/dist/dev/lib/write-attribute.js +1 -0
  209. package/dist/dev/lib/write-attribute.js.map +1 -1
  210. package/dist/skins/amber.css +8 -0
  211. package/dist/skins/flat.css +25 -0
  212. package/dist/skins/ivory.css +2 -2
  213. package/dist/skins/lime.css +2 -0
  214. package/dist/skins/slate.css +2 -0
  215. package/dist/styles/button.css +240 -0
  216. package/dist/styles/controls.css +8 -1
  217. package/dist/styles/dial.css +110 -56
  218. package/dist/styles/envelope.css +204 -0
  219. package/dist/styles/led.css +39 -8
  220. package/dist/styles/material.css +343 -100
  221. package/dist/styles/meter.css +41 -52
  222. package/dist/styles/number.css +5 -1
  223. package/dist/styles/panel.css +61 -0
  224. package/dist/styles/ring.css +74 -0
  225. package/dist/styles/segmented.css +46 -51
  226. package/dist/styles/slider.css +177 -40
  227. package/dist/styles/switch.css +386 -0
  228. package/dist/styles/waveform.css +196 -0
  229. package/dist/styles/wavestrip.css +232 -0
  230. package/dist/styles/xy.css +241 -0
  231. package/package.json +15 -6
  232. package/dist/default/define/key.js +0 -7
  233. package/dist/default/elements/key.js +0 -169
  234. package/dist/default/elements/range-element.js +0 -546
  235. package/dist/default/lib/detent.js +0 -17
  236. package/dist/dev/define/key.d.ts +0 -2
  237. package/dist/dev/define/key.js +0 -8
  238. package/dist/dev/define/key.js.map +0 -1
  239. package/dist/dev/elements/key.js +0 -170
  240. package/dist/dev/elements/key.js.map +0 -1
  241. package/dist/dev/elements/range-element.d.ts +0 -64
  242. package/dist/dev/elements/range-element.js +0 -547
  243. package/dist/dev/elements/range-element.js.map +0 -1
  244. package/dist/dev/elements/range-entry.js.map +0 -1
  245. package/dist/dev/lib/detent.js +0 -18
  246. package/dist/dev/lib/detent.js.map +0 -1
  247. 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,63 @@
1
+ import { trimFloat } from "./math.js";
2
+
3
+ //#region src/lib/frame-timeline.ts
4
+ const placeholderPeriodMs = 2500;
5
+ const lookAheadSeconds = 30;
6
+ function windowStart(input, seconds, windowSeconds) {
7
+ if (!input.isPaged) return seconds - windowSeconds / 2;
8
+ if (input.held) return input.held.grabbed.startSeconds + seconds - input.held.grabbed.seconds;
9
+ return Math.floor(trimFloat(seconds / windowSeconds)) * windowSeconds;
10
+ }
11
+ function overlap([from, to], [low, high]) {
12
+ const start = Math.max(from, low);
13
+ const end = Math.min(to, high);
14
+ return end > start ? [start, end] : void 0;
15
+ }
16
+ function pendingIn({ pending, range }, window) {
17
+ return pending.flatMap((region) => {
18
+ const inRange = overlap(region, range);
19
+ const shown = inRange && overlap(inRange, window);
20
+ return shown ? [shown] : [];
21
+ });
22
+ }
23
+ function placeholderPhase({ frameMs, isPaged }, pending) {
24
+ return !isPaged && pending.length > 0 ? frameMs / placeholderPeriodMs : void 0;
25
+ }
26
+ function frameTimeline(input) {
27
+ const { clockSeconds, held, range, size } = input;
28
+ const seconds = held?.seconds ?? clockSeconds;
29
+ const pixelsPerSecond = input.zoom * size.dpr;
30
+ const windowSeconds = size.width / pixelsPerSecond;
31
+ const startSeconds = windowStart(input, seconds, windowSeconds);
32
+ const pending = pendingIn(input, [startSeconds, startSeconds + windowSeconds]);
33
+ const placeholder = placeholderPhase(input, pending);
34
+ const phase = placeholder ?? 0;
35
+ const wanted = overlap([startSeconds - windowSeconds, startSeconds + windowSeconds + Math.max(windowSeconds, lookAheadSeconds)], range);
36
+ const at = (time) => (time - startSeconds) / windowSeconds;
37
+ return {
38
+ ...held ? { ghostAt: at(clockSeconds) } : {},
39
+ isMoving: held !== void 0 || placeholder !== void 0 || input.isPlaying,
40
+ paintKey: [
41
+ startSeconds,
42
+ windowSeconds,
43
+ ...range,
44
+ phase,
45
+ pending.join(",")
46
+ ].join(":"),
47
+ playheadAt: at(seconds),
48
+ seconds,
49
+ view: {
50
+ dpr: size.dpr,
51
+ height: size.height,
52
+ pending,
53
+ phase,
54
+ pixelsPerSecond,
55
+ startSeconds,
56
+ width: size.width
57
+ },
58
+ ...wanted ? { wanted } : {}
59
+ };
60
+ }
61
+
62
+ //#endregion
63
+ 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,80 @@
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 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) {
17
+ const part = document.createElement("div");
18
+ part.className = lane === void 0 ? "sonic-wavestrip-region" : "sonic-wavestrip-marker";
19
+ if (lane !== void 0) part.style.setProperty("--_sonic-marker-lane", String(lane));
20
+ part.style.setProperty("--_sonic-marker-from", String(from));
21
+ part.style.setProperty("--_sonic-marker-to", String(to));
22
+ writeKind(part, marker.kind, "--_sonic-marker-default");
23
+ if (marker.dimmed === true) part.dataset.sonicDimmed = "";
24
+ return part;
25
+ }
26
+ function createMarkerBand(band) {
27
+ let dots = [];
28
+ const px = (property) => readPxProperty(getComputedStyle(band), property, 0);
29
+ return {
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
+ },
42
+ render: (markers, proportions) => {
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))));
55
+ }
56
+ };
57
+ }
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) {
68
+ let nearest;
69
+ let nearestDistance = reach * reach;
70
+ for (const [index, dot] of dots.entries()) {
71
+ const distance = (dot.x - point.x) ** 2 + (dot.y - point.y) ** 2;
72
+ if (distance > nearestDistance) continue;
73
+ nearest = index;
74
+ nearestDistance = distance;
75
+ }
76
+ return nearest;
77
+ }
78
+
79
+ //#endregion
80
+ export { createMarkerBand, isKind, markerLanes, nearestDot, writeKind };
@@ -0,0 +1,120 @@
1
+ import { clamp } from "./math.js";
2
+ import { requireChild } from "./render.js";
3
+ import { readPxProperty } from "./read-px-property.js";
4
+ import { writeKind } from "./marker-band.js";
5
+
6
+ //#region src/lib/marker-rider.ts
7
+ const fadeShare = .25;
8
+ function lineX(seconds, view) {
9
+ return (seconds - view.startSeconds) / view.windowSeconds * view.widthPx;
10
+ }
11
+ function parkedPlacement(labels, index, [view, arrivingX]) {
12
+ const parked = labels[index];
13
+ if (!parked) return void 0;
14
+ const next = labels[index + 1];
15
+ const x = Math.max(view.insetPx, lineX(parked.start, view) + view.insetPx);
16
+ const approachPx = next ? lineX(next.start, view) - lineX(view.playheadSeconds, view) : Infinity;
17
+ return {
18
+ index,
19
+ opacity: clamp(approachPx / (view.widthPx * fadeShare), 0, 1),
20
+ shownPx: Math.min(parked.widthPx, arrivingX - x, view.widthPx - x),
21
+ x
22
+ };
23
+ }
24
+ function isDrawable(length) {
25
+ return Number.isFinite(length) && length > 0;
26
+ }
27
+ function layoutRider(labels, view) {
28
+ if (!isDrawable(view.windowSeconds) || !isDrawable(view.widthPx)) return {};
29
+ const parkedIndex = labels.findLastIndex((label) => label.start <= view.playheadSeconds);
30
+ const next = labels[parkedIndex + 1];
31
+ const arrivingX = next ? lineX(next.start, view) + view.insetPx : Infinity;
32
+ const parked = parkedPlacement(labels, parkedIndex, [view, arrivingX]);
33
+ const arriving = next && arrivingX < view.widthPx ? {
34
+ index: parkedIndex + 1,
35
+ opacity: 1,
36
+ shownPx: Math.min(next.widthPx, view.widthPx - arrivingX),
37
+ x: arrivingX
38
+ } : void 0;
39
+ return {
40
+ ...arriving ? { arriving } : {},
41
+ ...parked ? { parked } : {}
42
+ };
43
+ }
44
+ function labelSlot(element) {
45
+ return {
46
+ element,
47
+ kind: void 0,
48
+ opacity: NaN,
49
+ shownPx: NaN,
50
+ x: NaN
51
+ };
52
+ }
53
+ function writePlacement(slot, { opacity, shownPx, x }, widthPx) {
54
+ const { style } = slot.element;
55
+ if (slot.x !== x) {
56
+ slot.x = x;
57
+ style.setProperty("translate", `${x.toFixed(2)}px`);
58
+ }
59
+ if (slot.shownPx !== shownPx) {
60
+ slot.shownPx = shownPx;
61
+ style.setProperty("clip-path", shownPx < widthPx ? `inset(0 calc(100% - ${shownPx.toFixed(2)}px) 0 0)` : "");
62
+ }
63
+ if (slot.opacity !== opacity) {
64
+ slot.opacity = opacity;
65
+ style.setProperty("opacity", opacity < 1 ? opacity.toFixed(3) : "");
66
+ }
67
+ }
68
+ function writeLabel(slot, [labels, colourProperty], placement) {
69
+ const { element } = slot;
70
+ const label = placement && placement.shownPx > 0 ? labels[placement.index] : void 0;
71
+ if (element.hidden !== !label) element.hidden = !label;
72
+ if (!label || !placement) return;
73
+ if (element.textContent !== label.text) element.textContent = label.text;
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
+ }
79
+ writePlacement(slot, placement, label.widthPx);
80
+ }
81
+ function measureWidths(control, className, texts) {
82
+ const probes = texts.map((text) => {
83
+ const probe = document.createElement("div");
84
+ probe.className = className;
85
+ probe.textContent = text;
86
+ return probe;
87
+ });
88
+ control.append(...probes);
89
+ const widths = probes.map((probe) => probe.getBoundingClientRect().width);
90
+ for (const probe of probes) probe.remove();
91
+ return widths;
92
+ }
93
+ function createLabelRider(control, options) {
94
+ const { className, colourProperty, insetProperty } = options;
95
+ const parked = labelSlot(requireChild(control, `.${className}`, HTMLElement));
96
+ const arriving = labelSlot(requireChild(control, `.${className} + .${className}`, HTMLElement));
97
+ let insetPx = 0;
98
+ let measured = [];
99
+ return {
100
+ measure: (labels) => {
101
+ const widths = measureWidths(control, className, labels.map(({ text }) => text));
102
+ insetPx = readPxProperty(getComputedStyle(control), insetProperty, 0);
103
+ measured = labels.map((label, index) => ({
104
+ ...label,
105
+ widthPx: widths[index] ?? 0
106
+ })).toSorted((first, second) => first.start - second.start);
107
+ },
108
+ place: (window) => {
109
+ const layout = layoutRider(measured, {
110
+ ...window,
111
+ insetPx
112
+ });
113
+ writeLabel(parked, [measured, colourProperty], layout.parked);
114
+ writeLabel(arriving, [measured, colourProperty], layout.arriving);
115
+ }
116
+ };
117
+ }
118
+
119
+ //#endregion
120
+ export { createLabelRider, layoutRider };
@@ -2,15 +2,29 @@
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 toNumber(value) {
6
+ if (typeof value === "number") return value;
7
+ if (typeof value !== "string" || value.trim() === "") return NaN;
8
+ return Number(value);
9
+ }
10
+ function clampProportion(fraction) {
6
11
  return clamp(fraction, 0, 1);
7
12
  }
8
- function wrapUnit(fraction) {
13
+ function wrapProportion(fraction) {
9
14
  return fraction - Math.floor(fraction);
10
15
  }
11
16
  function trimFloat(value) {
12
17
  return Number(value.toPrecision(12));
13
18
  }
19
+ function decimalPlaces(value) {
20
+ const [digits = "", exponent = "0"] = String(value).split("e", 2);
21
+ const fraction = digits.split(".", 2)[1] ?? "";
22
+ return Math.max(0, fraction.length - Number(exponent));
23
+ }
24
+ function roundTo(value, places) {
25
+ const rounded = Number(value.toFixed(Math.min(100, places)));
26
+ return rounded === 0 ? 0 : rounded;
27
+ }
14
28
 
15
29
  //#endregion
16
- export { clamp, clampUnit, trimFloat, wrapUnit };
30
+ export { clamp, clampProportion, decimalPlaces, roundTo, toNumber, 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,46 @@
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", (drag, event) => {
35
+ end();
36
+ handlers.lift?.(drag, event);
37
+ });
38
+ signal.addEventListener("abort", end, { once: true });
39
+ return {
40
+ current: () => held?.drag,
41
+ end
42
+ };
43
+ }
44
+
45
+ //#endregion
46
+ 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,46 @@
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 rescale(resampled, peakOf(peaks));
15
+ }
16
+ function peakOf(peaks) {
17
+ let loudest = 0;
18
+ for (const peak of peaks) loudest = Math.max(loudest, peak);
19
+ return loudest;
20
+ }
21
+ function rescale(bars, loudest) {
22
+ const tallest = peakOf(bars);
23
+ if (tallest <= 0) return bars;
24
+ return bars.map((bar) => bar * loudest / tallest);
25
+ }
26
+ function upsample(peaks, count) {
27
+ const resampled = [];
28
+ const last = peaks.length - 1;
29
+ for (let index = 0; index < count; index += 1) {
30
+ const sourceIndex = count === 1 ? 0 : index * last / (count - 1);
31
+ const before = Math.floor(sourceIndex);
32
+ const weight = sourceIndex - before;
33
+ resampled.push((peaks[before] ?? 0) * (1 - weight) + (peaks[Math.min(last, before + 1)] ?? 0) * weight);
34
+ }
35
+ return resampled;
36
+ }
37
+ function resamplePeaks(peaks, count) {
38
+ if (count <= 0 || peaks.length === 0) return [];
39
+ const finite = Array.from(peaks, (peak) => Number.isFinite(peak) ? peak : 0);
40
+ if (count === finite.length) return finite;
41
+ if (count > finite.length) return upsample(finite, count);
42
+ return downsample(finite, count);
43
+ }
44
+
45
+ //#endregion
46
+ 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 };