@xsynaptic/sonic-ui 0.5.0 → 0.6.1

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 (138) hide show
  1. package/README.md +3 -1
  2. package/dist/default/dat.js +5 -4
  3. package/dist/default/define/button.js +1 -1
  4. package/dist/default/define.js +2 -2
  5. package/dist/default/elements/button.js +6 -5
  6. package/dist/default/elements/dial.js +5 -1
  7. package/dist/default/elements/envelope.js +46 -18
  8. package/dist/default/elements/meter.js +12 -8
  9. package/dist/default/elements/number.js +1 -1
  10. package/dist/default/elements/radio-group.js +46 -47
  11. package/dist/default/elements/readout-claim.js +0 -29
  12. package/dist/default/elements/readout.js +1 -1
  13. package/dist/default/elements/scale.js +43 -0
  14. package/dist/default/elements/slider.js +16 -3
  15. package/dist/default/elements/sonic-element.js +17 -55
  16. package/dist/default/elements/switch.js +33 -39
  17. package/dist/default/elements/value-element.js +142 -187
  18. package/dist/default/elements/wave-element.js +5 -2
  19. package/dist/default/elements/waveform.js +35 -12
  20. package/dist/default/elements/wavestrip.js +59 -18
  21. package/dist/default/elements/xy.js +34 -19
  22. package/dist/default/index.js +2 -2
  23. package/dist/default/lib/adsr-shape.js +5 -0
  24. package/dist/default/lib/ballistics.js +2 -2
  25. package/dist/default/lib/canvas-surface.js +13 -2
  26. package/dist/default/lib/distribute.js +1 -1
  27. package/dist/default/lib/drag-step.js +4 -4
  28. package/dist/default/lib/frame-timeline.js +2 -1
  29. package/dist/default/lib/marker-band.js +54 -10
  30. package/dist/default/lib/marker-rider.js +10 -4
  31. package/dist/default/lib/math.js +6 -1
  32. package/dist/default/lib/mirror-children.js +64 -0
  33. package/dist/default/lib/owned-control.js +10 -0
  34. package/dist/default/lib/pointer-drag.js +8 -5
  35. package/dist/default/lib/resample-peaks.js +11 -1
  36. package/dist/default/lib/time-regions.js +4 -1
  37. package/dist/default/lib/write-attribute.js +1 -0
  38. package/dist/dev/dat.d.ts +2 -2
  39. package/dist/dev/dat.js +5 -4
  40. package/dist/dev/dat.js.map +1 -1
  41. package/dist/dev/define/button.js +1 -1
  42. package/dist/dev/define.d.ts +4 -4
  43. package/dist/dev/define.js +2 -2
  44. package/dist/dev/elements/button.js +6 -5
  45. package/dist/dev/elements/button.js.map +1 -1
  46. package/dist/dev/elements/dial.d.ts +1 -1
  47. package/dist/dev/elements/dial.js +5 -1
  48. package/dist/dev/elements/dial.js.map +1 -1
  49. package/dist/dev/elements/envelope.js +46 -18
  50. package/dist/dev/elements/envelope.js.map +1 -1
  51. package/dist/dev/elements/meter.d.ts +1 -1
  52. package/dist/dev/elements/meter.js +12 -8
  53. package/dist/dev/elements/meter.js.map +1 -1
  54. package/dist/dev/elements/number.js +1 -1
  55. package/dist/dev/elements/number.js.map +1 -1
  56. package/dist/dev/elements/radio-group.js +46 -47
  57. package/dist/dev/elements/radio-group.js.map +1 -1
  58. package/dist/dev/elements/readout-claim.js +0 -29
  59. package/dist/dev/elements/readout-claim.js.map +1 -1
  60. package/dist/dev/elements/readout.js +1 -1
  61. package/dist/dev/elements/readout.js.map +1 -1
  62. package/dist/dev/elements/scale.js +44 -0
  63. package/dist/dev/elements/scale.js.map +1 -0
  64. package/dist/dev/elements/slider.d.ts +4 -1
  65. package/dist/dev/elements/slider.js +16 -3
  66. package/dist/dev/elements/slider.js.map +1 -1
  67. package/dist/dev/elements/sonic-element.d.ts +2 -8
  68. package/dist/dev/elements/sonic-element.js +18 -55
  69. package/dist/dev/elements/sonic-element.js.map +1 -1
  70. package/dist/dev/elements/switch.js +33 -39
  71. package/dist/dev/elements/switch.js.map +1 -1
  72. package/dist/dev/elements/value-element.d.ts +6 -8
  73. package/dist/dev/elements/value-element.js +142 -187
  74. package/dist/dev/elements/value-element.js.map +1 -1
  75. package/dist/dev/elements/wave-element.d.ts +1 -0
  76. package/dist/dev/elements/wave-element.js +5 -2
  77. package/dist/dev/elements/wave-element.js.map +1 -1
  78. package/dist/dev/elements/waveform.d.ts +1 -1
  79. package/dist/dev/elements/waveform.js +35 -12
  80. package/dist/dev/elements/waveform.js.map +1 -1
  81. package/dist/dev/elements/wavestrip.d.ts +4 -0
  82. package/dist/dev/elements/wavestrip.js +59 -18
  83. package/dist/dev/elements/wavestrip.js.map +1 -1
  84. package/dist/dev/elements/xy.d.ts +3 -3
  85. package/dist/dev/elements/xy.js +34 -19
  86. package/dist/dev/elements/xy.js.map +1 -1
  87. package/dist/dev/index.d.ts +2 -2
  88. package/dist/dev/index.js +2 -2
  89. package/dist/dev/lib/adsr-shape.js +5 -0
  90. package/dist/dev/lib/adsr-shape.js.map +1 -1
  91. package/dist/dev/lib/ballistics.js +2 -2
  92. package/dist/dev/lib/ballistics.js.map +1 -1
  93. package/dist/dev/lib/canvas-surface.d.ts +1 -0
  94. package/dist/dev/lib/canvas-surface.js +13 -2
  95. package/dist/dev/lib/canvas-surface.js.map +1 -1
  96. package/dist/dev/lib/distribute.js +1 -1
  97. package/dist/dev/lib/distribute.js.map +1 -1
  98. package/dist/dev/lib/drag-step.js +4 -4
  99. package/dist/dev/lib/drag-step.js.map +1 -1
  100. package/dist/dev/lib/frame-timeline.js +2 -1
  101. package/dist/dev/lib/frame-timeline.js.map +1 -1
  102. package/dist/dev/lib/marker-band.js +54 -10
  103. package/dist/dev/lib/marker-band.js.map +1 -1
  104. package/dist/dev/lib/marker-rider.js +10 -4
  105. package/dist/dev/lib/marker-rider.js.map +1 -1
  106. package/dist/dev/lib/math.js +6 -1
  107. package/dist/dev/lib/math.js.map +1 -1
  108. package/dist/dev/lib/mirror-children.js +80 -0
  109. package/dist/dev/lib/mirror-children.js.map +1 -0
  110. package/dist/dev/lib/owned-control.js +23 -0
  111. package/dist/dev/lib/owned-control.js.map +1 -0
  112. package/dist/dev/lib/pointer-drag.js +8 -5
  113. package/dist/dev/lib/pointer-drag.js.map +1 -1
  114. package/dist/dev/lib/resample-peaks.js +11 -1
  115. package/dist/dev/lib/resample-peaks.js.map +1 -1
  116. package/dist/dev/lib/time-regions.js +4 -1
  117. package/dist/dev/lib/time-regions.js.map +1 -1
  118. package/dist/dev/lib/write-attribute.js +1 -0
  119. package/dist/dev/lib/write-attribute.js.map +1 -1
  120. package/dist/skins/amber.css +8 -0
  121. package/dist/skins/flat.css +26 -0
  122. package/dist/skins/ivory.css +1 -0
  123. package/dist/skins/lime.css +2 -0
  124. package/dist/skins/slate.css +2 -0
  125. package/dist/styles/button.css +59 -10
  126. package/dist/styles/dial.css +24 -16
  127. package/dist/styles/envelope.css +58 -13
  128. package/dist/styles/led.css +20 -4
  129. package/dist/styles/material.css +143 -55
  130. package/dist/styles/meter.css +8 -4
  131. package/dist/styles/panel.css +7 -4
  132. package/dist/styles/segmented.css +27 -5
  133. package/dist/styles/slider.css +2 -1
  134. package/dist/styles/switch.css +65 -23
  135. package/dist/styles/waveform.css +28 -5
  136. package/dist/styles/wavestrip.css +23 -6
  137. package/dist/styles/xy.css +48 -27
  138. package/package.json +2 -5
