@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,383 @@
1
+ import { requireChild, template } from "../lib/render.js";
2
+ import { writeAttribute } from "../lib/write-attribute.js";
3
+ import { SonicElement } from "./sonic-element.js";
4
+ import { ReadoutClaim } from "./readout-claim.js";
5
+ import { Readout } from "./readout.js";
6
+ import { dragThresholdPx } from "../lib/drag-step.js";
7
+ import { bindDrag } from "../lib/pointer-drag.js";
8
+ import { SonicValueElement, linkValue } from "./value-element.js";
9
+ import { adsrPath, adsrShape, adsrStages, timeStages } from "../lib/adsr-shape.js";
10
+ import { pointerMove, pointerPosition, startFieldDrag, stepFieldDrag } from "../lib/field.js";
11
+
12
+ //#region src/elements/envelope.ts
13
+ const curveStages = [
14
+ "attack",
15
+ "decay",
16
+ "release"
17
+ ];
18
+ const bindable = [
19
+ ...adsrStages,
20
+ "attack-curve",
21
+ "decay-curve",
22
+ "release-curve"
23
+ ];
24
+ const bindableNames = new Set(bindable);
25
+ function flip(point, isFlipped) {
26
+ return isFlipped ? {
27
+ ...point,
28
+ y: -point.y
29
+ } : point;
30
+ }
31
+ function isParent(node) {
32
+ return "querySelector" in node;
33
+ }
34
+ const emptyPath = {
35
+ fill: "",
36
+ floor: {
37
+ from: 0,
38
+ to: 0
39
+ },
40
+ stroke: ""
41
+ };
42
+ function stageParts(envelope, hook, stages) {
43
+ return new Map(stages.map((stage) => [stage, requireChild(envelope, `.${hook}[data-sonic-stage="${stage}"]`, HTMLDivElement)]));
44
+ }
45
+ const renderEnvelope = template(`
46
+ <div class="sonic-envelope" aria-hidden="true">
47
+ <svg class="sonic-envelope-graph" preserveAspectRatio="none" viewBox="0 0 1 1">
48
+ <path class="sonic-envelope-fill" />
49
+ <rect class="sonic-envelope-fill sonic-envelope-floor" y="1" />
50
+ <path class="sonic-envelope-line" />
51
+ </svg>
52
+ <div class="sonic-envelope-handle" data-sonic-stage="delay" hidden></div>
53
+ <div class="sonic-envelope-handle" data-sonic-stage="attack" hidden></div>
54
+ <div class="sonic-envelope-handle" data-sonic-stage="hold" hidden></div>
55
+ <div class="sonic-envelope-handle" data-sonic-stage="decay" hidden></div>
56
+ <div class="sonic-envelope-handle" data-sonic-stage="release" hidden></div>
57
+ <div class="sonic-envelope-curve" data-sonic-stage="attack" hidden></div>
58
+ <div class="sonic-envelope-curve" data-sonic-stage="decay" hidden></div>
59
+ <div class="sonic-envelope-curve" data-sonic-stage="release" hidden></div>
60
+ <div class="sonic-envelope-readout" popover="manual"><span></span></div>
61
+ </div>
62
+ `, HTMLDivElement);
63
+ var SonicEnvelope = class extends SonicElement {
64
+ static observedAttributes = [
65
+ ...SonicElement.observedAttributes,
66
+ ...bindable,
67
+ "readout"
68
+ ];
69
+ get attack() {
70
+ return this.getAttribute("attack") ?? "";
71
+ }
72
+ set attack(id) {
73
+ this.reflect("attack", id);
74
+ }
75
+ get attackCurve() {
76
+ return this.getAttribute("attack-curve") ?? "";
77
+ }
78
+ set attackCurve(id) {
79
+ this.reflect("attack-curve", id);
80
+ }
81
+ get decay() {
82
+ return this.getAttribute("decay") ?? "";
83
+ }
84
+ set decay(id) {
85
+ this.reflect("decay", id);
86
+ }
87
+ get decayCurve() {
88
+ return this.getAttribute("decay-curve") ?? "";
89
+ }
90
+ set decayCurve(id) {
91
+ this.reflect("decay-curve", id);
92
+ }
93
+ get delay() {
94
+ return this.getAttribute("delay") ?? "";
95
+ }
96
+ set delay(id) {
97
+ this.reflect("delay", id);
98
+ }
99
+ get hold() {
100
+ return this.getAttribute("hold") ?? "";
101
+ }
102
+ set hold(id) {
103
+ this.reflect("hold", id);
104
+ }
105
+ get readout() {
106
+ return this.hasAttribute("readout");
107
+ }
108
+ set readout(isOn) {
109
+ this.reflect("readout", isOn);
110
+ }
111
+ get release() {
112
+ return this.getAttribute("release") ?? "";
113
+ }
114
+ set release(id) {
115
+ this.reflect("release", id);
116
+ }
117
+ get releaseCurve() {
118
+ return this.getAttribute("release-curve") ?? "";
119
+ }
120
+ set releaseCurve(id) {
121
+ this.reflect("release-curve", id);
122
+ }
123
+ get sustain() {
124
+ return this.getAttribute("sustain") ?? "";
125
+ }
126
+ set sustain(id) {
127
+ this.reflect("sustain", id);
128
+ }
129
+ #bindings = /* @__PURE__ */ new Map();
130
+ #claim = new ReadoutClaim(() => {
131
+ this.#renderReadout();
132
+ });
133
+ #envelope = renderEnvelope();
134
+ #curveHandles = stageParts(this.#envelope, "sonic-envelope-curve", curveStages);
135
+ #fill = requireChild(this.#envelope, ".sonic-envelope-fill", SVGElement);
136
+ #floor = requireChild(this.#envelope, ".sonic-envelope-floor", SVGElement);
137
+ #graph = requireChild(this.#envelope, ".sonic-envelope-graph", SVGElement);
138
+ #handles = stageParts(this.#envelope, "sonic-envelope-handle", timeStages);
139
+ #isDriving = false;
140
+ #line = requireChild(this.#envelope, ".sonic-envelope-line", SVGElement);
141
+ #pointerDrag;
142
+ #readout = new Readout(requireChild(this.#envelope, ".sonic-envelope-readout", HTMLDivElement));
143
+ #shape = adsrShape({});
144
+ #signal;
145
+ #waiting = /* @__PURE__ */ new Set();
146
+ #warned = /* @__PURE__ */ new Set();
147
+ attributeChangedCallback(name) {
148
+ if (!this.#signal) return;
149
+ if (name === "disabled") {
150
+ this.#pointerDrag?.end();
151
+ this.#draw();
152
+ return;
153
+ }
154
+ if (bindableNames.has(name)) this.#bind();
155
+ else this.#renderReadout();
156
+ }
157
+ connectedCallback() {
158
+ this.upgradeProperties(...adsrStages, "attackCurve", "decayCurve", "readout", "releaseCurve");
159
+ super.connectedCallback();
160
+ }
161
+ connect(signal) {
162
+ const envelope = this.#envelope;
163
+ this.#signal = signal;
164
+ this.keepControl(envelope, signal);
165
+ this.#bind();
166
+ this.checkStyles(envelope, "envelope.css");
167
+ this.#bindPointer(envelope, signal);
168
+ signal.addEventListener("abort", () => {
169
+ this.#signal = void 0;
170
+ this.#unbind();
171
+ }, { once: true });
172
+ }
173
+ #bind() {
174
+ this.#pointerDrag?.end();
175
+ this.#unbind();
176
+ for (const name of bindable) {
177
+ const element = this.#resolve(name);
178
+ if (!element) continue;
179
+ const link = linkValue(element);
180
+ const unwatch = link.watch(() => {
181
+ if (!this.#isDriving) this.#draw();
182
+ });
183
+ this.#bindings.set(name, {
184
+ element,
185
+ link,
186
+ unwatch
187
+ });
188
+ }
189
+ this.#draw();
190
+ }
191
+ #bindPointer(envelope, signal) {
192
+ this.#pointerDrag = bindDrag(envelope, {
193
+ cancel: (drag) => {
194
+ for (const { binding, from } of drag.drives) binding.link.input(from);
195
+ },
196
+ grab: (event) => this.#grab(event),
197
+ move: (drag, event) => {
198
+ this.#moveDrag(drag, event);
199
+ },
200
+ release: (drag) => {
201
+ this.#claim.press(false);
202
+ for (const { binding, from } of drag.drives) {
203
+ if (binding.link.value() === from) continue;
204
+ binding.element.dispatchEvent(new Event("change", { bubbles: true }));
205
+ }
206
+ },
207
+ toggle: (isDragging) => {
208
+ this.toggleState("dragging", isDragging);
209
+ }
210
+ }, signal);
211
+ }
212
+ #curveDrives(stage) {
213
+ const binding = this.#bindings.get(`${stage}-curve`);
214
+ return binding ? [{
215
+ axis: "y",
216
+ binding
217
+ }] : [];
218
+ }
219
+ #draw() {
220
+ const shape = this.#readShape();
221
+ const path = this.#bindings.size === 0 ? emptyPath : adsrPath(shape);
222
+ this.#shape = shape;
223
+ writeAttribute(this.#line, "d", path.stroke);
224
+ writeAttribute(this.#fill, "d", path.fill);
225
+ writeAttribute(this.#floor, "x", String(path.floor.from));
226
+ writeAttribute(this.#floor, "width", String(path.floor.to - path.floor.from));
227
+ for (const [stage, part] of this.#handles) this.#drawPoint(part, {
228
+ at: shape.handles.find((point) => point.stage === stage),
229
+ drives: this.#drives(stage)
230
+ });
231
+ for (const [stage, part] of this.#curveHandles) this.#drawPoint(part, {
232
+ at: shape.curveHandles.find((point) => point.stage === stage),
233
+ drives: this.#curveDrives(stage)
234
+ });
235
+ this.#renderReadout();
236
+ }
237
+ #drawPoint(part, point) {
238
+ const { at, drives } = point;
239
+ writeAttribute(part, "hidden", at ? void 0 : "");
240
+ if (!at) return;
241
+ const isStill = this.isDisabled() || drives.every(({ binding }) => binding.link.isDisabled());
242
+ part.style.setProperty("--_sonic-envelope-x", String(at.x));
243
+ part.style.setProperty("--_sonic-envelope-y", String(at.y));
244
+ writeAttribute(part, "data-sonic-axes", drives.map(({ axis }) => axis).join(""));
245
+ writeAttribute(part, "data-sonic-disabled", isStill ? "" : void 0);
246
+ }
247
+ #drives(stage) {
248
+ const x = this.#bindings.get(stage);
249
+ const y = stage === "decay" ? this.#bindings.get("sustain") : void 0;
250
+ return [...x ? [{
251
+ axis: "x",
252
+ binding: x
253
+ }] : [], ...y ? [{
254
+ axis: "y",
255
+ binding: y
256
+ }] : []];
257
+ }
258
+ #grab(event) {
259
+ const pressed = this.#pressed(event);
260
+ if (!pressed || pressed.drives.length === 0) return void 0;
261
+ const { drives, isFlipped, part } = pressed;
262
+ this.#claim.press(true);
263
+ const box = this.#graph.getBoundingClientRect();
264
+ const travel = {
265
+ x: Math.max(1, box.width * this.#shape.share),
266
+ y: Math.max(1, box.height)
267
+ };
268
+ const start = (axis) => {
269
+ const link = drives.find((drive) => drive.axis === axis)?.binding.link;
270
+ if (!link) return;
271
+ const mapping = link.mapping();
272
+ const from = link.value();
273
+ return {
274
+ from,
275
+ mapping,
276
+ proportion: mapping.proportionOf(from),
277
+ travelPx: travel[axis]
278
+ };
279
+ };
280
+ return {
281
+ drives: drives.map((drive) => ({
282
+ ...drive,
283
+ from: drive.binding.link.value()
284
+ })),
285
+ isFlipped,
286
+ part,
287
+ state: startFieldDrag({
288
+ position: flip(pointerPosition(event), isFlipped),
289
+ thresholdPx: dragThresholdPx(event.pointerType),
290
+ x: start("x"),
291
+ y: start("y")
292
+ })
293
+ };
294
+ }
295
+ #moveDrag(drag, event) {
296
+ const mappings = {};
297
+ for (const { axis, binding } of drag.drives) mappings[axis] = binding.link.mapping();
298
+ const step = stepFieldDrag(mappings, drag.state, flip(pointerMove(event), drag.isFlipped));
299
+ drag.state = step.state;
300
+ if (step.state.isEngaged) this.#claim.reveal("drag");
301
+ this.#isDriving = true;
302
+ for (const { axis, binding } of drag.drives) {
303
+ const next = step[axis];
304
+ if (next !== void 0) binding.link.input(next);
305
+ }
306
+ this.#isDriving = false;
307
+ this.#draw();
308
+ }
309
+ #pressed(event) {
310
+ const part = event.target instanceof Element ? event.target.closest(".sonic-envelope-handle, .sonic-envelope-curve") : void 0;
311
+ const stage = part?.dataset.sonicStage;
312
+ if (!part || stage === void 0 || this.isDisabled()) return void 0;
313
+ const isCurveHandle = part.classList.contains("sonic-envelope-curve");
314
+ return {
315
+ drives: (isCurveHandle ? this.#curveDrives(stage) : this.#drives(stage)).filter(({ binding }) => !binding.link.isDisabled()),
316
+ isFlipped: isCurveHandle && stage === "attack",
317
+ part
318
+ };
319
+ }
320
+ #readShape() {
321
+ const proportions = {};
322
+ const curves = {};
323
+ for (const stage of adsrStages) {
324
+ const link = this.#bindings.get(stage)?.link;
325
+ proportions[stage] = link?.mapping().proportionOf(link.value());
326
+ }
327
+ for (const stage of curveStages) curves[stage] = this.#bindings.get(`${stage}-curve`)?.link.value();
328
+ return adsrShape(proportions, curves);
329
+ }
330
+ #rebindOnce(key, arrives) {
331
+ if (this.#waiting.has(key)) return;
332
+ this.#waiting.add(key);
333
+ arrives(() => {
334
+ this.#waiting.delete(key);
335
+ if (this.#signal) this.#bind();
336
+ });
337
+ }
338
+ #renderReadout() {
339
+ const drag = this.#pointerDrag?.current();
340
+ if (!drag) {
341
+ this.#readout.close();
342
+ return;
343
+ }
344
+ this.#readout.show({
345
+ anchor: drag.part,
346
+ isOpen: this.#claim.isRevealed && this.readout,
347
+ text: drag.drives.map(({ binding }) => binding.element.valueText).join(", ")
348
+ });
349
+ }
350
+ #resolve(name) {
351
+ const id = this.getAttribute(name);
352
+ const root = this.getRootNode();
353
+ if (!id || !isParent(root)) return void 0;
354
+ const found = root.querySelector(`#${CSS.escape(id)}`);
355
+ if (found instanceof SonicValueElement) return found;
356
+ this.#wait(id, found);
357
+ }
358
+ #unbind() {
359
+ for (const { unwatch } of this.#bindings.values()) unwatch();
360
+ this.#bindings.clear();
361
+ }
362
+ #wait(id, found) {
363
+ const tag = found?.localName;
364
+ const { ownerDocument } = this;
365
+ if (tag?.includes("-") && !customElements.get(tag)) {
366
+ this.#rebindOnce(tag, (rebind) => {
367
+ customElements.whenDefined(tag).then(rebind);
368
+ });
369
+ return;
370
+ }
371
+ if (!found && ownerDocument.readyState === "loading") {
372
+ this.#rebindOnce(ownerDocument, (rebind) => {
373
+ ownerDocument.addEventListener("DOMContentLoaded", rebind, { once: true });
374
+ });
375
+ return;
376
+ }
377
+ this.#warn(id);
378
+ }
379
+ #warn(id) {}
380
+ };
381
+
382
+ //#endregion
383
+ export { SonicEnvelope };
@@ -1,61 +1,71 @@
1
+ import { toNumber } from "../lib/math.js";
1
2
  import { template } from "../lib/render.js";
