@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,81 @@
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 };
81
+ //# sourceMappingURL=marker-band.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"marker-band.js","names":[],"sources":["../../../src/lib/marker-band.ts"],"sourcesContent":["import { readPxProperty } from '#lib/read-px-property.ts';\n\ninterface MarkerLook {\n\tdimmed?: boolean;\n\tend?: number;\n\tkind?: string;\n}\n\ninterface Point {\n\tx: number;\n\ty: number;\n}\n\ninterface MarkerBand {\n\tat: (point: Point, widthPx: number) => number | undefined;\n\trender: (\n\t\tmarkers: ReadonlyArray<MarkerLook>,\n\t\tproportions: ReadonlyArray<[number, number]>,\n\t) => void;\n}\n\n// Interpolated into a style, so anything else falls back to the default colour\nconst kindPattern = /^[a-z][a-z0-9-]*$/;\n\nconst snapSlopPx = 4;\n\nexport function isKind(kind: string | undefined): kind is string {\n\treturn kind !== undefined && kindPattern.test(kind);\n}\n\nexport function writeKind(part: HTMLElement, kind: string | undefined, fallback: string): void {\n\tif (!isKind(kind)) {\n\t\tpart.style.removeProperty('--_sonic-marker');\n\t\treturn;\n\t}\n\n\tpart.style.setProperty('--_sonic-marker', `var(--sonic-marker-${kind}, var(${fallback}))`);\n}\n\nfunction renderMarker(\n\tmarker: MarkerLook,\n\t[from, to]: [number, number],\n\tlane: number | undefined,\n): HTMLElement {\n\tconst part = document.createElement('div');\n\n\tpart.className = lane === undefined ? 'sonic-wavestrip-region' : 'sonic-wavestrip-marker';\n\tif (lane !== undefined) part.style.setProperty('--_sonic-marker-lane', String(lane));\n\tpart.style.setProperty('--_sonic-marker-from', String(from));\n\tpart.style.setProperty('--_sonic-marker-to', String(to));\n\twriteKind(part, marker.kind, '--_sonic-marker-default');\n\tif (marker.dimmed === true) part.dataset.sonicDimmed = '';\n\n\treturn part;\n}\n\nexport function createMarkerBand(band: HTMLElement): MarkerBand {\n\tlet dots: Array<{ at: number; index: number; lane: number }> = [];\n\n\tconst px = (property: string): number => readPxProperty(getComputedStyle(band), property, 0);\n\n\treturn {\n\t\tat: ({ x, y }, widthPx) => {\n\t\t\tconst size = px('--_sonic-marker-size');\n\t\t\tconst step = px('--_sonic-marker-step');\n\t\t\tconst nearest = nearestDot(\n\t\t\t\t{ x, y },\n\t\t\t\tdots.map(({ at, lane }) => ({ x: at * widthPx, y: size / 2 + lane * step })),\n\t\t\t\tsize / 2 + snapSlopPx,\n\t\t\t);\n\n\t\t\treturn nearest === undefined ? undefined : dots[nearest]?.index;\n\t\t},\n\t\trender: (markers, proportions) => {\n\t\t\tconst widthPx = band.getBoundingClientRect().width;\n\t\t\tconst points = markers.flatMap((marker, index) =>\n\t\t\t\tmarker.end === undefined ? [{ at: proportions[index]?.[0] ?? 0, index }] : [],\n\t\t\t);\n\t\t\tconst lanes = markerLanes(\n\t\t\t\tpoints.map(({ at }) => at * widthPx),\n\t\t\t\tpx('--_sonic-marker-step'),\n\t\t\t);\n\n\t\t\tdots = points.map((point, order) => ({ ...point, lane: lanes[order] ?? 0 }));\n\n\t\t\tconst laneOf = new Map(dots.map(({ index, lane }) => [index, lane]));\n\n\t\t\tband.replaceChildren(\n\t\t\t\t...markers.map((marker, index) =>\n\t\t\t\t\trenderMarker(marker, proportions[index] ?? [0, 0], laneOf.get(index)),\n\t\t\t\t),\n\t\t\t);\n\t\t},\n\t};\n}\n\nexport function markerLanes(positions: ReadonlyArray<number>, minDistance: number): Array<number> {\n\tconst lastInLane: Array<number> = [];\n\n\treturn positions.map((position) => {\n\t\tconst free = lastInLane.findIndex((last) => position - last >= minDistance);\n\t\tconst lane = free === -1 ? lastInLane.length : free;\n\n\t\tlastInLane[lane] = position;\n\n\t\treturn lane;\n\t});\n}\n\nexport function nearestDot(\n\tpoint: Point,\n\tdots: ReadonlyArray<Point>,\n\treach: number,\n): number | undefined {\n\tlet nearest: number | undefined;\n\tlet nearestDistance = reach * reach;\n\n\tfor (const [index, dot] of dots.entries()) {\n\t\tconst distance = (dot.x - point.x) ** 2 + (dot.y - point.y) ** 2;\n\t\tif (distance > nearestDistance) continue;\n\n\t\tnearest = index;\n\t\tnearestDistance = distance;\n\t}\n\n\treturn nearest;\n}\n"],"mappings":";;;AAsBA,MAAM,cAAc;AAEpB,MAAM,aAAa;AAEnB,SAAgB,OAAO,MAA0C;CAChE,OAAO,SAAS,UAAa,YAAY,KAAK,IAAI;AACnD;AAEA,SAAgB,UAAU,MAAmB,MAA0B,UAAwB;CAC9F,IAAI,CAAC,OAAO,IAAI,GAAG;EAClB,KAAK,MAAM,eAAe,iBAAiB;EAC3C;CACD;CAEA,KAAK,MAAM,YAAY,mBAAmB,sBAAsB,KAAK,QAAQ,SAAS,GAAG;AAC1F;AAEA,SAAS,aACR,QACA,CAAC,MAAM,KACP,MACc;CACd,MAAM,OAAO,SAAS,cAAc,KAAK;CAEzC,KAAK,YAAY,SAAS,SAAY,2BAA2B;CACjE,IAAI,SAAS,QAAW,KAAK,MAAM,YAAY,wBAAwB,OAAO,IAAI,CAAC;CACnF,KAAK,MAAM,YAAY,wBAAwB,OAAO,IAAI,CAAC;CAC3D,KAAK,MAAM,YAAY,sBAAsB,OAAO,EAAE,CAAC;CACvD,UAAU,MAAM,OAAO,MAAM,yBAAyB;CACtD,IAAI,OAAO,WAAW,MAAM,KAAK,QAAQ,cAAc;CAEvD,OAAO;AACR;AAEA,SAAgB,iBAAiB,MAA+B;CAC/D,IAAI,OAA2D,CAAC;CAEhE,MAAM,MAAM,aAA6B,eAAe,iBAAiB,IAAI,GAAG,UAAU,CAAC;CAE3F,OAAO;EACN,KAAK,EAAE,GAAG,KAAK,YAAY;GAC1B,MAAM,OAAO,GAAG,sBAAsB;GACtC,MAAM,OAAO,GAAG,sBAAsB;GACtC,MAAM,UAAU,WACf;IAAE;IAAG;GAAE,GACP,KAAK,KAAK,EAAE,IAAI,YAAY;IAAE,GAAG,KAAK;IAAS,GAAG,OAAO,IAAI,OAAO;GAAK,EAAE,GAC3E,OAAO,IAAI,UACZ;GAEA,OAAO,YAAY,SAAY,SAAY,KAAK,QAAQ,EAAE;EAC3D;EACA,SAAS,SAAS,gBAAgB;GACjC,MAAM,UAAU,KAAK,sBAAsB,CAAC,CAAC;GAC7C,MAAM,SAAS,QAAQ,SAAS,QAAQ,UACvC,OAAO,QAAQ,SAAY,CAAC;IAAE,IAAI,YAAY,MAAM,GAAG,MAAM;IAAG;GAAM,CAAC,IAAI,CAAC,CAC7E;GACA,MAAM,QAAQ,YACb,OAAO,KAAK,EAAE,SAAS,KAAK,OAAO,GACnC,GAAG,sBAAsB,CAC1B;GAEA,OAAO,OAAO,KAAK,OAAO,WAAW;IAAE,GAAG;IAAO,MAAM,MAAM,UAAU;GAAE,EAAE;GAE3E,MAAM,SAAS,IAAI,IAAI,KAAK,KAAK,EAAE,OAAO,WAAW,CAAC,OAAO,IAAI,CAAC,CAAC;GAEnE,KAAK,gBACJ,GAAG,QAAQ,KAAK,QAAQ,UACvB,aAAa,QAAQ,YAAY,UAAU,CAAC,GAAG,CAAC,GAAG,OAAO,IAAI,KAAK,CAAC,CACrE,CACD;EACD;CACD;AACD;AAEA,SAAgB,YAAY,WAAkC,aAAoC;CACjG,MAAM,aAA4B,CAAC;CAEnC,OAAO,UAAU,KAAK,aAAa;EAClC,MAAM,OAAO,WAAW,WAAW,SAAS,WAAW,QAAQ,WAAW;EAC1E,MAAM,OAAO,SAAS,KAAK,WAAW,SAAS;EAE/C,WAAW,QAAQ;EAEnB,OAAO;CACR,CAAC;AACF;AAEA,SAAgB,WACf,OACA,MACA,OACqB;CACrB,IAAI;CACJ,IAAI,kBAAkB,QAAQ;CAE9B,KAAK,MAAM,CAAC,OAAO,QAAQ,KAAK,QAAQ,GAAG;EAC1C,MAAM,YAAY,IAAI,IAAI,MAAM,MAAM,KAAK,IAAI,IAAI,MAAM,MAAM;EAC/D,IAAI,WAAW,iBAAiB;EAEhC,UAAU;EACV,kBAAkB;CACnB;CAEA,OAAO;AACR"}
@@ -0,0 +1,121 @@
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 };
121
+ //# sourceMappingURL=marker-rider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"marker-rider.js","names":[],"sources":["../../../src/lib/marker-rider.ts"],"sourcesContent":["import { writeKind } from '#lib/marker-band.ts';\nimport { clamp } from '#lib/math.ts';\nimport { readPxProperty } from '#lib/read-px-property.ts';\nimport { requireChild } from '#lib/render.ts';\n\ninterface RiderWindow {\n\tplayheadSeconds: number;\n\tstartSeconds: number;\n\twidthPx: number;\n\twindowSeconds: number;\n}\n\nexport interface RiderView extends RiderWindow {\n\tinsetPx: number;\n}\n\nexport interface RiderLabel {\n\tisDimmed: boolean;\n\tkind?: string;\n\tstart: number;\n\ttext: string;\n\twidthPx: number;\n}\n\ninterface LabelPlacement {\n\tindex: number;\n\topacity: number;\n\tshownPx: number;\n\tx: number;\n}\n\ninterface RiderLayout {\n\tarriving?: LabelPlacement;\n\tparked?: LabelPlacement;\n}\n\ninterface LabelSlot {\n\telement: HTMLElement;\n\tkind: string | undefined;\n\topacity: number;\n\tshownPx: number;\n\tx: number;\n}\n\nconst fadeShare = 0.25;\n\nfunction lineX(seconds: number, view: RiderView): number {\n\treturn ((seconds - view.startSeconds) / view.windowSeconds) * view.widthPx;\n}\n\nfunction parkedPlacement(\n\tlabels: ReadonlyArray<RiderLabel>,\n\tindex: number,\n\t[view, arrivingX]: [RiderView, number],\n): LabelPlacement | undefined {\n\tconst parked = labels[index];\n\tif (!parked) return undefined;\n\n\tconst next = labels[index + 1];\n\tconst x = Math.max(view.insetPx, lineX(parked.start, view) + view.insetPx);\n\tconst approachPx = next ? lineX(next.start, view) - lineX(view.playheadSeconds, view) : Infinity;\n\n\treturn {\n\t\tindex,\n\t\topacity: clamp(approachPx / (view.widthPx * fadeShare), 0, 1),\n\t\tshownPx: Math.min(parked.widthPx, arrivingX - x, view.widthPx - x),\n\t\tx,\n\t};\n}\n\nfunction isDrawable(length: number): boolean {\n\treturn Number.isFinite(length) && length > 0;\n}\n\nexport function layoutRider(labels: ReadonlyArray<RiderLabel>, view: RiderView): RiderLayout {\n\tif (!isDrawable(view.windowSeconds) || !isDrawable(view.widthPx)) return {};\n\n\tconst parkedIndex = labels.findLastIndex((label) => label.start <= view.playheadSeconds);\n\tconst next = labels[parkedIndex + 1];\n\tconst arrivingX = next ? lineX(next.start, view) + view.insetPx : Infinity;\n\tconst parked = parkedPlacement(labels, parkedIndex, [view, arrivingX]);\n\tconst arriving: LabelPlacement | undefined =\n\t\tnext && arrivingX < view.widthPx\n\t\t\t? {\n\t\t\t\t\tindex: parkedIndex + 1,\n\t\t\t\t\topacity: 1,\n\t\t\t\t\tshownPx: Math.min(next.widthPx, view.widthPx - arrivingX),\n\t\t\t\t\tx: arrivingX,\n\t\t\t\t}\n\t\t\t: undefined;\n\n\treturn { ...(arriving ? { arriving } : {}), ...(parked ? { parked } : {}) };\n}\n\nfunction labelSlot(element: HTMLElement): LabelSlot {\n\treturn { element, kind: undefined, opacity: NaN, shownPx: NaN, x: NaN };\n}\n\nfunction writePlacement(\n\tslot: LabelSlot,\n\t{ opacity, shownPx, x }: LabelPlacement,\n\twidthPx: number,\n): void {\n\tconst { style } = slot.element;\n\n\tif (slot.x !== x) {\n\t\tslot.x = x;\n\t\tstyle.setProperty('translate', `${x.toFixed(2)}px`);\n\t}\n\tif (slot.shownPx !== shownPx) {\n\t\tslot.shownPx = shownPx;\n\t\tstyle.setProperty(\n\t\t\t'clip-path',\n\t\t\tshownPx < widthPx ? `inset(0 calc(100% - ${shownPx.toFixed(2)}px) 0 0)` : '',\n\t\t);\n\t}\n\tif (slot.opacity !== opacity) {\n\t\tslot.opacity = opacity;\n\t\tstyle.setProperty('opacity', opacity < 1 ? opacity.toFixed(3) : '');\n\t}\n}\n\nfunction writeLabel(\n\tslot: LabelSlot,\n\t[labels, colourProperty]: [ReadonlyArray<RiderLabel>, string],\n\tplacement: LabelPlacement | undefined,\n): void {\n\tconst { element } = slot;\n\tconst label = placement && placement.shownPx > 0 ? labels[placement.index] : undefined;\n\n\tif (element.hidden !== !label) element.hidden = !label;\n\tif (!label || !placement) return;\n\n\tif (element.textContent !== label.text) element.textContent = label.text;\n\telement.toggleAttribute('data-sonic-dimmed', label.isDimmed);\n\tif (slot.kind !== label.kind) {\n\t\tslot.kind = label.kind;\n\t\twriteKind(element, label.kind, colourProperty);\n\t}\n\twritePlacement(slot, placement, label.widthPx);\n}\n\nfunction measureWidths(\n\tcontrol: HTMLElement,\n\tclassName: string,\n\ttexts: Array<string>,\n): Array<number> {\n\tconst probes = texts.map((text) => {\n\t\tconst probe = document.createElement('div');\n\n\t\tprobe.className = className;\n\t\tprobe.textContent = text;\n\n\t\treturn probe;\n\t});\n\n\tcontrol.append(...probes);\n\n\tconst widths = probes.map((probe) => probe.getBoundingClientRect().width);\n\n\tfor (const probe of probes) probe.remove();\n\n\treturn widths;\n}\n\nexport function createLabelRider(\n\tcontrol: HTMLElement,\n\toptions: {\n\t\tclassName: string;\n\t\tcolourProperty: `--_sonic-${string}`;\n\t\tinsetProperty: `--_sonic-${string}`;\n\t},\n): {\n\tmeasure: (labels: ReadonlyArray<Omit<RiderLabel, 'widthPx'>>) => void;\n\tplace: (window: RiderWindow) => void;\n} {\n\tconst { className, colourProperty, insetProperty } = options;\n\tconst parked = labelSlot(requireChild(control, `.${className}`, HTMLElement));\n\tconst arriving = labelSlot(requireChild(control, `.${className} + .${className}`, HTMLElement));\n\tlet insetPx = 0;\n\tlet measured: Array<RiderLabel> = [];\n\n\treturn {\n\t\tmeasure: (labels) => {\n\t\t\tconst widths = measureWidths(\n\t\t\t\tcontrol,\n\t\t\t\tclassName,\n\t\t\t\tlabels.map(({ text }) => text),\n\t\t\t);\n\n\t\t\tinsetPx = readPxProperty(getComputedStyle(control), insetProperty, 0);\n\t\t\tmeasured = labels\n\t\t\t\t.map((label, index) => ({ ...label, widthPx: widths[index] ?? 0 }))\n\t\t\t\t.toSorted((first, second) => first.start - second.start);\n\t\t},\n\t\tplace: (window) => {\n\t\t\tconst layout = layoutRider(measured, { ...window, insetPx });\n\n\t\t\twriteLabel(parked, [measured, colourProperty], layout.parked);\n\t\t\twriteLabel(arriving, [measured, colourProperty], layout.arriving);\n\t\t},\n\t};\n}\n"],"mappings":"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"}
@@ -2,16 +2,30 @@
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 };
17
31
  //# sourceMappingURL=math.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"math.js","names":[],"sources":["../../../src/lib/math.ts"],"sourcesContent":["export function clamp(value: number, min: number, max: number): number {\n\treturn Math.min(max, Math.max(min, value));\n}\n\nexport function clampUnit(fraction: number): number {\n\treturn clamp(fraction, 0, 1);\n}\n\nexport function wrapUnit(fraction: number): number {\n\treturn fraction - Math.floor(fraction);\n}\n\nexport function trimFloat(value: number): number {\n\treturn Number(value.toPrecision(12));\n}\n"],"mappings":";AAAA,SAAgB,MAAM,OAAe,KAAa,KAAqB;CACtE,OAAO,KAAK,IAAI,KAAK,KAAK,IAAI,KAAK,KAAK,CAAC;AAC1C;AAEA,SAAgB,UAAU,UAA0B;CACnD,OAAO,MAAM,UAAU,GAAG,CAAC;AAC5B;AAEA,SAAgB,SAAS,UAA0B;CAClD,OAAO,WAAW,KAAK,MAAM,QAAQ;AACtC;AAEA,SAAgB,UAAU,OAAuB;CAChD,OAAO,OAAO,MAAM,YAAY,EAAE,CAAC;AACpC"}
