@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,114 @@
1
+ import { roundTo } from "./math.js";
2
+ import { envelopeCurve } from "./envelope-curve.js";
3
+
4
+ //#region src/lib/adsr-shape.ts
5
+ const adsrStages = [
6
+ "delay",
7
+ "attack",
8
+ "hold",
9
+ "decay",
10
+ "sustain",
11
+ "release"
12
+ ];
13
+ const timeStages = [
14
+ "delay",
15
+ "attack",
16
+ "hold",
17
+ "decay",
18
+ "release"
19
+ ];
20
+ const curveSamples = 24;
21
+ function along(from, to, at) {
22
+ return {
23
+ x: from.x + (to.x - from.x) * at.position,
24
+ y: from.y + (to.y - from.y) * envelopeCurve(at.position, at.curve)
25
+ };
26
+ }
27
+ function curved(from, to, curve) {
28
+ if (envelopeCurve(.5, curve) === .5) return [];
29
+ return Array.from({ length: 23 }, (_, index) => along(from, to, {
30
+ curve,
31
+ position: (index + 1) / curveSamples
32
+ }));
33
+ }
34
+ function leg(from, to, curve) {
35
+ if (curve === void 0) return {
36
+ curveHandle: void 0,
37
+ points: [to]
38
+ };
39
+ return {
40
+ curveHandle: along(from, to, {
41
+ curve,
42
+ position: .5
43
+ }),
44
+ points: [...curved(from, to, curve), to]
45
+ };
46
+ }
47
+ function isHandled(proportions, stage) {
48
+ return (stage === "decay" ? proportions.sustain ?? proportions.decay : proportions[stage]) !== void 0;
49
+ }
50
+ function adsrShape(proportions, curves = {}) {
51
+ const sustain = proportions.sustain ?? 1;
52
+ const levels = {
53
+ attack: 1,
54
+ decay: sustain,
55
+ delay: 0,
56
+ hold: 1,
57
+ release: 0
58
+ };
59
+ const share = 1 / (timeStages.filter((stage) => proportions[stage] !== void 0).length + 1);
60
+ const shape = {
61
+ curveHandles: [],
62
+ handles: [],
63
+ points: [{
64
+ x: 0,
65
+ y: 0
66
+ }],
67
+ share
68
+ };
69
+ let from = {
70
+ x: 0,
71
+ y: 0
72
+ };
73
+ for (const stage of timeStages) {
74
+ if (stage === "release") {
75
+ from = {
76
+ x: from.x + share,
77
+ y: sustain
78
+ };
79
+ shape.points.push(from);
80
+ }
81
+ const to = {
82
+ x: from.x + (proportions[stage] ?? 0) * share,
83
+ y: levels[stage]
84
+ };
85
+ const { curveHandle, points } = leg(from, to, curves[stage]);
86
+ shape.points.push(...points);
87
+ if (curveHandle) shape.curveHandles.push({
88
+ ...curveHandle,
89
+ stage
90
+ });
91
+ if (isHandled(proportions, stage)) shape.handles.push({
92
+ ...to,
93
+ stage
94
+ });
95
+ from = to;
96
+ }
97
+ return shape;
98
+ }
99
+ function adsrPath({ points }) {
100
+ const stroke = points.map(({ x, y }, index) => `${index === 0 ? "M" : "L"}${String(roundTo(x, 4))} ${String(roundTo(1 - y, 4))}`).join("");
101
+ const floor = {
102
+ from: roundTo(points[0]?.x ?? 0, 4),
103
+ to: roundTo(points.at(-1)?.x ?? 0, 4)
104
+ };
105
+ return {
106
+ fill: `${stroke}Z`,
107
+ floor,
108
+ stroke
109
+ };
110
+ }
111
+
112
+ //#endregion
113
+ export { adsrPath, adsrShape, adsrStages, timeStages };
114
+ //# sourceMappingURL=adsr-shape.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adsr-shape.js","names":[],"sources":["../../../src/lib/adsr-shape.ts"],"sourcesContent":["import { envelopeCurve } from '#lib/envelope-curve.ts';\nimport { roundTo } from '#lib/math.ts';\n\nexport const adsrStages = ['delay', 'attack', 'hold', 'decay', 'sustain', 'release'] as const;\n\nexport const timeStages = ['delay', 'attack', 'hold', 'decay', 'release'] as const;\n\ntype AdsrStage = (typeof adsrStages)[number];\n\ntype TimeStage = (typeof timeStages)[number];\n\nexport type AdsrProportions = Partial<Record<AdsrStage, number | undefined>>;\n\nexport type AdsrCurves = Partial<Record<TimeStage, number | undefined>>;\n\ninterface AdsrPoint {\n\tx: number;\n\ty: number;\n}\n\ninterface AdsrHandle extends AdsrPoint {\n\tstage: TimeStage;\n}\n\nexport interface AdsrShape {\n\tcurveHandles: Array<AdsrHandle>;\n\thandles: Array<AdsrHandle>;\n\tpoints: Array<AdsrPoint>;\n\tshare: number;\n}\n\nconst curveSamples = 24;\n\nfunction along(from: AdsrPoint, to: AdsrPoint, at: { curve: number; position: number }): AdsrPoint {\n\treturn {\n\t\tx: from.x + (to.x - from.x) * at.position,\n\t\ty: from.y + (to.y - from.y) * envelopeCurve(at.position, at.curve),\n\t};\n}\n\nfunction curved(from: AdsrPoint, to: AdsrPoint, curve: number): Array<AdsrPoint> {\n\tif (envelopeCurve(0.5, curve) === 0.5) return [];\n\n\treturn Array.from({ length: curveSamples - 1 }, (_, index) =>\n\t\talong(from, to, { curve, position: (index + 1) / curveSamples }),\n\t);\n}\n\nfunction leg(\n\tfrom: AdsrPoint,\n\tto: AdsrPoint,\n\tcurve: number | undefined,\n): { curveHandle: AdsrPoint | undefined; points: Array<AdsrPoint> } {\n\tif (curve === undefined) return { curveHandle: undefined, points: [to] };\n\n\treturn {\n\t\tcurveHandle: along(from, to, { curve, position: 0.5 }),\n\t\tpoints: [...curved(from, to, curve), to],\n\t};\n}\n\nfunction isHandled(proportions: AdsrProportions, stage: TimeStage): boolean {\n\tconst proportion =\n\t\tstage === 'decay' ? (proportions.sustain ?? proportions.decay) : proportions[stage];\n\n\treturn proportion !== undefined;\n}\n\nexport function adsrShape(proportions: AdsrProportions, curves: AdsrCurves = {}): AdsrShape {\n\tconst sustain = proportions.sustain ?? 1;\n\tconst levels = { attack: 1, decay: sustain, delay: 0, hold: 1, release: 0 };\n\tconst share = 1 / (timeStages.filter((stage) => proportions[stage] !== undefined).length + 1);\n\tconst shape: AdsrShape = { curveHandles: [], handles: [], points: [{ x: 0, y: 0 }], share };\n\tlet from = { x: 0, y: 0 };\n\n\tfor (const stage of timeStages) {\n\t\tif (stage === 'release') {\n\t\t\tfrom = { x: from.x + share, y: sustain };\n\t\t\tshape.points.push(from);\n\t\t}\n\n\t\tconst to = { x: from.x + (proportions[stage] ?? 0) * share, y: levels[stage] };\n\t\tconst { curveHandle, points } = leg(from, to, curves[stage]);\n\n\t\tshape.points.push(...points);\n\t\tif (curveHandle) shape.curveHandles.push({ ...curveHandle, stage });\n\t\tif (isHandled(proportions, stage)) shape.handles.push({ ...to, stage });\n\t\tfrom = to;\n\t}\n\n\treturn shape;\n}\n\nexport function adsrPath({ points }: AdsrShape): {\n\tfill: string;\n\tfloor: { from: number; to: number };\n\tstroke: string;\n} {\n\tconst stroke = points\n\t\t.map(\n\t\t\t({ x, y }, index) =>\n\t\t\t\t`${index === 0 ? 'M' : 'L'}${String(roundTo(x, 4))} ${String(roundTo(1 - y, 4))}`,\n\t\t)\n\t\t.join('');\n\n\tconst floor = {\n\t\tfrom: roundTo(points[0]?.x ?? 0, 4),\n\t\tto: roundTo(points.at(-1)?.x ?? 0, 4),\n\t};\n\n\treturn { fill: `${stroke}Z`, floor, stroke };\n}\n"],"mappings":";;;;AAGA,MAAa,aAAa;CAAC;CAAS;CAAU;CAAQ;CAAS;CAAW;AAAS;AAEnF,MAAa,aAAa;CAAC;CAAS;CAAU;CAAQ;CAAS;AAAS;AA0BxE,MAAM,eAAe;AAErB,SAAS,MAAM,MAAiB,IAAe,IAAoD;CAClG,OAAO;EACN,GAAG,KAAK,KAAK,GAAG,IAAI,KAAK,KAAK,GAAG;EACjC,GAAG,KAAK,KAAK,GAAG,IAAI,KAAK,KAAK,cAAc,GAAG,UAAU,GAAG,KAAK;CAClE;AACD;AAEA,SAAS,OAAO,MAAiB,IAAe,OAAiC;CAChF,IAAI,cAAc,IAAK,KAAK,MAAM,IAAK,OAAO,CAAC;CAE/C,OAAO,MAAM,KAAK,EAAE,QAAQ,GAAiB,IAAI,GAAG,UACnD,MAAM,MAAM,IAAI;EAAE;EAAO,WAAW,QAAQ,KAAK;CAAa,CAAC,CAChE;AACD;AAEA,SAAS,IACR,MACA,IACA,OACmE;CACnE,IAAI,UAAU,QAAW,OAAO;EAAE,aAAa;EAAW,QAAQ,CAAC,EAAE;CAAE;CAEvE,OAAO;EACN,aAAa,MAAM,MAAM,IAAI;GAAE;GAAO,UAAU;EAAI,CAAC;EACrD,QAAQ,CAAC,GAAG,OAAO,MAAM,IAAI,KAAK,GAAG,EAAE;CACxC;AACD;AAEA,SAAS,UAAU,aAA8B,OAA2B;CAI3E,QAFC,UAAU,UAAW,YAAY,WAAW,YAAY,QAAS,YAAY,YAExD;AACvB;AAEA,SAAgB,UAAU,aAA8B,SAAqB,CAAC,GAAc;CAC3F,MAAM,UAAU,YAAY,WAAW;CACvC,MAAM,SAAS;EAAE,QAAQ;EAAG,OAAO;EAAS,OAAO;EAAG,MAAM;EAAG,SAAS;CAAE;CAC1E,MAAM,QAAQ,KAAK,WAAW,QAAQ,UAAU,YAAY,WAAW,MAAS,CAAC,CAAC,SAAS;CAC3F,MAAM,QAAmB;EAAE,cAAc,CAAC;EAAG,SAAS,CAAC;EAAG,QAAQ,CAAC;GAAE,GAAG;GAAG,GAAG;EAAE,CAAC;EAAG;CAAM;CAC1F,IAAI,OAAO;EAAE,GAAG;EAAG,GAAG;CAAE;CAExB,KAAK,MAAM,SAAS,YAAY;EAC/B,IAAI,UAAU,WAAW;GACxB,OAAO;IAAE,GAAG,KAAK,IAAI;IAAO,GAAG;GAAQ;GACvC,MAAM,OAAO,KAAK,IAAI;EACvB;EAEA,MAAM,KAAK;GAAE,GAAG,KAAK,KAAK,YAAY,UAAU,KAAK;GAAO,GAAG,OAAO;EAAO;EAC7E,MAAM,EAAE,aAAa,WAAW,IAAI,MAAM,IAAI,OAAO,MAAM;EAE3D,MAAM,OAAO,KAAK,GAAG,MAAM;EAC3B,IAAI,aAAa,MAAM,aAAa,KAAK;GAAE,GAAG;GAAa;EAAM,CAAC;EAClE,IAAI,UAAU,aAAa,KAAK,GAAG,MAAM,QAAQ,KAAK;GAAE,GAAG;GAAI;EAAM,CAAC;EACtE,OAAO;CACR;CAEA,OAAO;AACR;AAEA,SAAgB,SAAS,EAAE,UAIzB;CACD,MAAM,SAAS,OACb,KACC,EAAE,GAAG,KAAK,UACV,GAAG,UAAU,IAAI,MAAM,MAAM,OAAO,QAAQ,GAAG,CAAC,CAAC,EAAE,GAAG,OAAO,QAAQ,IAAI,GAAG,CAAC,CAAC,GAChF,CAAC,CACA,KAAK,EAAE;CAET,MAAM,QAAQ;EACb,MAAM,QAAQ,OAAO,EAAE,EAAE,KAAK,GAAG,CAAC;EAClC,IAAI,QAAQ,OAAO,GAAG,EAAE,CAAC,EAAE,KAAK,GAAG,CAAC;CACrC;CAEA,OAAO;EAAE,MAAM,GAAG,OAAO;EAAI;EAAO;CAAO;AAC5C"}
@@ -0,0 +1,56 @@
1
+ //#region src/lib/ballistics.ts
2
+ const burstTimeConstants = -Math.log(1 - 10 ** (-2 / 20));
3
+ function ppmRate({ burstMs, fallDecibels, fallSeconds }) {
4
+ return {
5
+ fallDecibelsPerSecond: fallDecibels / fallSeconds,
6
+ riseTimeConstantMs: burstMs / burstTimeConstants
7
+ };
8
+ }
9
+ const ballisticsRates = {
10
+ peak: {
11
+ fallDecibelsPerSecond: 20,
12
+ riseTimeConstantMs: 0
13
+ },
14
+ "ppm-1": ppmRate({
15
+ burstMs: 5,
16
+ fallDecibels: 20,
17
+ fallSeconds: 1.7
18
+ }),
19
+ "ppm-2": ppmRate({
20
+ burstMs: 10,
21
+ fallDecibels: 24,
22
+ fallSeconds: 2.8
23
+ })
24
+ };
25
+ const needleDamping = .8;
26
+ const needleRadiansPerSecond = 13.5;
27
+ const needleStepMs = 4;
28
+ const needleSettleMs = 1e3;
29
+ function fall(current, { fallDecibelsPerSecond, floor }, elapsedMs) {
30
+ return Math.max(floor, current - fallDecibelsPerSecond * elapsedMs / 1e3);
31
+ }
32
+ function rise(current, { target, timeConstantMs }, elapsedMs) {
33
+ if (timeConstantMs <= 0) return target;
34
+ return target + (current - target) * Math.exp(-elapsedMs / timeConstantMs);
35
+ }
36
+ function stepNeedle(needle, target, elapsedMs) {
37
+ const dueMs = Math.min(needle.carryMs + elapsedMs, needleSettleMs);
38
+ const steps = Math.floor(dueMs / needleStepMs);
39
+ const seconds = needleStepMs / 1e3;
40
+ let { position, velocity } = needle;
41
+ for (let step = 0; step < steps; step += 1) {
42
+ const pull = needleRadiansPerSecond ** 2 * (target - position);
43
+ const drag = 2 * needleDamping * needleRadiansPerSecond * velocity;
44
+ velocity += (pull - drag) * seconds;
45
+ position += velocity * seconds;
46
+ }
47
+ return {
48
+ carryMs: dueMs - steps * needleStepMs,
49
+ position,
50
+ velocity
51
+ };
52
+ }
53
+
54
+ //#endregion
55
+ export { ballisticsRates, fall, rise, stepNeedle };
56
+ //# sourceMappingURL=ballistics.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ballistics.js","names":[],"sources":["../../../src/lib/ballistics.ts"],"sourcesContent":["interface Needle {\n\tcarryMs: number;\n\tposition: number;\n\tvelocity: number;\n}\n\ninterface BallisticsRate {\n\tfallDecibelsPerSecond: number;\n\triseTimeConstantMs: number;\n}\n\ninterface PpmSpec {\n\tburstMs: number;\n\tfallDecibels: number;\n\tfallSeconds: number;\n}\n\n// IEC 60268-10: a tone burst as long as the integration time reads this far under a steady tone\nconst burstShortfallDecibels = 2;\nconst burstTimeConstants = -Math.log(1 - 10 ** (-burstShortfallDecibels / 20));\n\nfunction ppmRate({ burstMs, fallDecibels, fallSeconds }: PpmSpec): BallisticsRate {\n\treturn {\n\t\tfallDecibelsPerSecond: fallDecibels / fallSeconds,\n\t\triseTimeConstantMs: burstMs / burstTimeConstants,\n\t};\n}\n\n// The PPM figures are IEC 60268-10's Type I and Type II\nexport const ballisticsRates = {\n\tpeak: { fallDecibelsPerSecond: 20, riseTimeConstantMs: 0 },\n\t'ppm-1': ppmRate({ burstMs: 5, fallDecibels: 20, fallSeconds: 1.7 }),\n\t'ppm-2': ppmRate({ burstMs: 10, fallDecibels: 24, fallSeconds: 2.8 }),\n} as const satisfies Record<string, BallisticsRate>;\n\n// Tuned to a VU's step response: 99% in 300ms with 1 to 1.5% overshoot\nconst needleDamping = 0.8;\nconst needleRadiansPerSecond = 13.5;\n\nconst needleStepMs = 4;\nconst needleSettleMs = 1000;\n\n// A meter's return is specified linear in dB\nexport function fall(\n\tcurrent: number,\n\t{ fallDecibelsPerSecond, floor }: { fallDecibelsPerSecond: number; floor: number },\n\telapsedMs: number,\n): number {\n\treturn Math.max(floor, current - (fallDecibelsPerSecond * elapsedMs) / 1000);\n}\n\nexport function rise(\n\tcurrent: number,\n\t{ target, timeConstantMs }: { target: number; timeConstantMs: number },\n\telapsedMs: number,\n): number {\n\tif (timeConstantMs <= 0) return target;\n\n\treturn target + (current - target) * Math.exp(-elapsedMs / timeConstantMs);\n}\n\nexport function stepNeedle(needle: Needle, target: number, elapsedMs: number): Needle {\n\tconst dueMs = Math.min(needle.carryMs + elapsedMs, needleSettleMs);\n\tconst steps = Math.floor(dueMs / needleStepMs);\n\tconst seconds = needleStepMs / 1000;\n\tlet { position, velocity } = needle;\n\n\tfor (let step = 0; step < steps; step += 1) {\n\t\tconst pull = needleRadiansPerSecond ** 2 * (target - position);\n\t\tconst drag = 2 * needleDamping * needleRadiansPerSecond * velocity;\n\n\t\tvelocity += (pull - drag) * seconds;\n\t\tposition += velocity * seconds;\n\t}\n\n\treturn { carryMs: dueMs - steps * needleStepMs, position, velocity };\n}\n"],"mappings":";AAmBA,MAAM,qBAAqB,CAAC,KAAK,IAAI,IAAI,OAAO,KAA0B,GAAG;AAE7E,SAAS,QAAQ,EAAE,SAAS,cAAc,eAAwC;CACjF,OAAO;EACN,uBAAuB,eAAe;EACtC,oBAAoB,UAAU;CAC/B;AACD;AAGA,MAAa,kBAAkB;CAC9B,MAAM;EAAE,uBAAuB;EAAI,oBAAoB;CAAE;CACzD,SAAS,QAAQ;EAAE,SAAS;EAAG,cAAc;EAAI,aAAa;CAAI,CAAC;CACnE,SAAS,QAAQ;EAAE,SAAS;EAAI,cAAc;EAAI,aAAa;CAAI,CAAC;AACrE;AAGA,MAAM,gBAAgB;AACtB,MAAM,yBAAyB;AAE/B,MAAM,eAAe;AACrB,MAAM,iBAAiB;AAGvB,SAAgB,KACf,SACA,EAAE,uBAAuB,SACzB,WACS;CACT,OAAO,KAAK,IAAI,OAAO,UAAW,wBAAwB,YAAa,GAAI;AAC5E;AAEA,SAAgB,KACf,SACA,EAAE,QAAQ,kBACV,WACS;CACT,IAAI,kBAAkB,GAAG,OAAO;CAEhC,OAAO,UAAU,UAAU,UAAU,KAAK,IAAI,CAAC,YAAY,cAAc;AAC1E;AAEA,SAAgB,WAAW,QAAgB,QAAgB,WAA2B;CACrF,MAAM,QAAQ,KAAK,IAAI,OAAO,UAAU,WAAW,cAAc;CACjE,MAAM,QAAQ,KAAK,MAAM,QAAQ,YAAY;CAC7C,MAAM,UAAU,eAAe;CAC/B,IAAI,EAAE,UAAU,aAAa;CAE7B,KAAK,IAAI,OAAO,GAAG,OAAO,OAAO,QAAQ,GAAG;EAC3C,MAAM,OAAO,0BAA0B,KAAK,SAAS;EACrD,MAAM,OAAO,IAAI,gBAAgB,yBAAyB;EAE1D,aAAa,OAAO,QAAQ;EAC5B,YAAY,WAAW;CACxB;CAEA,OAAO;EAAE,SAAS,QAAQ,QAAQ;EAAc;EAAU;CAAS;AACpE"}
@@ -0,0 +1,26 @@
1
+ //#region src/lib/canvas-surface.d.ts
2
+ export interface SurfaceSize {
3
+ dpr: number;
4
+ height: number;
5
+ width: number;
6
+ }
7
+ export interface SurfaceLook<Colour extends string, Length extends string = never> {
8
+ colours: Record<Colour, string>;
9
+ isReducedMotion: boolean;
10
+ lengths: Record<Length, number>;
11
+ }
12
+ export interface SurfaceFrame<Colour extends string, Length extends string = never> {
13
+ frameMs: number;
14
+ isDirty: boolean;
15
+ isRebuilt: boolean;
16
+ look: SurfaceLook<Colour, Length>;
17
+ size: SurfaceSize;
18
+ }
19
+ export interface Surface {
20
+ invalidate: () => void;
21
+ rebuild: () => void;
22
+ requestFrame: () => void;
23
+ readonly size: SurfaceSize;
24
+ }
25
+ //#endregion
26
+ //# sourceMappingURL=canvas-surface.d.ts.map
@@ -0,0 +1,204 @@
1
+ import { readPxProperty } from "./read-px-property.js";
2
+
3
+ //#region src/lib/canvas-surface.ts
4
+ function deviceRatio() {
5
+ return globalThis.devicePixelRatio || 1;
6
+ }
7
+ function deviceBox(entry, css) {
8
+ const device = "devicePixelContentBoxSize" in entry ? entry.devicePixelContentBoxSize[0] : void 0;
9
+ const dpr = deviceRatio();
10
+ if (!device || Math.abs(device.inlineSize - css.inlineSize * dpr) > 1) return void 0;
11
+ return Math.abs(device.blockSize - css.blockSize * dpr) > 1 ? void 0 : device;
12
+ }
13
+ function readEach(properties, read) {
14
+ return Object.fromEntries(Object.entries(properties).flatMap(([name, property]) => property === void 0 ? [] : [[name, read(property)]]));
15
+ }
16
+ var CanvasSurface = class {
17
+ get size() {
18
+ return this.#size;
19
+ }
20
+ #context;
21
+ #cssBox;
22
+ #frame;
23
+ #isDirty = true;
24
+ #isRebuildWanted = true;
25
+ #isVisible = true;
26
+ #look;
27
+ #options;
28
+ #reducedMotion = matchMedia("(prefers-reduced-motion: reduce)");
29
+ #size = {
30
+ dpr: deviceRatio(),
31
+ height: 0,
32
+ width: 0
33
+ };
34
+ constructor(options) {
35
+ const { canvas, signal } = options;
36
+ this.#options = options;
37
+ this.#context = canvas.getContext("2d") ?? void 0;
38
+ this.#armTransitions();
39
+ this.#observeSize();
40
+ this.#observeView();
41
+ this.#watchResolution();
42
+ this.#reducedMotion.addEventListener("change", () => {
43
+ this.rebuild();
44
+ }, { signal });
45
+ canvas.addEventListener("transitionrun", () => {
46
+ this.rebuild();
47
+ }, { signal });
48
+ signal.addEventListener("abort", () => {
49
+ if (this.#frame !== void 0) cancelAnimationFrame(this.#frame);
50
+ this.#frame = void 0;
51
+ }, { once: true });
52
+ this.#schedule();
53
+ }
54
+ invalidate() {
55
+ this.#isDirty = true;
56
+ this.#schedule();
57
+ }
58
+ rebuild() {
59
+ this.#isRebuildWanted = true;
60
+ this.invalidate();
61
+ }
62
+ requestFrame() {
63
+ this.#schedule();
64
+ }
65
+ #armTransitions() {
66
+ const { style } = this.#options.canvas;
67
+ style.setProperty("transition-property", Object.values(this.#options.colours).join(", "));
68
+ style.setProperty("transition-duration", "0.001ms");
69
+ style.setProperty("transition-timing-function", "step-start");
70
+ style.setProperty("transition-behavior", "allow-discrete");
71
+ }
72
+ #current() {
73
+ if (this.#look && !this.#isRebuildWanted) return {
74
+ isRebuilt: false,
75
+ look: this.#look
76
+ };
77
+ return {
78
+ isRebuilt: true,
79
+ look: this.#read()
80
+ };
81
+ }
82
+ #draw(frameMs) {
83
+ this.#frame = void 0;
84
+ const size = this.#size;
85
+ if (size.width === 0 || size.height === 0) return;
86
+ const { canvas, paint } = this.#options;
87
+ const { isRebuilt, look } = this.#current();
88
+ const isDirty = this.#isDirty;
89
+ this.#look = look;
90
+ this.#isDirty = false;
91
+ this.#isRebuildWanted = false;
92
+ if (canvas.width !== size.width) canvas.width = size.width;
93
+ if (canvas.height !== size.height) canvas.height = size.height;
94
+ if (this.#context) paint(this.#context, {
95
+ frameMs,
96
+ isDirty,
97
+ isRebuilt,
98
+ look,
99
+ size
100
+ });
101
+ }
102
+ #measure(entry) {
103
+ const [box] = entry.contentBoxSize;
104
+ if (!box) return;
105
+ const device = deviceBox(entry, box);
106
+ if (device) {
107
+ this.#cssBox = void 0;
108
+ this.#resize(device.inlineSize, device.blockSize);
109
+ return;
110
+ }
111
+ this.#cssBox = {
112
+ height: box.blockSize,
113
+ width: box.inlineSize
114
+ };
115
+ this.#rescale();
116
+ }
117
+ #observeSize() {
118
+ const { canvas, signal } = this.#options;
119
+ const observer = new ResizeObserver((entries) => {
120
+ const entry = entries.at(-1);
121
+ if (entry) this.#measure(entry);
122
+ });
123
+ try {
124
+ observer.observe(canvas, { box: "device-pixel-content-box" });
125
+ } catch {
126
+ observer.observe(canvas, { box: "content-box" });
127
+ }
128
+ signal.addEventListener("abort", () => {
129
+ observer.disconnect();
130
+ }, { once: true });
131
+ }
132
+ #observeView() {
133
+ const { canvas, signal } = this.#options;
134
+ const observer = new IntersectionObserver((entries) => {
135
+ const entry = entries.at(-1);
136
+ if (!entry) return;
137
+ const wasVisible = this.#isVisible;
138
+ this.#isVisible = entry.isIntersecting;
139
+ if (!entry.isIntersecting && this.#frame !== void 0) {
140
+ cancelAnimationFrame(this.#frame);
141
+ this.#frame = void 0;
142
+ }
143
+ if (!wasVisible && entry.isIntersecting) this.rebuild();
144
+ });
145
+ observer.observe(canvas);
146
+ signal.addEventListener("abort", () => {
147
+ observer.disconnect();
148
+ }, { once: true });
149
+ }
150
+ #read() {
151
+ const { canvas, colours, lengths = {} } = this.#options;
152
+ const styles = getComputedStyle(canvas);
153
+ return {
154
+ colours: readEach(colours, (property) => styles.getPropertyValue(property).trim().replaceAll("currentcolor", () => styles.color)),
155
+ isReducedMotion: this.#reducedMotion.matches,
156
+ lengths: readEach(lengths, (property) => readPxProperty(styles, property, 0))
157
+ };
158
+ }
159
+ #rescale() {
160
+ const box = this.#cssBox;
161
+ if (!box) {
162
+ this.#resize(this.#size.width, this.#size.height);
163
+ return;
164
+ }
165
+ const dpr = deviceRatio();
166
+ this.#resize(Math.round(box.width * dpr), Math.round(box.height * dpr));
167
+ }
168
+ #resize(width, height) {
169
+ const previous = this.#size;
170
+ const dpr = deviceRatio();
171
+ if (previous.width === width && previous.height === height && previous.dpr === dpr) return;
172
+ this.#size = {
173
+ dpr,
174
+ height,
175
+ width
176
+ };
177
+ this.#options.resize?.(this.#size);
178
+ if (previous.width === 0 || previous.height === 0) this.rebuild();
179
+ else this.invalidate();
180
+ }
181
+ #schedule() {
182
+ if (this.#options.signal.aborted || this.#frame !== void 0 || !this.#isVisible) return;
183
+ this.#frame = requestAnimationFrame((frameMs) => {
184
+ this.#draw(frameMs);
185
+ });
186
+ }
187
+ #watchResolution() {
188
+ const { signal } = this.#options;
189
+ matchMedia(`(resolution: ${String(deviceRatio())}dppx)`).addEventListener("change", () => {
190
+ this.#watchResolution();
191
+ this.#rescale();
192
+ }, {
193
+ once: true,
194
+ signal
195
+ });
196
+ }
197
+ };
198
+ function bindSurface(options) {
199
+ return new CanvasSurface(options);
200
+ }
201
+
202
+ //#endregion
203
+ export { bindSurface };
204
+ //# sourceMappingURL=canvas-surface.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canvas-surface.js","names":["named"],"sources":["../../../src/lib/canvas-surface.ts"],"sourcesContent":["import { readPxProperty } from '#lib/read-px-property.ts';\n\ntype PrivateProperty = `--_sonic-${string}`;\n\nexport interface SurfaceSize {\n\tdpr: number;\n\theight: number;\n\twidth: number;\n}\n\nexport interface SurfaceLook<Colour extends string, Length extends string = never> {\n\tcolours: Record<Colour, string>;\n\tisReducedMotion: boolean;\n\tlengths: Record<Length, number>;\n}\n\nexport interface SurfaceFrame<Colour extends string, Length extends string = never> {\n\tframeMs: number;\n\tisDirty: boolean;\n\tisRebuilt: boolean;\n\tlook: SurfaceLook<Colour, Length>;\n\tsize: SurfaceSize;\n}\n\nexport interface Surface {\n\tinvalidate: () => void;\n\trebuild: () => void;\n\trequestFrame: () => void;\n\treadonly size: SurfaceSize;\n}\n\ninterface SurfaceOptions<Colour extends string, Length extends string> {\n\tcanvas: HTMLCanvasElement;\n\tcolours: Record<Colour, PrivateProperty>;\n\tlengths?: Record<Length, PrivateProperty>;\n\tpaint: (context: CanvasRenderingContext2D, frame: SurfaceFrame<Colour, Length>) => void;\n\tresize?: (size: SurfaceSize) => void;\n\tsignal: AbortSignal;\n}\n\nfunction deviceRatio(): number {\n\treturn globalThis.devicePixelRatio || 1;\n}\n\n// WebKit reports none, and an emulated device scale reports it in CSS px, so it counts only where it agrees\nfunction deviceBox(\n\tentry: ResizeObserverEntry,\n\tcss: ResizeObserverSize,\n): ResizeObserverSize | undefined {\n\tconst device =\n\t\t'devicePixelContentBoxSize' in entry ? entry.devicePixelContentBoxSize[0] : undefined;\n\tconst dpr = deviceRatio();\n\tif (!device || Math.abs(device.inlineSize - css.inlineSize * dpr) > 1) return undefined;\n\n\treturn Math.abs(device.blockSize - css.blockSize * dpr) > 1 ? undefined : device;\n}\n\n// `Object.fromEntries` widens the keys to `string`\nfunction readEach<Name extends string, Value>(\n\tproperties: Partial<Record<Name, PrivateProperty>>,\n\tread: (property: PrivateProperty) => Value,\n): Record<Name, Value> {\n\tconst named: Readonly<Record<string, PrivateProperty | undefined>> = properties;\n\n\treturn Object.fromEntries(\n\t\tObject.entries(named).flatMap(([name, property]) =>\n\t\t\tproperty === undefined ? [] : [[name, read(property)]],\n\t\t),\n\t) as Record<Name, Value>;\n}\n\nclass CanvasSurface<Colour extends string, Length extends string> implements Surface {\n\tget size(): SurfaceSize {\n\t\treturn this.#size;\n\t}\n\n\t// happy-dom has no canvas context\n\treadonly #context: CanvasRenderingContext2D | undefined;\n\n\t#cssBox: undefined | { height: number; width: number };\n\n\t#frame: number | undefined;\n\n\t#isDirty = true;\n\n\t#isRebuildWanted = true;\n\n\t#isVisible = true;\n\n\t#look: SurfaceLook<Colour, Length> | undefined;\n\n\treadonly #options: SurfaceOptions<Colour, Length>;\n\n\treadonly #reducedMotion = matchMedia('(prefers-reduced-motion: reduce)');\n\n\t#size: SurfaceSize = { dpr: deviceRatio(), height: 0, width: 0 };\n\n\tconstructor(options: SurfaceOptions<Colour, Length>) {\n\t\tconst { canvas, signal } = options;\n\n\t\tthis.#options = options;\n\t\tthis.#context = canvas.getContext('2d') ?? undefined;\n\t\tthis.#armTransitions();\n\t\tthis.#observeSize();\n\t\tthis.#observeView();\n\t\tthis.#watchResolution();\n\t\tthis.#reducedMotion.addEventListener(\n\t\t\t'change',\n\t\t\t() => {\n\t\t\t\tthis.rebuild();\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t\t// `transitionstart` drops events when a change replaces a pending transition in WebKit\n\t\tcanvas.addEventListener(\n\t\t\t'transitionrun',\n\t\t\t() => {\n\t\t\t\tthis.rebuild();\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t\tsignal.addEventListener(\n\t\t\t'abort',\n\t\t\t() => {\n\t\t\t\tif (this.#frame !== undefined) cancelAnimationFrame(this.#frame);\n\t\t\t\tthis.#frame = undefined;\n\t\t\t},\n\t\t\t{ once: true },\n\t\t);\n\t\tthis.#schedule();\n\t}\n\n\tinvalidate(): void {\n\t\tthis.#isDirty = true;\n\t\tthis.#schedule();\n\t}\n\n\trebuild(): void {\n\t\tthis.#isRebuildWanted = true;\n\t\tthis.invalidate();\n\t}\n\n\trequestFrame(): void {\n\t\tthis.#schedule();\n\t}\n\n\t#armTransitions(): void {\n\t\tconst { style } = this.#options.canvas;\n\n\t\tstyle.setProperty('transition-property', Object.values(this.#options.colours).join(', '));\n\t\tstyle.setProperty('transition-duration', '0.001ms');\n\t\tstyle.setProperty('transition-timing-function', 'step-start');\n\t\tstyle.setProperty('transition-behavior', 'allow-discrete');\n\t}\n\n\t#current(): { isRebuilt: boolean; look: SurfaceLook<Colour, Length> } {\n\t\tif (this.#look && !this.#isRebuildWanted) return { isRebuilt: false, look: this.#look };\n\n\t\treturn { isRebuilt: true, look: this.#read() };\n\t}\n\n\t#draw(frameMs: number): void {\n\t\tthis.#frame = undefined;\n\n\t\tconst size = this.#size;\n\t\tif (size.width === 0 || size.height === 0) return;\n\n\t\tconst { canvas, paint } = this.#options;\n\t\tconst { isRebuilt, look } = this.#current();\n\t\tconst isDirty = this.#isDirty;\n\n\t\tthis.#look = look;\n\t\tthis.#isDirty = false;\n\t\tthis.#isRebuildWanted = false;\n\t\t// Assigning either clears the canvas and its state\n\t\tif (canvas.width !== size.width) canvas.width = size.width;\n\t\tif (canvas.height !== size.height) canvas.height = size.height;\n\t\tif (this.#context) paint(this.#context, { frameMs, isDirty, isRebuilt, look, size });\n\t}\n\n\t#measure(entry: ResizeObserverEntry): void {\n\t\tconst [box] = entry.contentBoxSize;\n\t\tif (!box) return;\n\n\t\tconst device = deviceBox(entry, box);\n\n\t\tif (device) {\n\t\t\tthis.#cssBox = undefined;\n\t\t\tthis.#resize(device.inlineSize, device.blockSize);\n\t\t\treturn;\n\t\t}\n\n\t\tthis.#cssBox = { height: box.blockSize, width: box.inlineSize };\n\t\tthis.#rescale();\n\t}\n\n\t#observeSize(): void {\n\t\tconst { canvas, signal } = this.#options;\n\t\tconst observer = new ResizeObserver((entries) => {\n\t\t\tconst entry = entries.at(-1);\n\n\t\t\tif (entry) this.#measure(entry);\n\t\t});\n\n\t\ttry {\n\t\t\tobserver.observe(canvas, { box: 'device-pixel-content-box' });\n\t\t} catch {\n\t\t\t// WebKit throws a TypeError on the option\n\t\t\tobserver.observe(canvas, { box: 'content-box' });\n\t\t}\n\t\tsignal.addEventListener(\n\t\t\t'abort',\n\t\t\t() => {\n\t\t\t\tobserver.disconnect();\n\t\t\t},\n\t\t\t{ once: true },\n\t\t);\n\t}\n\n\t#observeView(): void {\n\t\tconst { canvas, signal } = this.#options;\n\t\tconst observer = new IntersectionObserver((entries) => {\n\t\t\tconst entry = entries.at(-1);\n\t\t\tif (!entry) return;\n\n\t\t\tconst wasVisible = this.#isVisible;\n\n\t\t\tthis.#isVisible = entry.isIntersecting;\n\t\t\tif (!entry.isIntersecting && this.#frame !== undefined) {\n\t\t\t\tcancelAnimationFrame(this.#frame);\n\t\t\t\tthis.#frame = undefined;\n\t\t\t}\n\n\t\t\t// Chromium fires no transition inside a skipped `content-visibility` subtree\n\t\t\tif (!wasVisible && entry.isIntersecting) this.rebuild();\n\t\t});\n\n\t\tobserver.observe(canvas);\n\t\tsignal.addEventListener(\n\t\t\t'abort',\n\t\t\t() => {\n\t\t\t\tobserver.disconnect();\n\t\t\t},\n\t\t\t{ once: true },\n\t\t);\n\t}\n\n\t#read(): SurfaceLook<Colour, Length> {\n\t\tconst { canvas, colours, lengths = {} } = this.#options;\n\t\tconst styles = getComputedStyle(canvas);\n\n\t\treturn {\n\t\t\tcolours: readEach(colours, (property) =>\n\t\t\t\tstyles\n\t\t\t\t\t.getPropertyValue(property)\n\t\t\t\t\t.trim()\n\t\t\t\t\t.replaceAll('currentcolor', () => styles.color),\n\t\t\t),\n\t\t\tisReducedMotion: this.#reducedMotion.matches,\n\t\t\tlengths: readEach<Length, number>(lengths, (property) => readPxProperty(styles, property, 0)),\n\t\t};\n\t}\n\n\t#rescale(): void {\n\t\tconst box = this.#cssBox;\n\t\tif (!box) {\n\t\t\tthis.#resize(this.#size.width, this.#size.height);\n\t\t\treturn;\n\t\t}\n\n\t\tconst dpr = deviceRatio();\n\n\t\tthis.#resize(Math.round(box.width * dpr), Math.round(box.height * dpr));\n\t}\n\n\t#resize(width: number, height: number): void {\n\t\tconst previous = this.#size;\n\t\tconst dpr = deviceRatio();\n\t\tif (previous.width === width && previous.height === height && previous.dpr === dpr) return;\n\n\t\tthis.#size = { dpr, height, width };\n\t\tthis.#options.resize?.(this.#size);\n\t\tif (previous.width === 0 || previous.height === 0) this.rebuild();\n\t\telse this.invalidate();\n\t}\n\n\t#schedule(): void {\n\t\tif (this.#options.signal.aborted || this.#frame !== undefined || !this.#isVisible) return;\n\n\t\tthis.#frame = requestAnimationFrame((frameMs) => {\n\t\t\tthis.#draw(frameMs);\n\t\t});\n\t}\n\n\t#watchResolution(): void {\n\t\tconst { signal } = this.#options;\n\n\t\tmatchMedia(`(resolution: ${String(deviceRatio())}dppx)`).addEventListener(\n\t\t\t'change',\n\t\t\t() => {\n\t\t\t\tthis.#watchResolution();\n\t\t\t\tthis.#rescale();\n\t\t\t},\n\t\t\t{ once: true, signal },\n\t\t);\n\t}\n}\n\nexport function bindSurface<Colour extends string, Length extends string = never>(\n\toptions: SurfaceOptions<Colour, Length>,\n): Surface {\n\treturn new CanvasSurface(options);\n}\n"],"mappings":"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"}
@@ -2,14 +2,44 @@
2
2
  const copyPrefix = "sonic-copy-";