@@ -0,0 +1,64 @@
1
+ import { appendOnce } from "./owned-control.js";
2
+ import { attachSlots } from "./render.js";
3
+
4
+ //#region src/lib/mirror-children.ts
5
+ const hostSlots = /* @__PURE__ */ new WeakMap();
6
+ function childHolding(host, node) {
7
+ let current = node;
8
+ while (current && current.parentNode !== host) current = current.parentNode;
9
+ return current ?? void 0;
10
+ }
11
+ function isCopiedChange(host, record, { control, isCopied }) {
12
+ if (record.target === host) return record.removedNodes.length > 0 || [...record.addedNodes].some((node) => node !== control);
13
+ const child = childHolding(host, record.target);
14
+ if (child === void 0) return false;
15
+ return isCopied(child) || record.type === "attributes" && record.target === child;
16
+ }
17
+ function mirrorChildren(host, { control, copy, isCopied = () => true, isPassed = () => false, place }, signal) {
18
+ const slots = hostSlots.get(host) ?? attachSlots(host);
19
+ let copies = /* @__PURE__ */ new Map();
20
+ hostSlots.set(host, slots);
21
+ const mirror = (touched) => {
22
+ appendOnce(host, control);
23
+ const children = [...host.childNodes].filter((child) => child !== control);
24
+ const slotted = children.filter((child) => child instanceof Element || child instanceof Text);
25
+ const kept = copies;
26
+ const originals = children.filter((child) => isCopied(child));
27
+ copies = new Map(originals.map((original) => [original, touched && !touched.has(original) && kept.has(original) ? kept.get(original) : copy(original)]));
28
+ place([...copies.values()].filter((made) => made !== void 0));
29
+ slots.shown.assign(...slotted.filter((child) => !isCopied(child)), control);
30
+ slots.kept.assign(...slotted.filter((child) => isCopied(child)));
31
+ };
32
+ const watch = () => {
33
+ observer.disconnect();
34
+ observer.observe(host, { childList: true });
35
+ for (const child of host.childNodes) {
36
+ if (child === control) continue;
37
+ observer.observe(child, {
38
+ attributes: true,
39
+ characterData: true,
40
+ childList: true,
41
+ subtree: true
42
+ });
43
+ }
44
+ };
45
+ const observer = new MutationObserver((records) => {
46
+ const changes = records.filter((record) => isCopiedChange(host, record, {
47
+ control,
48
+ isCopied
49
+ }));
50
+ if (changes.length === 0) return;
51
+ if (changes.some((record) => record.target === host)) {
52
+ mirror();
53
+ watch();
54
+ } else mirror(new Set(changes.map((record) => childHolding(host, record.target))));
55
+ });
56
+ mirror();
57
+ watch();
58
+ signal.addEventListener("abort", () => {
59
+ observer.disconnect();
60
+ }, { once: true });
61
+ }
62
+
63
+ //#endregion
64
+ export { mirrorChildren };
@@ -0,0 +1,10 @@
1
+ //#region src/lib/owned-control.ts
2
+ function appendOnce(host, control) {
3
+ const [hook] = control.classList;
4
+ const stale = [...host.children].filter((child) => child !== control && hook !== void 0 && child.classList.contains(hook));
5
+ for (const child of stale) child.remove();
6
+ if (control.parentNode !== host) host.append(control);
7
+ }
8
+
9
+ //#endregion
10
+ export { appendOnce };
@@ -5,8 +5,8 @@ function bindDrag(target, handlers, signal) {
5
5
  if (!held) return;
6
6
  const { drag } = held;
7
7
  held = void 0;
8
- handlers.toggle(false);
9
- handlers.release(drag);
8
+ handlers.toggle?.(false);
9
+ handlers.release?.(drag);
10
10
  };