1
+ {"version":3,"file":"math.js","names":[],"sources":["../../../src/lib/math.ts"],"sourcesContent":["export function clamp(value: number, min: number, max: number): number {\n\treturn Math.min(max, Math.max(min, value));\n}\n\n// A framework may set a number property as a string; a blank one is no number, as in an attribute\nexport function toNumber(value: unknown): number {\n\tif (typeof value === 'number') return value;\n\tif (typeof value !== 'string' || value.trim() === '') return NaN;\n\n\treturn Number(value);\n}\n\nexport function clampProportion(fraction: number): number {\n\treturn clamp(fraction, 0, 1);\n}\n\nexport function wrapProportion(fraction: number): number {\n\treturn fraction - Math.floor(fraction);\n}\n\nexport function trimFloat(value: number): number {\n\treturn Number(value.toPrecision(12));\n}\n\n// `String(1e-7)` is \"1e-7\", so the exponent counts as places too\nexport function decimalPlaces(value: number): number {\n\tconst [digits = '', exponent = '0'] = String(value).split('e', 2);\n\tconst fraction = digits.split('.', 2)[1] ?? '';\n\n\treturn Math.max(0, fraction.length - Number(exponent));\n}\n\n// `toFixed` takes at most 100 places, and turns a residue below zero into -0\nexport function roundTo(value: number, places: number): number {\n\tconst rounded = Number(value.toFixed(Math.min(100, places)));\n\n\treturn rounded === 0 ? 0 : rounded;\n}\n"],"mappings":";AAAA,SAAgB,MAAM,OAAe,KAAa,KAAqB;CACtE,OAAO,KAAK,IAAI,KAAK,KAAK,IAAI,KAAK,KAAK,CAAC;AAC1C;AAGA,SAAgB,SAAS,OAAwB;CAChD,IAAI,OAAO,UAAU,UAAU,OAAO;CACtC,IAAI,OAAO,UAAU,YAAY,MAAM,KAAK,MAAM,IAAI,OAAO;CAE7D,OAAO,OAAO,KAAK;AACpB;AAEA,SAAgB,gBAAgB,UAA0B;CACzD,OAAO,MAAM,UAAU,GAAG,CAAC;AAC5B;AAEA,SAAgB,eAAe,UAA0B;CACxD,OAAO,WAAW,KAAK,MAAM,QAAQ;AACtC;AAEA,SAAgB,UAAU,OAAuB;CAChD,OAAO,OAAO,MAAM,YAAY,EAAE,CAAC;AACpC;AAGA,SAAgB,cAAc,OAAuB;CACpD,MAAM,CAAC,SAAS,IAAI,WAAW,OAAO,OAAO,KAAK,CAAC,CAAC,MAAM,KAAK,CAAC;CAChE,MAAM,WAAW,OAAO,MAAM,KAAK,CAAC,CAAC,CAAC,MAAM;CAE5C,OAAO,KAAK,IAAI,GAAG,SAAS,SAAS,OAAO,QAAQ,CAAC;AACtD;AAGA,SAAgB,QAAQ,OAAe,QAAwB;CAC9D,MAAM,UAAU,OAAO,MAAM,QAAQ,KAAK,IAAI,KAAK,MAAM,CAAC,CAAC;CAE3D,OAAO,YAAY,IAAI,IAAI;AAC5B"}
@@ -0,0 +1,12 @@
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 };
12
+ //# sourceMappingURL=modifier-press.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"modifier-press.js","names":[],"sources":["../../../src/lib/modifier-press.ts"],"sourcesContent":["const isApple = /^(Mac|iP)/.test(navigator.platform);\n\n// macOS sends Ctrl-click as a primary button press and opens the context menu\nexport function isMenuPress(event: MouseEvent): boolean {\n\treturn isApple && event.ctrlKey;\n}\n\nexport function isResetPress(event: MouseEvent): boolean {\n\treturn isApple ? event.metaKey : event.ctrlKey;\n}\n"],"mappings":";AAAA,MAAM,UAAU,YAAY,KAAK,UAAU,QAAQ;AAGnD,SAAgB,YAAY,OAA4B;CACvD,OAAO,WAAW,MAAM;AACzB;AAEA,SAAgB,aAAa,OAA4B;CACxD,OAAO,UAAU,MAAM,UAAU,MAAM;AACxC"}
@@ -1,15 +1,15 @@
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 };
15
15
  //# sourceMappingURL=number-list.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"number-list.js","names":[],"sources":["../../../src/lib/number-list.ts"],"sourcesContent":["export function parseNumberList(text: null | string): Array<number> | undefined {\n\tif (text === null) return undefined;\n\n\tconst entries = [\n\t\t...new Set(\n\t\t\ttext\n\t\t\t\t.split(/[\\s,]+/)\n\t\t\t\t.filter(Boolean)\n\t\t\t\t.map(Number)\n\t\t\t\t.filter((entry) => Number.isFinite(entry)),\n\t\t),\n\t].toSorted((first, second) => first - second);\n\n\treturn entries.length >= 2 ? entries : undefined;\n}\n\nexport function nearestEntry(entries: Array<number>, value: number): number {\n\tlet nearest = entries[0] ?? value;\n\n\tfor (const entry of entries) {\n\t\tif (Math.abs(entry - value) < Math.abs(nearest - value)) nearest = entry;\n\t}\n\n\treturn nearest;\n}\n"],"mappings":";AAAA,SAAgB,gBAAgB,MAAgD;CAC/E,IAAI,SAAS,MAAM,OAAO;CAE1B,MAAM,UAAU,CACf,GAAG,IAAI,IACN,KACE,MAAM,QAAQ,CAAC,CACf,OAAO,OAAO,CAAC,CACf,IAAI,MAAM,CAAC,CACX,QAAQ,UAAU,OAAO,SAAS,KAAK,CAAC,CAC3C,CACD,CAAC,CAAC,UAAU,OAAO,WAAW,QAAQ,MAAM;CAE5C,OAAO,QAAQ,UAAU,IAAI,UAAU;AACxC;AAEA,SAAgB,aAAa,SAAwB,OAAuB;CAC3E,IAAI,UAAU,QAAQ,MAAM;CAE5B,KAAK,MAAM,SAAS,SACnB,IAAI,KAAK,IAAI,QAAQ,KAAK,IAAI,KAAK,IAAI,UAAU,KAAK,GAAG,UAAU;CAGpE,OAAO;AACR"}