3
3
  const idListAttributes = /* @__PURE__ */ new Set(["aria-describedby", "aria-labelledby"]);
4
4
  const hrefAttributes = /* @__PURE__ */ new Set(["href", "xlink:href"]);
5
- const urlReference = /url\(\s*(['"]?)#([^'")\s]+)\1\s*\)/g;
5
+ const timingAttributes = /* @__PURE__ */ new Set(["begin", "end"]);
6
+ const urlAttributes = /* @__PURE__ */ new Set([
7
+ "clip-path",
8
+ "color-profile",
9
+ "fill",
10
+ "filter",
11
+ "marker-end",
12
+ "marker-mid",
13
+ "marker-start",
14
+ "mask",
15
+ "stroke",
16
+ "style"
17
+ ]);
18
+ const urlReference = /url\(\s*(['"]?)#([^'")\s]+)\1\s*\)/gi;
19
+ const idSelector = /#([\w-]+)(?=[^;{}]*\{)/g;
20
+ const timingReference = /(^|;\s*)([^\s;.]+)(?=\.[a-z])/gi;
21
+ function decoded(id) {
22
+ try {
23
+ return decodeURIComponent(id);
24
+ } catch {
25
+ return id;
26
+ }
27
+ }
28
+ function rewriteUrls(value, ids) {
29
+ return value.replaceAll(urlReference, (reference, quote, id) => ids.has(decoded(id)) ? `url(${quote}#${copyPrefix}${id}${quote})` : reference);
30
+ }
6
31
  function rewriteReferences(name, value, ids) {
7
32
  if (hrefAttributes.has(name)) return value.startsWith("#") && ids.has(value.slice(1)) ? `#${copyPrefix}${value.slice(1)}` : value;
8
33
  if (idListAttributes.has(name)) return value.split(/\s+/).map((id) => ids.has(id) ? copyPrefix + id : id).join(" ");
9
- return value.replaceAll(urlReference, (reference, quote, id) => ids.has(id) ? `url(${quote}#${copyPrefix}${id}${quote})` : reference);
34
+ if (timingAttributes.has(name)) return value.replaceAll(timingReference, (reference, lead, id) => ids.has(id) ? lead + copyPrefix + id : reference);
35
+ return urlAttributes.has(name) ? rewriteUrls(value, ids) : value;
36
+ }
37
+ function rewriteStyle(text, ids) {
38
+ return rewriteUrls(text, ids).replaceAll(idSelector, (selector, id) => ids.has(id) ? `#${copyPrefix}${id}` : selector);
10
39
  }
11
40
  function prefixIds(element, ids) {
12
41
  if (element.id !== "") element.id = copyPrefix + element.id;
42
+ if (element.localName === "style") element.textContent = rewriteStyle(element.textContent, ids);
13
43
  for (const attribute of element.attributes) {
14
44
  if (attribute.name === "id") continue;
15
45
  const value = rewriteReferences(attribute.name, attribute.value, ids);
@@ -1 +1 @@
1
- {"version":3,"file":"copy-node.js","names":[],"sources":["../../../src/lib/copy-node.ts"],"sourcesContent":["const copyPrefix = 'sonic-copy-';\n\nconst idListAttributes = new Set(['aria-describedby', 'aria-labelledby']);\n\nconst hrefAttributes = new Set(['href', 'xlink:href']);\n\nconst urlReference = /url\\(\\s*(['\"]?)#([^'\")\\s]+)\\1\\s*\\)/g;\n\nfunction rewriteReferences(name: string, value: string, ids: Set<string>): string {\n\tif (hrefAttributes.has(name)) {\n\t\treturn value.startsWith('#') && ids.has(value.slice(1))\n\t\t\t? `#${copyPrefix}${value.slice(1)}`\n\t\t\t: value;\n\t}\n\tif (idListAttributes.has(name)) {\n\t\treturn value\n\t\t\t.split(/\\s+/)\n\t\t\t.map((id) => (ids.has(id) ? copyPrefix + id : id))\n\t\t\t.join(' ');\n\t}\n\n\treturn value.replaceAll(urlReference, (reference, quote: string, id: string) =>\n\t\tids.has(id) ? `url(${quote}#${copyPrefix}${id}${quote})` : reference,\n\t);\n}\n\nfunction prefixIds(element: Element, ids: Set<string>): void {\n\tif (element.id !== '') element.id = copyPrefix + element.id;\n\tfor (const attribute of element.attributes) {\n\t\tif (attribute.name === 'id') continue;\n\n\t\tconst value = rewriteReferences(attribute.name, attribute.value, ids);\n\t\tif (value !== attribute.value) attribute.value = value;\n\t}\n}\n\n// The original stays in the document unrendered, and `url(#id)` in a copy would resolve to its `<defs>`\nexport function copyNode(node: Node): Node {\n\tconst copy = node.cloneNode(true);\n\tif (!(copy instanceof Element)) return copy;\n\n\tconst elements = [copy, ...copy.querySelectorAll('*')];\n\tconst ids = new Set(elements.map((element) => element.id).filter((id) => id !== ''));\n\tif (ids.size === 0) return copy;\n\n\tfor (const element of elements) prefixIds(element, ids);\n\n\treturn copy;\n}\n"],"mappings":";AAAA,MAAM,aAAa;AAEnB,MAAM,mCAAmB,IAAI,IAAI,CAAC,oBAAoB,iBAAiB,CAAC;AAExE,MAAM,iCAAiB,IAAI,IAAI,CAAC,QAAQ,YAAY,CAAC;AAErD,MAAM,eAAe;AAErB,SAAS,kBAAkB,MAAc,OAAe,KAA0B;CACjF,IAAI,eAAe,IAAI,IAAI,GAC1B,OAAO,MAAM,WAAW,GAAG,KAAK,IAAI,IAAI,MAAM,MAAM,CAAC,CAAC,IACnD,IAAI,aAAa,MAAM,MAAM,CAAC,MAC9B;CAEJ,IAAI,iBAAiB,IAAI,IAAI,GAC5B,OAAO,MACL,MAAM,KAAK,CAAC,CACZ,KAAK,OAAQ,IAAI,IAAI,EAAE,IAAI,aAAa,KAAK,EAAG,CAAC,CACjD,KAAK,GAAG;CAGX,OAAO,MAAM,WAAW,eAAe,WAAW,OAAe,OAChE,IAAI,IAAI,EAAE,IAAI,OAAO,MAAM,GAAG,aAAa,KAAK,MAAM,KAAK,SAC5D;AACD;AAEA,SAAS,UAAU,SAAkB,KAAwB;CAC5D,IAAI,QAAQ,OAAO,IAAI,QAAQ,KAAK,aAAa,QAAQ;CACzD,KAAK,MAAM,aAAa,QAAQ,YAAY;EAC3C,IAAI,UAAU,SAAS,MAAM;EAE7B,MAAM,QAAQ,kBAAkB,UAAU,MAAM,UAAU,OAAO,GAAG;EACpE,IAAI,UAAU,UAAU,OAAO,UAAU,QAAQ;CAClD;AACD;AAGA,SAAgB,SAAS,MAAkB;CAC1C,MAAM,OAAO,KAAK,UAAU,IAAI;CAChC,IAAI,EAAE,gBAAgB,UAAU,OAAO;CAEvC,MAAM,WAAW,CAAC,MAAM,GAAG,KAAK,iBAAiB,GAAG,CAAC;CACrD,MAAM,MAAM,IAAI,IAAI,SAAS,KAAK,YAAY,QAAQ,EAAE,CAAC,CAAC,QAAQ,OAAO,OAAO,EAAE,CAAC;CACnF,IAAI,IAAI,SAAS,GAAG,OAAO;CAE3B,KAAK,MAAM,WAAW,UAAU,UAAU,SAAS,GAAG;CAEtD,OAAO;AACR"}
1
+ {"version":3,"file":"copy-node.js","names":[],"sources":["../../../src/lib/copy-node.ts"],"sourcesContent":["const copyPrefix = 'sonic-copy-';\n\nconst idListAttributes = new Set(['aria-describedby', 'aria-labelledby']);\n\nconst hrefAttributes = new Set(['href', 'xlink:href']);\n\nconst timingAttributes = new Set(['begin', 'end']);\n\nconst urlAttributes = new Set([\n\t'clip-path',\n\t'color-profile',\n\t'fill',\n\t'filter',\n\t'marker-end',\n\t'marker-mid',\n\t'marker-start',\n\t'mask',\n\t'stroke',\n\t'style',\n]);\n\nconst urlReference = /url\\(\\s*(['\"]?)#([^'\")\\s]+)\\1\\s*\\)/gi;\n\n// A `#name` with a block still to open is a selector; inside a block it is a colour\nconst idSelector = /#([\\w-]+)(?=[^;{}]*\\{)/g;\n\n// SMIL: an id leads a `;` list entry, before `.event`; `2.5s` is a clock value\nconst timingReference = /(^|;\\s*)([^\\s;.]+)(?=\\.[a-z])/gi;\n\nfunction decoded(id: string): string {\n\ttry {\n\t\treturn decodeURIComponent(id);\n\t} catch {\n\t\treturn id;\n\t}\n}\n\nfunction rewriteUrls(value: string, ids: Set<string>): string {\n\treturn value.replaceAll(urlReference, (reference, quote: string, id: string) =>\n\t\tids.has(decoded(id)) ? `url(${quote}#${copyPrefix}${id}${quote})` : reference,\n\t);\n}\n\nfunction rewriteReferences(name: string, value: string, ids: Set<string>): string {\n\tif (hrefAttributes.has(name)) {\n\t\treturn value.startsWith('#') && ids.has(value.slice(1))\n\t\t\t? `#${copyPrefix}${value.slice(1)}`\n\t\t\t: value;\n\t}\n\tif (idListAttributes.has(name)) {\n\t\treturn value\n\t\t\t.split(/\\s+/)\n\t\t\t.map((id) => (ids.has(id) ? copyPrefix + id : id))\n\t\t\t.join(' ');\n\t}\n\tif (timingAttributes.has(name)) {\n\t\treturn value.replaceAll(timingReference, (reference, lead: string, id: string) =>\n\t\t\tids.has(id) ? lead + copyPrefix + id : reference,\n\t\t);\n\t}\n\n\treturn urlAttributes.has(name) ? rewriteUrls(value, ids) : value;\n}\n\nfunction rewriteStyle(text: string, ids: Set<string>): string {\n\treturn rewriteUrls(text, ids).replaceAll(idSelector, (selector, id: string) =>\n\t\tids.has(id) ? `#${copyPrefix}${id}` : selector,\n\t);\n}\n\nfunction prefixIds(element: Element, ids: Set<string>): void {\n\tif (element.id !== '') element.id = copyPrefix + element.id;\n\tif (element.localName === 'style') element.textContent = rewriteStyle(element.textContent, ids);\n\tfor (const attribute of element.attributes) {\n\t\tif (attribute.name === 'id') continue;\n\n\t\tconst value = rewriteReferences(attribute.name, attribute.value, ids);\n\t\tif (value !== attribute.value) attribute.value = value;\n\t}\n}\n\n// The original stays in the document unrendered, and `url(#id)` in a copy would resolve to its `<defs>`\nexport function copyNode(node: Node): Node {\n\tconst copy = node.cloneNode(true);\n\tif (!(copy instanceof Element)) return copy;\n\n\tconst elements = [copy, ...copy.querySelectorAll('*')];\n\tconst ids = new Set(elements.map((element) => element.id).filter((id) => id !== ''));\n\tif (ids.size === 0) return copy;\n\n\tfor (const element of elements) prefixIds(element, ids);\n\n\treturn copy;\n}\n"],"mappings":";AAAA,MAAM,aAAa;AAEnB,MAAM,mCAAmB,IAAI,IAAI,CAAC,oBAAoB,iBAAiB,CAAC;AAExE,MAAM,iCAAiB,IAAI,IAAI,CAAC,QAAQ,YAAY,CAAC;AAErD,MAAM,mCAAmB,IAAI,IAAI,CAAC,SAAS,KAAK,CAAC;AAEjD,MAAM,gCAAgB,IAAI,IAAI;CAC7B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACD,CAAC;AAED,MAAM,eAAe;AAGrB,MAAM,aAAa;AAGnB,MAAM,kBAAkB;AAExB,SAAS,QAAQ,IAAoB;CACpC,IAAI;EACH,OAAO,mBAAmB,EAAE;CAC7B,QAAQ;EACP,OAAO;CACR;AACD;AAEA,SAAS,YAAY,OAAe,KAA0B;CAC7D,OAAO,MAAM,WAAW,eAAe,WAAW,OAAe,OAChE,IAAI,IAAI,QAAQ,EAAE,CAAC,IAAI,OAAO,MAAM,GAAG,aAAa,KAAK,MAAM,KAAK,SACrE;AACD;AAEA,SAAS,kBAAkB,MAAc,OAAe,KAA0B;CACjF,IAAI,eAAe,IAAI,IAAI,GAC1B,OAAO,MAAM,WAAW,GAAG,KAAK,IAAI,IAAI,MAAM,MAAM,CAAC,CAAC,IACnD,IAAI,aAAa,MAAM,MAAM,CAAC,MAC9B;CAEJ,IAAI,iBAAiB,IAAI,IAAI,GAC5B,OAAO,MACL,MAAM,KAAK,CAAC,CACZ,KAAK,OAAQ,IAAI,IAAI,EAAE,IAAI,aAAa,KAAK,EAAG,CAAC,CACjD,KAAK,GAAG;CAEX,IAAI,iBAAiB,IAAI,IAAI,GAC5B,OAAO,MAAM,WAAW,kBAAkB,WAAW,MAAc,OAClE,IAAI,IAAI,EAAE,IAAI,OAAO,aAAa,KAAK,SACxC;CAGD,OAAO,cAAc,IAAI,IAAI,IAAI,YAAY,OAAO,GAAG,IAAI;AAC5D;AAEA,SAAS,aAAa,MAAc,KAA0B;CAC7D,OAAO,YAAY,MAAM,GAAG,CAAC,CAAC,WAAW,aAAa,UAAU,OAC/D,IAAI,IAAI,EAAE,IAAI,IAAI,aAAa,OAAO,QACvC;AACD;AAEA,SAAS,UAAU,SAAkB,KAAwB;CAC5D,IAAI,QAAQ,OAAO,IAAI,QAAQ,KAAK,aAAa,QAAQ;CACzD,IAAI,QAAQ,cAAc,SAAS,QAAQ,cAAc,aAAa,QAAQ,aAAa,GAAG;CAC9F,KAAK,MAAM,aAAa,QAAQ,YAAY;EAC3C,IAAI,UAAU,SAAS,MAAM;EAE7B,MAAM,QAAQ,kBAAkB,UAAU,MAAM,UAAU,OAAO,GAAG;EACpE,IAAI,UAAU,UAAU,OAAO,UAAU,QAAQ;CAClD;AACD;AAGA,SAAgB,SAAS,MAAkB;CAC1C,MAAM,OAAO,KAAK,UAAU,IAAI;CAChC,IAAI,EAAE,gBAAgB,UAAU,OAAO;CAEvC,MAAM,WAAW,CAAC,MAAM,GAAG,KAAK,iBAAiB,GAAG,CAAC;CACrD,MAAM,MAAM,IAAI,IAAI,SAAS,KAAK,YAAY,QAAQ,EAAE,CAAC,CAAC,QAAQ,OAAO,OAAO,EAAE,CAAC;CACnF,IAAI,IAAI,SAAS,GAAG,OAAO;CAE3B,KAAK,MAAM,WAAW,UAAU,UAAU,SAAS,GAAG;CAEtD,OAAO;AACR"}
@@ -0,0 +1,12 @@
1
+ //#region src/lib/crossfade.d.ts
2
+ interface CrossfadeOptions {
3
+ law?: 'linear' | 'power';
4
+ sharpness?: number;
5
+ }
6
+ interface CrossfadeGains {
7
+ a: number;
8
+ b: number;
9
+ }
10
+ export declare function crossfadeGains(position: number, options?: CrossfadeOptions): CrossfadeGains;
11
+ //#endregion
12
+ //# sourceMappingURL=crossfade.d.ts.map
@@ -0,0 +1,23 @@
1
+ import { clampProportion } from "./math.js";
2
+
3
+ //#region src/lib/crossfade.ts
4
+ function progress(side, span) {
5
+ if (span === 0) return side > 0 ? 1 : 0;
6
+ return Math.min(1, side / span);
7
+ }
8
+ function crossfadeGains(position, options = {}) {
9
+ const at = clampProportion(position);
10
+ const span = 1 - clampProportion(options.sharpness ?? 0);
11
+ const gain = (side) => {
12
+ const reached = progress(side, span);
13
+ return options.law === "linear" ? reached : Math.sin(reached * Math.PI / 2);
14
+ };
15
+ return {
16
+ a: gain(1 - at),
17
+ b: gain(at)
18
+ };
19
+ }
20
+
21
+ //#endregion
22
+ export { crossfadeGains };
23
+ //# sourceMappingURL=crossfade.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crossfade.js","names":[],"sources":["../../../src/lib/crossfade.ts"],"sourcesContent":["import { clampProportion } from '#lib/math.ts';\n\ninterface CrossfadeOptions {\n\tlaw?: 'linear' | 'power';\n\tsharpness?: number;\n}\n\ninterface CrossfadeGains {\n\ta: number;\n\tb: number;\n}\n\nfunction progress(side: number, span: number): number {\n\tif (span === 0) return side > 0 ? 1 : 0;\n\n\treturn Math.min(1, side / span);\n}\n\nexport function crossfadeGains(position: number, options: CrossfadeOptions = {}): CrossfadeGains {\n\tconst at = clampProportion(position);\n\tconst span = 1 - clampProportion(options.sharpness ?? 0);\n\tconst gain = (side: number): number => {\n\t\tconst reached = progress(side, span);\n\n\t\treturn options.law === 'linear' ? reached : Math.sin((reached * Math.PI) / 2);\n\t};\n\n\treturn { a: gain(1 - at), b: gain(at) };\n}\n"],"mappings":";;;AAYA,SAAS,SAAS,MAAc,MAAsB;CACrD,IAAI,SAAS,GAAG,OAAO,OAAO,IAAI,IAAI;CAEtC,OAAO,KAAK,IAAI,GAAG,OAAO,IAAI;AAC/B;AAEA,SAAgB,eAAe,UAAkB,UAA4B,CAAC,GAAmB;CAChG,MAAM,KAAK,gBAAgB,QAAQ;CACnC,MAAM,OAAO,IAAI,gBAAgB,QAAQ,aAAa,CAAC;CACvD,MAAM,QAAQ,SAAyB;EACtC,MAAM,UAAU,SAAS,MAAM,IAAI;EAEnC,OAAO,QAAQ,QAAQ,WAAW,UAAU,KAAK,IAAK,UAAU,KAAK,KAAM,CAAC;CAC7E;CAEA,OAAO;EAAE,GAAG,KAAK,IAAI,EAAE;EAAG,GAAG,KAAK,EAAE;CAAE;AACvC"}
@@ -0,0 +1,4 @@
1
+ //#region src/lib/distribute.d.ts
2
+ export type SplitMode = 'cascade' | 'equal' | 'proportional';
3
+ //#endregion
4
+ //# sourceMappingURL=distribute.d.ts.map