@xsynaptic/sonic-ui 0.16.0 → 0.18.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 (128) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +112 -19
  3. package/dist/default/dat.js +1 -0
  4. package/dist/default/elements/button.js +2 -0
  5. package/dist/default/elements/dial.js +2 -0
  6. package/dist/default/elements/display-element.js +9 -7
  7. package/dist/default/elements/envelope.js +31 -72
  8. package/dist/default/elements/field-gesture.js +81 -0
  9. package/dist/default/elements/meter.js +4 -0
  10. package/dist/default/elements/position-group.js +42 -26
  11. package/dist/default/elements/radio-group.js +59 -56
  12. package/dist/default/elements/scale.js +3 -3
  13. package/dist/default/elements/slider.js +11 -13
  14. package/dist/default/elements/split.js +14 -13
  15. package/dist/default/elements/switch.js +31 -46
  16. package/dist/default/elements/toggle.js +20 -44
  17. package/dist/default/elements/value-element.js +65 -91
  18. package/dist/default/elements/value-gestures.js +22 -22
  19. package/dist/default/elements/wave-element.js +9 -12
  20. package/dist/default/elements/waveform.js +64 -249
  21. package/dist/default/elements/wavestrip.js +10 -5
  22. package/dist/default/elements/xy.js +67 -112
  23. package/dist/default/lib/canvas-surface.js +25 -0
  24. package/dist/default/lib/crossfade.js +1 -0
  25. package/dist/default/lib/envelope-curve.js +1 -0
  26. package/dist/default/lib/marker-rider.js +37 -14
  27. package/dist/default/lib/peaks-request.js +44 -0
  28. package/dist/default/lib/positions.js +58 -0
  29. package/dist/default/lib/scrub.js +12 -0
  30. package/dist/default/lib/value-model.js +62 -0
  31. package/dist/default/lib/waveform-scene.js +108 -0
  32. package/dist/default/lib/zoom-gesture.js +97 -1
  33. package/dist/dev/dat.d.ts +1 -0
  34. package/dist/dev/dat.js +1 -0
  35. package/dist/dev/dat.js.map +1 -1
  36. package/dist/dev/elements/button.d.ts +2 -0
  37. package/dist/dev/elements/button.js +2 -0
  38. package/dist/dev/elements/button.js.map +1 -1
  39. package/dist/dev/elements/dial.d.ts +2 -0
  40. package/dist/dev/elements/dial.js +2 -0
  41. package/dist/dev/elements/dial.js.map +1 -1
  42. package/dist/dev/elements/display-element.d.ts +5 -3
  43. package/dist/dev/elements/display-element.js +9 -7
  44. package/dist/dev/elements/display-element.js.map +1 -1
  45. package/dist/dev/elements/envelope.js +31 -72
  46. package/dist/dev/elements/envelope.js.map +1 -1
  47. package/dist/dev/elements/field-gesture.js +82 -0
  48. package/dist/dev/elements/field-gesture.js.map +1 -0
  49. package/dist/dev/elements/meter.d.ts +10 -5
  50. package/dist/dev/elements/meter.js +4 -0
  51. package/dist/dev/elements/meter.js.map +1 -1
  52. package/dist/dev/elements/position-group.d.ts +12 -6
  53. package/dist/dev/elements/position-group.js +42 -26
  54. package/dist/dev/elements/position-group.js.map +1 -1
  55. package/dist/dev/elements/radio-group.d.ts +11 -7
  56. package/dist/dev/elements/radio-group.js +59 -56
  57. package/dist/dev/elements/radio-group.js.map +1 -1
  58. package/dist/dev/elements/scale.js +3 -3
  59. package/dist/dev/elements/scale.js.map +1 -1
  60. package/dist/dev/elements/slider.d.ts +4 -2
  61. package/dist/dev/elements/slider.js +11 -13
  62. package/dist/dev/elements/slider.js.map +1 -1
  63. package/dist/dev/elements/split.d.ts +1 -0
  64. package/dist/dev/elements/split.js +14 -13
  65. package/dist/dev/elements/split.js.map +1 -1
  66. package/dist/dev/elements/switch.d.ts +2 -2
  67. package/dist/dev/elements/switch.js +31 -46
  68. package/dist/dev/elements/switch.js.map +1 -1
  69. package/dist/dev/elements/toggle.js +20 -44
  70. package/dist/dev/elements/toggle.js.map +1 -1
  71. package/dist/dev/elements/value-element.d.ts +5 -5
  72. package/dist/dev/elements/value-element.js +65 -91
  73. package/dist/dev/elements/value-element.js.map +1 -1
  74. package/dist/dev/elements/value-gestures.js +22 -22
  75. package/dist/dev/elements/value-gestures.js.map +1 -1
  76. package/dist/dev/elements/wave-element.d.ts +7 -4
  77. package/dist/dev/elements/wave-element.js +9 -12
  78. package/dist/dev/elements/wave-element.js.map +1 -1
  79. package/dist/dev/elements/waveform.d.ts +23 -22
  80. package/dist/dev/elements/waveform.js +64 -249
  81. package/dist/dev/elements/waveform.js.map +1 -1
  82. package/dist/dev/elements/wavestrip.d.ts +5 -1
  83. package/dist/dev/elements/wavestrip.js +10 -5
  84. package/dist/dev/elements/wavestrip.js.map +1 -1
  85. package/dist/dev/elements/xy.js +67 -112
  86. package/dist/dev/elements/xy.js.map +1 -1
  87. package/dist/dev/index.d.ts +3 -2
  88. package/dist/dev/lib/canvas-surface.d.ts +1 -0
  89. package/dist/dev/lib/canvas-surface.js +25 -0
  90. package/dist/dev/lib/canvas-surface.js.map +1 -1
  91. package/dist/dev/lib/crossfade.d.ts +1 -0
  92. package/dist/dev/lib/crossfade.js +1 -0
  93. package/dist/dev/lib/crossfade.js.map +1 -1
  94. package/dist/dev/lib/envelope-curve.d.ts +1 -0
  95. package/dist/dev/lib/envelope-curve.js +1 -0
  96. package/dist/dev/lib/envelope-curve.js.map +1 -1
  97. package/dist/dev/lib/marker-rider.js +37 -14
  98. package/dist/dev/lib/marker-rider.js.map +1 -1
  99. package/dist/dev/lib/peaks-request.d.ts +5 -0
  100. package/dist/dev/lib/peaks-request.js +45 -0
  101. package/dist/dev/lib/peaks-request.js.map +1 -0
  102. package/dist/dev/lib/positions.d.ts +7 -0
  103. package/dist/dev/lib/positions.js +59 -0
  104. package/dist/dev/lib/positions.js.map +1 -0
  105. package/dist/dev/lib/scrub.d.ts +8 -0
  106. package/dist/dev/lib/scrub.js +13 -0
  107. package/dist/dev/lib/scrub.js.map +1 -0
  108. package/dist/dev/lib/time-regions.d.ts +1 -0
  109. package/dist/dev/lib/time-regions.js.map +1 -1
  110. package/dist/dev/lib/units.d.ts +8 -0
  111. package/dist/dev/lib/value-model.js +63 -0
  112. package/dist/dev/lib/value-model.js.map +1 -0
  113. package/dist/dev/lib/waveform-buckets.d.ts +1 -0
  114. package/dist/dev/lib/waveform-buckets.js.map +1 -1
  115. package/dist/dev/lib/waveform-scene.d.ts +11 -0
  116. package/dist/dev/lib/waveform-scene.js +109 -0
  117. package/dist/dev/lib/waveform-scene.js.map +1 -0
  118. package/dist/dev/lib/zoom-gesture.js +97 -1
  119. package/dist/dev/lib/zoom-gesture.js.map +1 -1
  120. package/dist/styles/button.css +16 -6
  121. package/dist/styles/waveform.css +12 -4
  122. package/package.json +1 -1
  123. package/dist/default/lib/asked-value.js +0 -21
  124. package/dist/default/lib/fill-watch.js +0 -29
  125. package/dist/dev/lib/asked-value.js +0 -22
  126. package/dist/dev/lib/asked-value.js.map +0 -1
  127. package/dist/dev/lib/fill-watch.js +0 -30
  128. package/dist/dev/lib/fill-watch.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"waveform-buckets.js","names":[],"sources":["../../../src/lib/waveform-buckets.ts"],"sourcesContent":["// Interleaved min, max per channel per pair; `fullScale` is 128, 32768 or 1\nexport interface WaveformPeaks {\n\tchannels?: number;\n\tfullScale: number;\n\tpairsPerSecond: number;\n\tsamples: Float32Array | Int8Array | Int16Array;\n}\n\ninterface WaveformBucket {\n\tfromPair: number;\n\thigh: number;\n\tlow: number;\n\ttoPair: number;\n\tx: number;\n}\n\ninterface BucketView {\n\tpixelsPerSecond: number;\n\tstartSeconds: number;\n\twidth: number;\n}\n\nfunction foldPairs(peaks: WaveformPeaks, fromPair: number, toPair: number): [number, number] {\n\tconst { samples } = peaks;\n\tconst channels = Math.max(1, peaks.channels ?? 1);\n\tconst stride = channels * 2;\n\tconst pairsTotal = Math.floor(samples.length / stride);\n\tlet low = 0;\n\tlet high = 0;\n\n\tfor (let pair = Math.max(0, fromPair); pair < Math.min(pairsTotal, toPair); pair += 1) {\n\t\tfor (let channel = 0; channel < channels; channel += 1) {\n\t\t\tconst at = pair * stride + channel * 2;\n\n\t\t\tlow = Math.min(low, samples[at] ?? 0);\n\t\t\thigh = Math.max(high, samples[at + 1] ?? 0);\n\t\t}\n\t}\n\n\treturn [low / peaks.fullScale, high / peaks.fullScale];\n}\n\n// Keyed to absolute pairs; keyed to the window, the same samples re-bucket every frame and judder\nexport function waveformBuckets(peaks: WaveformPeaks, view: BucketView): Array<WaveformBucket> {\n\tconst { pairsPerSecond } = peaks;\n\tif (pairsPerSecond <= 0 || view.pixelsPerSecond <= 0) return [];\n\n\tconst pxPerPair = view.pixelsPerSecond / pairsPerSecond;\n\tconst pairs = Math.max(1, Math.round(1 / pxPerPair));\n\tconst openingPair = view.startSeconds * pairsPerSecond;\n\tconst first = Math.floor(openingPair / pairs) - 1;\n\tconst count = Math.ceil(view.width / (pairs * pxPerPair)) + 3;\n\tconst buckets: Array<WaveformBucket> = [];\n\n\tfor (let index = 0; index < count; index += 1) {\n\t\tconst fromPair = (first + index) * pairs;\n\t\tconst toPair = fromPair + pairs;\n\t\tconst [low, high] = foldPairs(peaks, fromPair, toPair);\n\n\t\tbuckets.push({ fromPair, high, low, toPair, x: (fromPair - openingPair) * pxPerPair });\n\t}\n\n\treturn buckets;\n}\n"],"mappings":";AAsBA,SAAS,UAAU,OAAsB,UAAkB,QAAkC;CAC5F,MAAM,EAAE,YAAY;CACpB,MAAM,WAAW,KAAK,IAAI,GAAG,MAAM,YAAY,CAAC;CAChD,MAAM,SAAS,WAAW;CAC1B,MAAM,aAAa,KAAK,MAAM,QAAQ,SAAS,MAAM;CACrD,IAAI,MAAM;CACV,IAAI,OAAO;CAEX,KAAK,IAAI,OAAO,KAAK,IAAI,GAAG,QAAQ,GAAG,OAAO,KAAK,IAAI,YAAY,MAAM,GAAG,QAAQ,GACnF,KAAK,IAAI,UAAU,GAAG,UAAU,UAAU,WAAW,GAAG;EACvD,MAAM,KAAK,OAAO,SAAS,UAAU;EAErC,MAAM,KAAK,IAAI,KAAK,QAAQ,OAAO,CAAC;EACpC,OAAO,KAAK,IAAI,MAAM,QAAQ,KAAK,MAAM,CAAC;CAC3C;CAGD,OAAO,CAAC,MAAM,MAAM,WAAW,OAAO,MAAM,SAAS;AACtD;AAGA,SAAgB,gBAAgB,OAAsB,MAAyC;CAC9F,MAAM,EAAE,mBAAmB;CAC3B,IAAI,kBAAkB,KAAK,KAAK,mBAAmB,GAAG,OAAO,CAAC;CAE9D,MAAM,YAAY,KAAK,kBAAkB;CACzC,MAAM,QAAQ,KAAK,IAAI,GAAG,KAAK,MAAM,IAAI,SAAS,CAAC;CACnD,MAAM,cAAc,KAAK,eAAe;CACxC,MAAM,QAAQ,KAAK,MAAM,cAAc,KAAK,IAAI;CAChD,MAAM,QAAQ,KAAK,KAAK,KAAK,SAAS,QAAQ,UAAU,IAAI;CAC5D,MAAM,UAAiC,CAAC;CAExC,KAAK,IAAI,QAAQ,GAAG,QAAQ,OAAO,SAAS,GAAG;EAC9C,MAAM,YAAY,QAAQ,SAAS;EACnC,MAAM,SAAS,WAAW;EAC1B,MAAM,CAAC,KAAK,QAAQ,UAAU,OAAO,UAAU,MAAM;EAErD,QAAQ,KAAK;GAAE;GAAU;GAAM;GAAK;GAAQ,IAAI,WAAW,eAAe;EAAU,CAAC;CACtF;CAEA,OAAO;AACR"}