11
11
  const whileHeld = (type, handle) => {
12
12
  target.addEventListener(type, (event) => {
@@ -23,15 +23,18 @@ function bindDrag(target, handlers, signal) {
23
23
  pointerId: event.pointerId
24
24
  };
25
25
  target.setPointerCapture(event.pointerId);
26
- handlers.toggle(true);
26
+ handlers.toggle?.(true);
27
27
  }, { signal });
28
28
  whileHeld("pointermove", handlers.move);
29
29
  whileHeld("pointercancel", (drag) => {
30
- handlers.cancel(drag);
30
+ handlers.cancel?.(drag);
31
31
  end();
32
32
  });
33
33
  whileHeld("lostpointercapture", end);
34
- whileHeld("pointerup", end);
34
+ whileHeld("pointerup", (drag, event) => {
35
+ end();
36
+ handlers.lift?.(drag, event);
37
+ });
35
38
  signal.addEventListener("abort", end, { once: true });
36
39
  return {
37
40
  current: () => held?.drag,
@@ -11,7 +11,17 @@ function downsample(peaks, count) {
11
11
  for (let source = start; source < end; source += 1) total += peaks[source] ?? 0;
12
12
  resampled.push(total / (end - start));
13
13
  }
14
- return resampled;
14
+ return rescale(resampled, peakOf(peaks));
15
+ }
16
+ function peakOf(peaks) {
17
+ let loudest = 0;
18
+ for (const peak of peaks) loudest = Math.max(loudest, peak);
19
+ return loudest;
20
+ }
21
+ function rescale(bars, loudest) {
22
+ const tallest = peakOf(bars);
23
+ if (tallest <= 0) return bars;
24
+ return bars.map((bar) => bar * loudest / tallest);
15
25
  }
16
26
  function upsample(peaks, count) {
17
27
  const resampled = [];
@@ -3,6 +3,9 @@ function readRegions(regions) {
3
3
  if ("start" in regions) return Array.from({ length: regions.length }, (_entry, index) => [regions.start(index), regions.end(index)]);
4
4
  return [...regions].map(([start, end]) => [start, end]);
5
5
  }
6
+ function sortedRegions(regions) {
7
+ return regions ? readRegions(regions).toSorted((first, second) => first[0] - second[0]) : [];
8
+ }
6
9
 
7
10
  //#endregion
8
- export { readRegions };
11
+ export { readRegions, sortedRegions };
@@ -4,6 +4,7 @@ function writeAttribute(element, name, value) {
4
4
  element.removeAttribute(name);
5
5
  return;
6
6
  }
7
+ if (element.getAttribute(name) === value) return;
7
8
  element.setAttribute(name, value);
8
9
  }
9
10
 
package/dist/dev/dat.d.ts CHANGED
@@ -12,8 +12,8 @@ export interface DatHeader {
12
12
  }
13
13
  export declare function readDatHeader(buffer: ArrayBuffer): DatHeader;
14
14
  export declare function datByteRange(header: DatHeader, fromPair: number, toPair: number): [number, number] | undefined;
15
- export declare function readDatSamples(header: DatHeader, body: ArrayBuffer): Int8Array | Int16Array;
16
- export declare function readDatPeaks(header: DatHeader, body: ArrayBuffer): WaveformPeaks;
15
+ export declare function readDatSamples(header: DatHeader, body: ArrayBuffer, byteOffset?: number): Int8Array | Int16Array;
16
+ export declare function readDatPeaks(header: DatHeader, body: ArrayBuffer, byteOffset?: number): WaveformPeaks;
17
17
  //#endregion
18
18
  export type { WaveformPeaks };
19
19
  //# sourceMappingURL=dat.d.ts.map
package/dist/dev/dat.js CHANGED
@@ -16,6 +16,7 @@ function readDatHeader(buffer) {
16
16
  if (channels < 1) throw new Error("A .dat header names no channels");
17
17
  const sampleRate = view.getInt32(8, true);
18
18
  const samplesPerPixel = view.getInt32(12, true);
19
+ if (samplesPerPixel < 1) throw new Error("A .dat header names no samples per pixel");
19
20
  return {
20
21
  bits: (view.getUint32(4, true) & 1) === 1 ? 8 : 16,
21
22
  channels,
@@ -34,15 +35,15 @@ function datByteRange(header, fromPair, toPair) {
34
35
  const pairBytes = header.channels * 2 * header.bits / 8;
35
36
  return [header.dataOffset + from * pairBytes, header.dataOffset + to * pairBytes - 1];
36
37
  }
37
- function readDatSamples(header, body) {
38
- return header.bits === 8 ? new Int8Array(body) : new Int16Array(body, 0, Math.floor(body.byteLength / 2));
38
+ function readDatSamples(header, body, byteOffset = 0) {
39
+ return header.bits === 8 ? new Int8Array(body, byteOffset) : new Int16Array(body, byteOffset, Math.floor((body.byteLength - byteOffset) / 2));
39
40
  }
40
- function readDatPeaks(header, body) {
41
+ function readDatPeaks(header, body, byteOffset = 0) {
41
42
  return {
42
43
  channels: header.channels,
43
44
  fullScale: header.bits === 8 ? 128 : 32768,
44
45
  pairsPerSecond: header.pairsPerSecond,
45
- samples: readDatSamples(header, body)
46
+ samples: readDatSamples(header, body, byteOffset)
46
47
  };
47
48
  }
48
49
 
@@ -1 +1 @@
1
- {"version":3,"file":"dat.js","names":[],"sources":["../../src/dat.ts"],"sourcesContent":["import type { WaveformPeaks } from '#lib/waveform-buckets.ts';\n\nimport { clamp } from '#lib/math.ts';\n\nexport type { WaveformPeaks } from '#lib/waveform-buckets.ts';\n\nexport interface DatHeader {\n\tbits: 8 | 16;\n\tchannels: number;\n\tdataOffset: number;\n\tpairs: number;\n\tpairsPerSecond: number;\n\tsampleRate: number;\n\tsamplesPerPixel: number;\n\tversion: 1 | 2;\n}\n\nfunction readVersion(view: DataView): 1 | 2 {\n\tconst version = view.getInt32(0, true);\n\tif (version === 1 || version === 2) return version;\n\n\tthrow new Error(`A .dat file of version ${String(version)} is not one this reads`);\n}\n\nexport function readDatHeader(buffer: ArrayBuffer): DatHeader {\n\tif (buffer.byteLength < 20) throw new Error('A .dat header takes at least 20 bytes');\n\n\tconst view = new DataView(buffer);\n\tconst version = readVersion(view);\n\tconst dataOffset = version === 1 ? 20 : 24;\n\tif (buffer.byteLength < dataOffset) {\n\t\tthrow new Error(`A version ${String(version)} .dat header takes ${String(dataOffset)} bytes`);\n\t}\n\n\tconst channels = version === 1 ? 1 : view.getInt32(20, true);\n\tif (channels < 1) throw new Error('A .dat header names no channels');\n\n\tconst sampleRate = view.getInt32(8, true);\n\tconst samplesPerPixel = view.getInt32(12, true);\n\n\treturn {\n\t\tbits: (view.getUint32(4, true) & 1) === 1 ? 8 : 16,\n\t\tchannels,\n\t\tdataOffset,\n\t\tpairs: view.getUint32(16, true),\n\t\tpairsPerSecond: sampleRate / samplesPerPixel,\n\t\tsampleRate,\n\t\tsamplesPerPixel,\n\t\tversion,\n\t};\n}\n\nexport function datByteRange(\n\theader: DatHeader,\n\tfromPair: number,\n\ttoPair: number,\n): [number, number] | undefined {\n\tconst from = clamp(Math.floor(fromPair), 0, header.pairs);\n\tconst to = clamp(Math.ceil(toPair), from, header.pairs);\n\t// A server ignores an inverted `Range` and sends the whole file\n\tif (!(to > from)) return undefined;\n\n\tconst pairBytes = (header.channels * 2 * header.bits) / 8;\n\n\treturn [header.dataOffset + from * pairBytes, header.dataOffset + to * pairBytes - 1];\n}\n\n// Typed arrays read the platform's byte order; every browser platform is little-endian, as the format is\n// A range response can end mid-sample; the odd byte is dropped\nexport function readDatSamples(header: DatHeader, body: ArrayBuffer): Int8Array | Int16Array {\n\treturn header.bits === 8\n\t\t? new Int8Array(body)\n\t\t: new Int16Array(body, 0, Math.floor(body.byteLength / 2));\n}\n\nexport function readDatPeaks(header: DatHeader, body: ArrayBuffer): WaveformPeaks {\n\treturn {\n\t\tchannels: header.channels,\n\t\tfullScale: header.bits === 8 ? 128 : 32_768,\n\t\tpairsPerSecond: header.pairsPerSecond,\n\t\tsamples: readDatSamples(header, body),\n\t};\n}\n"],"mappings":";;;AAiBA,SAAS,YAAY,MAAuB;CAC3C,MAAM,UAAU,KAAK,SAAS,GAAG,IAAI;CACrC,IAAI,YAAY,KAAK,YAAY,GAAG,OAAO;CAE3C,MAAM,IAAI,MAAM,0BAA0B,OAAO,OAAO,EAAE,uBAAuB;AAClF;AAEA,SAAgB,cAAc,QAAgC;CAC7D,IAAI,OAAO,aAAa,IAAI,MAAM,IAAI,MAAM,uCAAuC;CAEnF,MAAM,OAAO,IAAI,SAAS,MAAM;CAChC,MAAM,UAAU,YAAY,IAAI;CAChC,MAAM,aAAa,YAAY,IAAI,KAAK;CACxC,IAAI,OAAO,aAAa,YACvB,MAAM,IAAI,MAAM,aAAa,OAAO,OAAO,EAAE,qBAAqB,OAAO,UAAU,EAAE,OAAO;CAG7F,MAAM,WAAW,YAAY,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI;CAC3D,IAAI,WAAW,GAAG,MAAM,IAAI,MAAM,iCAAiC;CAEnE,MAAM,aAAa,KAAK,SAAS,GAAG,IAAI;CACxC,MAAM,kBAAkB,KAAK,SAAS,IAAI,IAAI;CAE9C,OAAO;EACN,OAAO,KAAK,UAAU,GAAG,IAAI,IAAI,OAAO,IAAI,IAAI;EAChD;EACA;EACA,OAAO,KAAK,UAAU,IAAI,IAAI;EAC9B,gBAAgB,aAAa;EAC7B;EACA;EACA;CACD;AACD;AAEA,SAAgB,aACf,QACA,UACA,QAC+B;CAC/B,MAAM,OAAO,MAAM,KAAK,MAAM,QAAQ,GAAG,GAAG,OAAO,KAAK;CACxD,MAAM,KAAK,MAAM,KAAK,KAAK,MAAM,GAAG,MAAM,OAAO,KAAK;CAEtD,IAAI,EAAE,KAAK,OAAO,OAAO;CAEzB,MAAM,YAAa,OAAO,WAAW,IAAI,OAAO,OAAQ;CAExD,OAAO,CAAC,OAAO,aAAa,OAAO,WAAW,OAAO,aAAa,KAAK,YAAY,CAAC;AACrF;AAIA,SAAgB,eAAe,QAAmB,MAA2C;CAC5F,OAAO,OAAO,SAAS,IACpB,IAAI,UAAU,IAAI,IAClB,IAAI,WAAW,MAAM,GAAG,KAAK,MAAM,KAAK,aAAa,CAAC,CAAC;AAC3D;AAEA,SAAgB,aAAa,QAAmB,MAAkC;CACjF,OAAO;EACN,UAAU,OAAO;EACjB,WAAW,OAAO,SAAS,IAAI,MAAM;EACrC,gBAAgB,OAAO;EACvB,SAAS,eAAe,QAAQ,IAAI;CACrC;AACD"}
1
+ {"version":3,"file":"dat.js","names":[],"sources":["../../src/dat.ts"],"sourcesContent":["import type { WaveformPeaks } from '#lib/waveform-buckets.ts';\n\nimport { clamp } from '#lib/math.ts';\n\nexport type { WaveformPeaks } from '#lib/waveform-buckets.ts';\n\nexport interface DatHeader {\n\tbits: 8 | 16;\n\tchannels: number;\n\tdataOffset: number;\n\tpairs: number;\n\tpairsPerSecond: number;\n\tsampleRate: number;\n\tsamplesPerPixel: number;\n\tversion: 1 | 2;\n}\n\nfunction readVersion(view: DataView): 1 | 2 {\n\tconst version = view.getInt32(0, true);\n\tif (version === 1 || version === 2) return version;\n\n\tthrow new Error(`A .dat file of version ${String(version)} is not one this reads`);\n}\n\nexport function readDatHeader(buffer: ArrayBuffer): DatHeader {\n\tif (buffer.byteLength < 20) throw new Error('A .dat header takes at least 20 bytes');\n\n\tconst view = new DataView(buffer);\n\tconst version = readVersion(view);\n\tconst dataOffset = version === 1 ? 20 : 24;\n\tif (buffer.byteLength < dataOffset) {\n\t\tthrow new Error(`A version ${String(version)} .dat header takes ${String(dataOffset)} bytes`);\n\t}\n\n\tconst channels = version === 1 ? 1 : view.getInt32(20, true);\n\tif (channels < 1) throw new Error('A .dat header names no channels');\n\n\tconst sampleRate = view.getInt32(8, true);\n\tconst samplesPerPixel = view.getInt32(12, true);\n\tif (samplesPerPixel < 1) throw new Error('A .dat header names no samples per pixel');\n\n\treturn {\n\t\tbits: (view.getUint32(4, true) & 1) === 1 ? 8 : 16,\n\t\tchannels,\n\t\tdataOffset,\n\t\tpairs: view.getUint32(16, true),\n\t\tpairsPerSecond: sampleRate / samplesPerPixel,\n\t\tsampleRate,\n\t\tsamplesPerPixel,\n\t\tversion,\n\t};\n}\n\nexport function datByteRange(\n\theader: DatHeader,\n\tfromPair: number,\n\ttoPair: number,\n): [number, number] | undefined {\n\tconst from = clamp(Math.floor(fromPair), 0, header.pairs);\n\tconst to = clamp(Math.ceil(toPair), from, header.pairs);\n\t// A server ignores an inverted `Range` and sends the whole file\n\tif (!(to > from)) return undefined;\n\n\tconst pairBytes = (header.channels * 2 * header.bits) / 8;\n\n\treturn [header.dataOffset + from * pairBytes, header.dataOffset + to * pairBytes - 1];\n}\n\n// Typed arrays read the platform's byte order; every browser platform is little-endian, as the format is\n// A range response can end mid-sample; the odd byte is dropped\nexport function readDatSamples(\n\theader: DatHeader,\n\tbody: ArrayBuffer,\n\tbyteOffset = 0,\n): Int8Array | Int16Array {\n\treturn header.bits === 8\n\t\t? new Int8Array(body, byteOffset)\n\t\t: new Int16Array(body, byteOffset, Math.floor((body.byteLength - byteOffset) / 2));\n}\n\nexport function readDatPeaks(header: DatHeader, body: ArrayBuffer, byteOffset = 0): WaveformPeaks {\n\treturn {\n\t\tchannels: header.channels,\n\t\tfullScale: header.bits === 8 ? 128 : 32_768,\n\t\tpairsPerSecond: header.pairsPerSecond,\n\t\tsamples: readDatSamples(header, body, byteOffset),\n\t};\n}\n"],"mappings":";;;AAiBA,SAAS,YAAY,MAAuB;CAC3C,MAAM,UAAU,KAAK,SAAS,GAAG,IAAI;CACrC,IAAI,YAAY,KAAK,YAAY,GAAG,OAAO;CAE3C,MAAM,IAAI,MAAM,0BAA0B,OAAO,OAAO,EAAE,uBAAuB;AAClF;AAEA,SAAgB,cAAc,QAAgC;CAC7D,IAAI,OAAO,aAAa,IAAI,MAAM,IAAI,MAAM,uCAAuC;CAEnF,MAAM,OAAO,IAAI,SAAS,MAAM;CAChC,MAAM,UAAU,YAAY,IAAI;CAChC,MAAM,aAAa,YAAY,IAAI,KAAK;CACxC,IAAI,OAAO,aAAa,YACvB,MAAM,IAAI,MAAM,aAAa,OAAO,OAAO,EAAE,qBAAqB,OAAO,UAAU,EAAE,OAAO;CAG7F,MAAM,WAAW,YAAY,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI;CAC3D,IAAI,WAAW,GAAG,MAAM,IAAI,MAAM,iCAAiC;CAEnE,MAAM,aAAa,KAAK,SAAS,GAAG,IAAI;CACxC,MAAM,kBAAkB,KAAK,SAAS,IAAI,IAAI;CAC9C,IAAI,kBAAkB,GAAG,MAAM,IAAI,MAAM,0CAA0C;CAEnF,OAAO;EACN,OAAO,KAAK,UAAU,GAAG,IAAI,IAAI,OAAO,IAAI,IAAI;EAChD;EACA;EACA,OAAO,KAAK,UAAU,IAAI,IAAI;EAC9B,gBAAgB,aAAa;EAC7B;EACA;EACA;CACD;AACD;AAEA,SAAgB,aACf,QACA,UACA,QAC+B;CAC/B,MAAM,OAAO,MAAM,KAAK,MAAM,QAAQ,GAAG,GAAG,OAAO,KAAK;CACxD,MAAM,KAAK,MAAM,KAAK,KAAK,MAAM,GAAG,MAAM,OAAO,KAAK;CAEtD,IAAI,EAAE,KAAK,OAAO,OAAO;CAEzB,MAAM,YAAa,OAAO,WAAW,IAAI,OAAO,OAAQ;CAExD,OAAO,CAAC,OAAO,aAAa,OAAO,WAAW,OAAO,aAAa,KAAK,YAAY,CAAC;AACrF;AAIA,SAAgB,eACf,QACA,MACA,aAAa,GACY;CACzB,OAAO,OAAO,SAAS,IACpB,IAAI,UAAU,MAAM,UAAU,IAC9B,IAAI,WAAW,MAAM,YAAY,KAAK,OAAO,KAAK,aAAa,cAAc,CAAC,CAAC;AACnF;AAEA,SAAgB,aAAa,QAAmB,MAAmB,aAAa,GAAkB;CACjG,OAAO;EACN,UAAU,OAAO;EACjB,WAAW,OAAO,SAAS,IAAI,MAAM;EACrC,gBAAgB,OAAO;EACvB,SAAS,eAAe,QAAQ,MAAM,UAAU;CACjD;AACD"}
@@ -1,5 +1,5 @@
1
- import { defineOnce } from "../elements/define-once.js";
2
1
  import { SonicButton } from "../elements/button.js";
2
+ import { defineOnce } from "../elements/define-once.js";
3
3
 
4
4
  //#region src/define/button.ts
5
5
  defineOnce("sonic-button", SonicButton);
@@ -1,11 +1,9 @@
1
+ import { SonicButton } from "./elements/button.js";
2
+ import "./define/button.js";
1
3
  import { SonicDial } from "./elements/dial.js";
2
4
  import "./define/dial.js";
3
5
  import { SonicEnvelope } from "./elements/envelope.js";
4
6
  import "./define/envelope.js";
5
- import { SonicButton } from "./elements/button.js";
6
- import "./define/button.js";
7
- import { SonicSwitch } from "./elements/switch.js";
8
- import "./define/switch.js";
9
7
  import { SonicMeter } from "./elements/meter.js";
10
8
  import "./define/meter.js";
11
9
  import { SonicNumber } from "./elements/number.js";
@@ -16,6 +14,8 @@ import { SonicSlider } from "./elements/slider.js";
16
14
  import "./define/slider.js";
17
15
  import { SonicSplit } from "./elements/split.js";
18
16
  import "./define/split.js";
17
+ import { SonicSwitch } from "./elements/switch.js";
18
+ import "./define/switch.js";
19
19
  import { SonicWaveform } from "./elements/waveform.js";
20
20
  import "./define/waveform.js";
21
21
  import { SonicWavestrip } from "./elements/wavestrip.js";
@@ -1,12 +1,12 @@
1
+ import "./define/button.js";
1
2
  import "./define/dial.js";
2
3
  import "./define/envelope.js";
3
- import "./define/button.js";
4
- import "./define/switch.js";
5
4
  import "./define/meter.js";
6
5
  import "./define/number.js";
7
6
  import "./define/segmented.js";
8
7
  import "./define/slider.js";
9
8
  import "./define/split.js";
9
+ import "./define/switch.js";
10
10
  import "./define/waveform.js";
11
11
  import "./define/wavestrip.js";
12
12
  import "./define/xy.js";
@@ -1,8 +1,9 @@
1
- import { placeChildren, requireChild, template } from "../lib/render.js";
2
1
  import { writeAttribute } from "../lib/write-attribute.js";
3
2
  import { SonicFormElement } from "./form-element.js";
4
3
  import { copyNode } from "../lib/copy-node.js";
5
4
  import { bindHold } from "../lib/hold.js";
5
+ import { placeChildren, requireChild, template } from "../lib/render.js";
6
+ import { mirrorChildren } from "../lib/mirror-children.js";
6
7
 
7
8
  //#region src/elements/button.ts
8
9
  const renderButton = template(`
@@ -88,7 +89,7 @@ var SonicButton = class extends SonicFormElement {
88
89
  const button = this.#button;
89
90
  const cap = requireChild(button, ".sonic-button-cap", HTMLSpanElement);
90
91
  this.upgradeProperties("armed", "busy", "defaultPressed", "momentary", "softDisabled", "latching", "pressed", "value");
91
- this.mirrorChildren({
92
+ mirrorChildren(this, {
92
93
  control: button,
93
94
  copy: copyNode,
94
95
  place: (copies) => {
@@ -113,12 +114,12 @@ var SonicButton = class extends SonicFormElement {
113
114
  return this.#button;
114
115
  }
115
116
  render() {
117
+ if (!this.isBound()) return;
116
118
  const button = this.#button;
117
- button.disabled = this.isDisabled();
119
+ writeAttribute(button, "disabled", this.isDisabled() ? "" : void 0);
118
120
  writeAttribute(button, "aria-disabled", this.#isSoftDisabled() ? "true" : void 0);
119
121
  writeAttribute(button, "aria-busy", this.busy ? "true" : void 0);
120
- button.removeAttribute("aria-pressed");
121
- if (this.latching) button.setAttribute("aria-pressed", String(this.pressed));
122
+ writeAttribute(button, "aria-pressed", this.latching ? String(this.pressed) : void 0);
122
123
  this.toggleState("pressed", this.pressed);
123
124
  this.forwardNaming(button, true);
124
125
  const submitted = this.latching && this.pressed ? this.value : null;
@@ -1 +1 @@
1
- {"version":3,"file":"button.js","names":[],"sources":["../../../src/elements/button.ts"],"sourcesContent":["import type { Hold } from '#lib/hold.ts';\n\nimport { SonicFormElement } from '#elements/form-element.ts';\nimport { copyNode } from '#lib/copy-node.ts';\nimport { bindHold } from '#lib/hold.ts';\nimport { placeChildren, requireChild, template } from '#lib/render.ts';\nimport { writeAttribute } from '#lib/write-attribute.ts';\n\ndeclare global {\n\tinterface HTMLElementTagNameMap {\n\t\t'sonic-button': SonicButton;\n\t}\n}\n\nconst renderButton = template(\n\t/* HTML */ `\n\t\t<button class=\"sonic-button\" type=\"button\"><span class=\"sonic-button-cap\"></span></button>\n\t`,\n\tHTMLButtonElement,\n);\n\nconst holdKeys = new Set([' ', 'Enter']);\n\nexport class SonicButton extends SonicFormElement {\n\tstatic override readonly observedAttributes = [\n\t\t...SonicFormElement.observedAttributes,\n\t\t'busy',\n\t\t'momentary',\n\t\t'pressed',\n\t\t'soft-disabled',\n\t\t'latching',\n\t\t'value',\n\t];\n\n\tget armed(): boolean {\n\t\treturn this.hasAttribute('armed');\n\t}\n\n\tset armed(isArmed: boolean) {\n\t\tthis.reflect('armed', isArmed);\n\t}\n\n\tget busy(): boolean {\n\t\treturn this.hasAttribute('busy');\n\t}\n\n\tset busy(isBusy: boolean) {\n\t\tthis.reflect('busy', isBusy);\n\t}\n\n\tget defaultPressed(): boolean {\n\t\treturn this.hasAttribute('pressed');\n\t}\n\n\tset defaultPressed(isPressed: boolean) {\n\t\tthis.reflect('pressed', isPressed);\n\t}\n\n\tget latching(): boolean {\n\t\treturn this.hasAttribute('latching');\n\t}\n\n\tset latching(isLatching: boolean) {\n\t\tthis.reflect('latching', isLatching);\n\t}\n\n\tget momentary(): boolean {\n\t\treturn this.hasAttribute('momentary');\n\t}\n\n\tset momentary(isMomentary: boolean) {\n\t\tthis.reflect('momentary', isMomentary);\n\t}\n\n\tget pressed(): boolean {\n\t\treturn this.#isDirty ? this.#pressed : this.defaultPressed;\n\t}\n\n\tset pressed(isPressed: boolean | undefined) {\n\t\tthis.#isDirty = true;\n\t\tthis.#pressed = isPressed === true;\n\t\tthis.render();\n\t}\n\n\tget softDisabled(): boolean {\n\t\treturn this.hasAttribute('soft-disabled');\n\t}\n\n\tset softDisabled(isSoftDisabled: boolean) {\n\t\tthis.reflect('soft-disabled', isSoftDisabled);\n\t}\n\n\tget value(): string {\n\t\treturn this.getAttribute('value') ?? 'on';\n\t}\n\n\tset value(value: string | undefined) {\n\t\tthis.reflect('value', value);\n\t}\n\n\treadonly #button = renderButton();\n\n\t#holding: Hold | undefined;\n\n\t#isDirty = false;\n\n\t#pressed = false;\n\n\tattributeChangedCallback(name: string): void {\n\t\tif (name === 'pressed') this.#isDirty = false;\n\t\tif (this.isDisabled() || this.#isSoftDisabled()) this.#holding?.release();\n\t\tthis.render();\n\t}\n\n\toverride formResetCallback(): void {\n\t\tthis.attributeChangedCallback('pressed');\n\t}\n\n\tprotected override activate(): void {\n\t\tthis.#button.click();\n\t}\n\n\tprotected connect(signal: AbortSignal): void {\n\t\tconst button = this.#button;\n\t\tconst cap = requireChild(button, '.sonic-button-cap', HTMLSpanElement);\n\n\t\tthis.upgradeProperties(\n\t\t\t'armed',\n\t\t\t'busy',\n\t\t\t'defaultPressed',\n\t\t\t'momentary',\n\t\t\t'softDisabled',\n\t\t\t'latching',\n\t\t\t'pressed',\n\t\t\t'value',\n\t\t);\n\t\tthis.mirrorChildren(\n\t\t\t{\n\t\t\t\tcontrol: button,\n\t\t\t\tcopy: copyNode,\n\t\t\t\tplace: (copies) => {\n\t\t\t\t\tplaceChildren(cap, copies);\n\t\t\t\t},\n\t\t\t},\n\t\t\tsignal,\n\t\t);\n\t\tthis.render();\n\t\tthis.checkStyles(button, 'button.css');\n\n\t\tbutton.addEventListener(\n\t\t\t'click',\n\t\t\t(event) => {\n\t\t\t\t// A native disabled button fires no click at all\n\t\t\t\tif (this.#isSoftDisabled()) {\n\t\t\t\t\tevent.preventDefault();\n\t\t\t\t\tevent.stopImmediatePropagation();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (!this.latching) return;\n\n\t\t\t\tthis.pressed = !this.pressed;\n\t\t\t\tthis.dispatchEvent(new Event('change', { bubbles: true }));\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t\tthis.#bindMomentary(button, signal);\n\t}\n\n\tprotected override focusTarget(): HTMLElement {\n\t\treturn this.#button;\n\t}\n\n\tprotected render(): void {\n\t\tconst button = this.#button;\n\n\t\tbutton.disabled = this.isDisabled();\n\t\twriteAttribute(button, 'aria-disabled', this.#isSoftDisabled() ? 'true' : undefined);\n\t\twriteAttribute(button, 'aria-busy', this.busy ? 'true' : undefined);\n\t\tbutton.removeAttribute('aria-pressed');\n\t\tif (this.latching) button.setAttribute('aria-pressed', String(this.pressed));\n\t\tthis.toggleState('pressed', this.pressed);\n\t\tthis.forwardNaming(button, true);\n\n\t\t// eslint-disable-next-line unicorn/no-null -- `null` submits nothing\n\t\tconst submitted = this.latching && this.pressed ? this.value : null;\n\n\t\tthis.writeFormValue(submitted, String(this.pressed));\n\t}\n\n\tprotected restoreState(state: string): void {\n\t\tif (this.latching) this.pressed = state === 'true';\n\t}\n\n\t#bindMomentary(button: HTMLButtonElement, signal: AbortSignal): void {\n\t\tconst holding = bindHold(\n\t\t\tbutton,\n\t\t\t{\n\t\t\t\tcanHold: () => !this.isDisabled() && !this.#isSoftDisabled(),\n\t\t\t\tleave: 'blur',\n\t\t\t\tonHold: () => {\n\t\t\t\t\tthis.#press(true);\n\t\t\t\t},\n\t\t\t\tonRelease: () => {\n\t\t\t\t\tthis.#press(false);\n\t\t\t\t},\n\t\t\t},\n\t\t\tsignal,\n\t\t);\n\n\t\tthis.#holding = holding;\n\t\tbutton.addEventListener(\n\t\t\t'pointerdown',\n\t\t\t(event) => {\n\t\t\t\tif (event.button !== 0 || !this.#isMomentary()) return;\n\n\t\t\t\tbutton.setPointerCapture(event.pointerId);\n\t\t\t\tholding.hold(event.pointerId);\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t\tbutton.addEventListener(\n\t\t\t'keydown',\n\t\t\t(event) => {\n\t\t\t\tif (event.repeat || !holdKeys.has(event.key) || !this.#isMomentary()) return;\n\n\t\t\t\tholding.hold(event.key);\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t}\n\n\t#isMomentary(): boolean {\n\t\treturn this.momentary && !this.latching;\n\t}\n\n\t#isSoftDisabled(): boolean {\n\t\treturn this.softDisabled && !this.isDisabled();\n\t}\n\n\t#press(isPressed: boolean): void {\n\t\tthis.pressed = isPressed;\n\t\tthis.dispatchEvent(new Event('change', { bubbles: true }));\n\t}\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"button.js","names":[],"sources":["../../../src/elements/button.ts"],"sourcesContent":["import type { Hold } from '#lib/hold.ts';\n\nimport { SonicFormElement } from '#elements/form-element.ts';\nimport { copyNode } from '#lib/copy-node.ts';\nimport { bindHold } from '#lib/hold.ts';\nimport { mirrorChildren } from '#lib/mirror-children.ts';\nimport { placeChildren, requireChild, template } from '#lib/render.ts';\nimport { writeAttribute } from '#lib/write-attribute.ts';\n\ndeclare global {\n\tinterface HTMLElementTagNameMap {\n\t\t'sonic-button': SonicButton;\n\t}\n}\n\nconst renderButton = template(\n\t/* HTML */ `\n\t\t<button class=\"sonic-button\" type=\"button\"><span class=\"sonic-button-cap\"></span></button>\n\t`,\n\tHTMLButtonElement,\n);\n\nconst holdKeys = new Set([' ', 'Enter']);\n\nexport class SonicButton extends SonicFormElement {\n\tstatic override readonly observedAttributes = [\n\t\t...SonicFormElement.observedAttributes,\n\t\t'busy',\n\t\t'momentary',\n\t\t'pressed',\n\t\t'soft-disabled',\n\t\t'latching',\n\t\t'value',\n\t];\n\n\tget armed(): boolean {\n\t\treturn this.hasAttribute('armed');\n\t}\n\n\tset armed(isArmed: boolean) {\n\t\tthis.reflect('armed', isArmed);\n\t}\n\n\tget busy(): boolean {\n\t\treturn this.hasAttribute('busy');\n\t}\n\n\tset busy(isBusy: boolean) {\n\t\tthis.reflect('busy', isBusy);\n\t}\n\n\tget defaultPressed(): boolean {\n\t\treturn this.hasAttribute('pressed');\n\t}\n\n\tset defaultPressed(isPressed: boolean) {\n\t\tthis.reflect('pressed', isPressed);\n\t}\n\n\tget latching(): boolean {\n\t\treturn this.hasAttribute('latching');\n\t}\n\n\tset latching(isLatching: boolean) {\n\t\tthis.reflect('latching', isLatching);\n\t}\n\n\tget momentary(): boolean {\n\t\treturn this.hasAttribute('momentary');\n\t}\n\n\tset momentary(isMomentary: boolean) {\n\t\tthis.reflect('momentary', isMomentary);\n\t}\n\n\tget pressed(): boolean {\n\t\treturn this.#isDirty ? this.#pressed : this.defaultPressed;\n\t}\n\n\tset pressed(isPressed: boolean | undefined) {\n\t\tthis.#isDirty = true;\n\t\tthis.#pressed = isPressed === true;\n\t\tthis.render();\n\t}\n\n\tget softDisabled(): boolean {\n\t\treturn this.hasAttribute('soft-disabled');\n\t}\n\n\tset softDisabled(isSoftDisabled: boolean) {\n\t\tthis.reflect('soft-disabled', isSoftDisabled);\n\t}\n\n\tget value(): string {\n\t\treturn this.getAttribute('value') ?? 'on';\n\t}\n\n\tset value(value: string | undefined) {\n\t\tthis.reflect('value', value);\n\t}\n\n\treadonly #button = renderButton();\n\n\t#holding: Hold | undefined;\n\n\t#isDirty = false;\n\n\t#pressed = false;\n\n\tattributeChangedCallback(name: string): void {\n\t\tif (name === 'pressed') this.#isDirty = false;\n\t\tif (this.isDisabled() || this.#isSoftDisabled()) this.#holding?.release();\n\t\tthis.render();\n\t}\n\n\toverride formResetCallback(): void {\n\t\tthis.attributeChangedCallback('pressed');\n\t}\n\n\tprotected override activate(): void {\n\t\tthis.#button.click();\n\t}\n\n\tprotected connect(signal: AbortSignal): void {\n\t\tconst button = this.#button;\n\t\tconst cap = requireChild(button, '.sonic-button-cap', HTMLSpanElement);\n\n\t\tthis.upgradeProperties(\n\t\t\t'armed',\n\t\t\t'busy',\n\t\t\t'defaultPressed',\n\t\t\t'momentary',\n\t\t\t'softDisabled',\n\t\t\t'latching',\n\t\t\t'pressed',\n\t\t\t'value',\n\t\t);\n\t\tmirrorChildren(\n\t\t\tthis,\n\t\t\t{\n\t\t\t\tcontrol: button,\n\t\t\t\tcopy: copyNode,\n\t\t\t\tplace: (copies) => {\n\t\t\t\t\tplaceChildren(cap, copies);\n\t\t\t\t},\n\t\t\t},\n\t\t\tsignal,\n\t\t);\n\t\tthis.render();\n\t\tthis.checkStyles(button, 'button.css');\n\n\t\tbutton.addEventListener(\n\t\t\t'click',\n\t\t\t(event) => {\n\t\t\t\t// A native disabled button fires no click at all\n\t\t\t\tif (this.#isSoftDisabled()) {\n\t\t\t\t\tevent.preventDefault();\n\t\t\t\t\tevent.stopImmediatePropagation();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (!this.latching) return;\n\n\t\t\t\tthis.pressed = !this.pressed;\n\t\t\t\tthis.dispatchEvent(new Event('change', { bubbles: true }));\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t\tthis.#bindMomentary(button, signal);\n\t}\n\n\tprotected override focusTarget(): HTMLElement {\n\t\treturn this.#button;\n\t}\n\n\tprotected render(): void {\n\t\tif (!this.isBound()) return;\n\n\t\tconst button = this.#button;\n\n\t\twriteAttribute(button, 'disabled', this.isDisabled() ? '' : undefined);\n\t\twriteAttribute(button, 'aria-disabled', this.#isSoftDisabled() ? 'true' : undefined);\n\t\twriteAttribute(button, 'aria-busy', this.busy ? 'true' : undefined);\n\t\twriteAttribute(button, 'aria-pressed', this.latching ? String(this.pressed) : undefined);\n\t\tthis.toggleState('pressed', this.pressed);\n\t\tthis.forwardNaming(button, true);\n\n\t\t// eslint-disable-next-line unicorn/no-null -- `null` submits nothing\n\t\tconst submitted = this.latching && this.pressed ? this.value : null;\n\n\t\tthis.writeFormValue(submitted, String(this.pressed));\n\t}\n\n\tprotected restoreState(state: string): void {\n\t\tif (this.latching) this.pressed = state === 'true';\n\t}\n\n\t#bindMomentary(button: HTMLButtonElement, signal: AbortSignal): void {\n\t\tconst holding = bindHold(\n\t\t\tbutton,\n\t\t\t{\n\t\t\t\tcanHold: () => !this.isDisabled() && !this.#isSoftDisabled(),\n\t\t\t\tleave: 'blur',\n\t\t\t\tonHold: () => {\n\t\t\t\t\tthis.#press(true);\n\t\t\t\t},\n\t\t\t\tonRelease: () => {\n\t\t\t\t\tthis.#press(false);\n\t\t\t\t},\n\t\t\t},\n\t\t\tsignal,\n\t\t);\n\n\t\tthis.#holding = holding;\n\t\tbutton.addEventListener(\n\t\t\t'pointerdown',\n\t\t\t(event) => {\n\t\t\t\tif (event.button !== 0 || !this.#isMomentary()) return;\n\n\t\t\t\tbutton.setPointerCapture(event.pointerId);\n\t\t\t\tholding.hold(event.pointerId);\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t\tbutton.addEventListener(\n\t\t\t'keydown',\n\t\t\t(event) => {\n\t\t\t\tif (event.repeat || !holdKeys.has(event.key) || !this.#isMomentary()) return;\n\n\t\t\t\tholding.hold(event.key);\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t}\n\n\t#isMomentary(): boolean {\n\t\treturn this.momentary && !this.latching;\n\t}\n\n\t#isSoftDisabled(): boolean {\n\t\treturn this.softDisabled && !this.isDisabled();\n\t}\n\n\t#press(isPressed: boolean): void {\n\t\tthis.pressed = isPressed;\n\t\tthis.dispatchEvent(new Event('change', { bubbles: true }));\n\t}\n}\n"],"mappings":"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"}
@@ -13,7 +13,7 @@ export declare class SonicDial extends SonicValueElement {
13
13
  get modulation(): number;
14
14
  set modulation(amount: number | undefined);
15
15
  get modulationValue(): number | undefined;
16
- set modulationValue(value: number | undefined);
16
+ set modulationValue(value: null | number | undefined);
17
17
  connectedCallback(): void;
18
18
  protected connect(signal: AbortSignal): void;
19
19
  protected draw(): void;
@@ -1,5 +1,6 @@
1
1
  import { requireChild, template } from "../lib/render.js";
2
2
  import { SonicValueElement } from "./value-element.js";
3
+ import { bindScale } from "./scale.js";
3
4
 
4
5
  //#region src/elements/dial.ts
5
6
  const renderDial = template(`
@@ -54,7 +55,10 @@ var SonicDial = class extends SonicValueElement {
54
55
  }
55
56
  connect(signal) {
56
57
  const dial = this.#dial;
57
- this.bindScale(dial, requireChild(dial, ".sonic-dial-scale", HTMLDivElement), signal);
58
+ bindScale(this, {
59
+ control: dial,
60
+ marks: requireChild(dial, ".sonic-dial-scale", HTMLDivElement)
61
+ }, signal);
58
62
  this.render();
59
63
  this.checkStyles(dial, "dial.css");
60
64
  this.bindGestures(dial, signal, () => ({
@@ -1 +1 @@
1
- {"version":3,"file":"dial.js","names":[],"sources":["../../../src/elements/dial.ts"],"sourcesContent":["import { SonicValueElement } from '#elements/value-element.ts';\nimport { requireChild, template } from '#lib/render.ts';\n\ndeclare global {\n\tinterface HTMLElementTagNameMap {\n\t\t'sonic-dial': SonicDial;\n\t}\n}\n\nconst renderDial = template(\n\t/* HTML */ `\n\t\t<div class=\"sonic-dial\" role=\"slider\" tabindex=\"0\">\n\t\t\t<div class=\"sonic-dial-scale\" aria-hidden=\"true\"></div>\n\t\t\t<div class=\"sonic-dial-notches\"></div>\n\t\t\t<div class=\"sonic-dial-ring\"></div>\n\t\t\t<div class=\"sonic-dial-modulation\"></div>\n\t\t\t<div class=\"sonic-dial-cap\"></div>\n\t\t\t<div class=\"sonic-dial-indicator\"></div>\n\t\t\t<div class=\"sonic-dial-readout\" popover=\"manual\">\n\t\t\t\t<span></span>\n\t\t\t\t<input\n\t\t\t\t\tautocomplete=\"off\"\n\t\t\t\t\tclass=\"sonic-dial-entry\"\n\t\t\t\t\tenterkeyhint=\"done\"\n\t\t\t\t\thidden\n\t\t\t\t\tspellcheck=\"false\"\n\t\t\t\t\ttype=\"text\"\n\t\t\t\t/>\n\t\t\t</div>\n\t\t</div>\n\t`,\n\tHTMLDivElement,\n);\n\nexport class SonicDial extends SonicValueElement {\n\tstatic override readonly observedAttributes = [\n\t\t...SonicValueElement.observedAttributes,\n\t\t'endless',\n\t\t'modulation',\n\t];\n\n\tget endless(): boolean {\n\t\treturn this.hasAttribute('endless');\n\t}\n\n\tset endless(isEndless: boolean) {\n\t\tthis.reflect('endless', isEndless);\n\t}\n\n\t// fallow-ignore-next-line code-duplication -- one accessor pair per reflected attribute\n\tget modulation(): number {\n\t\treturn this.numberAttribute('modulation', 0);\n\t}\n\n\tset modulation(amount: number | undefined) {\n\t\tthis.reflect('modulation', amount);\n\t}\n\n\t// fallow-ignore-next-line code-duplication -- one accessor pair per property\n\tget modulationValue(): number | undefined {\n\t\treturn this.readModulationValue();\n\t}\n\n\tset modulationValue(value: number | undefined) {\n\t\tthis.writeModulationValue(this.#dial, 'dial', value);\n\t}\n\n\treadonly #dial = renderDial();\n\n\toverride connectedCallback(): void {\n\t\tthis.upgradeProperties('endless', 'modulationValue', 'modulation');\n\t\tsuper.connectedCallback();\n\t}\n\n\tprotected connect(signal: AbortSignal): void {\n\t\tconst dial = this.#dial;\n\n\t\tthis.bindScale(dial, requireChild(dial, '.sonic-dial-scale', HTMLDivElement), signal);\n\t\tthis.render();\n\t\tthis.checkStyles(dial, 'dial.css');\n\t\tthis.bindGestures(dial, signal, () => ({\n\t\t\tposition: (event) => -event.clientY,\n\t\t\ttravelPx: this.travelPx(dial, '--_sonic-dial-travel'),\n\t\t}));\n\t}\n\n\tprotected draw(): void {\n\t\tthis.writeProportions(this.#dial, 'dial', this.endless ? 0 : this.modulation);\n\t}\n\n\tprotected override focusTarget(): HTMLElement {\n\t\treturn this.#dial;\n\t}\n\n\tprotected override isWrapping(): boolean {\n\t\treturn this.endless;\n\t}\n}\n"],"mappings":";;;;AASA,MAAM,aAAa,SACP;;;;;;;;;;;;;;;;;;;;IAqBX,cACD;AAEA,IAAa,YAAb,cAA+B,kBAAkB;CAChD,OAAyB,qBAAqB;EAC7C,GAAG,kBAAkB;EACrB;EACA;CACD;CAEA,IAAI,UAAmB;EACtB,OAAO,KAAK,aAAa,SAAS;CACnC;CAEA,IAAI,QAAQ,WAAoB;EAC/B,KAAK,QAAQ,WAAW,SAAS;CAClC;CAGA,IAAI,aAAqB;EACxB,OAAO,KAAK,gBAAgB,cAAc,CAAC;CAC5C;CAEA,IAAI,WAAW,QAA4B;EAC1C,KAAK,QAAQ,cAAc,MAAM;CAClC;CAGA,IAAI,kBAAsC;EACzC,OAAO,KAAK,oBAAoB;CACjC;CAEA,IAAI,gBAAgB,OAA2B;EAC9C,KAAK,qBAAqB,KAAK,OAAO,QAAQ,KAAK;CACpD;CAEA,AAAS,QAAQ,WAAW;CAE5B,AAAS,oBAA0B;EAClC,KAAK,kBAAkB,WAAW,mBAAmB,YAAY;EACjE,MAAM,kBAAkB;CACzB;CAEA,AAAU,QAAQ,QAA2B;EAC5C,MAAM,OAAO,KAAK;EAElB,KAAK,UAAU,MAAM,aAAa,MAAM,qBAAqB,cAAc,GAAG,MAAM;EACpF,KAAK,OAAO;EACZ,KAAK,YAAY,MAAM,UAAU;EACjC,KAAK,aAAa,MAAM,eAAe;GACtC,WAAW,UAAU,CAAC,MAAM;GAC5B,UAAU,KAAK,SAAS,MAAM,sBAAsB;EACrD,EAAE;CACH;CAEA,AAAU,OAAa;EACtB,KAAK,iBAAiB,KAAK,OAAO,QAAQ,KAAK,UAAU,IAAI,KAAK,UAAU;CAC7E;CAEA,AAAmB,cAA2B;EAC7C,OAAO,KAAK;CACb;CAEA,AAAmB,aAAsB;EACxC,OAAO,KAAK;CACb;AACD"}
1
+ {"version":3,"file":"dial.js","names":[],"sources":["../../../src/elements/dial.ts"],"sourcesContent":["import { bindScale } from '#elements/scale.ts';\nimport { SonicValueElement } from '#elements/value-element.ts';\nimport { requireChild, template } from '#lib/render.ts';\n\ndeclare global {\n\tinterface HTMLElementTagNameMap {\n\t\t'sonic-dial': SonicDial;\n\t}\n}\n\nconst renderDial = template(\n\t/* HTML */ `\n\t\t<div class=\"sonic-dial\" role=\"slider\" tabindex=\"0\">\n\t\t\t<div class=\"sonic-dial-scale\" aria-hidden=\"true\"></div>\n\t\t\t<div class=\"sonic-dial-notches\"></div>\n\t\t\t<div class=\"sonic-dial-ring\"></div>\n\t\t\t<div class=\"sonic-dial-modulation\"></div>\n\t\t\t<div class=\"sonic-dial-cap\"></div>\n\t\t\t<div class=\"sonic-dial-indicator\"></div>\n\t\t\t<div class=\"sonic-dial-readout\" popover=\"manual\">\n\t\t\t\t<span></span>\n\t\t\t\t<input\n\t\t\t\t\tautocomplete=\"off\"\n\t\t\t\t\tclass=\"sonic-dial-entry\"\n\t\t\t\t\tenterkeyhint=\"done\"\n\t\t\t\t\thidden\n\t\t\t\t\tspellcheck=\"false\"\n\t\t\t\t\ttype=\"text\"\n\t\t\t\t/>\n\t\t\t</div>\n\t\t</div>\n\t`,\n\tHTMLDivElement,\n);\n\nexport class SonicDial extends SonicValueElement {\n\tstatic override readonly observedAttributes = [\n\t\t...SonicValueElement.observedAttributes,\n\t\t'endless',\n\t\t'modulation',\n\t];\n\n\tget endless(): boolean {\n\t\treturn this.hasAttribute('endless');\n\t}\n\n\tset endless(isEndless: boolean) {\n\t\tthis.reflect('endless', isEndless);\n\t}\n\n\t// fallow-ignore-next-line code-duplication -- one accessor pair per reflected attribute\n\tget modulation(): number {\n\t\treturn this.numberAttribute('modulation', 0);\n\t}\n\n\tset modulation(amount: number | undefined) {\n\t\tthis.reflect('modulation', amount);\n\t}\n\n\t// fallow-ignore-next-line code-duplication -- one accessor pair per property\n\tget modulationValue(): number | undefined {\n\t\treturn this.readModulationValue();\n\t}\n\n\tset modulationValue(value: null | number | undefined) {\n\t\tthis.writeModulationValue(this.#dial, 'dial', value);\n\t}\n\n\treadonly #dial = renderDial();\n\n\toverride connectedCallback(): void {\n\t\tthis.upgradeProperties('endless', 'modulationValue', 'modulation');\n\t\tsuper.connectedCallback();\n\t}\n\n\tprotected connect(signal: AbortSignal): void {\n\t\tconst dial = this.#dial;\n\n\t\tbindScale(\n\t\t\tthis,\n\t\t\t{ control: dial, marks: requireChild(dial, '.sonic-dial-scale', HTMLDivElement) },\n\t\t\tsignal,\n\t\t);\n\t\tthis.render();\n\t\tthis.checkStyles(dial, 'dial.css');\n\t\tthis.bindGestures(dial, signal, () => ({\n\t\t\tposition: (event) => -event.clientY,\n\t\t\ttravelPx: this.travelPx(dial, '--_sonic-dial-travel'),\n\t\t}));\n\t}\n\n\tprotected draw(): void {\n\t\tthis.writeProportions(this.#dial, 'dial', this.endless ? 0 : this.modulation);\n\t}\n\n\tprotected override focusTarget(): HTMLElement {\n\t\treturn this.#dial;\n\t}\n\n\tprotected override isWrapping(): boolean {\n\t\treturn this.endless;\n\t}\n}\n"],"mappings":";;;;;AAUA,MAAM,aAAa,SACP;;;;;;;;;;;;;;;;;;;;IAqBX,cACD;AAEA,IAAa,YAAb,cAA+B,kBAAkB;CAChD,OAAyB,qBAAqB;EAC7C,GAAG,kBAAkB;EACrB;EACA;CACD;CAEA,IAAI,UAAmB;EACtB,OAAO,KAAK,aAAa,SAAS;CACnC;CAEA,IAAI,QAAQ,WAAoB;EAC/B,KAAK,QAAQ,WAAW,SAAS;CAClC;CAGA,IAAI,aAAqB;EACxB,OAAO,KAAK,gBAAgB,cAAc,CAAC;CAC5C;CAEA,IAAI,WAAW,QAA4B;EAC1C,KAAK,QAAQ,cAAc,MAAM;CAClC;CAGA,IAAI,kBAAsC;EACzC,OAAO,KAAK,oBAAoB;CACjC;CAEA,IAAI,gBAAgB,OAAkC;EACrD,KAAK,qBAAqB,KAAK,OAAO,QAAQ,KAAK;CACpD;CAEA,AAAS,QAAQ,WAAW;CAE5B,AAAS,oBAA0B;EAClC,KAAK,kBAAkB,WAAW,mBAAmB,YAAY;EACjE,MAAM,kBAAkB;CACzB;CAEA,AAAU,QAAQ,QAA2B;EAC5C,MAAM,OAAO,KAAK;EAElB,UACC,MACA;GAAE,SAAS;GAAM,OAAO,aAAa,MAAM,qBAAqB,cAAc;EAAE,GAChF,MACD;EACA,KAAK,OAAO;EACZ,KAAK,YAAY,MAAM,UAAU;EACjC,KAAK,aAAa,MAAM,eAAe;GACtC,WAAW,UAAU,CAAC,MAAM;GAC5B,UAAU,KAAK,SAAS,MAAM,sBAAsB;EACrD,EAAE;CACH;CAEA,AAAU,OAAa;EACtB,KAAK,iBAAiB,KAAK,OAAO,QAAQ,KAAK,UAAU,IAAI,KAAK,UAAU;CAC7E;CAEA,AAAmB,cAA2B;EAC7C,OAAO,KAAK;CACb;CAEA,AAAmB,aAAsB;EACxC,OAAO,KAAK;CACb;AACD"}
@@ -1,13 +1,13 @@
1
- import { requireChild, template } from "../lib/render.js";
2
1
  import { writeAttribute } from "../lib/write-attribute.js";
3
2
  import { SonicElement } from "./sonic-element.js";
3
+ import { requireChild, template } from "../lib/render.js";
4
4
  import { ReadoutClaim } from "./readout-claim.js";
5
5
  import { Readout } from "./readout.js";
6
6
  import { dragThresholdPx } from "../lib/drag-step.js";
7
+ import { bindDrag } from "../lib/pointer-drag.js";
7
8
  import { SonicValueElement, linkValue } from "./value-element.js";
8
9
  import { adsrPath, adsrShape, adsrStages, timeStages } from "../lib/adsr-shape.js";
9
10
  import { pointerMove, pointerPosition, startFieldDrag, stepFieldDrag } from "../lib/field.js";
10
- import { bindDrag } from "../lib/pointer-drag.js";
11
11
 
12
12
  //#region src/elements/envelope.ts
13
13
  const curveStages = [
@@ -31,10 +31,23 @@ function flip(point, isFlipped) {
31
31
  function isParent(node) {
32
32
  return "querySelector" in node;
33
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
+ }
34
45
  const renderEnvelope = template(`
35
46
  <div class="sonic-envelope" aria-hidden="true">
47
+ <div class="sonic-envelope-bracket"></div>
36
48
  <svg class="sonic-envelope-graph" preserveAspectRatio="none" viewBox="0 0 1 1">
37
49
  <path class="sonic-envelope-fill" />
50
+ <rect class="sonic-envelope-fill sonic-envelope-floor" y="1" />
38
51
  <path class="sonic-envelope-line" />
39
52
  </svg>
40
53
  <div class="sonic-envelope-handle" data-sonic-stage="delay" hidden></div>
@@ -115,12 +128,18 @@ var SonicEnvelope = class extends SonicElement {
115
128
  this.reflect("sustain", id);
116
129
  }
117
130
  #bindings = /* @__PURE__ */ new Map();
131
+ #envelope = renderEnvelope();
132
+ #bracket = requireChild(this.#envelope, ".sonic-envelope-bracket", HTMLDivElement);
118
133
  #claim = new ReadoutClaim(() => {
119
134
  this.#renderReadout();
120
135
  });
121
- #envelope = renderEnvelope();
136
+ #curveHandles = stageParts(this.#envelope, "sonic-envelope-curve", curveStages);
122
137
  #fill = requireChild(this.#envelope, ".sonic-envelope-fill", SVGElement);
138
+ #floor = requireChild(this.#envelope, ".sonic-envelope-floor", SVGElement);
123
139
  #graph = requireChild(this.#envelope, ".sonic-envelope-graph", SVGElement);
140
+ #handles = stageParts(this.#envelope, "sonic-envelope-handle", timeStages);
141
+ #held;
142
+ #isDriving = false;
124
143
  #line = requireChild(this.#envelope, ".sonic-envelope-line", SVGElement);
125
144
  #pointerDrag;
126
145
  #readout = new Readout(requireChild(this.#envelope, ".sonic-envelope-readout", HTMLDivElement));
@@ -145,7 +164,7 @@ var SonicEnvelope = class extends SonicElement {
145
164
  connect(signal) {
146
165
  const envelope = this.#envelope;
147
166
  this.#signal = signal;
148
- this.appendOnce(envelope);
167
+ this.keepControl(envelope, signal);
149
168
  this.#bind();
150
169
  this.checkStyles(envelope, "envelope.css");
151
170
  this.#bindPointer(envelope, signal);
@@ -162,7 +181,7 @@ var SonicEnvelope = class extends SonicElement {
162
181
  if (!element) continue;
163
182
  const link = linkValue(element);
164
183
  const unwatch = link.watch(() => {
165
- this.#draw();
184
+ if (!this.#isDriving) this.#draw();
166
185
  });
167
186
  this.#bindings.set(name, {
168
187
  element,
@@ -202,32 +221,37 @@ var SonicEnvelope = class extends SonicElement {
202
221
  }
203
222
  #draw() {
204
223
  const shape = this.#readShape();
205
- const path = this.#bindings.size === 0 ? {
206
- fill: "",
207
- stroke: ""
208
- } : adsrPath(shape);
224
+ const path = this.#bindings.size === 0 ? emptyPath : adsrPath(shape);
209
225
  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}"]`, {
226
+ writeAttribute(this.#line, "d", path.stroke);
227
+ writeAttribute(this.#fill, "d", path.fill);
228
+ writeAttribute(this.#floor, "x", String(path.floor.from));
229
+ writeAttribute(this.#floor, "width", String(path.floor.to - path.floor.from));
230
+ for (const [stage, part] of this.#handles) this.#drawPoint(part, {
213
231
  at: shape.handles.find((point) => point.stage === stage),
214
232
  drives: this.#drives(stage)
215
233
  });
216
- for (const stage of curveStages) this.#drawPoint(`.sonic-envelope-curve[data-sonic-stage="${stage}"]`, {
234
+ for (const [stage, part] of this.#curveHandles) this.#drawPoint(part, {
217
235
  at: shape.curveHandles.find((point) => point.stage === stage),
218
236
  drives: this.#curveDrives(stage)
219
237
  });
238
+ this.#drawBracket();
220
239
  this.#renderReadout();
221
240
  }
222
- #drawPoint(selector, point) {
223
- const part = requireChild(this.#envelope, selector, HTMLDivElement);
241
+ #drawBracket() {
242
+ const part = this.#held;
243
+ if (!part) return;
244
+ for (const name of ["--_sonic-envelope-x", "--_sonic-envelope-y"]) this.#bracket.style.setProperty(name, part.style.getPropertyValue(name));
245
+ writeAttribute(this.#bracket, "data-sonic-part", part.classList.contains("sonic-envelope-curve") ? "curve" : void 0);
246
+ }
247
+ #drawPoint(part, point) {
224
248
  const { at, drives } = point;
225
- part.hidden = !at;
249
+ writeAttribute(part, "hidden", at ? void 0 : "");
226
250
  if (!at) return;
227
251
  const isStill = this.isDisabled() || drives.every(({ binding }) => binding.link.isDisabled());
228
252
  part.style.setProperty("--_sonic-envelope-x", String(at.x));
229
253
  part.style.setProperty("--_sonic-envelope-y", String(at.y));
230
- part.dataset.sonicAxes = drives.map(({ axis }) => axis).join("");
254
+ writeAttribute(part, "data-sonic-axes", drives.map(({ axis }) => axis).join(""));
231
255
  writeAttribute(part, "data-sonic-disabled", isStill ? "" : void 0);
232
256
  }
233
257
  #drives(stage) {
@@ -246,6 +270,8 @@ var SonicEnvelope = class extends SonicElement {
246
270
  if (!pressed || pressed.drives.length === 0) return void 0;
247
271
  const { drives, isFlipped, part } = pressed;
248
272
  this.#claim.press(true);
273
+ this.#held = part;
274
+ this.#drawBracket();
249
275
  const box = this.#graph.getBoundingClientRect();
250
276
  const travel = {
251
277
  x: Math.max(1, box.width * this.#shape.share),
@@ -284,11 +310,13 @@ var SonicEnvelope = class extends SonicElement {
284
310
  const step = stepFieldDrag(mappings, drag.state, flip(pointerMove(event), drag.isFlipped));
285
311
  drag.state = step.state;
286
312
  if (step.state.isEngaged) this.#claim.reveal("drag");
313
+ this.#isDriving = true;
287
314
  for (const { axis, binding } of drag.drives) {
288
315
  const next = step[axis];
289
316
  if (next !== void 0) binding.link.input(next);
290
317
  }
291
- this.#renderReadout();
318
+ this.#isDriving = false;
319
+ this.#draw();
292
320
  }
293
321
  #pressed(event) {
294
322
  const part = event.target instanceof Element ? event.target.closest(".sonic-envelope-handle, .sonic-envelope-curve") : void 0;