1
+ {"version":3,"file":"number-list.js","names":[],"sources":["../../../src/lib/number-list.ts"],"sourcesContent":["export function parseNumberList(text: null | string): Array<number> | undefined {\n\tif (text === null) return undefined;\n\n\tconst numbers = [\n\t\t...new Set(\n\t\t\ttext\n\t\t\t\t.split(/[\\s,]+/)\n\t\t\t\t.filter(Boolean)\n\t\t\t\t.map(Number)\n\t\t\t\t.filter((entry) => Number.isFinite(entry)),\n\t\t),\n\t].toSorted((first, second) => first - second);\n\n\treturn numbers.length >= 2 ? numbers : undefined;\n}\n\nexport function nearestPosition(positions: ReadonlyArray<number>, value: number): number {\n\tlet nearest = positions[0] ?? value;\n\n\tfor (const position of positions) {\n\t\tif (Math.abs(position - value) < Math.abs(nearest - value)) nearest = position;\n\t}\n\n\treturn nearest;\n}\n"],"mappings":";AAAA,SAAgB,gBAAgB,MAAgD;CAC/E,IAAI,SAAS,MAAM,OAAO;CAE1B,MAAM,UAAU,CACf,GAAG,IAAI,IACN,KACE,MAAM,QAAQ,CAAC,CACf,OAAO,OAAO,CAAC,CACf,IAAI,MAAM,CAAC,CACX,QAAQ,UAAU,OAAO,SAAS,KAAK,CAAC,CAC3C,CACD,CAAC,CAAC,UAAU,OAAO,WAAW,QAAQ,MAAM;CAE5C,OAAO,QAAQ,UAAU,IAAI,UAAU;AACxC;AAEA,SAAgB,gBAAgB,WAAkC,OAAuB;CACxF,IAAI,UAAU,UAAU,MAAM;CAE9B,KAAK,MAAM,YAAY,WACtB,IAAI,KAAK,IAAI,WAAW,KAAK,IAAI,KAAK,IAAI,UAAU,KAAK,GAAG,UAAU;CAGvE,OAAO;AACR"}
@@ -0,0 +1,47 @@
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 };
47
+ //# sourceMappingURL=pointer-drag.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pointer-drag.js","names":[],"sources":["../../../src/lib/pointer-drag.ts"],"sourcesContent":["interface DragHandlers<Drag> {\n\tcancel?: (drag: Drag) => void;\n\tgrab: (event: PointerEvent) => Drag | undefined;\n\tlift?: (drag: Drag, event: PointerEvent) => void;\n\tmove: (drag: Drag, event: PointerEvent) => void;\n\trelease?: (drag: Drag) => void;\n\ttoggle?: (isDragging: boolean) => void;\n}\n\nexport interface PointerDrag<Drag> {\n\tcurrent: () => Drag | undefined;\n\tend: () => void;\n}\n\nexport function bindDrag<Drag>(\n\ttarget: HTMLElement,\n\thandlers: DragHandlers<Drag>,\n\tsignal: AbortSignal,\n): PointerDrag<Drag> {\n\tlet held: undefined | { drag: Drag; pointerId: number };\n\n\tconst end = (): void => {\n\t\tif (!held) return;\n\n\t\tconst { drag } = held;\n\n\t\theld = undefined;\n\t\thandlers.toggle?.(false);\n\t\thandlers.release?.(drag);\n\t};\n\tconst whileHeld = (\n\t\ttype: 'lostpointercapture' | 'pointercancel' | 'pointermove' | 'pointerup',\n\t\thandle: (drag: Drag, event: PointerEvent) => void,\n\t): void => {\n\t\ttarget.addEventListener(\n\t\t\ttype,\n\t\t\t(event) => {\n\t\t\t\tif (held?.pointerId === event.pointerId) handle(held.drag, event);\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t};\n\n\ttarget.addEventListener(\n\t\t'pointerdown',\n\t\t(event) => {\n\t\t\tif (event.button !== 0 || (held && held.pointerId !== event.pointerId)) return;\n\n\t\t\tend();\n\n\t\t\tconst drag = handlers.grab(event);\n\t\t\tif (drag === undefined) return;\n\n\t\t\theld = { drag, pointerId: event.pointerId };\n\t\t\ttarget.setPointerCapture(event.pointerId);\n\t\t\thandlers.toggle?.(true);\n\t\t},\n\t\t{ signal },\n\t);\n\twhileHeld('pointermove', handlers.move);\n\twhileHeld('pointercancel', (drag) => {\n\t\thandlers.cancel?.(drag);\n\t\tend();\n\t});\n\twhileHeld('lostpointercapture', end);\n\twhileHeld('pointerup', (drag, event) => {\n\t\tend();\n\t\thandlers.lift?.(drag, event);\n\t});\n\tsignal.addEventListener('abort', end, { once: true });\n\n\treturn { current: () => held?.drag, end };\n}\n"],"mappings":";AAcA,SAAgB,SACf,QACA,UACA,QACoB;CACpB,IAAI;CAEJ,MAAM,YAAkB;EACvB,IAAI,CAAC,MAAM;EAEX,MAAM,EAAE,SAAS;EAEjB,OAAO;EACP,SAAS,SAAS,KAAK;EACvB,SAAS,UAAU,IAAI;CACxB;CACA,MAAM,aACL,MACA,WACU;EACV,OAAO,iBACN,OACC,UAAU;GACV,IAAI,MAAM,cAAc,MAAM,WAAW,OAAO,KAAK,MAAM,KAAK;EACjE,GACA,EAAE,OAAO,CACV;CACD;CAEA,OAAO,iBACN,gBACC,UAAU;EACV,IAAI,MAAM,WAAW,KAAM,QAAQ,KAAK,cAAc,MAAM,WAAY;EAExE,IAAI;EAEJ,MAAM,OAAO,SAAS,KAAK,KAAK;EAChC,IAAI,SAAS,QAAW;EAExB,OAAO;GAAE;GAAM,WAAW,MAAM;EAAU;EAC1C,OAAO,kBAAkB,MAAM,SAAS;EACxC,SAAS,SAAS,IAAI;CACvB,GACA,EAAE,OAAO,CACV;CACA,UAAU,eAAe,SAAS,IAAI;CACtC,UAAU,kBAAkB,SAAS;EACpC,SAAS,SAAS,IAAI;EACtB,IAAI;CACL,CAAC;CACD,UAAU,sBAAsB,GAAG;CACnC,UAAU,cAAc,MAAM,UAAU;EACvC,IAAI;EACJ,SAAS,OAAO,MAAM,KAAK;CAC5B,CAAC;CACD,OAAO,iBAAiB,SAAS,KAAK,EAAE,MAAM,KAAK,CAAC;CAEpD,OAAO;EAAE,eAAe,MAAM;EAAM;CAAI;AACzC"}
@@ -1 +1 @@
1
- {"version":3,"file":"read-px-property.js","names":[],"sources":["../../../src/lib/read-px-property.ts"],"sourcesContent":["// Registered `<length>` properties only; their computed value is always px\nexport function readPxProperty(\n\tstyles: CSSStyleDeclaration,\n\tproperty: string,\n\tfallback: number,\n): number {\n\t// eslint-disable-next-line unicorn/prefer-number-coercion -- parsing has to stop at the unit\n\tconst parsed = Number.parseFloat(styles.getPropertyValue(property));\n\n\treturn Number.isFinite(parsed) ? parsed : fallback;\n}\n"],"mappings":";AACA,SAAgB,eACf,QACA,UACA,UACS;CAET,MAAM,SAAS,OAAO,WAAW,OAAO,iBAAiB,QAAQ,CAAC;CAElE,OAAO,OAAO,SAAS,MAAM,IAAI,SAAS;AAC3C"}
1
+ {"version":3,"file":"read-px-property.js","names":[],"sources":["../../../src/lib/read-px-property.ts"],"sourcesContent":["// Registered properties only; a registered `<length>` always computes to px\nexport function readPxProperty(\n\tstyles: CSSStyleDeclaration,\n\tproperty: string,\n\tfallback: number,\n): number {\n\t// eslint-disable-next-line unicorn/prefer-number-coercion -- parsing has to stop at the unit\n\tconst parsed = Number.parseFloat(styles.getPropertyValue(property));\n\n\treturn Number.isFinite(parsed) ? parsed : fallback;\n}\n"],"mappings":";AACA,SAAgB,eACf,QACA,UACA,UACS;CAET,MAAM,SAAS,OAAO,WAAW,OAAO,iBAAiB,QAAQ,CAAC;CAElE,OAAO,OAAO,SAAS,MAAM,IAAI,SAAS;AAC3C"}
@@ -13,15 +13,30 @@ 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 };
27
42
  //# sourceMappingURL=render.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"render.js","names":[],"sources":["../../../src/lib/render.ts"],"sourcesContent":["export function requireChild<Child extends Element>(\n\troot: Element,\n\tselector: string,\n\tchild: new () => Child,\n): Child {\n\tconst found = root.querySelector(selector);\n\tif (!(found instanceof child)) throw new Error(`A template is missing its ${selector}`);\n\n\treturn found;\n}\n\nexport function template<Root extends Element>(html: string, root: new () => Root): () => Root {\n\tconst parsed = document.createElement('template');\n\n\tparsed.innerHTML = html;\n\n\treturn () => {\n\t\tconst clone = document.importNode(parsed.content, true).firstElementChild;\n\t\tif (!(clone instanceof root)) throw new Error(`A template's root is not a ${root.name}`);\n\n\t\treturn clone;\n\t};\n}\n\n// Only the nodes the element assigns render, so a framework's own children stay put, bare text included\nexport function attachSlot(host: HTMLElement): HTMLSlotElement {\n\tconst slot = document.createElement('slot');\n\n\thost.attachShadow({ mode: 'open', slotAssignment: 'manual' }).append(slot);\n\n\treturn slot;\n}\n"],"mappings":";AAAA,SAAgB,aACf,MACA,UACA,OACQ;CACR,MAAM,QAAQ,KAAK,cAAc,QAAQ;CACzC,IAAI,EAAE,iBAAiB,QAAQ,MAAM,IAAI,MAAM,6BAA6B,UAAU;CAEtF,OAAO;AACR;AAEA,SAAgB,SAA+B,MAAc,MAAkC;CAC9F,MAAM,SAAS,SAAS,cAAc,UAAU;CAEhD,OAAO,YAAY;CAEnB,aAAa;EACZ,MAAM,QAAQ,SAAS,WAAW,OAAO,SAAS,IAAI,CAAC,CAAC;EACxD,IAAI,EAAE,iBAAiB,OAAO,MAAM,IAAI,MAAM,8BAA8B,KAAK,MAAM;EAEvF,OAAO;CACR;AACD;AAGA,SAAgB,WAAW,MAAoC;CAC9D,MAAM,OAAO,SAAS,cAAc,MAAM;CAE1C,KAAK,aAAa;EAAE,MAAM;EAAQ,gBAAgB;CAAS,CAAC,CAAC,CAAC,OAAO,IAAI;CAEzE,OAAO;AACR"}