1
+ {"version":3,"file":"waveform-buckets.js","names":[],"sources":["../../../src/lib/waveform-buckets.ts"],"sourcesContent":["/** Interleaved min, max per channel per pair; `fullScale` is 128, 32768 or 1 */\nexport interface WaveformPeaks {\n\tchannels?: number;\n\tfullScale: number;\n\tpairsPerSecond: number;\n\tsamples: Float32Array | Int8Array | Int16Array;\n}\n\ninterface WaveformBucket {\n\tfromPair: number;\n\thigh: number;\n\tlow: number;\n\ttoPair: number;\n\tx: number;\n}\n\ninterface BucketView {\n\tpixelsPerSecond: number;\n\tstartSeconds: number;\n\twidth: number;\n}\n\nfunction foldPairs(peaks: WaveformPeaks, fromPair: number, toPair: number): [number, number] {\n\tconst { samples } = peaks;\n\tconst channels = Math.max(1, peaks.channels ?? 1);\n\tconst stride = channels * 2;\n\tconst pairsTotal = Math.floor(samples.length / stride);\n\tlet low = 0;\n\tlet high = 0;\n\n\tfor (let pair = Math.max(0, fromPair); pair < Math.min(pairsTotal, toPair); pair += 1) {\n\t\tfor (let channel = 0; channel < channels; channel += 1) {\n\t\t\tconst at = pair * stride + channel * 2;\n\n\t\t\tlow = Math.min(low, samples[at] ?? 0);\n\t\t\thigh = Math.max(high, samples[at + 1] ?? 0);\n\t\t}\n\t}\n\n\treturn [low / peaks.fullScale, high / peaks.fullScale];\n}\n\n// Keyed to absolute pairs; keyed to the window, the same samples re-bucket every frame and judder\nexport function waveformBuckets(peaks: WaveformPeaks, view: BucketView): Array<WaveformBucket> {\n\tconst { pairsPerSecond } = peaks;\n\tif (pairsPerSecond <= 0 || view.pixelsPerSecond <= 0) return [];\n\n\tconst pxPerPair = view.pixelsPerSecond / pairsPerSecond;\n\tconst pairs = Math.max(1, Math.round(1 / pxPerPair));\n\tconst openingPair = view.startSeconds * pairsPerSecond;\n\tconst first = Math.floor(openingPair / pairs) - 1;\n\tconst count = Math.ceil(view.width / (pairs * pxPerPair)) + 3;\n\tconst buckets: Array<WaveformBucket> = [];\n\n\tfor (let index = 0; index < count; index += 1) {\n\t\tconst fromPair = (first + index) * pairs;\n\t\tconst toPair = fromPair + pairs;\n\t\tconst [low, high] = foldPairs(peaks, fromPair, toPair);\n\n\t\tbuckets.push({ fromPair, high, low, toPair, x: (fromPair - openingPair) * pxPerPair });\n\t}\n\n\treturn buckets;\n}\n"],"mappings":";AAsBA,SAAS,UAAU,OAAsB,UAAkB,QAAkC;CAC5F,MAAM,EAAE,YAAY;CACpB,MAAM,WAAW,KAAK,IAAI,GAAG,MAAM,YAAY,CAAC;CAChD,MAAM,SAAS,WAAW;CAC1B,MAAM,aAAa,KAAK,MAAM,QAAQ,SAAS,MAAM;CACrD,IAAI,MAAM;CACV,IAAI,OAAO;CAEX,KAAK,IAAI,OAAO,KAAK,IAAI,GAAG,QAAQ,GAAG,OAAO,KAAK,IAAI,YAAY,MAAM,GAAG,QAAQ,GACnF,KAAK,IAAI,UAAU,GAAG,UAAU,UAAU,WAAW,GAAG;EACvD,MAAM,KAAK,OAAO,SAAS,UAAU;EAErC,MAAM,KAAK,IAAI,KAAK,QAAQ,OAAO,CAAC;EACpC,OAAO,KAAK,IAAI,MAAM,QAAQ,KAAK,MAAM,CAAC;CAC3C;CAGD,OAAO,CAAC,MAAM,MAAM,WAAW,OAAO,MAAM,SAAS;AACtD;AAGA,SAAgB,gBAAgB,OAAsB,MAAyC;CAC9F,MAAM,EAAE,mBAAmB;CAC3B,IAAI,kBAAkB,KAAK,KAAK,mBAAmB,GAAG,OAAO,CAAC;CAE9D,MAAM,YAAY,KAAK,kBAAkB;CACzC,MAAM,QAAQ,KAAK,IAAI,GAAG,KAAK,MAAM,IAAI,SAAS,CAAC;CACnD,MAAM,cAAc,KAAK,eAAe;CACxC,MAAM,QAAQ,KAAK,MAAM,cAAc,KAAK,IAAI;CAChD,MAAM,QAAQ,KAAK,KAAK,KAAK,SAAS,QAAQ,UAAU,IAAI;CAC5D,MAAM,UAAiC,CAAC;CAExC,KAAK,IAAI,QAAQ,GAAG,QAAQ,OAAO,SAAS,GAAG;EAC9C,MAAM,YAAY,QAAQ,SAAS;EACnC,MAAM,SAAS,WAAW;EAC1B,MAAM,CAAC,KAAK,QAAQ,UAAU,OAAO,UAAU,MAAM;EAErD,QAAQ,KAAK;GAAE;GAAU;GAAM;GAAK;GAAQ,IAAI,WAAW,eAAe;EAAU,CAAC;CACtF;CAEA,OAAO;AACR"}
@@ -0,0 +1,11 @@
1
+ //#region src/lib/waveform-scene.d.ts
2
+ export declare const waveformColours: {
3
+ readonly ends: "--_sonic-waveform-ends";
4
+ readonly grid: "--_sonic-waveform-grid";
5
+ readonly marker: "--_sonic-waveform-marker";
6
+ readonly placeholder: "--_sonic-waveform-placeholder";
7
+ readonly wave: "--_sonic-waveform-wave";
8
+ readonly waveEdge: "--_sonic-waveform-wave-edge";
9
+ };
10
+ //#endregion
11
+ //# sourceMappingURL=waveform-scene.d.ts.map
@@ -0,0 +1,109 @@
1
+ import { clamp } from "./math.js";
2
+ import { isKind } from "./marker-band.js";
3
+ import { waveformBuckets } from "./waveform-buckets.js";
4
+
5
+ //#region src/lib/waveform-scene.ts
6
+ const waveformColours = {
7
+ ends: "--_sonic-waveform-ends",
8
+ grid: "--_sonic-waveform-grid",
9
+ marker: "--_sonic-waveform-marker",
10
+ placeholder: "--_sonic-waveform-placeholder",
11
+ wave: "--_sonic-waveform-wave",
12
+ waveEdge: "--_sonic-waveform-wave-edge"
13
+ };
14
+ const amplitudeMargin = .94;
15
+ const hatchPitchPx = 8;
16
+ const placeholderWavelengthPx = 48;
17
+ const placeholderAmplitude = .2;
18
+ function paintHatch(context, scene, [from, to]) {
19
+ if (to <= from) return;
20
+ const { dpr, height } = scene;
21
+ const pitch = Math.max(2, Math.round(hatchPitchPx * dpr));
22
+ const offset = scene.startSeconds * scene.pixelsPerSecond;
23
+ context.save();
24
+ context.beginPath();
25
+ context.rect(from, 0, to - from, height);
26
+ context.clip();
27
+ context.beginPath();
28
+ for (let x = Math.floor((from - height + offset) / pitch) * pitch - offset; x < to; x += pitch) {
29
+ context.moveTo(x, height);
30
+ context.lineTo(x + height, 0);
31
+ }
32
+ context.lineWidth = Math.max(1, Math.round(dpr));
33
+ context.strokeStyle = scene.colours.ends;
34
+ context.stroke();
35
+ context.restore();
36
+ }
37
+ function paintPlaceholders(context, scene) {
38
+ if (scene.pending.length === 0) return;
39
+ const { dpr, height, pixelsPerSecond, startSeconds } = scene;
40
+ const wavelength = placeholderWavelengthPx * dpr;
41
+ const center = height / 2;
42
+ const origin = startSeconds * pixelsPerSecond;
43
+ const waveY = (x) => center - Math.sin(((x + origin) / wavelength + scene.phase) * 2 * Math.PI) * center * placeholderAmplitude;
44
+ context.beginPath();
45
+ for (const [from, to] of scene.pending) {
46
+ const opening = (from - startSeconds) * pixelsPerSecond;
47
+ const closing = (to - startSeconds) * pixelsPerSecond;
48
+ context.moveTo(opening, waveY(opening));
49
+ for (let x = opening + dpr; x < closing; x += dpr) context.lineTo(x, waveY(x));
50
+ context.lineTo(closing, waveY(closing));
51
+ }
52
+ context.lineWidth = Math.max(1, Math.round(dpr));
53
+ context.strokeStyle = scene.colours.placeholder;
54
+ context.stroke();
55
+ }
56
+ function waveFill(context, scene) {
57
+ const { wave, waveEdge } = scene.colours;
58
+ const gradient = context.createLinearGradient(0, 0, 0, scene.height);
59
+ gradient.addColorStop(0, waveEdge);
60
+ gradient.addColorStop(.5, wave);
61
+ gradient.addColorStop(1, waveEdge);
62
+ return gradient;
63
+ }
64
+ function paintWave(context, scene, peaks) {
65
+ const buckets = waveformBuckets(peaks, scene);
66
+ const [first] = buckets;
67
+ if (!first) return;
68
+ const center = scene.height / 2;
69
+ const amplitudePx = center * amplitudeMargin;
70
+ context.fillStyle = waveFill(context, scene);
71
+ context.beginPath();
72
+ context.moveTo(first.x, center);
73
+ for (const bucket of buckets) context.lineTo(bucket.x, center - bucket.high * amplitudePx);
74
+ for (const bucket of buckets.toReversed()) context.lineTo(bucket.x, center - bucket.low * amplitudePx);
75
+ context.closePath();
76
+ context.fill();
77
+ }
78
+ function paintMarkers(context, scene, drawn) {
79
+ const line = Math.max(1, Math.round(scene.dpr));
80
+ for (const { dimmed, kind, start } of drawn.markers) {
81
+ const x = (start - scene.startSeconds) * scene.pixelsPerSecond;
82
+ context.fillStyle = (isKind(kind) ? drawn.kindColours.get(kind) : void 0) ?? scene.colours.marker;
83
+ context.globalAlpha = dimmed === true ? .5 : 1;
84
+ context.fillRect(x - line / 2, 0, line, scene.height);
85
+ }
86
+ context.globalAlpha = 1;
87
+ }
88
+ function paintWaveform(context, scene, drawn) {
89
+ const { dpr, height, pixelsPerSecond, startSeconds, width } = scene;
90
+ const x = (seconds) => (seconds - startSeconds) * pixelsPerSecond;
91
+ const { ends, grid } = scene.colours;
92
+ const line = Math.max(1, Math.round(dpr));
93
+ const [opening, closing] = [x(drawn.range[0]), x(drawn.range[1])];
94
+ const [from, to] = [clamp(opening, 0, width), clamp(closing, 0, width)];
95
+ context.clearRect(0, 0, width, height);
96
+ paintHatch(context, scene, [0, from]);
97
+ paintHatch(context, scene, [to, width]);
98
+ context.fillStyle = grid;
99
+ if (to > from) context.fillRect(from, Math.round((height - line) / 2), to - from, line);
100
+ paintPlaceholders(context, scene);
101
+ if (drawn.peaks) paintWave(context, scene, drawn.peaks);
102
+ context.fillStyle = ends;
103
+ for (const edge of [opening, closing]) context.fillRect(edge - line / 2, 0, line, height);
104
+ paintMarkers(context, scene, drawn);
105
+ }
106
+
107
+ //#endregion
108
+ export { paintWaveform, waveformColours };
109
+ //# sourceMappingURL=waveform-scene.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"waveform-scene.js","names":[],"sources":["../../../src/lib/waveform-scene.ts"],"sourcesContent":["import type { View } from '#lib/frame-timeline.ts';\nimport type { WaveformPeaks } from '#lib/waveform-buckets.ts';\n\nimport { isKind } from '#lib/marker-band.ts';\nimport { clamp } from '#lib/math.ts';\nimport { waveformBuckets } from '#lib/waveform-buckets.ts';\n\ntype Colour = keyof typeof waveformColours;\n\ninterface Scene extends View {\n\tcolours: Record<Colour, string>;\n}\n\ninterface Drawn {\n\tkindColours: ReadonlyMap<string, string>;\n\tmarkers: ReadonlyArray<{ dimmed?: boolean; kind?: string; start: number }>;\n\tpeaks: undefined | WaveformPeaks;\n\trange: [number, number];\n}\n\nexport const waveformColours = {\n\tends: '--_sonic-waveform-ends',\n\tgrid: '--_sonic-waveform-grid',\n\tmarker: '--_sonic-waveform-marker',\n\tplaceholder: '--_sonic-waveform-placeholder',\n\twave: '--_sonic-waveform-wave',\n\twaveEdge: '--_sonic-waveform-wave-edge',\n} as const;\n\nconst amplitudeMargin = 0.94;\n\nconst hatchPitchPx = 8;\nconst placeholderWavelengthPx = 48;\nconst placeholderAmplitude = 0.2;\n\nfunction paintHatch(\n\tcontext: CanvasRenderingContext2D,\n\tscene: Scene,\n\t[from, to]: [number, number],\n): void {\n\tif (to <= from) return;\n\n\tconst { dpr, height } = scene;\n\tconst pitch = Math.max(2, Math.round(hatchPitchPx * dpr));\n\tconst offset = scene.startSeconds * scene.pixelsPerSecond;\n\n\tcontext.save();\n\tcontext.beginPath();\n\tcontext.rect(from, 0, to - from, height);\n\tcontext.clip();\n\tcontext.beginPath();\n\tfor (let x = Math.floor((from - height + offset) / pitch) * pitch - offset; x < to; x += pitch) {\n\t\tcontext.moveTo(x, height);\n\t\tcontext.lineTo(x + height, 0);\n\t}\n\tcontext.lineWidth = Math.max(1, Math.round(dpr));\n\tcontext.strokeStyle = scene.colours.ends;\n\tcontext.stroke();\n\tcontext.restore();\n}\n\nfunction paintPlaceholders(context: CanvasRenderingContext2D, scene: Scene): void {\n\tif (scene.pending.length === 0) return;\n\n\tconst { dpr, height, pixelsPerSecond, startSeconds } = scene;\n\tconst wavelength = placeholderWavelengthPx * dpr;\n\tconst center = height / 2;\n\tconst origin = startSeconds * pixelsPerSecond;\n\tconst waveY = (x: number): number =>\n\t\tcenter -\n\t\tMath.sin(((x + origin) / wavelength + scene.phase) * 2 * Math.PI) *\n\t\t\tcenter *\n\t\t\tplaceholderAmplitude;\n\n\tcontext.beginPath();\n\tfor (const [from, to] of scene.pending) {\n\t\tconst opening = (from - startSeconds) * pixelsPerSecond;\n\t\tconst closing = (to - startSeconds) * pixelsPerSecond;\n\n\t\tcontext.moveTo(opening, waveY(opening));\n\t\tfor (let x = opening + dpr; x < closing; x += dpr) context.lineTo(x, waveY(x));\n\t\tcontext.lineTo(closing, waveY(closing));\n\t}\n\tcontext.lineWidth = Math.max(1, Math.round(dpr));\n\tcontext.strokeStyle = scene.colours.placeholder;\n\tcontext.stroke();\n}\n\nfunction waveFill(context: CanvasRenderingContext2D, scene: Scene): CanvasGradient {\n\tconst { wave, waveEdge } = scene.colours;\n\tconst gradient = context.createLinearGradient(0, 0, 0, scene.height);\n\n\tgradient.addColorStop(0, waveEdge);\n\tgradient.addColorStop(0.5, wave);\n\tgradient.addColorStop(1, waveEdge);\n\n\treturn gradient;\n}\n\nfunction paintWave(context: CanvasRenderingContext2D, scene: Scene, peaks: WaveformPeaks): void {\n\tconst buckets = waveformBuckets(peaks, scene);\n\tconst [first] = buckets;\n\tif (!first) return;\n\n\tconst center = scene.height / 2;\n\tconst amplitudePx = center * amplitudeMargin;\n\n\tcontext.fillStyle = waveFill(context, scene);\n\tcontext.beginPath();\n\tcontext.moveTo(first.x, center);\n\tfor (const bucket of buckets) context.lineTo(bucket.x, center - bucket.high * amplitudePx);\n\tfor (const bucket of buckets.toReversed())\n\t\tcontext.lineTo(bucket.x, center - bucket.low * amplitudePx);\n\tcontext.closePath();\n\tcontext.fill();\n}\n\nfunction paintMarkers(context: CanvasRenderingContext2D, scene: Scene, drawn: Drawn): void {\n\tconst line = Math.max(1, Math.round(scene.dpr));\n\n\tfor (const { dimmed, kind, start } of drawn.markers) {\n\t\tconst x = (start - scene.startSeconds) * scene.pixelsPerSecond;\n\n\t\tcontext.fillStyle =\n\t\t\t(isKind(kind) ? drawn.kindColours.get(kind) : undefined) ?? scene.colours.marker;\n\t\tcontext.globalAlpha = dimmed === true ? 0.5 : 1;\n\t\tcontext.fillRect(x - line / 2, 0, line, scene.height);\n\t}\n\tcontext.globalAlpha = 1;\n}\n\nexport function paintWaveform(context: CanvasRenderingContext2D, scene: Scene, drawn: Drawn): void {\n\tconst { dpr, height, pixelsPerSecond, startSeconds, width } = scene;\n\tconst x = (seconds: number): number => (seconds - startSeconds) * pixelsPerSecond;\n\tconst { ends, grid } = scene.colours;\n\tconst line = Math.max(1, Math.round(dpr));\n\tconst [opening, closing] = [x(drawn.range[0]), x(drawn.range[1])];\n\tconst [from, to] = [clamp(opening, 0, width), clamp(closing, 0, width)];\n\n\tcontext.clearRect(0, 0, width, height);\n\tpaintHatch(context, scene, [0, from]);\n\tpaintHatch(context, scene, [to, width]);\n\tcontext.fillStyle = grid;\n\tif (to > from) context.fillRect(from, Math.round((height - line) / 2), to - from, line);\n\tpaintPlaceholders(context, scene);\n\tif (drawn.peaks) paintWave(context, scene, drawn.peaks);\n\t// Scrolling lines sit at fractional x; rounding them judders\n\tcontext.fillStyle = ends;\n\tfor (const edge of [opening, closing]) context.fillRect(edge - line / 2, 0, line, height);\n\tpaintMarkers(context, scene, drawn);\n}\n"],"mappings":"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"}
@@ -1,4 +1,5 @@
1
1
  import { clamp } from "./math.js";
