@xsynaptic/sonic-ui 0.17.0 → 0.19.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 (178) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +122 -19
  3. package/dist/default/elements/button.js +2 -5
  4. package/dist/default/elements/dial.js +2 -7
  5. package/dist/default/elements/display-element.js +9 -12
  6. package/dist/default/elements/envelope.js +31 -78
  7. package/dist/default/elements/field-gesture.js +81 -0
  8. package/dist/default/elements/meter.js +4 -1
  9. package/dist/default/elements/number.js +0 -7
  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/segmented.js +0 -5
  14. package/dist/default/elements/slider.js +11 -20
  15. package/dist/default/elements/sonic-element.js +3 -1
  16. package/dist/default/elements/split.js +14 -13
  17. package/dist/default/elements/switch.js +31 -47
  18. package/dist/default/elements/toggle.js +20 -49
  19. package/dist/default/elements/value-element.js +72 -94
  20. package/dist/default/elements/value-gestures.js +22 -22
  21. package/dist/default/elements/wave-element.js +16 -20
  22. package/dist/default/elements/waveform.js +42 -241
  23. package/dist/default/elements/wavestrip.js +10 -5
  24. package/dist/default/elements/xy.js +67 -119
  25. package/dist/default/index.js +2 -1
  26. package/dist/default/lib/canvas-surface.js +25 -0
  27. package/dist/default/lib/clock.js +23 -0
  28. package/dist/default/lib/crossfade.js +1 -0
  29. package/dist/default/lib/envelope-curve.js +1 -0
  30. package/dist/default/lib/peaks-request.js +44 -0
  31. package/dist/default/lib/positions.js +58 -0
  32. package/dist/default/lib/scrub.js +12 -0
  33. package/dist/default/lib/spoken-duration.js +30 -0
  34. package/dist/default/lib/value-model.js +62 -0
  35. package/dist/default/lib/waveform-buckets.js +7 -1
  36. package/dist/default/lib/waveform-scene.js +108 -0
  37. package/dist/default/lib/zoom-gesture.js +97 -1
  38. package/dist/dev/elements/button.d.ts +2 -0
  39. package/dist/dev/elements/button.js +14 -5
  40. package/dist/dev/elements/button.js.map +1 -1
  41. package/dist/dev/elements/dial.d.ts +2 -0
  42. package/dist/dev/elements/dial.js +16 -7
  43. package/dist/dev/elements/dial.js.map +1 -1
  44. package/dist/dev/elements/display-element.d.ts +5 -3
  45. package/dist/dev/elements/display-element.js +11 -9
  46. package/dist/dev/elements/display-element.js.map +1 -1
  47. package/dist/dev/elements/envelope.js +48 -78
  48. package/dist/dev/elements/envelope.js.map +1 -1
  49. package/dist/dev/elements/field-gesture.js +82 -0
  50. package/dist/dev/elements/field-gesture.js.map +1 -0
  51. package/dist/dev/elements/meter.d.ts +10 -5
  52. package/dist/dev/elements/meter.js +5 -1
  53. package/dist/dev/elements/meter.js.map +1 -1
  54. package/dist/dev/elements/number.js +13 -7
  55. package/dist/dev/elements/number.js.map +1 -1
  56. package/dist/dev/elements/position-group.d.ts +12 -6
  57. package/dist/dev/elements/position-group.js +42 -26
  58. package/dist/dev/elements/position-group.js.map +1 -1
  59. package/dist/dev/elements/radio-group.d.ts +11 -7
  60. package/dist/dev/elements/radio-group.js +59 -56
  61. package/dist/dev/elements/radio-group.js.map +1 -1
  62. package/dist/dev/elements/scale.js +3 -3
  63. package/dist/dev/elements/scale.js.map +1 -1
  64. package/dist/dev/elements/segmented.js +12 -5
  65. package/dist/dev/elements/segmented.js.map +1 -1
  66. package/dist/dev/elements/slider.d.ts +4 -2
  67. package/dist/dev/elements/slider.js +25 -20
  68. package/dist/dev/elements/slider.js.map +1 -1
  69. package/dist/dev/elements/sonic-element.d.ts +13 -2
  70. package/dist/dev/elements/sonic-element.js +63 -11
  71. package/dist/dev/elements/sonic-element.js.map +1 -1
  72. package/dist/dev/elements/split.d.ts +1 -0
  73. package/dist/dev/elements/split.js +14 -13
  74. package/dist/dev/elements/split.js.map +1 -1
  75. package/dist/dev/elements/switch.d.ts +2 -2
  76. package/dist/dev/elements/switch.js +38 -47
  77. package/dist/dev/elements/switch.js.map +1 -1
  78. package/dist/dev/elements/toggle.js +32 -49
  79. package/dist/dev/elements/toggle.js.map +1 -1
  80. package/dist/dev/elements/value-element.d.ts +11 -5
  81. package/dist/dev/elements/value-element.js +72 -94
  82. package/dist/dev/elements/value-element.js.map +1 -1
  83. package/dist/dev/elements/value-gestures.js +22 -22
  84. package/dist/dev/elements/value-gestures.js.map +1 -1
  85. package/dist/dev/elements/wave-element.d.ts +8 -5
  86. package/dist/dev/elements/wave-element.js +30 -20
  87. package/dist/dev/elements/wave-element.js.map +1 -1
  88. package/dist/dev/elements/waveform.d.ts +20 -22
  89. package/dist/dev/elements/waveform.js +42 -241
  90. package/dist/dev/elements/waveform.js.map +1 -1
  91. package/dist/dev/elements/wavestrip.d.ts +5 -1
  92. package/dist/dev/elements/wavestrip.js +10 -5
  93. package/dist/dev/elements/wavestrip.js.map +1 -1
  94. package/dist/dev/elements/xy.js +81 -119
  95. package/dist/dev/elements/xy.js.map +1 -1
  96. package/dist/dev/index.d.ts +5 -3
  97. package/dist/dev/index.js +2 -1
  98. package/dist/dev/lib/canvas-surface.d.ts +1 -0
  99. package/dist/dev/lib/canvas-surface.js +25 -0
  100. package/dist/dev/lib/canvas-surface.js.map +1 -1
  101. package/dist/dev/lib/clock.d.ts +5 -0
  102. package/dist/dev/lib/clock.js +24 -0
  103. package/dist/dev/lib/clock.js.map +1 -0
  104. package/dist/dev/lib/crossfade.d.ts +1 -0
  105. package/dist/dev/lib/crossfade.js +1 -0
  106. package/dist/dev/lib/crossfade.js.map +1 -1
  107. package/dist/dev/lib/envelope-curve.d.ts +1 -0
  108. package/dist/dev/lib/envelope-curve.js +1 -0
  109. package/dist/dev/lib/envelope-curve.js.map +1 -1
  110. package/dist/dev/lib/mirror-children.js.map +1 -1
  111. package/dist/dev/lib/owned-control.js.map +1 -1
  112. package/dist/dev/lib/peaks-request.d.ts +5 -0
  113. package/dist/dev/lib/peaks-request.js +45 -0
  114. package/dist/dev/lib/peaks-request.js.map +1 -0
  115. package/dist/dev/lib/positions.d.ts +7 -0
  116. package/dist/dev/lib/positions.js +59 -0
  117. package/dist/dev/lib/positions.js.map +1 -0
  118. package/dist/dev/lib/scrub.d.ts +8 -0
  119. package/dist/dev/lib/scrub.js +13 -0
  120. package/dist/dev/lib/scrub.js.map +1 -0
  121. package/dist/dev/lib/spoken-duration.js +31 -0
  122. package/dist/dev/lib/spoken-duration.js.map +1 -0
  123. package/dist/dev/lib/time-regions.d.ts +1 -0
  124. package/dist/dev/lib/time-regions.js.map +1 -1
  125. package/dist/dev/lib/units.d.ts +8 -0
  126. package/dist/dev/lib/value-model.js +63 -0
  127. package/dist/dev/lib/value-model.js.map +1 -0
  128. package/dist/dev/lib/waveform-buckets.d.ts +2 -1
  129. package/dist/dev/lib/waveform-buckets.js +7 -1
  130. package/dist/dev/lib/waveform-buckets.js.map +1 -1
  131. package/dist/dev/lib/waveform-scene.d.ts +11 -0
  132. package/dist/dev/lib/waveform-scene.js +109 -0
  133. package/dist/dev/lib/waveform-scene.js.map +1 -0
  134. package/dist/dev/lib/zoom-gesture.js +97 -1
  135. package/dist/dev/lib/zoom-gesture.js.map +1 -1
  136. package/dist/skins/amber.css +1 -0
  137. package/dist/skins/ivory.css +1 -0
  138. package/dist/skins/lime.css +1 -0
  139. package/dist/skins/slate.css +1 -0
  140. package/dist/styles/button.css +45 -11
  141. package/dist/styles/dial.css +10 -9
  142. package/dist/styles/envelope.css +10 -4
  143. package/dist/styles/led.css +5 -1
  144. package/dist/styles/material/arc.css +3 -2
  145. package/dist/styles/material/bracket.css +2 -2
  146. package/dist/styles/material/cap.css +33 -12
  147. package/dist/styles/material/core.css +4 -0
  148. package/dist/styles/material/glass.css +1 -1
  149. package/dist/styles/material/groove.css +4 -3
  150. package/dist/styles/material/keycap.css +3 -2
  151. package/dist/styles/material/lens.css +1 -1
  152. package/dist/styles/material/pane.css +5 -4
  153. package/dist/styles/material/readout.css +6 -6
  154. package/dist/styles/material/well.css +2 -2
  155. package/dist/styles/meter.css +2 -7
  156. package/dist/styles/number.css +5 -1
  157. package/dist/styles/panel.css +4 -3
  158. package/dist/styles/ring.css +1 -1
  159. package/dist/styles/screen.css +5 -1
  160. package/dist/styles/segmented.css +17 -4
  161. package/dist/styles/slider.css +9 -3
  162. package/dist/styles/spectrum.css +16 -3
  163. package/dist/styles/switch.css +35 -10
  164. package/dist/styles/toggle.css +14 -7
  165. package/dist/styles/waveform.css +17 -5
  166. package/dist/styles/wavestrip.css +11 -3
  167. package/dist/styles/xy.css +9 -5
  168. package/package.json +1 -6
  169. package/dist/default/dat.js +0 -51
  170. package/dist/default/lib/asked-value.js +0 -21
  171. package/dist/default/lib/fill-watch.js +0 -29
  172. package/dist/dev/dat.d.ts +0 -19
  173. package/dist/dev/dat.js +0 -52
  174. package/dist/dev/dat.js.map +0 -1
  175. package/dist/dev/lib/asked-value.js +0 -22
  176. package/dist/dev/lib/asked-value.js.map +0 -1
  177. package/dist/dev/lib/fill-watch.js +0 -30
  178. 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` unset follows the array: 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 fullScaleOf({ fullScale, samples }: WaveformPeaks): number {\n\tif (fullScale !== undefined) return fullScale;\n\tif (samples instanceof Int8Array) return 128;\n\n\treturn samples instanceof Int16Array ? 32_768 : 1;\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\tconst fullScale = fullScaleOf(peaks);\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 / fullScale, high / 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,YAAY,EAAE,WAAW,WAAkC;CACnE,IAAI,cAAc,QAAW,OAAO;CACpC,IAAI,mBAAmB,WAAW,OAAO;CAEzC,OAAO,mBAAmB,aAAa,QAAS;AACjD;AAEA,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,MAAM,YAAY,YAAY,KAAK;CACnC,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,WAAW,OAAO,SAAS;AAC1C;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":";;;;;AAoBA,MAAa,kBAAkB;CAC9B,MAAM;CACN,MAAM;CACN,QAAQ;CACR,aAAa;CACb,MAAM;CACN,UAAU;AACX;AAEA,MAAM,kBAAkB;AAExB,MAAM,eAAe;AACrB,MAAM,0BAA0B;AAChC,MAAM,uBAAuB;AAE7B,SAAS,WACR,SACA,OACA,CAAC,MAAM,KACA;CACP,IAAI,MAAM,MAAM;CAEhB,MAAM,EAAE,KAAK,WAAW;CACxB,MAAM,QAAQ,KAAK,IAAI,GAAG,KAAK,MAAM,eAAe,GAAG,CAAC;CACxD,MAAM,SAAS,MAAM,eAAe,MAAM;CAE1C,QAAQ,KAAK;CACb,QAAQ,UAAU;CAClB,QAAQ,KAAK,MAAM,GAAG,KAAK,MAAM,MAAM;CACvC,QAAQ,KAAK;CACb,QAAQ,UAAU;CAClB,KAAK,IAAI,IAAI,KAAK,OAAO,OAAO,SAAS,UAAU,KAAK,IAAI,QAAQ,QAAQ,IAAI,IAAI,KAAK,OAAO;EAC/F,QAAQ,OAAO,GAAG,MAAM;EACxB,QAAQ,OAAO,IAAI,QAAQ,CAAC;CAC7B;CACA,QAAQ,YAAY,KAAK,IAAI,GAAG,KAAK,MAAM,GAAG,CAAC;CAC/C,QAAQ,cAAc,MAAM,QAAQ;CACpC,QAAQ,OAAO;CACf,QAAQ,QAAQ;AACjB;AAEA,SAAS,kBAAkB,SAAmC,OAAoB;CACjF,IAAI,MAAM,QAAQ,WAAW,GAAG;CAEhC,MAAM,EAAE,KAAK,QAAQ,iBAAiB,iBAAiB;CACvD,MAAM,aAAa,0BAA0B;CAC7C,MAAM,SAAS,SAAS;CACxB,MAAM,SAAS,eAAe;CAC9B,MAAM,SAAS,MACd,SACA,KAAK,MAAM,IAAI,UAAU,aAAa,MAAM,SAAS,IAAI,KAAK,EAAE,IAC/D,SACA;CAEF,QAAQ,UAAU;CAClB,KAAK,MAAM,CAAC,MAAM,OAAO,MAAM,SAAS;EACvC,MAAM,WAAW,OAAO,gBAAgB;EACxC,MAAM,WAAW,KAAK,gBAAgB;EAEtC,QAAQ,OAAO,SAAS,MAAM,OAAO,CAAC;EACtC,KAAK,IAAI,IAAI,UAAU,KAAK,IAAI,SAAS,KAAK,KAAK,QAAQ,OAAO,GAAG,MAAM,CAAC,CAAC;EAC7E,QAAQ,OAAO,SAAS,MAAM,OAAO,CAAC;CACvC;CACA,QAAQ,YAAY,KAAK,IAAI,GAAG,KAAK,MAAM,GAAG,CAAC;CAC/C,QAAQ,cAAc,MAAM,QAAQ;CACpC,QAAQ,OAAO;AAChB;AAEA,SAAS,SAAS,SAAmC,OAA8B;CAClF,MAAM,EAAE,MAAM,aAAa,MAAM;CACjC,MAAM,WAAW,QAAQ,qBAAqB,GAAG,GAAG,GAAG,MAAM,MAAM;CAEnE,SAAS,aAAa,GAAG,QAAQ;CACjC,SAAS,aAAa,IAAK,IAAI;CAC/B,SAAS,aAAa,GAAG,QAAQ;CAEjC,OAAO;AACR;AAEA,SAAS,UAAU,SAAmC,OAAc,OAA4B;CAC/F,MAAM,UAAU,gBAAgB,OAAO,KAAK;CAC5C,MAAM,CAAC,SAAS;CAChB,IAAI,CAAC,OAAO;CAEZ,MAAM,SAAS,MAAM,SAAS;CAC9B,MAAM,cAAc,SAAS;CAE7B,QAAQ,YAAY,SAAS,SAAS,KAAK;CAC3C,QAAQ,UAAU;CAClB,QAAQ,OAAO,MAAM,GAAG,MAAM;CAC9B,KAAK,MAAM,UAAU,SAAS,QAAQ,OAAO,OAAO,GAAG,SAAS,OAAO,OAAO,WAAW;CACzF,KAAK,MAAM,UAAU,QAAQ,WAAW,GACvC,QAAQ,OAAO,OAAO,GAAG,SAAS,OAAO,MAAM,WAAW;CAC3D,QAAQ,UAAU;CAClB,QAAQ,KAAK;AACd;AAEA,SAAS,aAAa,SAAmC,OAAc,OAAoB;CAC1F,MAAM,OAAO,KAAK,IAAI,GAAG,KAAK,MAAM,MAAM,GAAG,CAAC;CAE9C,KAAK,MAAM,EAAE,QAAQ,MAAM,WAAW,MAAM,SAAS;EACpD,MAAM,KAAK,QAAQ,MAAM,gBAAgB,MAAM;EAE/C,QAAQ,aACN,OAAO,IAAI,IAAI,MAAM,YAAY,IAAI,IAAI,IAAI,WAAc,MAAM,QAAQ;EAC3E,QAAQ,cAAc,WAAW,OAAO,KAAM;EAC9C,QAAQ,SAAS,IAAI,OAAO,GAAG,GAAG,MAAM,MAAM,MAAM;CACrD;CACA,QAAQ,cAAc;AACvB;AAEA,SAAgB,cAAc,SAAmC,OAAc,OAAoB;CAClG,MAAM,EAAE,KAAK,QAAQ,iBAAiB,cAAc,UAAU;CAC9D,MAAM,KAAK,aAA6B,UAAU,gBAAgB;CAClE,MAAM,EAAE,MAAM,SAAS,MAAM;CAC7B,MAAM,OAAO,KAAK,IAAI,GAAG,KAAK,MAAM,GAAG,CAAC;CACxC,MAAM,CAAC,SAAS,WAAW,CAAC,EAAE,MAAM,MAAM,EAAE,GAAG,EAAE,MAAM,MAAM,EAAE,CAAC;CAChE,MAAM,CAAC,MAAM,MAAM,CAAC,MAAM,SAAS,GAAG,KAAK,GAAG,MAAM,SAAS,GAAG,KAAK,CAAC;CAEtE,QAAQ,UAAU,GAAG,GAAG,OAAO,MAAM;CACrC,WAAW,SAAS,OAAO,CAAC,GAAG,IAAI,CAAC;CACpC,WAAW,SAAS,OAAO,CAAC,IAAI,KAAK,CAAC;CACtC,QAAQ,YAAY;CACpB,IAAI,KAAK,MAAM,QAAQ,SAAS,MAAM,KAAK,OAAO,SAAS,QAAQ,CAAC,GAAG,KAAK,MAAM,IAAI;CACtF,kBAAkB,SAAS,KAAK;CAChC,IAAI,MAAM,OAAO,UAAU,SAAS,OAAO,MAAM,KAAK;CAEtD,QAAQ,YAAY;CACpB,KAAK,MAAM,QAAQ,CAAC,SAAS,OAAO,GAAG,QAAQ,SAAS,OAAO,OAAO,GAAG,GAAG,MAAM,MAAM;CACxF,aAAa,SAAS,OAAO,KAAK;AACnC"}
@@ -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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"}
@@ -4,5 +4,6 @@
4
4
  --sonic-cap: oklch(37% 0.012 286deg);
5
5
  --sonic-cap-hover: oklch(44.2% 0.015 286deg);
6
6
  --sonic-ink: color-mix(in oklab, var(--sonic-cap) 30%, #fff);
7
+ --sonic-wavestrip-inset-ratio: 0.1;
7
8
  }
8
9
  }
@@ -4,5 +4,6 @@
4
4
  --sonic-cap: oklch(85.5% 0.024 85deg);
5
5
  --sonic-cap-hover: oklch(90.5% 0.02 85deg);
6
6
  --sonic-ink: oklch(18.5% 0.012 60deg);
7
+ --sonic-wavestrip-inset-ratio: 0.1;
7
8
  }
8
9
  }
@@ -8,5 +8,6 @@
8
8
  --sonic-ink: color-mix(in oklab, var(--sonic-cap) 30%, #fff);
9
9
  --sonic-dial-ring-ratio: 0.05;
10
10
  --sonic-dial-gap-ratio: 0.1;
11
+ --sonic-wavestrip-inset-ratio: 0.1;
11
12
  }
12
13
  }
@@ -8,5 +8,6 @@
8
8
  --sonic-ink: color-mix(in oklab, var(--sonic-cap) 30%, #fff);
9
9
  --sonic-dial-ring-ratio: 0.12;
10
10
  --sonic-dial-gap-ratio: 0.07;
11
+ --sonic-wavestrip-inset-ratio: 0.1;
11
12
  }
12
13
  }
@@ -31,22 +31,36 @@
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);
44
54
  --_sonic-button-bevel-ratio: var(--sonic-button-bevel-ratio, 0.075);
45
55
  --_sonic-button-face-radius-ratio: var(--sonic-button-face-radius-ratio, 0.15);
56
+ --_sonic-button-face-radius-own: calc(
57
+ var(--_sonic-button-size) * var(--sonic-button-face-radius-ratio)
58
+ );
46
59
  --_sonic-button-icon-ratio: var(--sonic-button-icon-ratio, 0.5);
47
60
  --_sonic-button-led-ratio: var(--sonic-button-led-ratio, 0.2);
48
61
  --_sonic-button-led-icon-ratio: var(--sonic-button-led-icon-ratio, 0.35);
49
62
  --_sonic-button-font-ratio: var(--sonic-button-font-ratio, 0.34);
63
+ --_sonic-button-font-own: calc(var(--_sonic-button-size) * var(--sonic-button-font-ratio));
50
64
  --_sonic-button-padding-ratio: var(--sonic-button-padding-ratio, 0.25);
51
65
  --_sonic-button-depth-ratio: var(--sonic-button-depth-ratio, calc(0.03 * var(--_sonic-relief)));
52
66
  --_sonic-button-press-scale: var(--sonic-button-press-scale, 0.985);
@@ -55,7 +69,11 @@
55
69
  var(--_sonic-button-press-scale)
56
70
  );
57
71
  --_sonic-unit: calc(var(--_sonic-button-size) / 32);
58
- --_sonic-button-corner: calc(var(--_sonic-button-size) * var(--_sonic-button-radius-ratio));
72
+ --_sonic-button-radius-own: calc(var(--_sonic-button-size) * var(--sonic-button-radius-ratio));
73
+ --_sonic-button-corner: var(
74
+ --_sonic-button-radius-own,
75
+ var(--sonic-radius, calc(var(--_sonic-button-size) * var(--_sonic-button-radius-ratio)))
76
+ );
59
77
  --_sonic-button-gap: calc(var(--_sonic-button-size) * var(--_sonic-button-gap-ratio));
60
78
  --_sonic-keycap-depth: calc(var(--_sonic-button-size) * var(--_sonic-button-depth-ratio));
61
79
 
@@ -71,11 +89,11 @@
71
89
  border: 0;
72
90
  border-radius: var(--_sonic-button-corner);
73
91
  box-shadow: var(--_sonic-well);
74
- transition: color 120ms ease-out;
92
+ transition: color var(--_sonic-fade-duration) ease-out;
75
93
  }
76
94
 
77
95
  /* An `auto` width stretches in a column flex or grid parent */
78
- .sonic-button:where(:has(> .sonic-button-cap > :not(svg, .sonic-led))) {
96
+ .sonic-button:where(:has(> .sonic-button-cap > :not(.sonic-led))) {
79
97
  inline-size: max-content;
80
98
  min-inline-size: calc(var(--_sonic-button-size) * var(--_sonic-button-aspect-ratio));
81
99
  }
@@ -222,8 +240,18 @@
222
240
  .sonic-button-cap {
223
241
  --_sonic-keycap-corner: max(0px, var(--_sonic-button-corner) - var(--_sonic-button-gap));
224
242
  --_sonic-keycap-bevel: calc(var(--_sonic-button-size) * var(--_sonic-button-bevel-ratio));
225
- --_sonic-keycap-face-corner: calc(
226
- var(--_sonic-button-size) * var(--_sonic-button-face-radius-ratio)
243
+
244
+ /* Invalid without the shared radius; with it the face follows the cap's corner, as an option's does */
245
+ --_sonic-button-face-follow: max(
246
+ 0px,
247
+ var(--_sonic-keycap-corner) - var(--_sonic-keycap-bevel) + 0 * var(--sonic-radius)
248
+ );
249
+ --_sonic-keycap-face-corner: var(
250
+ --_sonic-button-face-radius-own,
251
+ var(
252
+ --_sonic-button-face-follow,
253
+ calc(var(--_sonic-button-size) * var(--_sonic-button-face-radius-ratio))
254
+ )
227
255
  );
228
256
 
229
257
  position: absolute;
@@ -244,14 +272,21 @@
244
272
 
245
273
  & > :not(svg, .sonic-led) {
246
274
  padding-inline: calc(var(--_sonic-button-size) * var(--_sonic-button-padding-ratio));
247
- font-size: calc(var(--_sonic-button-size) * var(--_sonic-button-font-ratio));
275
+ font-size: var(
276
+ --_sonic-button-font-own,
277
+ var(
278
+ --sonic-legend-font-size,
279
+ calc(var(--_sonic-button-size) * var(--_sonic-button-font-ratio))
280
+ )
281
+ );
248
282
  line-height: 1;
249
283
  white-space: nowrap;
250
284
  }
251
285
 
252
286
  & > svg {
253
- inline-size: calc(var(--_sonic-button-size) * var(--_sonic-button-icon-ratio));
287
+ inline-size: auto;
254
288
  block-size: calc(var(--_sonic-button-size) * var(--_sonic-button-icon-ratio));
289
+ aspect-ratio: auto 1;
255
290
  mask-image: linear-gradient(
256
291
  var(--_sonic-light-fall),
257
292
  #000 15%,
@@ -261,7 +296,7 @@
261
296
  }
262
297
  }
263
298
 
264
- .sonic-button-cap:has(> :not(svg, .sonic-led)) {
299
+ .sonic-button-cap:has(> :not(.sonic-led)) {
265
300
  position: relative;
266
301
  inset: auto;
267
302
  block-size: calc(var(--_sonic-button-size) - 2 * var(--_sonic-button-gap));
@@ -269,7 +304,6 @@
269
304
  }
270
305
 
271
306
  .sonic-button-cap:has(> .sonic-led) > svg {
272
- inline-size: calc(var(--_sonic-button-size) * var(--_sonic-button-led-icon-ratio));
273
307
  block-size: calc(var(--_sonic-button-size) * var(--_sonic-button-led-icon-ratio));
274
308
  }
275
309
 
@@ -313,10 +313,11 @@
313
313
  &::before {
314
314
  inset: 0;
315
315
  background: repeating-conic-gradient(
316
- rgb(0 0 0 / calc(0.3 * var(--_sonic-relief))) 0,
317
- rgb(255 255 255 / calc(0.12 * var(--_sonic-relief)))
316
+ rgb(from var(--_sonic-shade) r g b / calc(0.3 * var(--_sonic-relief))) 0,
317
+ rgb(from var(--_sonic-shine) r g b / calc(0.12 * var(--_sonic-relief)))
318
318
  calc(var(--_sonic-dial-ridge-angle) / 2),
319
- rgb(0 0 0 / calc(0.3 * var(--_sonic-relief))) var(--_sonic-dial-ridge-angle)
319
+ rgb(from var(--_sonic-shade) r g b / calc(0.3 * var(--_sonic-relief)))
320
+ var(--_sonic-dial-ridge-angle)
320
321
  );
321
322
  rotate: var(--_sonic-dial-angle);
322
323
  }
@@ -329,15 +330,15 @@
329
330
  calc(
330
331
  var(--_sonic-dial-face-drift) * var(--_sonic-dial-angle) - var(--_sonic-light-tilt)
331
332
  ),
332
- rgb(255 255 255 / calc(0.16 * var(--_sonic-relief))),
333
+ rgb(from var(--_sonic-shine) r g b / calc(0.16 * var(--_sonic-relief))),
333
334
  transparent 12%,
334
- rgb(0 0 0 / calc(0.1 * var(--_sonic-relief))) 25%,
335
+ rgb(from var(--_sonic-shade) r g b / calc(0.1 * var(--_sonic-relief))) 25%,
335
336
  transparent 38%,
336
- rgb(255 255 255 / calc(0.12 * var(--_sonic-relief))) 50%,
337
+ rgb(from var(--_sonic-shine) r g b / calc(0.12 * var(--_sonic-relief))) 50%,
337
338
  transparent 62%,
338
- rgb(0 0 0 / calc(0.1 * var(--_sonic-relief))) 75%,
339
+ rgb(from var(--_sonic-shade) r g b / calc(0.1 * var(--_sonic-relief))) 75%,
339
340
  transparent 88%,
340
- rgb(255 255 255 / calc(0.16 * var(--_sonic-relief)))
341
+ rgb(from var(--_sonic-shine) r g b / calc(0.16 * var(--_sonic-relief)))
341
342
  ),
342
343
  var(--_sonic-face);
343
344
  box-shadow: var(--_sonic-face-shadow);
@@ -368,7 +369,7 @@
368
369
  background: var(--_sonic-ink);
369
370
  border-radius: calc(var(--_sonic-dial-indicator-width) / 2);
370
371
  box-shadow: inset 0 0 calc(0.75 * var(--_sonic-unit))
371
- rgb(0 0 0 / calc(0.55 * var(--_sonic-relief)));
372
+ rgb(from var(--_sonic-shade) r g b / calc(0.55 * var(--_sonic-relief)));
372
373
  clip-path: shape(
373
374
  from 0 100%,
374
375
  line to calc(50% - var(--_sonic-dial-indicator-tip)) var(--_sonic-dial-indicator-tip),
@@ -32,7 +32,13 @@
32
32
  --_sonic-envelope-curve-ratio: var(--sonic-envelope-curve-ratio, 0.08);
33
33
  --_sonic-unit: calc(var(--_sonic-envelope-size) / 64);
34
34
  --_sonic-envelope-inset: calc(var(--_sonic-envelope-size) * var(--_sonic-envelope-inset-ratio));
35
- --_sonic-glass-radius: calc(var(--_sonic-envelope-size) * var(--_sonic-envelope-radius-ratio));
35
+ --_sonic-envelope-radius-own: calc(
36
+ var(--_sonic-envelope-size) * var(--sonic-envelope-radius-ratio)
37
+ );
38
+ --_sonic-glass-radius: var(
39
+ --_sonic-envelope-radius-own,
40
+ var(--sonic-radius, calc(var(--_sonic-envelope-size) * var(--_sonic-envelope-radius-ratio)))
41
+ );
36
42
 
37
43
  inline-size: calc(var(--_sonic-envelope-size) * var(--_sonic-envelope-aspect-ratio));
38
44
  block-size: var(--_sonic-envelope-size);
@@ -124,9 +130,9 @@
124
130
  var(--_sonic-point-halo),
125
131
  var(--_sonic-focus-shadow, 0 0 transparent);
126
132
  transition:
127
- background-color 120ms ease-out,
128
- border-color 120ms ease-out,
129
- box-shadow 120ms ease-out;
133
+ background-color var(--_sonic-fade-duration) ease-out,
134
+ border-color var(--_sonic-fade-duration) ease-out,
135
+ box-shadow var(--_sonic-fade-duration) ease-out;
130
136
  }
131
137
 
132
138
  .sonic-envelope-handle {
@@ -117,7 +117,11 @@
117
117
  &::after {
118
118
  background: radial-gradient(
119
119
  closest-side,
120
- color-mix(in oklab, var(--_sonic-lit) calc(100% - 45% * var(--_sonic-relief)), #fff),
120
+ color-mix(
121
+ in oklab,
122
+ var(--_sonic-lit) calc(100% - 45% * var(--_sonic-relief)),
123
+ var(--_sonic-shine)
124
+ ),
121
125
  var(--_sonic-lit)
122
126
  );
123
127
  }
@@ -13,9 +13,10 @@
13
13
  );
14
14
  --_sonic-arc-shade: radial-gradient(
15
15
  closest-side,
16
- rgb(0 0 0 / calc(0.55 * var(--_sonic-relief))) calc(100% - var(--_sonic-arc-width)),
16
+ rgb(from var(--_sonic-shade) r g b / calc(0.55 * var(--_sonic-relief)))
17
+ calc(100% - var(--_sonic-arc-width)),
17
18
  transparent calc(100% - var(--_sonic-arc-width) * 0.4),
18
- rgb(0 0 0 / calc(0.25 * var(--_sonic-relief))) 100%
19
+ rgb(from var(--_sonic-shade) r g b / calc(0.25 * var(--_sonic-relief))) 100%
19
20
  );
20
21
  }
21
22
  }
@@ -19,8 +19,8 @@
19
19
  );
20
20
  scale: 0.6;
21
21
  transition:
22
- opacity 120ms ease-out,
23
- scale 120ms ease-out;
22
+ opacity var(--_sonic-fade-duration) ease-out,
23
+ scale var(--_sonic-fade-duration) ease-out;
24
24
  }
25
25
 
26
26
  :state(dragging) > * > .sonic-xy-bracket,
@@ -23,36 +23,57 @@
23
23
  --_sonic-lit-disabled: color-mix(in oklab, var(--_sonic-ink) 25%, var(--_sonic-unlit));
24
24
  --_sonic-skirt: linear-gradient(
25
25
  var(--_sonic-light-fall),
26
- color-mix(in oklab, var(--_sonic-cap) calc(100% - 30% * var(--_sonic-relief)), #fff),
27
- color-mix(in oklab, var(--_sonic-cap) calc(100% - 45% * var(--_sonic-relief)), #000)
26
+ color-mix(
27
+ in oklab,
28
+ var(--_sonic-cap) calc(100% - 30% * var(--_sonic-relief)),
29
+ var(--_sonic-shine)
30
+ ),
31
+ color-mix(
32
+ in oklab,
33
+ var(--_sonic-cap) calc(100% - 45% * var(--_sonic-relief)),
34
+ var(--_sonic-shade)
35
+ )
28
36
  );
29
37
  --_sonic-face: linear-gradient(
30
38
  var(--_sonic-light-fall),
31
- color-mix(in oklab, var(--_sonic-cap) calc(100% - 15% * var(--_sonic-relief)), #000),
32
- color-mix(in oklab, var(--_sonic-cap) calc(100% - 10% * var(--_sonic-relief)), #fff)
39
+ color-mix(
40
+ in oklab,
41
+ var(--_sonic-cap) calc(100% - 15% * var(--_sonic-relief)),
42
+ var(--_sonic-shade)
43
+ ),
44
+ color-mix(
45
+ in oklab,
46
+ var(--_sonic-cap) calc(100% - 10% * var(--_sonic-relief)),
47
+ var(--_sonic-shine)
48
+ )
33
49
  );
34
50
  --_sonic-sheen: linear-gradient(
35
51
  105deg,
36
52
  transparent 20%,
37
- rgb(255 255 255 / calc(0.1 * var(--_sonic-relief))) 45%,
53
+ rgb(from var(--_sonic-shine) r g b / calc(0.1 * var(--_sonic-relief))) 45%,
38
54
  transparent 55%,
39
- rgb(0 0 0 / calc(0.06 * var(--_sonic-relief))) 80%
55
+ rgb(from var(--_sonic-shade) r g b / calc(0.06 * var(--_sonic-relief))) 80%
40
56
  );
41
57
  --_sonic-press-transition:
42
- scale 60ms cubic-bezier(0.4, 0, 0.2, 1), translate 60ms cubic-bezier(0.4, 0, 0.2, 1),
43
- box-shadow 60ms cubic-bezier(0.4, 0, 0.2, 1);
58
+ scale var(--_sonic-press-duration) cubic-bezier(0.4, 0, 0.2, 1),
59
+ translate var(--_sonic-press-duration) cubic-bezier(0.4, 0, 0.2, 1),
60
+ box-shadow var(--_sonic-press-duration) cubic-bezier(0.4, 0, 0.2, 1);
44
61
  --_sonic-face-shadow:
45
62
  inset var(--_sonic-cast-x) var(--_sonic-cast-y) calc(1.5 * var(--_sonic-unit))
46
- rgb(0 0 0 / calc(0.35 * var(--_sonic-relief))),
63
+ rgb(from var(--_sonic-shade) r g b / calc(0.35 * var(--_sonic-relief))),
47
64
  calc(0.5 * var(--_sonic-cast-x)) calc(0.5 * var(--_sonic-cast-y)) var(--_sonic-unit)
48
- rgb(255 255 255 / calc(0.12 * var(--_sonic-relief)));
65
+ rgb(from var(--_sonic-shine) r g b / calc(0.12 * var(--_sonic-relief)));
49
66
  --_sonic-rim-width: max(1px, round(0.75 * var(--_sonic-unit), var(--_sonic-snap)));
50
67
  --_sonic-rim: 0 0 0 var(--_sonic-rim-width)
51
68
  var(
52
69
  --sonic-cap-edge,
53
- color-mix(in oklab, var(--_sonic-cap) calc(100% - 60% * var(--_sonic-relief)), #000)
70
+ color-mix(
71
+ in oklab,
72
+ var(--_sonic-cap) calc(100% - 60% * var(--_sonic-relief)),
73
+ var(--_sonic-shade)
74
+ )
54
75
  );
55
- --_sonic-drop-shade: rgb(0 0 0 / calc(0.65 * var(--_sonic-relief)));
76
+ --_sonic-drop-shade: rgb(from var(--_sonic-shade) r g b / calc(0.65 * var(--_sonic-relief)));
56
77
  --_sonic-focus-shadow: 0 0 transparent;
57
78
  --_sonic-focus-glow: 0 0 max(4px, 3 * var(--_sonic-unit)) max(1px, 0.5 * var(--_sonic-unit))
58
79
  color-mix(in oklab, var(--_sonic-focus) calc(60% * var(--_sonic-focus-strength)), transparent);