@xsynaptic/sonic-ui 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (237) hide show
  1. package/README.md +2 -21
  2. package/dist/default/dat.js +50 -0
  3. package/dist/default/define/button.js +7 -0
  4. package/dist/default/define/envelope.js +7 -0
  5. package/dist/default/define/split.js +7 -0
  6. package/dist/default/define/switch.js +7 -0
  7. package/dist/default/define/waveform.js +7 -0
  8. package/dist/default/define/wavestrip.js +7 -0
  9. package/dist/default/define/xy.js +7 -0
  10. package/dist/default/define.js +7 -1
  11. package/dist/default/elements/button.js +165 -0
  12. package/dist/default/elements/dial.js +19 -23
  13. package/dist/default/elements/envelope.js +367 -0
  14. package/dist/default/elements/meter.js +168 -26
  15. package/dist/default/elements/number.js +21 -8
  16. package/dist/default/elements/radio-group.js +185 -0
  17. package/dist/default/elements/readout-claim.js +81 -0
  18. package/dist/default/elements/readout.js +35 -0
  19. package/dist/default/elements/segmented.js +9 -159
  20. package/dist/default/elements/slider.js +64 -27
  21. package/dist/default/elements/sonic-element.js +24 -13
  22. package/dist/default/elements/split.js +249 -0
  23. package/dist/default/elements/switch.js +279 -0
  24. package/dist/default/elements/value-element.js +691 -0
  25. package/dist/default/elements/{range-entry.js → value-entry.js} +3 -3
  26. package/dist/default/elements/wave-element.js +77 -0
  27. package/dist/default/elements/waveform.js +305 -0
  28. package/dist/default/elements/wavestrip.js +182 -0
  29. package/dist/default/elements/xy.js +473 -0
  30. package/dist/default/index.js +10 -2
  31. package/dist/default/lib/adsr-shape.js +108 -0
  32. package/dist/default/lib/ballistics.js +55 -0
  33. package/dist/default/lib/canvas-surface.js +192 -0
  34. package/dist/default/lib/copy-node.js +32 -2
  35. package/dist/default/lib/crossfade.js +22 -0
  36. package/dist/default/lib/distribute.js +215 -0
  37. package/dist/default/lib/drag-step.js +99 -0
  38. package/dist/default/lib/envelope-curve.js +9 -0
  39. package/dist/default/lib/field.js +94 -0
  40. package/dist/default/lib/focus-by-pointer.js +28 -0
  41. package/dist/default/lib/frame-timeline.js +62 -0
  42. package/dist/default/lib/hold.js +38 -0
  43. package/dist/default/lib/marker-band.js +36 -0
  44. package/dist/default/lib/marker-rider.js +114 -0
  45. package/dist/default/lib/math.js +25 -0
  46. package/dist/default/lib/modifier-press.js +11 -0
  47. package/dist/default/lib/number-list.js +14 -0
  48. package/dist/default/lib/pointer-drag.js +43 -0
  49. package/dist/default/lib/render.js +20 -5
  50. package/dist/default/lib/resample-peaks.js +36 -0
  51. package/dist/default/lib/strip-scene.js +48 -0
  52. package/dist/default/lib/taper.js +32 -16
  53. package/dist/default/lib/time-regions.js +8 -0
  54. package/dist/default/lib/tracking-clock.js +35 -0
  55. package/dist/default/lib/value-mapping.js +115 -0
  56. package/dist/default/lib/waveform-buckets.js +41 -0
  57. package/dist/dev/dat.d.ts +19 -0
  58. package/dist/dev/dat.js +51 -0
  59. package/dist/dev/dat.js.map +1 -0
  60. package/dist/dev/define/button.d.ts +2 -0
  61. package/dist/dev/define/button.js +8 -0
  62. package/dist/dev/define/button.js.map +1 -0
  63. package/dist/dev/define/dial.js.map +1 -1
  64. package/dist/dev/define/envelope.d.ts +2 -0
  65. package/dist/dev/define/envelope.js +8 -0
  66. package/dist/dev/define/envelope.js.map +1 -0
  67. package/dist/dev/define/meter.js.map +1 -1
  68. package/dist/dev/define/number.js.map +1 -1
  69. package/dist/dev/define/segmented.js.map +1 -1
  70. package/dist/dev/define/slider.js.map +1 -1
  71. package/dist/dev/define/split.d.ts +2 -0
  72. package/dist/dev/define/split.js +8 -0
  73. package/dist/dev/define/split.js.map +1 -0
  74. package/dist/dev/define/switch.d.ts +2 -0
  75. package/dist/dev/define/switch.js +8 -0
  76. package/dist/dev/define/switch.js.map +1 -0
  77. package/dist/dev/define/waveform.d.ts +2 -0
  78. package/dist/dev/define/waveform.js +8 -0
  79. package/dist/dev/define/waveform.js.map +1 -0
  80. package/dist/dev/define/wavestrip.d.ts +2 -0
  81. package/dist/dev/define/wavestrip.js +8 -0
  82. package/dist/dev/define/wavestrip.js.map +1 -0
  83. package/dist/dev/define/xy.d.ts +2 -0
  84. package/dist/dev/define/xy.js +8 -0
  85. package/dist/dev/define/xy.js.map +1 -0
  86. package/dist/dev/define.d.ts +15 -3
  87. package/dist/dev/define.js +7 -1
  88. package/dist/dev/elements/{key.d.ts → button.d.ts} +12 -6
  89. package/dist/dev/elements/button.js +166 -0
  90. package/dist/dev/elements/button.js.map +1 -0
  91. package/dist/dev/elements/dial.d.ts +6 -4
  92. package/dist/dev/elements/dial.js +19 -23
  93. package/dist/dev/elements/dial.js.map +1 -1
  94. package/dist/dev/elements/envelope.d.ts +36 -0
  95. package/dist/dev/elements/envelope.js +372 -0
  96. package/dist/dev/elements/envelope.js.map +1 -0
  97. package/dist/dev/elements/form-element.d.ts +1 -1
  98. package/dist/dev/elements/form-element.js.map +1 -1
  99. package/dist/dev/elements/meter.d.ts +12 -1
  100. package/dist/dev/elements/meter.js +168 -26
  101. package/dist/dev/elements/meter.js.map +1 -1
  102. package/dist/dev/elements/number.d.ts +7 -3
  103. package/dist/dev/elements/number.js +21 -8
  104. package/dist/dev/elements/number.js.map +1 -1
  105. package/dist/dev/elements/radio-group.d.ts +25 -0
  106. package/dist/dev/elements/radio-group.js +186 -0
  107. package/dist/dev/elements/radio-group.js.map +1 -0
  108. package/dist/dev/elements/readout-claim.js +82 -0
  109. package/dist/dev/elements/readout-claim.js.map +1 -0
  110. package/dist/dev/elements/readout.js +36 -0
  111. package/dist/dev/elements/readout.js.map +1 -0
  112. package/dist/dev/elements/segmented.d.ts +4 -10
  113. package/dist/dev/elements/segmented.js +9 -159
  114. package/dist/dev/elements/segmented.js.map +1 -1
  115. package/dist/dev/elements/slider.d.ts +9 -3
  116. package/dist/dev/elements/slider.js +64 -27
  117. package/dist/dev/elements/slider.js.map +1 -1
  118. package/dist/dev/elements/sonic-element.d.ts +4 -3
  119. package/dist/dev/elements/sonic-element.js +51 -22
  120. package/dist/dev/elements/sonic-element.js.map +1 -1
  121. package/dist/dev/elements/split.d.ts +21 -0
  122. package/dist/dev/elements/split.js +258 -0
  123. package/dist/dev/elements/split.js.map +1 -0
  124. package/dist/dev/elements/switch.d.ts +35 -0
  125. package/dist/dev/elements/switch.js +280 -0
  126. package/dist/dev/elements/switch.js.map +1 -0
  127. package/dist/dev/elements/value-element.d.ts +84 -0
  128. package/dist/dev/elements/value-element.js +692 -0
  129. package/dist/dev/elements/value-element.js.map +1 -0
  130. package/dist/dev/elements/{range-entry.js → value-entry.js} +4 -4
  131. package/dist/dev/elements/value-entry.js.map +1 -0
  132. package/dist/dev/elements/wave-element.d.ts +36 -0
  133. package/dist/dev/elements/wave-element.js +78 -0
  134. package/dist/dev/elements/wave-element.js.map +1 -0
  135. package/dist/dev/elements/waveform.d.ts +55 -0
  136. package/dist/dev/elements/waveform.js +306 -0
  137. package/dist/dev/elements/waveform.js.map +1 -0
  138. package/dist/dev/elements/wavestrip.d.ts +51 -0
  139. package/dist/dev/elements/wavestrip.js +183 -0
  140. package/dist/dev/elements/wavestrip.js.map +1 -0
  141. package/dist/dev/elements/xy.d.ts +62 -0
  142. package/dist/dev/elements/xy.js +474 -0
  143. package/dist/dev/elements/xy.js.map +1 -0
  144. package/dist/dev/index.d.ts +11 -2
  145. package/dist/dev/index.js +10 -2
  146. package/dist/dev/lib/adsr-shape.js +109 -0
  147. package/dist/dev/lib/adsr-shape.js.map +1 -0
  148. package/dist/dev/lib/ballistics.js +56 -0
  149. package/dist/dev/lib/ballistics.js.map +1 -0
  150. package/dist/dev/lib/canvas-surface.d.ts +25 -0
  151. package/dist/dev/lib/canvas-surface.js +193 -0
  152. package/dist/dev/lib/canvas-surface.js.map +1 -0
  153. package/dist/dev/lib/copy-node.js +32 -2
  154. package/dist/dev/lib/copy-node.js.map +1 -1
  155. package/dist/dev/lib/crossfade.d.ts +12 -0
  156. package/dist/dev/lib/crossfade.js +23 -0
  157. package/dist/dev/lib/crossfade.js.map +1 -0
  158. package/dist/dev/lib/distribute.d.ts +4 -0
  159. package/dist/dev/lib/distribute.js +216 -0
  160. package/dist/dev/lib/distribute.js.map +1 -0
  161. package/dist/dev/lib/drag-step.js +100 -0
  162. package/dist/dev/lib/drag-step.js.map +1 -0
  163. package/dist/dev/lib/envelope-curve.d.ts +4 -0
  164. package/dist/dev/lib/envelope-curve.js +10 -0
  165. package/dist/dev/lib/envelope-curve.js.map +1 -0
  166. package/dist/dev/lib/field.d.ts +4 -0
  167. package/dist/dev/lib/field.js +95 -0
  168. package/dist/dev/lib/field.js.map +1 -0
  169. package/dist/dev/lib/focus-by-pointer.js +29 -0
  170. package/dist/dev/lib/focus-by-pointer.js.map +1 -0
  171. package/dist/dev/lib/frame-timeline.js +63 -0
  172. package/dist/dev/lib/frame-timeline.js.map +1 -0
  173. package/dist/dev/lib/hold.js +39 -0
  174. package/dist/dev/lib/hold.js.map +1 -0
  175. package/dist/dev/lib/marker-band.js +37 -0
  176. package/dist/dev/lib/marker-band.js.map +1 -0
  177. package/dist/dev/lib/marker-rider.js +115 -0
  178. package/dist/dev/lib/marker-rider.js.map +1 -0
  179. package/dist/dev/lib/math.js +26 -0
  180. package/dist/dev/lib/math.js.map +1 -0
  181. package/dist/dev/lib/modifier-press.js +12 -0
  182. package/dist/dev/lib/modifier-press.js.map +1 -0
  183. package/dist/dev/lib/number-list.js +15 -0
  184. package/dist/dev/lib/number-list.js.map +1 -0
  185. package/dist/dev/lib/pointer-drag.js +44 -0
  186. package/dist/dev/lib/pointer-drag.js.map +1 -0
  187. package/dist/dev/lib/read-px-property.js.map +1 -1
  188. package/dist/dev/lib/render.js +20 -5
  189. package/dist/dev/lib/render.js.map +1 -1
  190. package/dist/dev/lib/resample-peaks.js +37 -0
  191. package/dist/dev/lib/resample-peaks.js.map +1 -0
  192. package/dist/dev/lib/strip-scene.js +49 -0
  193. package/dist/dev/lib/strip-scene.js.map +1 -0
  194. package/dist/dev/lib/taper.js +32 -16
  195. package/dist/dev/lib/taper.js.map +1 -1
  196. package/dist/dev/lib/time-regions.d.ts +4 -0
  197. package/dist/dev/lib/time-regions.js +9 -0
  198. package/dist/dev/lib/time-regions.js.map +1 -0
  199. package/dist/dev/lib/tracking-clock.js +36 -0
  200. package/dist/dev/lib/tracking-clock.js.map +1 -0
  201. package/dist/dev/lib/value-mapping.d.ts +23 -0
  202. package/dist/dev/lib/value-mapping.js +116 -0
  203. package/dist/dev/lib/value-mapping.js.map +1 -0
  204. package/dist/dev/lib/waveform-buckets.d.ts +9 -0
  205. package/dist/dev/lib/waveform-buckets.js +42 -0
  206. package/dist/dev/lib/waveform-buckets.js.map +1 -0
  207. package/dist/skins/ivory.css +1 -2
  208. package/dist/styles/button.css +212 -0
  209. package/dist/styles/controls.css +8 -1
  210. package/dist/styles/dial.css +130 -44
  211. package/dist/styles/envelope.css +159 -0
  212. package/dist/styles/led.css +44 -6
  213. package/dist/styles/material.css +321 -71
  214. package/dist/styles/meter.css +109 -28
  215. package/dist/styles/number.css +5 -1
  216. package/dist/styles/panel.css +59 -0
  217. package/dist/styles/ring.css +74 -0
  218. package/dist/styles/segmented.css +22 -54
  219. package/dist/styles/slider.css +256 -58
  220. package/dist/styles/switch.css +347 -0
  221. package/dist/styles/waveform.css +173 -0
  222. package/dist/styles/wavestrip.css +215 -0
  223. package/dist/styles/xy.css +204 -0
  224. package/package.json +14 -2
  225. package/dist/default/define/key.js +0 -7
  226. package/dist/default/elements/key.js +0 -144
  227. package/dist/default/elements/range-element.js +0 -433
  228. package/dist/dev/define/key.d.ts +0 -2
  229. package/dist/dev/define/key.js +0 -8
  230. package/dist/dev/define/key.js.map +0 -1
  231. package/dist/dev/elements/key.js +0 -145
  232. package/dist/dev/elements/key.js.map +0 -1
  233. package/dist/dev/elements/range-element.d.ts +0 -56
  234. package/dist/dev/elements/range-element.js +0 -434
  235. package/dist/dev/elements/range-element.js.map +0 -1
  236. package/dist/dev/elements/range-entry.js.map +0 -1
  237. package/dist/styles/key.css +0 -197