2
3
  import { SonicElement } from "./sonic-element.js";
3
4
  import { parseNumberList } from "../lib/number-list.js";
4
5
  import { linearTaper } from "../lib/taper.js";
6
+ import { ballisticsRates, fall, rise, stepNeedle } from "../lib/ballistics.js";
5
7
 
6
8
  //#region src/elements/meter.ts
7
9
  const renderMeter = template(`
8
10
  <div aria-hidden="true" class="sonic-meter">
9
11
  <div class="sonic-meter-segments"></div>
10
- <div class="sonic-meter-level"></div>
12
+ <div class="sonic-meter-bar"></div>
11
13
  <div class="sonic-meter-clip"></div>
12
14
  </div>
13
15
  `, HTMLDivElement);
14
- const releaseDecibelsPerSecond = 20;
15
16
  const holdMs = 1500;
17
+ const ballisticsModes = [
18
+ "peak",
19
+ "ppm-1",
20
+ "ppm-2",
21
+ "vu"
22
+ ];
23
+ const settledDecibels = .01;
24
+ const settledAmplitude = 1e-4;
25
+ const settledPerSecond = .01;
16
26
  function toDecibels(amplitude) {
17
27
  return 20 * Math.log10(Math.abs(amplitude));
18
28
  }
19
- function lightsBelow(lights, zone) {
20
- return `calc(${lights.map((threshold) => `sign(max(0, var(--_sonic-meter-${zone}-from) - (${String(threshold)})))`).join(" + ")})`;
29
+ function toAmplitude(decibels) {
30
+ return 10 ** (decibels / 20);
31
+ }
32
+ function segmentsBelow(segments, zone) {
33
+ return `calc(${segments.map((threshold) => `sign(max(0, var(--_sonic-meter-${zone}-from) - (${String(threshold)})))`).join(" + ")})`;
21
34
  }
22
35
  var SonicMeter = class extends SonicElement {
23
36
  static observedAttributes = [
37
+ "ballistics",
24
38
  "disabled",
25
- "lights",
26
39
  "max",
27
40
  "min",
28
41
  "origin",
29
- "scale"
42
+ "segments",
43
+ "value"
30
44
  ];
45
+ get ballistics() {
46
+ const name = this.getAttribute("ballistics");
47
+ return ballisticsModes.find((mode) => mode === name) ?? "peak";
48
+ }
49
+ set ballistics(mode) {
50
+ this.reflect("ballistics", mode);
51
+ }
31
52
  get level() {
32
53
  return this.#level;
33
54
  }
34
- set level(amplitude) {
55
+ set level(level) {
56
+ const amplitude = toNumber(level);
35
57
  if (!Number.isFinite(amplitude)) return;
36
- const now = performance.now();
37
- this.#level = amplitude;
38
- if (this.scale === "linear") {
39
- this.#render(now);
40
- return;
41
- }
42
58
  const decibels = toDecibels(amplitude);
59
+ this.#level = amplitude;
43
60
  this.#target = decibels;
44
- this.#bar = Math.max(this.#bar, decibels);
45
- if (decibels >= this.#peak) {
46
- this.#peak = decibels;
47
- this.#peakAt = now;
48
- }
61
+ if (this.#isValueShown) return;
62
+ const now = performance.now();
63
+ this.#peak = Math.max(this.#peak, decibels);
49
64
  if (decibels >= 0) this.#clipAt = now;
65
+ this.#step(now, 0);
50
66
  this.#render(now);
51
67
  this.#schedule(now);
52
68
  }
53
- get lights() {
54
- return parseNumberList(this.getAttribute("lights"));
55
- }
56
- set lights(thresholds) {
57
- this.reflect("lights", thresholds?.join(" "));
58
- }
59
69
  get max() {
60
70
  return this.numberAttribute("max", 0);
61
71
  }
@@ -80,33 +90,52 @@ var SonicMeter = class extends SonicElement {
80
90
  set origin(value) {
81
91
  this.reflect("origin", value);
82
92
  }
83
- get scale() {
84
- return this.getAttribute("scale") === "linear" ? "linear" : "db";
93
+ get peak() {
94
+ return this.#peak;
95
+ }
96
+ get segments() {
97
+ return parseNumberList(this.getAttribute("segments"));
98
+ }
99
+ set segments(thresholds) {
100
+ this.reflect("segments", thresholds?.join(" "));
101
+ }
102
+ get value() {
103
+ return this.optionalNumberAttribute("value");
85
104
  }
86
- set scale(scale) {
87
- this.reflect("scale", scale);
105
+ set value(value) {
106
+ this.reflect("value", value);
88
107
  }
89
108
  #bar = -Infinity;
109
+ #carryMs = 0;
90
110
  #clipAt = -Infinity;
91
111
  #frame;
112
+ #isValueShown = false;
92
113
  #lastFrame;
93
114
  #level = 0;
94
- #lights;
95
115
  #meter = renderMeter();
96
116
  #peak = -Infinity;
97
- #peakAt = -Infinity;
117
+ #peakHold = -Infinity;
118
+ #peakHoldAt = -Infinity;
119
+ #segments;
98
120
  #target = -Infinity;
99
- attributeChangedCallback() {
121
+ #velocity = 0;
122
+ attributeChangedCallback(name) {
100
123
  const now = performance.now();
101
- this.#renderLights();
124
+ if (name === "ballistics") this.#velocity = 0;
125
+ else if (name === "value") this.#readValue(now);
126
+ this.#segments = this.segments;
127
+ if (name === "segments") this.#renderSegments();
102
128
  this.#render(now);
103
129
  this.#schedule(now);
104
130
  }
131
+ resetPeak() {
132
+ this.#peak = -Infinity;
133
+ }
105
134
  connect(signal) {
106
135
  const now = performance.now();
107
- this.upgradeProperties("lights", "max", "min", "orientation", "origin", "scale", "level");
108
- this.appendOnce(this.#meter);
109
- this.#renderLights();
136
+ this.upgradeProperties("ballistics", "max", "min", "orientation", "origin", "segments", "value", "level");
137
+ this.keepControl(this.#meter, signal);
138
+ this.#renderSegments();
110
139
  this.#render(now);
111
140
  this.#schedule(now);
112
141
  this.checkStyles(this.#meter, "meter.css");
@@ -116,61 +145,124 @@ var SonicMeter = class extends SonicElement {
116
145
  this.#lastFrame = void 0;
117
146
  }, { once: true });
118
147
  }
119
- #fraction(level) {
120
- const lights = this.#lights;
121
- if (lights) return lights.filter((threshold) => threshold <= level).length / lights.length;
122
- return linearTaper(this.min, this.max).position(level);
123
- }
124
148
  #isSettled(now) {
125
- if (this.scale === "linear") return true;
149
+ if (this.#isValueShown) return true;
150
+ if (now - this.#clipAt < holdMs) return false;
151
+ if (this.ballistics === "vu") return this.#bar === this.#target && this.#velocity === 0;
126
152
  const floor = this.min;
127
- return this.#bar <= Math.max(this.#target, floor) && this.#peak <= Math.max(this.#bar, floor) && now - this.#clipAt >= holdMs;
153
+ return this.#bar >= this.#target && this.#bar <= Math.max(this.#target, floor) && this.#peakHold <= Math.max(this.#bar, floor);
154
+ }
155
+ #proportionOf(level) {
156
+ const segments = this.#segments;
157
+ if (segments) return segments.filter((threshold) => threshold <= level).length / segments.length;
158
+ return linearTaper(this.min, this.max).proportionOf(level);
159
+ }
160
+ #readValue(now) {
161
+ const value = this.value;
162
+ const isValueShown = value !== void 0;
163
+ if (isValueShown !== this.#isValueShown) {
164
+ this.#isValueShown = isValueShown;
165
+ this.resetPeak();
166
+ if (!isValueShown) this.#step(now, 0);
167
+ }
168
+ if (value !== void 0) this.#peak = Math.max(this.#peak, value);
128
169
  }
129
170
  #render(now) {
171
+ if (!this.isBound()) return;
130
172
  const meter = this.#meter;
131
- const origin = this.origin === void 0 ? 0 : this.#fraction(this.origin);
173
+ const origin = this.origin === void 0 ? 0 : this.#proportionOf(this.origin);
132
174
  meter.style.setProperty("--_sonic-meter-min", String(this.min));
133
175
  meter.style.setProperty("--_sonic-meter-max", String(this.max));
134
176
  meter.style.setProperty("--_sonic-meter-origin", String(origin));
135
- if (this.disabled || this.scale === "linear") {
136
- const level = this.disabled ? origin : this.#fraction(this.#level);
137
- meter.style.setProperty("--_sonic-meter-level", String(level));
138
- meter.style.setProperty("--_sonic-meter-peak", "0");
139
- meter.style.setProperty("--_sonic-meter-clipped", "0");
177
+ if (this.disabled) {
178
+ this.#renderStill(origin);
179
+ return;
180
+ }
181
+ const value = this.value;
182
+ if (value !== void 0) {
183
+ this.#renderStill(this.#proportionOf(value));
140
184
  return;
141
185
  }
142
- meter.style.setProperty("--_sonic-meter-level", String(this.#fraction(this.#bar)));
143
- meter.style.setProperty("--_sonic-meter-peak", String(this.#fraction(this.#peak)));
186
+ const peakHold = this.ballistics === "vu" ? 0 : this.#proportionOf(this.#peakHold);
187
+ meter.style.setProperty("--_sonic-meter-bar", String(this.#proportionOf(this.#bar)));
188
+ meter.style.setProperty("--_sonic-meter-peak-hold", String(peakHold));
144
189
  meter.style.setProperty("--_sonic-meter-clipped", now - this.#clipAt < holdMs ? "1" : "0");
145
190
  }
146
- #renderLights() {
191
+ #renderSegments() {
192
+ if (!this.isBound()) return;
147
193
  const meter = this.#meter;
148
- const lights = this.lights;
149
- this.#lights = lights;
150
- this.toggleState("ladder", lights !== void 0);
151
- if (!lights) {
194
+ const segments = this.#segments;
195
+ this.toggleState("ladder", segments !== void 0);
196
+ if (!segments) {
152
197
  for (const name of [
153
198
  "count",
154
- "hot-lights",
155
- "clip-lights"
199
+ "hot-segments",
200
+ "clip-segments"
156
201
  ]) meter.style.removeProperty(`--_sonic-meter-${name}`);
157
202
  return;
158
203
  }
159
- meter.style.setProperty("--_sonic-meter-count", String(lights.length));
160
- meter.style.setProperty("--_sonic-meter-hot-lights", lightsBelow(lights, "hot"));
161
- meter.style.setProperty("--_sonic-meter-clip-lights", lightsBelow(lights, "clip"));
204
+ meter.style.setProperty("--_sonic-meter-count", String(segments.length));
205
+ meter.style.setProperty("--_sonic-meter-hot-segments", segmentsBelow(segments, "hot"));
206
+ meter.style.setProperty("--_sonic-meter-clip-segments", segmentsBelow(segments, "clip"));
207
+ }
208
+ #renderStill(bar) {
209
+ const { style } = this.#meter;
210
+ style.setProperty("--_sonic-meter-bar", String(bar));
211
+ style.setProperty("--_sonic-meter-peak-hold", "0");
212
+ style.setProperty("--_sonic-meter-clipped", "0");
213
+ }
214
+ #risen(timeConstantMs, elapsedMs) {
215
+ const target = toAmplitude(this.#target);
216
+ const risen = toDecibels(rise(toAmplitude(this.#bar), {
217
+ target,
218
+ timeConstantMs
219
+ }, elapsedMs));
220
+ return this.#target - risen < settledDecibels ? this.#target : risen;
162
221
  }
163
222
  #schedule(now) {
164
223
  if (this.#frame !== void 0 || !this.isConnected || this.#isSettled(now)) return;
165
224
  if (this.#lastFrame === void 0) this.#lastFrame = now;
166
225
  this.#frame = requestAnimationFrame(this.#tick);
167
226
  }
227
+ #step(now, elapsedMs) {
228
+ const mode = this.ballistics;
229
+ if (mode === "vu") {
230
+ this.#stepNeedle(elapsedMs);
231
+ return;
232
+ }
233
+ const { fallDecibelsPerSecond, riseTimeConstantMs } = ballisticsRates[mode];
234
+ const floor = Math.max(this.#target, this.min);
235
+ this.#bar = this.#target > this.#bar ? this.#risen(riseTimeConstantMs, elapsedMs) : fall(this.#bar, {
236
+ fallDecibelsPerSecond,
237
+ floor
238
+ }, elapsedMs);
239
+ if (this.#bar >= this.#peakHold) {
240
+ this.#peakHold = this.#bar;
241
+ this.#peakHoldAt = now;
242
+ }
243
+ const pastHoldMs = now - this.#peakHoldAt - holdMs;
244
+ if (pastHoldMs >= 0) this.#peakHold = fall(this.#peakHold, {
245
+ fallDecibelsPerSecond,
246
+ floor: this.#bar
247
+ }, Math.min(elapsedMs, pastHoldMs));
248
+ }
249
+ #stepNeedle(elapsedMs) {
250
+ const target = toAmplitude(this.#target);
251
+ const needle = stepNeedle({
252
+ carryMs: this.#carryMs,
253
+ position: toAmplitude(this.#bar),
254
+ velocity: this.#velocity
255
+ }, target, elapsedMs);
256
+ const isLanded = Math.abs(needle.position - target) < settledAmplitude && Math.abs(needle.velocity) < settledPerSecond;
257
+ this.#carryMs = needle.carryMs;
258
+ this.#velocity = isLanded ? 0 : needle.velocity;
259
+ this.#bar = isLanded ? this.#target : toDecibels(Math.max(0, needle.position));
260
+ }
168
261
  #tick = (time) => {
169
- const fall = releaseDecibelsPerSecond * Math.max(0, time - (this.#lastFrame ?? time)) / 1e3;
262
+ const elapsedMs = Math.max(0, time - (this.#lastFrame ?? time));
170
263
  this.#frame = void 0;
171
264
  this.#lastFrame = time;
172
- this.#bar = Math.max(this.#target, this.min, this.#bar - fall);
173
- if (time - this.#peakAt >= holdMs) this.#peak = Math.max(this.#bar, this.#peak - fall);
265
+ this.#step(time, elapsedMs);
174
266
  this.#render(time);
175
267
  if (this.#isSettled(time)) {
176
268
  this.#lastFrame = void 0;