2
+ import { capturePointer } from "./pointer-drag.js";
2
3
 
3
4
  //#region src/lib/zoom-gesture.ts
4
5
  const wheelRate = .01;
@@ -6,6 +7,11 @@ const linePx = 16;
6
7
  const pixelMode = 0;
7
8
  const wheelCapPx = 25;
8
9
  const minSpanPx = 16;
10
+ const zoomKeys = /* @__PURE__ */ new Map([
11
+ ["+", 1],
12
+ ["-", -1],
13
+ ["=", 1]
14
+ ]);
9
15
  const zoomKeyFactor = Math.exp(wheelCapPx * wheelRate);
10
16
  function wheelFactor(deltaY, deltaMode) {
11
17
  const pixels = deltaMode === pixelMode ? deltaY : deltaY * linePx;
@@ -14,7 +20,97 @@ function wheelFactor(deltaY, deltaMode) {
14
20
  function pinchFactor(startSpanPx, spanPx) {
15
21
  return Math.max(minSpanPx, Math.abs(spanPx)) / Math.max(minSpanPx, Math.abs(startSpanPx));
16
22
  }
23
+ function bindPinch(control, options, signal) {
24
+ const touches = /* @__PURE__ */ new Map();
25
+ let pinch;
26
+ let isPinchSpent = false;
27
+ const spanPx = ([first, second]) => (touches.get(second) ?? 0) - (touches.get(first) ?? 0);
28
+ const touchPair = () => {
29
+ const [first, second, third] = touches.keys();
30
+ if (first === void 0 || second === void 0 || third !== void 0) return void 0;
31
+ return [first, second];
32
+ };
33
+ control.addEventListener("pointerdown", (event) => {
34
+ if (event.pointerType !== "touch" || !options.isZoomable() || options.isDisabled()) return;
35
+ touches.set(event.pointerId, event.clientX);
36
+ if (pinch || isPinchSpent) return;
37
+ const pointerIds = touchPair();
38
+ if (!pointerIds) return;
39
+ options.claim();
40
+ capturePointer(control, event.pointerId);
41
+ pinch = {
42
+ pointerIds,
43
+ startSpanPx: spanPx(pointerIds),
44
+ startZoom: options.zoom()
45
+ };
46
+ }, { signal });
47
+ control.addEventListener("pointermove", (event) => {
48
+ if (!touches.has(event.pointerId)) return;
49
+ touches.set(event.pointerId, event.clientX);
50
+ if (!pinch?.pointerIds.includes(event.pointerId)) return;
51
+ options.zoomTo(pinch.startZoom * pinchFactor(pinch.startSpanPx, spanPx(pinch.pointerIds)));
52
+ }, { signal });
53
+ for (const type of ["pointerup", "pointercancel"]) control.addEventListener(type, (event) => {
54
+ if (!touches.delete(event.pointerId)) return;
55
+ if (pinch?.pointerIds.includes(event.pointerId)) {
56
+ pinch = void 0;
57
+ isPinchSpent = true;
58
+ }
59
+ if (touches.size === 0) isPinchSpent = false;
60
+ }, { signal });
61
+ return {
62
+ isPinching: () => pinch !== void 0 || isPinchSpent,
63
+ isTouched: () => touches.size > 0
64
+ };
65
+ }
66
+ function bindWheel(control, options, signal) {
67
+ control.addEventListener("wheel", (event) => {
68
+ if (!event.ctrlKey && !event.metaKey || options.isDisabled()) return;
69
+ event.preventDefault();
70
+ options.zoomTo(options.zoom() * wheelFactor(event.deltaY, event.deltaMode));
71
+ }, {
72
+ passive: false,
73
+ signal
74
+ });
75
+ let startZoom = options.zoom();
76
+ for (const type of ["gesturestart", "gesturechange"]) control.addEventListener(type, (event) => {
77
+ if (options.isDisabled() || options.isTouched()) return;
78
+ event.preventDefault();
79
+ if (type === "gesturestart") startZoom = options.zoom();
80
+ else if ("scale" in event && typeof event.scale === "number") options.zoomTo(startZoom * event.scale);
81
+ }, { signal });
82
+ }
83
+ function bindZoom(control, options, signal) {
84
+ const { isPinching, isTouched } = bindPinch(control, options, signal);
85
+ let wheel;
86
+ const sync = () => {
87
+ wheel?.abort();
88
+ wheel = void 0;
89
+ if (signal.aborted || !options.isZoomable()) return;
90
+ wheel = new AbortController();
91
+ bindWheel(control, {
92
+ ...options,
93
+ isTouched
94
+ }, wheel.signal);
95
+ };
96
+ signal.addEventListener("abort", sync, { once: true });
97
+ sync();
98
+ return {
99
+ isPinching,
100
+ sync
101
+ };
102
+ }
103
+ function bindZoomKeys(control, target, signal) {
104
+ control.addEventListener("keydown", (event) => {
105
+ if (!target.isZoomable() || target.isDisabled() || event.target !== control) return;
106
+ if (event.ctrlKey || event.metaKey || event.altKey) return;
107
+ const direction = zoomKeys.get(event.key);
108
+ if (direction === void 0) return;
109
+ event.preventDefault();
110
+ target.zoomTo(target.zoom() * zoomKeyFactor ** direction);
111
+ }, { signal });
112
+ }
17
113
 
18
114
  //#endregion
19
- export { pinchFactor, wheelFactor, zoomKeyFactor };
115
+ export { bindZoom, bindZoomKeys, pinchFactor, wheelFactor, zoomKeyFactor };
20
116
  //# sourceMappingURL=zoom-gesture.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"zoom-gesture.js","names":[],"sources":["../../../src/lib/zoom-gesture.ts"],"sourcesContent":["import { clamp } from '#lib/math.ts';\n\nconst wheelRate = 0.01;\nconst linePx = 16;\nconst pixelMode = 0;\n\nconst wheelCapPx = 25;\n\nconst minSpanPx = 16;\n\nexport const zoomKeyFactor = Math.exp(wheelCapPx * wheelRate);\n\nexport function wheelFactor(deltaY: number, deltaMode: number): number {\n\tconst pixels = deltaMode === pixelMode ? deltaY : deltaY * linePx;\n\n\treturn Math.exp(-clamp(pixels, -wheelCapPx, wheelCapPx) * wheelRate);\n}\n\nexport function pinchFactor(startSpanPx: number, spanPx: number): number {\n\treturn Math.max(minSpanPx, Math.abs(spanPx)) / Math.max(minSpanPx, Math.abs(startSpanPx));\n}\n"],"mappings":";;;AAEA,MAAM,YAAY;AAClB,MAAM,SAAS;AACf,MAAM,YAAY;AAElB,MAAM,aAAa;AAEnB,MAAM,YAAY;AAElB,MAAa,gBAAgB,KAAK,IAAI,aAAa,SAAS;AAE5D,SAAgB,YAAY,QAAgB,WAA2B;CACtE,MAAM,SAAS,cAAc,YAAY,SAAS,SAAS;CAE3D,OAAO,KAAK,IAAI,CAAC,MAAM,QAAQ,KAAa,UAAU,IAAI,SAAS;AACpE;AAEA,SAAgB,YAAY,aAAqB,QAAwB;CACxE,OAAO,KAAK,IAAI,WAAW,KAAK,IAAI,MAAM,CAAC,IAAI,KAAK,IAAI,WAAW,KAAK,IAAI,WAAW,CAAC;AACzF"}
1
+ {"version":3,"file":"zoom-gesture.js","names":[],"sources":["../../../src/lib/zoom-gesture.ts"],"sourcesContent":["import { clamp } from '#lib/math.ts';\nimport { capturePointer } from '#lib/pointer-drag.ts';\n\ninterface Pinch {\n\tpointerIds: [number, number];\n\tstartSpanPx: number;\n\tstartZoom: number;\n}\n\ninterface ZoomTarget {\n\tisDisabled: () => boolean;\n\tisZoomable: () => boolean;\n\tzoom: () => number;\n\tzoomTo: (target: number) => void;\n}\n\ninterface ZoomOptions extends ZoomTarget {\n\tclaim: () => void;\n}\n\ninterface Touches {\n\tisPinching: () => boolean;\n\tisTouched: () => boolean;\n}\n\nexport interface ZoomGesture {\n\tisPinching: () => boolean;\n\tsync: () => void;\n}\n\nconst wheelRate = 0.01;\nconst linePx = 16;\nconst pixelMode = 0;\n\nconst wheelCapPx = 25;\n\nconst minSpanPx = 16;\n\nconst zoomKeys = new Map([\n\t['+', 1],\n\t['-', -1],\n\t['=', 1],\n]);\n\nexport const zoomKeyFactor = Math.exp(wheelCapPx * wheelRate);\n\nexport function wheelFactor(deltaY: number, deltaMode: number): number {\n\tconst pixels = deltaMode === pixelMode ? deltaY : deltaY * linePx;\n\n\treturn Math.exp(-clamp(pixels, -wheelCapPx, wheelCapPx) * wheelRate);\n}\n\nexport function pinchFactor(startSpanPx: number, spanPx: number): number {\n\treturn Math.max(minSpanPx, Math.abs(spanPx)) / Math.max(minSpanPx, Math.abs(startSpanPx));\n}\n\nfunction bindPinch(control: HTMLElement, options: ZoomOptions, signal: AbortSignal): Touches {\n\tconst touches = new Map<number, number>();\n\tlet pinch: Pinch | undefined;\n\tlet isPinchSpent = false;\n\n\tconst spanPx = ([first, second]: [number, number]): number =>\n\t\t(touches.get(second) ?? 0) - (touches.get(first) ?? 0);\n\tconst touchPair = (): [number, number] | undefined => {\n\t\tconst [first, second, third] = touches.keys();\n\t\tif (first === undefined || second === undefined || third !== undefined) return undefined;\n\n\t\treturn [first, second];\n\t};\n\n\tcontrol.addEventListener(\n\t\t'pointerdown',\n\t\t(event) => {\n\t\t\tif (event.pointerType !== 'touch' || !options.isZoomable() || options.isDisabled()) return;\n\n\t\t\ttouches.set(event.pointerId, event.clientX);\n\t\t\tif (pinch || isPinchSpent) return;\n\n\t\t\tconst pointerIds = touchPair();\n\t\t\tif (!pointerIds) return;\n\n\t\t\toptions.claim();\n\t\t\tcapturePointer(control, event.pointerId);\n\t\t\tpinch = { pointerIds, startSpanPx: spanPx(pointerIds), startZoom: options.zoom() };\n\t\t},\n\t\t{ signal },\n\t);\n\tcontrol.addEventListener(\n\t\t'pointermove',\n\t\t(event) => {\n\t\t\tif (!touches.has(event.pointerId)) return;\n\n\t\t\ttouches.set(event.pointerId, event.clientX);\n\t\t\tif (!pinch?.pointerIds.includes(event.pointerId)) return;\n\n\t\t\toptions.zoomTo(pinch.startZoom * pinchFactor(pinch.startSpanPx, spanPx(pinch.pointerIds)));\n\t\t},\n\t\t{ signal },\n\t);\n\tfor (const type of ['pointerup', 'pointercancel'] as const) {\n\t\tcontrol.addEventListener(\n\t\t\ttype,\n\t\t\t(event) => {\n\t\t\t\tif (!touches.delete(event.pointerId)) return;\n\n\t\t\t\tif (pinch?.pointerIds.includes(event.pointerId)) {\n\t\t\t\t\tpinch = undefined;\n\t\t\t\t\tisPinchSpent = true;\n\t\t\t\t}\n\t\t\t\tif (touches.size === 0) isPinchSpent = false;\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t}\n\n\treturn {\n\t\tisPinching: () => pinch !== undefined || isPinchSpent,\n\t\tisTouched: () => touches.size > 0,\n\t};\n}\n\nfunction bindWheel(\n\tcontrol: HTMLElement,\n\toptions: Pick<Touches, 'isTouched'> & ZoomTarget,\n\tsignal: AbortSignal,\n): void {\n\t// Chromium cannot cancel a wheel heard only on the `display: contents` host\n\tcontrol.addEventListener(\n\t\t'wheel',\n\t\t(event) => {\n\t\t\tif ((!event.ctrlKey && !event.metaKey) || options.isDisabled()) return;\n\n\t\t\tevent.preventDefault();\n\t\t\toptions.zoomTo(options.zoom() * wheelFactor(event.deltaY, event.deltaMode));\n\t\t},\n\t\t{ passive: false, signal },\n\t);\n\n\tlet startZoom = options.zoom();\n\n\t// Safari sends a trackpad pinch as gesture events, never as a ctrl wheel\n\tfor (const type of ['gesturestart', 'gesturechange']) {\n\t\tcontrol.addEventListener(\n\t\t\ttype,\n\t\t\t(event) => {\n\t\t\t\tif (options.isDisabled() || options.isTouched()) return;\n\n\t\t\t\tevent.preventDefault();\n\t\t\t\tif (type === 'gesturestart') startZoom = options.zoom();\n\t\t\t\telse if ('scale' in event && typeof event.scale === 'number') {\n\t\t\t\t\toptions.zoomTo(startZoom * event.scale);\n\t\t\t\t}\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t}\n}\n\nexport function bindZoom(\n\tcontrol: HTMLElement,\n\toptions: ZoomOptions,\n\tsignal: AbortSignal,\n): ZoomGesture {\n\tconst { isPinching, isTouched } = bindPinch(control, options, signal);\n\tlet wheel: AbortController | undefined;\n\n\t// The wheel listener is not passive, so it is bound only while zooming is on\n\tconst sync = (): void => {\n\t\twheel?.abort();\n\t\twheel = undefined;\n\t\tif (signal.aborted || !options.isZoomable()) return;\n\n\t\twheel = new AbortController();\n\t\tbindWheel(control, { ...options, isTouched }, wheel.signal);\n\t};\n\n\tsignal.addEventListener('abort', sync, { once: true });\n\tsync();\n\n\treturn { isPinching, sync };\n}\n\nexport function bindZoomKeys(control: HTMLElement, target: ZoomTarget, signal: AbortSignal): void {\n\tcontrol.addEventListener(\n\t\t'keydown',\n\t\t(event) => {\n\t\t\tif (!target.isZoomable() || target.isDisabled() || event.target !== control) return;\n\t\t\tif (event.ctrlKey || event.metaKey || event.altKey) return;\n\n\t\t\tconst direction = zoomKeys.get(event.key);\n\t\t\tif (direction === undefined) return;\n\n\t\t\tevent.preventDefault();\n\t\t\ttarget.zoomTo(target.zoom() * zoomKeyFactor ** direction);\n\t\t},\n\t\t{ signal },\n\t);\n}\n"],"mappings":"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"}
@@ -31,13 +31,23 @@
31
31
  overflow: hidden;
32
32
  }
33
33
 
34
- sonic-button:not(:defined, [hidden]):has(> :not(svg, .sonic-led)) {
34
+ sonic-button:not(:defined, [hidden]):has(> :not(.sonic-led)) {
35
35
  inline-size: max-content;
36
36
  min-inline-size: calc(var(--_sonic-button-size) * var(--_sonic-button-aspect-ratio));
37
- padding-inline: calc(var(--_sonic-button-size) * var(--sonic-button-padding-ratio, 0.25));
38
37
  white-space: nowrap;
39
38
  }
40
39
 
40
+ sonic-button:not(:defined, [hidden]):has(> :not(svg, .sonic-led)) {
41
+ padding-inline: calc(var(--_sonic-button-size) * var(--sonic-button-padding-ratio, 0.25));
42
+ }
43
+
44
+ :where(sonic-button:not(:defined)) > svg {
45
+ display: block;
46
+ inline-size: auto;
47
+ block-size: calc(var(--_sonic-button-size) * var(--sonic-button-icon-ratio, 0.5));
48
+ aspect-ratio: auto 1;
49
+ }
50
+
41
51
  .sonic-button {
42
52
  --_sonic-button-gap-ratio: var(--sonic-button-gap-ratio, 0);
43
53
  --_sonic-button-radius-ratio: var(--sonic-button-radius-ratio, 0.2);
@@ -75,7 +85,7 @@
75
85
  }
76
86
 
77
87
  /* An `auto` width stretches in a column flex or grid parent */
78
- .sonic-button:where(:has(> .sonic-button-cap > :not(svg, .sonic-led))) {
88
+ .sonic-button:where(:has(> .sonic-button-cap > :not(.sonic-led))) {
79
89
  inline-size: max-content;
80
90
  min-inline-size: calc(var(--_sonic-button-size) * var(--_sonic-button-aspect-ratio));
81
91
  }
@@ -250,8 +260,9 @@
250
260
  }
251
261
 
252
262
  & > svg {
253
- inline-size: calc(var(--_sonic-button-size) * var(--_sonic-button-icon-ratio));
263
+ inline-size: auto;
254
264
  block-size: calc(var(--_sonic-button-size) * var(--_sonic-button-icon-ratio));
265
+ aspect-ratio: auto 1;
255
266
  mask-image: linear-gradient(
256
267
  var(--_sonic-light-fall),
257
268
  #000 15%,
@@ -261,7 +272,7 @@
261
272
  }
262
273
  }
263
274
 
264
- .sonic-button-cap:has(> :not(svg, .sonic-led)) {
275
+ .sonic-button-cap:has(> :not(.sonic-led)) {
265
276
  position: relative;
266
277
  inset: auto;
267
278
  block-size: calc(var(--_sonic-button-size) - 2 * var(--_sonic-button-gap));
@@ -269,7 +280,6 @@
269
280
  }
270
281
 
271
282
  .sonic-button-cap:has(> .sonic-led) > svg {
272
- inline-size: calc(var(--_sonic-button-size) * var(--_sonic-button-led-icon-ratio));
273
283
  block-size: calc(var(--_sonic-button-size) * var(--_sonic-button-led-icon-ratio));
274
284
  }
275
285
 
@@ -54,10 +54,16 @@
54
54
  initial-value: rgb(96 96 104);
55
55
  }
56
56
 
57
- @property --_sonic-waveform-label-fade {
57
+ @property --_sonic-waveform-label-fade-start {
58
58
  syntax: '<number>';
59
59
  inherits: true;
60
- initial-value: 0.25;
60
+ initial-value: 0;
61
+ }
62
+
63
+ @property --_sonic-waveform-label-fade-end {
64
+ syntax: '<number>';
65
+ inherits: true;
66
+ initial-value: 1;
61
67
  }
62
68
 
63
69
  @property --_sonic-waveform-marker {
@@ -105,7 +111,8 @@
105
111
  --_sonic-waveform-placeholder: var(--sonic-waveform-placeholder, var(--_sonic-etch));
106
112
  --_sonic-waveform-marker: var(--sonic-marker, var(--_sonic-ink));
107
113
  --_sonic-waveform-label: var(--sonic-waveform-label, var(--_sonic-waveform-marker));
108
- --_sonic-waveform-label-fade: var(--sonic-waveform-label-fade-ratio, 0.25);
114
+ --_sonic-waveform-label-fade-start: var(--sonic-waveform-label-fade-start, 0);
115
+ --_sonic-waveform-label-fade-end: var(--sonic-waveform-label-fade-end, 1);
109
116
  --_sonic-waveform-label-inset: calc(
110
117
  var(--_sonic-waveform-size) * var(--sonic-waveform-label-inset-ratio, 0.04)
111
118
  );
@@ -206,7 +213,7 @@
206
213
  }
207
214
 
208
215
  .sonic-waveform-label[data-sonic-dimmed] {
209
- color: color-mix(in oklab, var(--_sonic-marker, var(--_sonic-waveform-label)) 50%, transparent);
216
+ filter: opacity(0.5);
210
217
  }
211
218
 
212
219
  /* Opaque, since a system highlight carries alpha that would blend the wave into the glass */
@@ -238,6 +245,7 @@
238
245
 
239
246
  &[data-sonic-dimmed] {
240
247
  color: GrayText;
248
+ filter: none;
241
249
  }
242
250
  }
243
251
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xsynaptic/sonic-ui",
3
- "version": "0.16.0",
3
+ "version": "0.18.0",
4
4
  "description": "Native custom elements for audio interfaces, skinned with CSS custom properties",
5
5
  "keywords": [
6
6
  "custom-elements",
@@ -1,21 +0,0 @@
1
- //#region src/lib/asked-value.ts
2
- function moveTo(cell, mapping, next) {
3
- if (!Number.isFinite(next)) return false;
4
- const snapped = mapping.snap(next);
5
- cell.asked = snapped;
6
- if (snapped === cell.value) return false;
7
- cell.value = snapped;
8
- return true;
9
- }
10
- function setAsked(cell, mapping, next) {
11
- if (!Number.isFinite(next)) return false;
12
- const hasMoved = moveTo(cell, mapping, next);
13
- cell.asked = next;
14
- return hasMoved;
15
- }
16
- function resnap(cell, mapping, fallback) {
17
- cell.value = mapping.snap(cell.asked ?? fallback);
18
- }
19
-
20
- //#endregion
21
- export { moveTo, resnap, setAsked };
@@ -1,29 +0,0 @@
1
- //#region src/lib/fill-watch.ts
2
- function bindFill(control, sizeProperty, signal) {
3
- let watch;
4
- const stop = () => {
5
- watch?.disconnect();
6
- watch = void 0;
7
- };
8
- signal.addEventListener("abort", stop, { once: true });
9
- return (isFilling) => {
10
- if (signal.aborted || isFilling === (watch !== void 0)) return;
11
- stop();
12
- if (!isFilling) {
13
- control.style.removeProperty(sizeProperty);
14
- return;
15
- }
16
- const started = new ResizeObserver((entries) => {
17
- const box = entries.at(-1)?.borderBoxSize[0];
18
- if (!box) return;
19
- requestAnimationFrame(() => {
20
- if (watch === started) control.style.setProperty(sizeProperty, `${String(box.blockSize)}px`);
21
- });
22
- });
23
- watch = started;
24
- started.observe(control);
25
- };
26
- }
27
-
28
- //#endregion
29
- export { bindFill };
@@ -1,22 +0,0 @@
1
- //#region src/lib/asked-value.ts
2
- function moveTo(cell, mapping, next) {
3
- if (!Number.isFinite(next)) return false;
4
- const snapped = mapping.snap(next);
5
- cell.asked = snapped;
6
- if (snapped === cell.value) return false;
7
- cell.value = snapped;
8
- return true;
9
- }
10
- function setAsked(cell, mapping, next) {
11
- if (!Number.isFinite(next)) return false;
12
- const hasMoved = moveTo(cell, mapping, next);
13
- cell.asked = next;
14
- return hasMoved;
15
- }
16
- function resnap(cell, mapping, fallback) {
17
- cell.value = mapping.snap(cell.asked ?? fallback);
18
- }
19
-
20
- //#endregion
21
- export { moveTo, resnap, setAsked };
22
- //# sourceMappingURL=asked-value.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"asked-value.js","names":[],"sources":["../../../src/lib/asked-value.ts"],"sourcesContent":["import type { ValueMapping } from '#lib/value-mapping.ts';\n\nexport interface AskedValue {\n\tasked: number | undefined;\n\tvalue: number;\n}\n\n// A gesture: what was asked is the snapped value\n// eslint-disable-next-line unicorn/consistent-boolean-name -- a writer; the result says whether the value moved\nexport function moveTo(cell: AskedValue, mapping: ValueMapping, next: number): boolean {\n\tif (!Number.isFinite(next)) return false;\n\n\tconst snapped = mapping.snap(next);\n\n\tcell.asked = snapped;\n\tif (snapped === cell.value) return false;\n\n\tcell.value = snapped;\n\n\treturn true;\n}\n\n// A host write: the raw ask survives a change of range\n// eslint-disable-next-line unicorn/consistent-boolean-name -- a writer; the result says whether the value moved\nexport function setAsked(cell: AskedValue, mapping: ValueMapping, next: number): boolean {\n\tif (!Number.isFinite(next)) return false;\n\n\tconst hasMoved = moveTo(cell, mapping, next);\n\n\tcell.asked = next;\n\n\treturn hasMoved;\n}\n\nexport function resnap(cell: AskedValue, mapping: ValueMapping, fallback: number): void {\n\tcell.value = mapping.snap(cell.asked ?? fallback);\n}\n"],"mappings":";AASA,SAAgB,OAAO,MAAkB,SAAuB,MAAuB;CACtF,IAAI,CAAC,OAAO,SAAS,IAAI,GAAG,OAAO;CAEnC,MAAM,UAAU,QAAQ,KAAK,IAAI;CAEjC,KAAK,QAAQ;CACb,IAAI,YAAY,KAAK,OAAO,OAAO;CAEnC,KAAK,QAAQ;CAEb,OAAO;AACR;AAIA,SAAgB,SAAS,MAAkB,SAAuB,MAAuB;CACxF,IAAI,CAAC,OAAO,SAAS,IAAI,GAAG,OAAO;CAEnC,MAAM,WAAW,OAAO,MAAM,SAAS,IAAI;CAE3C,KAAK,QAAQ;CAEb,OAAO;AACR;AAEA,SAAgB,OAAO,MAAkB,SAAuB,UAAwB;CACvF,KAAK,QAAQ,QAAQ,KAAK,KAAK,SAAS,QAAQ;AACjD"}
@@ -1,30 +0,0 @@
1
- //#region src/lib/fill-watch.ts
2
- function bindFill(control, sizeProperty, signal) {
3
- let watch;
4
- const stop = () => {
5
- watch?.disconnect();
6
- watch = void 0;
7
- };
8
- signal.addEventListener("abort", stop, { once: true });
9
- return (isFilling) => {
10
- if (signal.aborted || isFilling === (watch !== void 0)) return;
11
- stop();
12
- if (!isFilling) {
13
- control.style.removeProperty(sizeProperty);
14
- return;
15
- }
16
- const started = new ResizeObserver((entries) => {
17
- const box = entries.at(-1)?.borderBoxSize[0];
18
- if (!box) return;
19
- requestAnimationFrame(() => {
20
- if (watch === started) control.style.setProperty(sizeProperty, `${String(box.blockSize)}px`);
21
- });
22
- });
23
- watch = started;
24
- started.observe(control);
25
- };
26
- }
27
-
28
- //#endregion
29
- export { bindFill };
30
- //# sourceMappingURL=fill-watch.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"fill-watch.js","names":[],"sources":["../../../src/lib/fill-watch.ts"],"sourcesContent":["export function bindFill(\n\tcontrol: HTMLElement,\n\tsizeProperty: `--_sonic-${string}`,\n\tsignal: AbortSignal,\n): (isFilling: boolean) => void {\n\tlet watch: ResizeObserver | undefined;\n\n\tconst stop = (): void => {\n\t\twatch?.disconnect();\n\t\twatch = undefined;\n\t};\n\n\tsignal.addEventListener('abort', stop, { once: true });\n\n\treturn (isFilling) => {\n\t\tif (signal.aborted || isFilling === (watch !== undefined)) return;\n\n\t\tstop();\n\t\tif (!isFilling) {\n\t\t\tcontrol.style.removeProperty(sizeProperty);\n\t\t\treturn;\n\t\t}\n\n\t\tconst started = new ResizeObserver((entries) => {\n\t\t\tconst box = entries.at(-1)?.borderBoxSize[0];\n\t\t\tif (!box) return;\n\n\t\t\t// Written a frame on, or the canvas resizes inside this delivery and WebKit reports a loop\n\t\t\trequestAnimationFrame(() => {\n\t\t\t\tif (watch === started) {\n\t\t\t\t\tcontrol.style.setProperty(sizeProperty, `${String(box.blockSize)}px`);\n\t\t\t\t}\n\t\t\t});\n\t\t});\n\n\t\twatch = started;\n\t\tstarted.observe(control);\n\t};\n}\n"],"mappings":";AAAA,SAAgB,SACf,SACA,cACA,QAC+B;CAC/B,IAAI;CAEJ,MAAM,aAAmB;EACxB,OAAO,WAAW;EAClB,QAAQ;CACT;CAEA,OAAO,iBAAiB,SAAS,MAAM,EAAE,MAAM,KAAK,CAAC;CAErD,QAAQ,cAAc;EACrB,IAAI,OAAO,WAAW,eAAe,UAAU,SAAY;EAE3D,KAAK;EACL,IAAI,CAAC,WAAW;GACf,QAAQ,MAAM,eAAe,YAAY;GACzC;EACD;EAEA,MAAM,UAAU,IAAI,gBAAgB,YAAY;GAC/C,MAAM,MAAM,QAAQ,GAAG,EAAE,CAAC,EAAE,cAAc;GAC1C,IAAI,CAAC,KAAK;GAGV,4BAA4B;IAC3B,IAAI,UAAU,SACb,QAAQ,MAAM,YAAY,cAAc,GAAG,OAAO,IAAI,SAAS,EAAE,GAAG;GAEtE,CAAC;EACF,CAAC;EAED,QAAQ;EACR,QAAQ,QAAQ,OAAO;CACxB;AACD"}