@@ -0,0 +1,26 @@
1
+ //#region src/lib/math.ts
2
+ function clamp(value, min, max) {
3
+ return Math.min(max, Math.max(min, value));
4
+ }
5
+ function clampProportion(fraction) {
6
+ return clamp(fraction, 0, 1);
7
+ }
8
+ function wrapProportion(fraction) {
9
+ return fraction - Math.floor(fraction);
10
+ }
11
+ function trimFloat(value) {
12
+ return Number(value.toPrecision(12));
13
+ }
14
+ function decimalPlaces(value) {
15
+ const [digits = "", exponent = "0"] = String(value).split("e", 2);
16
+ const fraction = digits.split(".", 2)[1] ?? "";
17
+ return Math.max(0, fraction.length - Number(exponent));
18
+ }
19
+ function roundTo(value, places) {
20
+ const rounded = Number(value.toFixed(Math.min(100, places)));
21
+ return rounded === 0 ? 0 : rounded;
22
+ }
23
+
24
+ //#endregion
25
+ export { clamp, clampProportion, decimalPlaces, roundTo, trimFloat, wrapProportion };
26
+ //# sourceMappingURL=math.js.map
@@ -0,0 +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 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;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"}
@@ -0,0 +1,15 @@
1
+ //#region src/lib/number-list.ts
2
+ function parseNumberList(text) {
3
+ if (text === null) return 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
+ }
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
+ return nearest;
11
+ }
12
+
13
+ //#endregion
14
+ export { nearestPosition, parseNumberList };
15
+ //# sourceMappingURL=number-list.js.map
@@ -0,0 +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 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,44 @@
1
+ //#region src/lib/pointer-drag.ts
2
+ function bindDrag(target, handlers, signal) {
3
+ let held;
4
+ const end = () => {
5
+ if (!held) return;
6
+ const { drag } = held;
7
+ held = void 0;
8
+ handlers.toggle(false);
9
+ handlers.release(drag);
10
+ };
11
+ const whileHeld = (type, handle) => {
12
+ target.addEventListener(type, (event) => {
13
+ if (held?.pointerId === event.pointerId) handle(held.drag, event);
14
+ }, { signal });
15
+ };
16
+ target.addEventListener("pointerdown", (event) => {
17
+ if (event.button !== 0 || held && held.pointerId !== event.pointerId) return;
18
+ end();
19
+ const drag = handlers.grab(event);
20
+ if (drag === void 0) return;
21
+ held = {
22
+ drag,
23
+ pointerId: event.pointerId
24
+ };
25
+ target.setPointerCapture(event.pointerId);
26
+ handlers.toggle(true);
27
+ }, { signal });
28
+ whileHeld("pointermove", handlers.move);
29
+ whileHeld("pointercancel", (drag) => {
30
+ handlers.cancel(drag);
31
+ end();
32
+ });
33
+ whileHeld("lostpointercapture", end);
34
+ whileHeld("pointerup", end);
35
+ signal.addEventListener("abort", end, { once: true });
36
+ return {
37
+ current: () => held?.drag,
38
+ end
39
+ };
40
+ }
41
+
42
+ //#endregion
43
+ export { bindDrag };
44
+ //# 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\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', end);\n\tsignal.addEventListener('abort', end, { once: true });\n\n\treturn { current: () => held?.drag, end };\n}\n"],"mappings":";AAaA,SAAgB,SACf,QACA,UACA,QACoB;CACpB,IAAI;CAEJ,MAAM,YAAkB;EACvB,IAAI,CAAC,MAAM;EAEX,MAAM,EAAE,SAAS;EAEjB,OAAO;EACP,SAAS,OAAO,KAAK;EACrB,SAAS,QAAQ,IAAI;CACtB;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,OAAO,IAAI;CACrB,GACA,EAAE,OAAO,CACV;CACA,UAAU,eAAe,SAAS,IAAI;CACtC,UAAU,kBAAkB,SAAS;EACpC,SAAS,OAAO,IAAI;EACpB,IAAI;CACL,CAAC;CACD,UAAU,sBAAsB,GAAG;CACnC,UAAU,aAAa,GAAG;CAC1B,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,37 @@
1
+ //#region src/lib/resample-peaks.ts
2
+ function bucketBounds(sourceLength, index, count) {
3
+ const start = Math.floor(index * sourceLength / count);
4
+ return [start, Math.max(start + 1, Math.floor((index + 1) * sourceLength / count))];
5
+ }
6
+ function downsample(peaks, count) {
7
+ const resampled = [];
8
+ for (let index = 0; index < count; index += 1) {
9
+ const [start, end] = bucketBounds(peaks.length, index, count);
10
+ let total = 0;
11
+ for (let source = start; source < end; source += 1) total += peaks[source] ?? 0;
12
+ resampled.push(total / (end - start));
13
+ }
14
+ return resampled;
15
+ }
16
+ function upsample(peaks, count) {
17
+ const resampled = [];
18
+ const last = peaks.length - 1;
19
+ for (let index = 0; index < count; index += 1) {
20
+ const sourceIndex = count === 1 ? 0 : index * last / (count - 1);
21
+ const before = Math.floor(sourceIndex);
22
+ const weight = sourceIndex - before;
23
+ resampled.push((peaks[before] ?? 0) * (1 - weight) + (peaks[Math.min(last, before + 1)] ?? 0) * weight);
24
+ }
25
+ return resampled;
26
+ }
27
+ function resamplePeaks(peaks, count) {
28
+ if (count <= 0 || peaks.length === 0) return [];
29
+ const finite = Array.from(peaks, (peak) => Number.isFinite(peak) ? peak : 0);
30
+ if (count === finite.length) return finite;
31
+ if (count > finite.length) return upsample(finite, count);
32
+ return downsample(finite, count);
33
+ }
34
+
35
+ //#endregion
36
+ export { resamplePeaks };
37
+ //# 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: ArrayLike<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 resampled;\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,OAA0B,OAA8B;CAC3E,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;AACR;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,36 +1,52 @@
1
+ import { clampProportion } from "./math.js";
2
+
1
3
  //#region src/lib/taper.ts