1
+ {"version":3,"file":"render.js","names":[],"sources":["../../../src/lib/render.ts"],"sourcesContent":["export function requireChild<Child extends Element>(\n\troot: Element,\n\tselector: string,\n\tchild: new () => Child,\n): Child {\n\tconst found = root.querySelector(selector);\n\tif (!(found instanceof child)) throw new Error(`A template is missing its ${selector}`);\n\n\treturn found;\n}\n\nexport function template<Root extends Element>(html: string, root: new () => Root): () => Root {\n\tconst parsed = document.createElement('template');\n\n\tparsed.innerHTML = html;\n\n\treturn () => {\n\t\tconst clone = document.importNode(parsed.content, true).firstElementChild;\n\t\tif (!(clone instanceof root)) throw new Error(`A template's root is not a ${root.name}`);\n\n\t\treturn clone;\n\t};\n}\n\nexport function placeChildren(parent: Element, children: Array<Node>): void {\n\tconst dropped = [...parent.childNodes].filter((child) => !children.includes(child));\n\n\tfor (const child of dropped) child.remove();\n\tfor (const [index, child] of children.entries()) {\n\t\tconst current = parent.childNodes[index];\n\n\t\t// A node taken out and put back restarts its animations\n\t\tif (current === child) continue;\n\n\t\tif (current) current.before(child);\n\t\telse parent.append(child);\n\t}\n}\n\nexport interface MirrorSlots {\n\tkept: HTMLSlotElement;\n\tshown: HTMLSlotElement;\n}\n\n// An id resolves to the first element in the tree that carries it, and paints nothing when that one has no box\nexport function attachSlots(host: HTMLElement): MirrorSlots {\n\tconst shown = document.createElement('slot');\n\tconst kept = document.createElement('slot');\n\n\tkept.style.cssText =\n\t\t'position: absolute; display: block; inline-size: 0; block-size: 0; overflow: hidden; visibility: hidden';\n\thost.attachShadow({ mode: 'open', slotAssignment: 'manual' }).append(shown, kept);\n\n\treturn { kept, shown };\n}\n"],"mappings":";AAAA,SAAgB,aACf,MACA,UACA,OACQ;CACR,MAAM,QAAQ,KAAK,cAAc,QAAQ;CACzC,IAAI,EAAE,iBAAiB,QAAQ,MAAM,IAAI,MAAM,6BAA6B,UAAU;CAEtF,OAAO;AACR;AAEA,SAAgB,SAA+B,MAAc,MAAkC;CAC9F,MAAM,SAAS,SAAS,cAAc,UAAU;CAEhD,OAAO,YAAY;CAEnB,aAAa;EACZ,MAAM,QAAQ,SAAS,WAAW,OAAO,SAAS,IAAI,CAAC,CAAC;EACxD,IAAI,EAAE,iBAAiB,OAAO,MAAM,IAAI,MAAM,8BAA8B,KAAK,MAAM;EAEvF,OAAO;CACR;AACD;AAEA,SAAgB,cAAc,QAAiB,UAA6B;CAC3E,MAAM,UAAU,CAAC,GAAG,OAAO,UAAU,CAAC,CAAC,QAAQ,UAAU,CAAC,SAAS,SAAS,KAAK,CAAC;CAElF,KAAK,MAAM,SAAS,SAAS,MAAM,OAAO;CAC1C,KAAK,MAAM,CAAC,OAAO,UAAU,SAAS,QAAQ,GAAG;EAChD,MAAM,UAAU,OAAO,WAAW;EAGlC,IAAI,YAAY,OAAO;EAEvB,IAAI,SAAS,QAAQ,OAAO,KAAK;OAC5B,OAAO,OAAO,KAAK;CACzB;AACD;AAQA,SAAgB,YAAY,MAAgC;CAC3D,MAAM,QAAQ,SAAS,cAAc,MAAM;CAC3C,MAAM,OAAO,SAAS,cAAc,MAAM;CAE1C,KAAK,MAAM,UACV;CACD,KAAK,aAAa;EAAE,MAAM;EAAQ,gBAAgB;CAAS,CAAC,CAAC,CAAC,OAAO,OAAO,IAAI;CAEhF,OAAO;EAAE;EAAM;CAAM;AACtB"}
@@ -0,0 +1,47 @@
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 };
47
+ //# sourceMappingURL=resample-peaks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resample-peaks.js","names":[],"sources":["../../../src/lib/resample-peaks.ts"],"sourcesContent":["function bucketBounds(sourceLength: number, index: number, count: number): [number, number] {\n\tconst start = Math.floor((index * sourceLength) / count);\n\n\treturn [start, Math.max(start + 1, Math.floor(((index + 1) * sourceLength) / count))];\n}\n\nfunction downsample(peaks: ReadonlyArray<number>, count: number): Array<number> {\n\tconst resampled: Array<number> = [];\n\n\tfor (let index = 0; index < count; index += 1) {\n\t\tconst [start, end] = bucketBounds(peaks.length, index, count);\n\t\tlet total = 0;\n\n\t\tfor (let source = start; source < end; source += 1) total += peaks[source] ?? 0;\n\t\tresampled.push(total / (end - start));\n\t}\n\n\treturn rescale(resampled, peakOf(peaks));\n}\n\nfunction peakOf(peaks: ReadonlyArray<number>): number {\n\tlet loudest = 0;\n\n\tfor (const peak of peaks) loudest = Math.max(loudest, peak);\n\n\treturn loudest;\n}\n\nfunction rescale(bars: Array<number>, loudest: number): Array<number> {\n\tconst tallest = peakOf(bars);\n\tif (tallest <= 0) return bars;\n\n\treturn bars.map((bar) => (bar * loudest) / tallest);\n}\n\nfunction upsample(peaks: ArrayLike<number>, count: number): Array<number> {\n\tconst resampled: Array<number> = [];\n\tconst last = peaks.length - 1;\n\n\tfor (let index = 0; index < count; index += 1) {\n\t\tconst sourceIndex = count === 1 ? 0 : (index * last) / (count - 1);\n\t\tconst before = Math.floor(sourceIndex);\n\t\tconst weight = sourceIndex - before;\n\n\t\tresampled.push(\n\t\t\t(peaks[before] ?? 0) * (1 - weight) + (peaks[Math.min(last, before + 1)] ?? 0) * weight,\n\t\t);\n\t}\n\n\treturn resampled;\n}\n\nexport function resamplePeaks(peaks: ArrayLike<number>, count: number): Array<number> {\n\tif (count <= 0 || peaks.length === 0) return [];\n\n\tconst finite = Array.from(peaks, (peak) => (Number.isFinite(peak) ? peak : 0));\n\n\tif (count === finite.length) return finite;\n\tif (count > finite.length) return upsample(finite, count);\n\n\treturn downsample(finite, count);\n}\n"],"mappings":";AAAA,SAAS,aAAa,cAAsB,OAAe,OAAiC;CAC3F,MAAM,QAAQ,KAAK,MAAO,QAAQ,eAAgB,KAAK;CAEvD,OAAO,CAAC,OAAO,KAAK,IAAI,QAAQ,GAAG,KAAK,OAAQ,QAAQ,KAAK,eAAgB,KAAK,CAAC,CAAC;AACrF;AAEA,SAAS,WAAW,OAA8B,OAA8B;CAC/E,MAAM,YAA2B,CAAC;CAElC,KAAK,IAAI,QAAQ,GAAG,QAAQ,OAAO,SAAS,GAAG;EAC9C,MAAM,CAAC,OAAO,OAAO,aAAa,MAAM,QAAQ,OAAO,KAAK;EAC5D,IAAI,QAAQ;EAEZ,KAAK,IAAI,SAAS,OAAO,SAAS,KAAK,UAAU,GAAG,SAAS,MAAM,WAAW;EAC9E,UAAU,KAAK,SAAS,MAAM,MAAM;CACrC;CAEA,OAAO,QAAQ,WAAW,OAAO,KAAK,CAAC;AACxC;AAEA,SAAS,OAAO,OAAsC;CACrD,IAAI,UAAU;CAEd,KAAK,MAAM,QAAQ,OAAO,UAAU,KAAK,IAAI,SAAS,IAAI;CAE1D,OAAO;AACR;AAEA,SAAS,QAAQ,MAAqB,SAAgC;CACrE,MAAM,UAAU,OAAO,IAAI;CAC3B,IAAI,WAAW,GAAG,OAAO;CAEzB,OAAO,KAAK,KAAK,QAAS,MAAM,UAAW,OAAO;AACnD;AAEA,SAAS,SAAS,OAA0B,OAA8B;CACzE,MAAM,YAA2B,CAAC;CAClC,MAAM,OAAO,MAAM,SAAS;CAE5B,KAAK,IAAI,QAAQ,GAAG,QAAQ,OAAO,SAAS,GAAG;EAC9C,MAAM,cAAc,UAAU,IAAI,IAAK,QAAQ,QAAS,QAAQ;EAChE,MAAM,SAAS,KAAK,MAAM,WAAW;EACrC,MAAM,SAAS,cAAc;EAE7B,UAAU,MACR,MAAM,WAAW,MAAM,IAAI,WAAW,MAAM,KAAK,IAAI,MAAM,SAAS,CAAC,MAAM,KAAK,MAClF;CACD;CAEA,OAAO;AACR;AAEA,SAAgB,cAAc,OAA0B,OAA8B;CACrF,IAAI,SAAS,KAAK,MAAM,WAAW,GAAG,OAAO,CAAC;CAE9C,MAAM,SAAS,MAAM,KAAK,QAAQ,SAAU,OAAO,SAAS,IAAI,IAAI,OAAO,CAAE;CAE7E,IAAI,UAAU,OAAO,QAAQ,OAAO;CACpC,IAAI,QAAQ,OAAO,QAAQ,OAAO,SAAS,QAAQ,KAAK;CAExD,OAAO,WAAW,QAAQ,KAAK;AAChC"}
@@ -0,0 +1,49 @@
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 };
49
+ //# sourceMappingURL=strip-scene.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"strip-scene.js","names":[],"sources":["../../../src/lib/strip-scene.ts"],"sourcesContent":["import type { SurfaceSize } from '#lib/canvas-surface.ts';\n\nimport { clampProportion } from '#lib/math.ts';\nimport { resamplePeaks } from '#lib/resample-peaks.ts';\n\ninterface BarRect {\n\theight: number;\n\tradius: number;\n\twidth: number;\n\tx: number;\n\ty: number;\n}\n\ninterface BarGrid {\n\tgapRatio: number;\n\tpitch: number;\n\tradiusRatio: number;\n}\n\ninterface RegionState {\n\tbuffered: ReadonlyArray<[number, number]>;\n\tplayed: number;\n\tscrub?: number;\n}\n\nexport interface StripRegions {\n\tkey: string;\n\tregions: Array<{ from: number; kind: 'buffered' | 'played' | 'scrub'; to: number }>;\n}\n\n// Whole device pixels, or bars alias unevenly and Skia's CPU raster crawls\nexport function stripBars(\n\tpeaks: ArrayLike<number>,\n\tsize: SurfaceSize,\n\tgrid: BarGrid,\n): Array<BarRect> {\n\tconst pitch = Math.max(1, Math.round(grid.pitch * size.dpr));\n\tconst gap = Math.min(pitch - 1, Math.max(0, Math.round(pitch * grid.gapRatio)));\n\tconst width = pitch - gap;\n\tconst count = Math.max(0, Math.floor((size.width + gap) / pitch));\n\n\treturn resamplePeaks(peaks, count).map((peak, index) => {\n\t\tconst height = Math.max(1, Math.round(clampProportion(peak) * size.height));\n\n\t\treturn {\n\t\t\theight,\n\t\t\tradius: width * grid.radiusRatio,\n\t\t\twidth,\n\t\t\tx: index * pitch,\n\t\t\ty: Math.round((size.height - height) / 2),\n\t\t};\n\t});\n}\n\nexport function stripRegions(state: RegionState, widthPx: number): StripRegions {\n\tconst px = (proportion: number): number =>\n\t\tMath.round((Number.isFinite(proportion) ? proportion : 0) * widthPx);\n\tconst buffered = state.buffered\n\t\t.map(([start, end]): [number, number] => [px(start), px(end)])\n\t\t.filter(([start, end]) => end > start);\n\tconst played = px(state.played);\n\tconst scrub = state.scrub === undefined ? played : px(state.scrub);\n\tconst regions: StripRegions['regions'] = buffered.map(([from, to]) => ({\n\t\tfrom,\n\t\tkind: 'buffered',\n\t\tto,\n\t}));\n\n\tif (played > 0) regions.push({ from: 0, kind: 'played', to: played });\n\tif (scrub !== played) {\n\t\tregions.push({ from: Math.min(played, scrub), kind: 'scrub', to: Math.max(played, scrub) });\n\t}\n\n\treturn { key: `${String(played)}:${String(scrub)}:${buffered.join(',')}`, regions };\n}\n"],"mappings":";;;;AA+BA,SAAgB,UACf,OACA,MACA,MACiB;CACjB,MAAM,QAAQ,KAAK,IAAI,GAAG,KAAK,MAAM,KAAK,QAAQ,KAAK,GAAG,CAAC;CAC3D,MAAM,MAAM,KAAK,IAAI,QAAQ,GAAG,KAAK,IAAI,GAAG,KAAK,MAAM,QAAQ,KAAK,QAAQ,CAAC,CAAC;CAC9E,MAAM,QAAQ,QAAQ;CACtB,MAAM,QAAQ,KAAK,IAAI,GAAG,KAAK,OAAO,KAAK,QAAQ,OAAO,KAAK,CAAC;CAEhE,OAAO,cAAc,OAAO,KAAK,CAAC,CAAC,KAAK,MAAM,UAAU;EACvD,MAAM,SAAS,KAAK,IAAI,GAAG,KAAK,MAAM,gBAAgB,IAAI,IAAI,KAAK,MAAM,CAAC;EAE1E,OAAO;GACN;GACA,QAAQ,QAAQ,KAAK;GACrB;GACA,GAAG,QAAQ;GACX,GAAG,KAAK,OAAO,KAAK,SAAS,UAAU,CAAC;EACzC;CACD,CAAC;AACF;AAEA,SAAgB,aAAa,OAAoB,SAA+B;CAC/E,MAAM,MAAM,eACX,KAAK,OAAO,OAAO,SAAS,UAAU,IAAI,aAAa,KAAK,OAAO;CACpE,MAAM,WAAW,MAAM,SACrB,KAAK,CAAC,OAAO,SAA2B,CAAC,GAAG,KAAK,GAAG,GAAG,GAAG,CAAC,CAAC,CAAC,CAC7D,QAAQ,CAAC,OAAO,SAAS,MAAM,KAAK;CACtC,MAAM,SAAS,GAAG,MAAM,MAAM;CAC9B,MAAM,QAAQ,MAAM,UAAU,SAAY,SAAS,GAAG,MAAM,KAAK;CACjE,MAAM,UAAmC,SAAS,KAAK,CAAC,MAAM,SAAS;EACtE;EACA,MAAM;EACN;CACD,EAAE;CAEF,IAAI,SAAS,GAAG,QAAQ,KAAK;EAAE,MAAM;EAAG,MAAM;EAAU,IAAI;CAAO,CAAC;CACpE,IAAI,UAAU,QACb,QAAQ,KAAK;EAAE,MAAM,KAAK,IAAI,QAAQ,KAAK;EAAG,MAAM;EAAS,IAAI,KAAK,IAAI,QAAQ,KAAK;CAAE,CAAC;CAG3F,OAAO;EAAE,KAAK,GAAG,OAAO,MAAM,EAAE,GAAG,OAAO,KAAK,EAAE,GAAG,SAAS,KAAK,GAAG;EAAK;CAAQ;AACnF"}
@@ -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