@xsynaptic/sonic-ui 0.4.0 → 0.5.0

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