2
- function clampUnit(fraction) {
3
- return Math.min(1, Math.max(0, fraction));
4
- }
5
- function wrapUnit(fraction) {
6
- return fraction - Math.floor(fraction);
7
- }
8
4
  function linearTaper(min, max) {
9
5
  const range = max - min;
10
6
  return {
11
- position: (value) => range > 0 ? clampUnit((value - min) / range) : 0,
12
- value: (position) => min + clampUnit(position) * range
7
+ proportionOf: (value) => range > 0 ? clampProportion((value - min) / range) : 0,
8
+ valueAt: (proportion) => min + clampProportion(proportion) * range
13
9
  };
14
10
  }
15
11
  function logTaper(min, max) {
16
12
  if (min <= 0 || max <= min) return void 0;
17
13
  const span = Math.log(max / min);
18
14
  return {
19
- position: (value) => clampUnit(Math.log(Math.max(min, value) / min) / span),
20
- 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
+ }
21
22
  };
22
23
  }
23
24
  function skewTaper(min, max, midpoint) {
24
25
  const range = max - min;
25
- const proportion = (midpoint - min) / range;
26
- if (proportion === .5 || !(proportion > 0 && proportion < 1)) return void 0;
27
- 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);
29
+ return {
30
+ proportionOf: (value) => clampProportion((value - min) / range) ** skew,
31
+ valueAt: (proportion) => min + range * clampProportion(proportion) ** (1 / skew)
32
+ };
33
+ }
34
+ function listTaper(positions) {
35
+ const last = positions.length - 1;
28
36
  return {
29
- position: (value) => clampUnit((value - min) / range) ** skew,
30
- value: (position) => min + range * clampUnit(position) ** (1 / skew)
37
+ proportionOf: (value) => {
38
+ if (Number.isNaN(value)) return NaN;
39
+ const above = positions.findIndex((position) => position >= value);
40
+ if (above === -1) return 1;
41
+ const high = positions[above];
42
+ const low = positions[above - 1];
43
+ if (high === void 0 || low === void 0) return 0;
44
+ return (above - 1 + (value - low) / (high - low)) / last;
45
+ },
46
+ valueAt: (proportion) => positions[Math.round(clampProportion(proportion) * last)] ?? NaN
31
47
  };
32
48
  }
33
49
 
34
50
  //#endregion
35
- export { clampUnit, linearTaper, logTaper, skewTaper, wrapUnit };
51
+ export { linearTaper, listTaper, logTaper, skewTaper };
36
52
  //# sourceMappingURL=taper.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"taper.js","names":[],"sources":["../../../src/lib/taper.ts"],"sourcesContent":["// Positions run 0 to 1 along the travel\nexport interface Taper {\n\tposition: (value: number) => number;\n\tvalue: (position: number) => number;\n}\n\nexport function clampUnit(fraction: number): number {\n\treturn Math.min(1, Math.max(0, fraction));\n}\n\nexport function wrapUnit(fraction: number): number {\n\treturn fraction - Math.floor(fraction);\n}\n\nexport function linearTaper(min: number, max: number): Taper {\n\tconst range = max - min;\n\n\treturn {\n\t\tposition: (value) => (range > 0 ? clampUnit((value - min) / range) : 0),\n\t\tvalue: (position) => min + clampUnit(position) * range,\n\t};\n}\n\nexport function logTaper(min: number, max: number): Taper | undefined {\n\tif (min <= 0 || max <= min) return undefined;\n\n\tconst span = Math.log(max / min);\n\n\treturn {\n\t\tposition: (value) => clampUnit(Math.log(Math.max(min, value) / min) / span),\n\t\tvalue: (position) => min * Math.exp(clampUnit(position) * span),\n\t};\n}\n\n// JUCE's skew: a power curve through `midpoint` at half the travel\nexport function skewTaper(min: number, max: number, midpoint: number): Taper | undefined {\n\tconst range = max - min;\n\tconst proportion = (midpoint - min) / range;\n\n\tif (proportion === 0.5 || !(proportion > 0 && proportion < 1)) return undefined;\n\n\tconst skew = Math.log(0.5) / Math.log(proportion);\n\n\treturn {\n\t\tposition: (value) => clampUnit((value - min) / range) ** skew,\n\t\tvalue: (position) => min + range * clampUnit(position) ** (1 / skew),\n\t};\n}\n"],"mappings":";AAMA,SAAgB,UAAU,UAA0B;CACnD,OAAO,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,QAAQ,CAAC;AACzC;AAEA,SAAgB,SAAS,UAA0B;CAClD,OAAO,WAAW,KAAK,MAAM,QAAQ;AACtC;AAEA,SAAgB,YAAY,KAAa,KAAoB;CAC5D,MAAM,QAAQ,MAAM;CAEpB,OAAO;EACN,WAAW,UAAW,QAAQ,IAAI,WAAW,QAAQ,OAAO,KAAK,IAAI;EACrE,QAAQ,aAAa,MAAM,UAAU,QAAQ,IAAI;CAClD;AACD;AAEA,SAAgB,SAAS,KAAa,KAAgC;CACrE,IAAI,OAAO,KAAK,OAAO,KAAK,OAAO;CAEnC,MAAM,OAAO,KAAK,IAAI,MAAM,GAAG;CAE/B,OAAO;EACN,WAAW,UAAU,UAAU,KAAK,IAAI,KAAK,IAAI,KAAK,KAAK,IAAI,GAAG,IAAI,IAAI;EAC1E,QAAQ,aAAa,MAAM,KAAK,IAAI,UAAU,QAAQ,IAAI,IAAI;CAC/D;AACD;AAGA,SAAgB,UAAU,KAAa,KAAa,UAAqC;CACxF,MAAM,QAAQ,MAAM;CACpB,MAAM,cAAc,WAAW,OAAO;CAEtC,IAAI,eAAe,MAAO,EAAE,aAAa,KAAK,aAAa,IAAI,OAAO;CAEtE,MAAM,OAAO,KAAK,IAAI,EAAG,IAAI,KAAK,IAAI,UAAU;CAEhD,OAAO;EACN,WAAW,UAAU,WAAW,QAAQ,OAAO,KAAK,KAAK;EACzD,QAAQ,aAAa,MAAM,QAAQ,UAAU,QAAQ,MAAM,IAAI;CAChE;AACD"}
1
+ {"version":3,"file":"taper.js","names":[],"sources":["../../../src/lib/taper.ts"],"sourcesContent":["import { clampProportion } from '#lib/math.ts';\n\nexport interface Taper {\n\tproportionOf: (value: number) => number;\n\tvalueAt: (proportion: number) => number;\n}\n\nexport function linearTaper(min: number, max: number): Taper {\n\tconst range = max - min;\n\n\treturn {\n\t\tproportionOf: (value) => (range > 0 ? clampProportion((value - min) / range) : 0),\n\t\tvalueAt: (proportion) => min + clampProportion(proportion) * range,\n\t};\n}\n\nexport function logTaper(min: number, max: number): Taper | undefined {\n\tif (min <= 0 || max <= min) return undefined;\n\n\tconst span = Math.log(max / min);\n\n\treturn {\n\t\tproportionOf: (value) => clampProportion(Math.log(Math.max(min, value) / min) / span),\n\t\tvalueAt: (proportion) => {\n\t\t\tconst clamped = clampProportion(proportion);\n\t\t\tif (clamped === 0) return min;\n\t\t\tif (clamped === 1) return max;\n\n\t\t\treturn min * Math.exp(clamped * span);\n\t\t},\n\t};\n}\n\n// JUCE's skew: a power curve through `midpoint` at half the travel\nexport function skewTaper(min: number, max: number, midpoint: number): Taper | undefined {\n\tconst range = max - min;\n\tconst midpointProportion = (midpoint - min) / range;\n\n\tif (midpointProportion === 0.5 || !(midpointProportion > 0 && midpointProportion < 1))\n\t\treturn undefined;\n\n\tconst skew = Math.log(0.5) / Math.log(midpointProportion);\n\n\treturn {\n\t\tproportionOf: (value) => clampProportion((value - min) / range) ** skew,\n\t\tvalueAt: (proportion) => min + range * clampProportion(proportion) ** (1 / skew),\n\t};\n}\n\nexport function listTaper(positions: ReadonlyArray<number>): Taper {\n\tconst last = positions.length - 1;\n\n\treturn {\n\t\tproportionOf: (value) => {\n\t\t\tif (Number.isNaN(value)) return NaN;\n\n\t\t\tconst above = positions.findIndex((position) => position >= value);\n\t\t\tif (above === -1) return 1;\n\n\t\t\tconst high = positions[above];\n\t\t\tconst low = positions[above - 1];\n\t\t\tif (high === undefined || low === undefined) return 0;\n\n\t\t\treturn (above - 1 + (value - low) / (high - low)) / last;\n\t\t},\n\t\tvalueAt: (proportion) => positions[Math.round(clampProportion(proportion) * last)] ?? NaN,\n\t};\n}\n"],"mappings":";;;AAOA,SAAgB,YAAY,KAAa,KAAoB;CAC5D,MAAM,QAAQ,MAAM;CAEpB,OAAO;EACN,eAAe,UAAW,QAAQ,IAAI,iBAAiB,QAAQ,OAAO,KAAK,IAAI;EAC/E,UAAU,eAAe,MAAM,gBAAgB,UAAU,IAAI;CAC9D;AACD;AAEA,SAAgB,SAAS,KAAa,KAAgC;CACrE,IAAI,OAAO,KAAK,OAAO,KAAK,OAAO;CAEnC,MAAM,OAAO,KAAK,IAAI,MAAM,GAAG;CAE/B,OAAO;EACN,eAAe,UAAU,gBAAgB,KAAK,IAAI,KAAK,IAAI,KAAK,KAAK,IAAI,GAAG,IAAI,IAAI;EACpF,UAAU,eAAe;GACxB,MAAM,UAAU,gBAAgB,UAAU;GAC1C,IAAI,YAAY,GAAG,OAAO;GAC1B,IAAI,YAAY,GAAG,OAAO;GAE1B,OAAO,MAAM,KAAK,IAAI,UAAU,IAAI;EACrC;CACD;AACD;AAGA,SAAgB,UAAU,KAAa,KAAa,UAAqC;CACxF,MAAM,QAAQ,MAAM;CACpB,MAAM,sBAAsB,WAAW,OAAO;CAE9C,IAAI,uBAAuB,MAAO,EAAE,qBAAqB,KAAK,qBAAqB,IAClF,OAAO;CAER,MAAM,OAAO,KAAK,IAAI,EAAG,IAAI,KAAK,IAAI,kBAAkB;CAExD,OAAO;EACN,eAAe,UAAU,iBAAiB,QAAQ,OAAO,KAAK,KAAK;EACnE,UAAU,eAAe,MAAM,QAAQ,gBAAgB,UAAU,MAAM,IAAI;CAC5E;AACD;AAEA,SAAgB,UAAU,WAAyC;CAClE,MAAM,OAAO,UAAU,SAAS;CAEhC,OAAO;EACN,eAAe,UAAU;GACxB,IAAI,OAAO,MAAM,KAAK,GAAG,OAAO;GAEhC,MAAM,QAAQ,UAAU,WAAW,aAAa,YAAY,KAAK;GACjE,IAAI,UAAU,IAAI,OAAO;GAEzB,MAAM,OAAO,UAAU;GACvB,MAAM,MAAM,UAAU,QAAQ;GAC9B,IAAI,SAAS,UAAa,QAAQ,QAAW,OAAO;GAEpD,QAAQ,QAAQ,KAAK,QAAQ,QAAQ,OAAO,QAAQ;EACrD;EACA,UAAU,eAAe,UAAU,KAAK,MAAM,gBAAgB,UAAU,IAAI,IAAI,MAAM;CACvF;AACD"}
@@ -0,0 +1,4 @@
1
+ //#region src/lib/time-regions.d.ts
2
+ export type TimeRegions = Iterable<readonly [number, number]> | TimeRanges;
3
+ //#endregion
4
+ //# sourceMappingURL=time-regions.d.ts.map
@@ -0,0 +1,9 @@
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 };
9
+ //# sourceMappingURL=time-regions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"time-regions.js","names":[],"sources":["../../../src/lib/time-regions.ts"],"sourcesContent":["export type TimeRegions = Iterable<readonly [number, number]> | TimeRanges;\n\nexport function readRegions(regions: TimeRegions): Array<[number, number]> {\n\tif ('start' in regions) {\n\t\treturn Array.from({ length: regions.length }, (_entry, index) => [\n\t\t\tregions.start(index),\n\t\t\tregions.end(index),\n\t\t]);\n\t}\n\n\treturn [...regions].map(([start, end]) => [start, end]);\n}\n"],"mappings":";AAEA,SAAgB,YAAY,SAA+C;CAC1E,IAAI,WAAW,SACd,OAAO,MAAM,KAAK,EAAE,QAAQ,QAAQ,OAAO,IAAI,QAAQ,UAAU,CAChE,QAAQ,MAAM,KAAK,GACnB,QAAQ,IAAI,KAAK,CAClB,CAAC;CAGF,OAAO,CAAC,GAAG,OAAO,CAAC,CAAC,KAAK,CAAC,OAAO,SAAS,CAAC,OAAO,GAAG,CAAC;AACvD"}
@@ -0,0 +1,36 @@
1
+ //#region src/lib/tracking-clock.ts
2
+ const seekThresholdSeconds = .5;
3
+ const jitterSeconds = .05;
4
+ function isLandingOnSource({ isPlaying, seconds }, carried, lastSourceSeconds) {
5
+ const hasSteppedBack = lastSourceSeconds !== void 0 && lastSourceSeconds - seconds > jitterSeconds;
6
+ return !isPlaying || hasSteppedBack || Math.abs(seconds - carried) > seekThresholdSeconds;
7
+ }
8
+ function createTrackingClock() {
9
+ let positionSeconds;
10
+ let lastFrameMs;
11
+ let lastSourceSeconds;
12
+ let wasPlaying = false;
13
+ return { read: (frameMs, source) => {
14
+ const { isPlaying, playbackRate, seconds } = source;
15
+ const elapsedSeconds = wasPlaying ? Math.max(0, frameMs - (lastFrameMs ?? frameMs)) / 1e3 : 0;
16
+ const carried = (positionSeconds ?? seconds) + elapsedSeconds * playbackRate;
17
+ lastFrameMs = frameMs;
18
+ wasPlaying = isPlaying;
19
+ if (!Number.isFinite(seconds)) {
20
+ if (positionSeconds !== void 0) positionSeconds = carried;
21
+ return carried;
22
+ }
23
+ const isLanding = isLandingOnSource(source, carried, lastSourceSeconds);
24
+ lastSourceSeconds = seconds;
25
+ if (positionSeconds === void 0 || isLanding) {
26
+ positionSeconds = seconds;
27
+ return positionSeconds;
28
+ }
29
+ positionSeconds = carried + (seconds - carried) * (1 - .94 ** (elapsedSeconds * 60));
30
+ return positionSeconds;
31
+ } };
32
+ }
33
+
34
+ //#endregion
35
+ export { createTrackingClock };
36
+ //# sourceMappingURL=tracking-clock.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tracking-clock.js","names":[],"sources":["../../../src/lib/tracking-clock.ts"],"sourcesContent":["const seekThresholdSeconds = 0.5;\nconst jitterSeconds = 0.05;\n\n// The share of the error closed in a 60 Hz frame; scaled by elapsed time so every refresh rate converges alike\nconst catchUpPerFrame = 0.06;\n\ninterface ClockSource {\n\tisPlaying: boolean;\n\tplaybackRate: number;\n\tseconds: number;\n}\n\ninterface TrackingClock {\n\t// `frameMs` is the frame's timestamp, never `performance.now()` in the callback\n\tread: (frameMs: number, source: ClockSource) => number;\n}\n\nfunction isLandingOnSource(\n\t{ isPlaying, seconds }: ClockSource,\n\tcarried: number,\n\tlastSourceSeconds: number | undefined,\n): boolean {\n\tconst hasSteppedBack =\n\t\tlastSourceSeconds !== undefined && lastSourceSeconds - seconds > jitterSeconds;\n\n\treturn !isPlaying || hasSteppedBack || Math.abs(seconds - carried) > seekThresholdSeconds;\n}\n\nexport function createTrackingClock(): TrackingClock {\n\tlet positionSeconds: number | undefined;\n\tlet lastFrameMs: number | undefined;\n\tlet lastSourceSeconds: number | undefined;\n\tlet wasPlaying = false;\n\n\treturn {\n\t\tread: (frameMs, source) => {\n\t\t\tconst { isPlaying, playbackRate, seconds } = source;\n\t\t\tconst elapsedSeconds = wasPlaying\n\t\t\t\t? Math.max(0, frameMs - (lastFrameMs ?? frameMs)) / 1000\n\t\t\t\t: 0;\n\t\t\tconst carried = (positionSeconds ?? seconds) + elapsedSeconds * playbackRate;\n\n\t\t\tlastFrameMs = frameMs;\n\t\t\twasPlaying = isPlaying;\n\n\t\t\tif (!Number.isFinite(seconds)) {\n\t\t\t\tif (positionSeconds !== undefined) positionSeconds = carried;\n\t\t\t\treturn carried;\n\t\t\t}\n\n\t\t\tconst isLanding = isLandingOnSource(source, carried, lastSourceSeconds);\n\n\t\t\tlastSourceSeconds = seconds;\n\n\t\t\tif (positionSeconds === undefined || isLanding) {\n\t\t\t\tpositionSeconds = seconds;\n\t\t\t\treturn positionSeconds;\n\t\t\t}\n\n\t\t\tpositionSeconds =\n\t\t\t\tcarried + (seconds - carried) * (1 - (1 - catchUpPerFrame) ** (elapsedSeconds * 60));\n\n\t\t\treturn positionSeconds;\n\t\t},\n\t};\n}\n"],"mappings":";AAAA,MAAM,uBAAuB;AAC7B,MAAM,gBAAgB;AAgBtB,SAAS,kBACR,EAAE,WAAW,WACb,SACA,mBACU;CACV,MAAM,iBACL,sBAAsB,UAAa,oBAAoB,UAAU;CAElE,OAAO,CAAC,aAAa,kBAAkB,KAAK,IAAI,UAAU,OAAO,IAAI;AACtE;AAEA,SAAgB,sBAAqC;CACpD,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,IAAI,aAAa;CAEjB,OAAO,EACN,OAAO,SAAS,WAAW;EAC1B,MAAM,EAAE,WAAW,cAAc,YAAY;EAC7C,MAAM,iBAAiB,aACpB,KAAK,IAAI,GAAG,WAAW,eAAe,QAAQ,IAAI,MAClD;EACH,MAAM,WAAW,mBAAmB,WAAW,iBAAiB;EAEhE,cAAc;EACd,aAAa;EAEb,IAAI,CAAC,OAAO,SAAS,OAAO,GAAG;GAC9B,IAAI,oBAAoB,QAAW,kBAAkB;GACrD,OAAO;EACR;EAEA,MAAM,YAAY,kBAAkB,QAAQ,SAAS,iBAAiB;EAEtE,oBAAoB;EAEpB,IAAI,oBAAoB,UAAa,WAAW;GAC/C,kBAAkB;GAClB,OAAO;EACR;EAEA,kBACC,WAAW,UAAU,YAAY,IAAK,QAAyB,iBAAiB;EAEjF,OAAO;CACR,EACD;AACD"}
@@ -0,0 +1,23 @@
1
+ //#region src/lib/value-mapping.d.ts
2
+ export interface ValueSpec {
3
+ detent?: number;
4
+ isNotched: boolean;
5
+ isWrapping: boolean;
6
+ max: number;
7
+ midpoint?: number;
8
+ min: number;
9
+ positions?: ReadonlyArray<number>;
10
+ step: number;
11
+ taper: 'linear' | 'log';
12
+ }
13
+ export interface ValueMapping {
14
+ readonly bounds: [number, number];
15
+ readonly isWrapping: boolean;
16
+ readonly keyTarget: (key: string, from: number, keyStep?: number) => number | undefined;
17
+ readonly positionCount: number | undefined;
18
+ readonly proportionOf: (value: number) => number;
19
+ readonly snap: (value: number) => number;
20
+ readonly valueAt: (proportion: number) => number;
21
+ }
22
+ //#endregion
23
+ //# sourceMappingURL=value-mapping.d.ts.map
@@ -0,0 +1,116 @@
1
+ import { clamp, decimalPlaces, roundTo, trimFloat } from "./math.js";
2
+ import { nearestPosition } from "./number-list.js";
3
+ import { linearTaper, listTaper, logTaper, skewTaper } from "./taper.js";
4
+
5
+ //#region src/lib/value-mapping.ts
6
+ const resetKeys = /* @__PURE__ */ new Set(["Backspace", "Delete"]);
7
+ const keySteps = /* @__PURE__ */ new Map([
8
+ ["ArrowDown", -1],
9
+ ["ArrowLeft", -1],
10
+ ["ArrowRight", 1],
11
+ ["ArrowUp", 1],
12
+ ["PageDown", -10],
13
+ ["PageUp", 10]
14
+ ]);
15
+ function curveOf(spec, max) {
16
+ const { min, positions } = spec;
17
+ if (positions) return listTaper(positions);
18
+ if (spec.isNotched || spec.isWrapping) return void 0;
19
+ return (spec.taper === "log" ? logTaper(min, max) : void 0) ?? skewTaper(min, max, spec.midpoint ?? NaN);
20
+ }
21
+ function countPositions(spec, count) {
22
+ if (!spec.isNotched || !Number.isFinite(count) || count < 2) return;
23
+ return count;
24
+ }
25
+ function countSteps(spec, range) {
26
+ if (spec.step <= 0) return NaN;
27
+ const fit = trimFloat(range / spec.step);
28
+ return spec.isWrapping ? Math.max(1, Math.ceil(fit)) : Math.floor(fit) + 1;
29
+ }
30
+ function valueMapping(spec) {
31
+ const { min, positions, step } = spec;
32
+ const max = Math.max(min, spec.max);
33
+ const range = max - min;
34
+ const count = countSteps(spec, range);
35
+ const isStepped = step > 0 && !positions;
36
+ const curve = curveOf(spec, max);
37
+ const taper = curve ?? linearTaper(min, max);
38
+ function round(value) {
39
+ if (step <= 0) return trimFloat(value);
40
+ return roundTo(value, Math.max(decimalPlaces(step), decimalPlaces(min)));
41
+ }
42
+ const top = round((count - 1) * step);
43
+ function limits() {
44
+ if (positions) return [positions[0] ?? min, positions.at(-1) ?? max];
45
+ return [min, isStepped && !spec.isWrapping ? round(min + top) : max];
46
+ }
47
+ const bounds = limits();
48
+ function wrap(value, span) {
49
+ const offset = round(((value - min) % span + span) % span);
50
+ return offset >= span ? 0 : offset;
51
+ }
52
+ function bound(value) {
53
+ if (!spec.isWrapping) return clamp(value, ...bounds);
54
+ if (range <= 0) return min;
55
+ return round(min + wrap(value, range));
56
+ }
57
+ function ring(value, span) {
58
+ if (range <= 0) return min;
59
+ const offset = wrap(value, span);
60
+ if (offset > top) return span - offset <= offset - top ? min : round(min + top);
61
+ return round(min + Math.round(offset / step) * step);
62
+ }
63
+ function snap(value) {
64
+ if (positions) return nearestPosition(positions, value);
65
+ if (step <= 0) return bound(round(value));
66
+ if (spec.isWrapping) return ring(value, range);
67
+ return bound(round(min + Math.round((value - min) / step) * step));
68
+ }
69
+ function stepBy(from, steps, size) {
70
+ if (positions) {
71
+ const index = clamp(positions.indexOf(from) + steps, 0, positions.length - 1);
72
+ return positions[index] ?? from;
73
+ }
74
+ if (!curve) return from + steps * size;
75
+ const next = snap(curve.valueAt(curve.proportionOf(from) + steps / 100));
76
+ return next === from ? from + Math.sign(steps) * size : next;
77
+ }
78
+ function stopAtDetent(from, next) {
79
+ if (spec.detent === void 0) return next;
80
+ const detent = snap(spec.detent);
81
+ const toward = Math.sign(next - from) * (detent - from);
82
+ const ahead = spec.isWrapping ? wrap(min + toward, isStepped ? count * step : range) : toward;
83
+ return ahead > 0 && ahead < Math.abs(next - from) ? detent : next;
84
+ }
85
+ function keyStride(keyStep) {
86
+ if (positions || !(keyStep > 0)) return void 0;
87
+ return step > 0 ? Math.max(1, Math.round(trimFloat(keyStep / step))) * step : keyStep;
88
+ }
89
+ function end(size) {
90
+ if (!spec.isWrapping) return bounds[1];
91
+ return min + (isStepped ? top : (Math.ceil(range / size) - 1) * size);
92
+ }
93
+ function target(key, from, keyStep) {
94
+ if (key === "Home") return bounds[0];
95
+ const size = step > 0 ? step : range / 100;
96
+ if (key === "End") return snap(end(size));
97
+ const steps = keySteps.get(key);
98
+ if (steps === void 0) return void 0;
99
+ const stride = keyStride(keyStep);
100
+ const next = stopAtDetent(from, stride === void 0 ? stepBy(from, steps, size) : from + steps * stride);
101
+ return isStepped && spec.isWrapping ? ring(next, count * step) : snap(next);
102
+ }
103
+ return {
104
+ bounds,
105
+ isWrapping: spec.isWrapping,
106
+ keyTarget: (key, from, keyStep = 0) => target(key, from, keyStep),
107
+ positionCount: countPositions(spec, positions?.length ?? count),
108
+ proportionOf: (value) => taper.proportionOf(bound(value)),
109
+ snap,
110
+ valueAt: (proportion) => taper.valueAt(proportion)
111
+ };
112
+ }
113
+
114
+ //#endregion
115
+ export { resetKeys, valueMapping };
116
+ //# sourceMappingURL=value-mapping.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"value-mapping.js","names":[],"sources":["../../../src/lib/value-mapping.ts"],"sourcesContent":["import type { Taper } from '#lib/taper.ts';\n\nimport { clamp, decimalPlaces, roundTo, trimFloat } from '#lib/math.ts';\nimport { nearestPosition } from '#lib/number-list.ts';\nimport { linearTaper, listTaper, logTaper, skewTaper } from '#lib/taper.ts';\n\nexport interface ValueSpec {\n\tdetent?: number;\n\tisNotched: boolean;\n\tisWrapping: boolean;\n\tmax: number;\n\tmidpoint?: number;\n\tmin: number;\n\tpositions?: ReadonlyArray<number>;\n\tstep: number;\n\ttaper: 'linear' | 'log';\n}\n\nexport interface ValueMapping {\n\treadonly bounds: [number, number];\n\treadonly isWrapping: boolean;\n\treadonly keyTarget: (key: string, from: number, keyStep?: number) => number | undefined;\n\treadonly positionCount: number | undefined;\n\treadonly proportionOf: (value: number) => number;\n\treadonly snap: (value: number) => number;\n\treadonly valueAt: (proportion: number) => number;\n}\n\n// A Mac keyboard's Delete sends Backspace\nexport const resetKeys = new Set(['Backspace', 'Delete']);\n\nconst pageSteps = 10;\n\nconst keySteps = new Map([\n\t['ArrowDown', -1],\n\t['ArrowLeft', -1],\n\t['ArrowRight', 1],\n\t['ArrowUp', 1],\n\t['PageDown', -pageSteps],\n\t['PageUp', pageSteps],\n]);\n\nfunction curveOf(spec: ValueSpec, max: number): Taper | undefined {\n\tconst { min, positions } = spec;\n\tif (positions) return listTaper(positions);\n\tif (spec.isNotched || spec.isWrapping) return undefined;\n\n\tconst log = spec.taper === 'log' ? logTaper(min, max) : undefined;\n\n\treturn log ?? skewTaper(min, max, spec.midpoint ?? NaN);\n}\n\nfunction countPositions(spec: ValueSpec, count: number): number | undefined {\n\tif (!spec.isNotched || !Number.isFinite(count) || count < 2) return;\n\n\treturn count;\n}\n\n// A wrapping mapping stops a step short of max, which is min again\nfunction countSteps(spec: ValueSpec, range: number): number {\n\tif (spec.step <= 0) return NaN;\n\n\tconst fit = trimFloat(range / spec.step);\n\n\treturn spec.isWrapping ? Math.max(1, Math.ceil(fit)) : Math.floor(fit) + 1;\n}\n\nexport function valueMapping(spec: ValueSpec): ValueMapping {\n\tconst { min, positions, step } = spec;\n\tconst max = Math.max(min, spec.max);\n\tconst range = max - min;\n\tconst count = countSteps(spec, range);\n\tconst isStepped = step > 0 && !positions;\n\tconst curve = curveOf(spec, max);\n\tconst taper = curve ?? linearTaper(min, max);\n\n\t// Decimal places, since significant digits keep the float residue beside 0\n\tfunction round(value: number): number {\n\t\tif (step <= 0) return trimFloat(value);\n\n\t\treturn roundTo(value, Math.max(decimalPlaces(step), decimalPlaces(min)));\n\t}\n\n\tconst top = round((count - 1) * step);\n\n\tfunction limits(): [number, number] {\n\t\tif (positions) return [positions[0] ?? min, positions.at(-1) ?? max];\n\n\t\treturn [min, isStepped && !spec.isWrapping ? round(min + top) : max];\n\t}\n\n\tconst bounds = limits();\n\n\tfunction wrap(value: number, span: number): number {\n\t\tconst offset = round((((value - min) % span) + span) % span);\n\n\t\treturn offset >= span ? 0 : offset;\n\t}\n\n\tfunction bound(value: number): number {\n\t\tif (!spec.isWrapping) return clamp(value, ...bounds);\n\t\tif (range <= 0) return min;\n\n\t\treturn round(min + wrap(value, range));\n\t}\n\n\t// A step that does not divide the range leaves a short seam, where a value goes to the nearer side\n\tfunction ring(value: number, span: number): number {\n\t\tif (range <= 0) return min;\n\n\t\tconst offset = wrap(value, span);\n\t\tif (offset > top) return span - offset <= offset - top ? min : round(min + top);\n\n\t\treturn round(min + Math.round(offset / step) * step);\n\t}\n\n\tfunction snap(value: number): number {\n\t\tif (positions) return nearestPosition(positions, value);\n\t\tif (step <= 0) return bound(round(value));\n\t\tif (spec.isWrapping) return ring(value, range);\n\n\t\treturn bound(round(min + Math.round((value - min) / step) * step));\n\t}\n\n\tfunction stepBy(from: number, steps: number, size: number): number {\n\t\tif (positions) {\n\t\t\tconst index = clamp(positions.indexOf(from) + steps, 0, positions.length - 1);\n\n\t\t\treturn positions[index] ?? from;\n\t\t}\n\t\tif (!curve) return from + steps * size;\n\n\t\tconst next = snap(curve.valueAt(curve.proportionOf(from) + steps / 100));\n\n\t\treturn next === from ? from + Math.sign(steps) * size : next;\n\t}\n\n\tfunction stopAtDetent(from: number, next: number): number {\n\t\tif (spec.detent === undefined) return next;\n\n\t\tconst detent = snap(spec.detent);\n\t\tconst toward = Math.sign(next - from) * (detent - from);\n\t\tconst ahead = spec.isWrapping ? wrap(min + toward, isStepped ? count * step : range) : toward;\n\n\t\treturn ahead > 0 && ahead < Math.abs(next - from) ? detent : next;\n\t}\n\n\tfunction keyStride(keyStep: number): number | undefined {\n\t\tif (positions || !(keyStep > 0)) return undefined;\n\n\t\treturn step > 0 ? Math.max(1, Math.round(trimFloat(keyStep / step))) * step : keyStep;\n\t}\n\n\tfunction end(size: number): number {\n\t\tif (!spec.isWrapping) return bounds[1];\n\n\t\treturn min + (isStepped ? top : (Math.ceil(range / size) - 1) * size);\n\t}\n\n\tfunction target(key: string, from: number, keyStep: number): number | undefined {\n\t\tif (key === 'Home') return bounds[0];\n\n\t\tconst size = step > 0 ? step : range / 100;\n\t\tif (key === 'End') return snap(end(size));\n\n\t\tconst steps = keySteps.get(key);\n\t\tif (steps === undefined) return undefined;\n\n\t\tconst stride = keyStride(keyStep);\n\t\tconst moved = stride === undefined ? stepBy(from, steps, size) : from + steps * stride;\n\t\tconst next = stopAtDetent(from, moved);\n\n\t\treturn isStepped && spec.isWrapping ? ring(next, count * step) : snap(next);\n\t}\n\n\treturn {\n\t\tbounds,\n\t\tisWrapping: spec.isWrapping,\n\t\tkeyTarget: (key, from, keyStep = 0) => target(key, from, keyStep),\n\t\tpositionCount: countPositions(spec, positions?.length ?? count),\n\t\tproportionOf: (value) => taper.proportionOf(bound(value)),\n\t\tsnap,\n\t\tvalueAt: (proportion) => taper.valueAt(proportion),\n\t};\n}\n"],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ //#region src/lib/waveform-buckets.d.ts
2
+ export interface WaveformPeaks {
3
+ channels?: number;
4
+ fullScale: number;
5
+ pairsPerSecond: number;
6
+ samples: Float32Array | Int8Array | Int16Array;
7
+ }
8
+ //#endregion
9
+ //# sourceMappingURL=waveform-buckets.d.ts.map