@xsynaptic/sonic-ui 0.23.0 → 0.24.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/default/elements/readout.js +3 -2
  3. package/dist/default/elements/sonic-element.js +23 -4
  4. package/dist/default/elements/value-element.js +2 -2
  5. package/dist/default/lib/spoken-duration.js +3 -0
  6. package/dist/default/lib/waveform-bands.js +37 -24
  7. package/dist/default/lib/waveform-scene.js +12 -1
  8. package/dist/dev/elements/readout.js +3 -2
  9. package/dist/dev/elements/readout.js.map +1 -1
  10. package/dist/dev/elements/sonic-element.js +23 -4
  11. package/dist/dev/elements/sonic-element.js.map +1 -1
  12. package/dist/dev/elements/value-element.js +2 -2
  13. package/dist/dev/elements/value-element.js.map +1 -1
  14. package/dist/dev/elements/waveform.d.ts +3 -0
  15. package/dist/dev/lib/spoken-duration.js +3 -0
  16. package/dist/dev/lib/spoken-duration.js.map +1 -1
  17. package/dist/dev/lib/waveform-bands.js +37 -24
  18. package/dist/dev/lib/waveform-bands.js.map +1 -1
  19. package/dist/dev/lib/waveform-scene.d.ts +3 -0
  20. package/dist/dev/lib/waveform-scene.js +12 -1
  21. package/dist/dev/lib/waveform-scene.js.map +1 -1
  22. package/dist/styles/button.css +30 -11
  23. package/dist/styles/dial.css +13 -15
  24. package/dist/styles/envelope.css +2 -2
  25. package/dist/styles/led.css +28 -13
  26. package/dist/styles/material/cap.css +6 -6
  27. package/dist/styles/material/core.css +53 -1
  28. package/dist/styles/material/glass.css +11 -1
  29. package/dist/styles/material/keycap.css +3 -3
  30. package/dist/styles/material/pane.css +8 -6
  31. package/dist/styles/material/readout.css +19 -7
  32. package/dist/styles/material/scale.css +4 -4
  33. package/dist/styles/material/value.css +12 -3
  34. package/dist/styles/meter.css +6 -6
  35. package/dist/styles/panel.css +2 -3
  36. package/dist/styles/ring.css +3 -3
  37. package/dist/styles/segmented.css +6 -6
  38. package/dist/styles/slider.css +28 -29
  39. package/dist/styles/switch.css +49 -51
  40. package/dist/styles/toggle.css +31 -37
  41. package/dist/styles/waveform.css +23 -2
  42. package/dist/styles/wavestrip.css +4 -4
  43. package/dist/styles/xy.css +10 -11
  44. package/package.json +2 -2
@@ -1 +1 @@
1
- {"version":3,"file":"spoken-duration.js","names":[],"sources":["../../../src/lib/spoken-duration.ts"],"sourcesContent":["const formatters = new Map<string, Intl.DurationFormat>();\n\nfunction formatterFor(language: string, isZero: boolean): Intl.DurationFormat {\n\tconst key = `${language}|${String(isZero)}`;\n\tconst cached = formatters.get(key);\n\tif (cached) return cached;\n\n\t// All-zero units format to an empty string unless the seconds are forced\n\tconst options = { secondsDisplay: isZero ? 'always' : 'auto', style: 'long' } as const;\n\tlet formatter: Intl.DurationFormat;\n\n\ttry {\n\t\tformatter = new Intl.DurationFormat(language || undefined, options);\n\t} catch {\n\t\tformatter = new Intl.DurationFormat(undefined, options);\n\t}\n\tformatters.set(key, formatter);\n\n\treturn formatter;\n}\n\nexport function spokenDuration(seconds: number, language = ''): string {\n\t// Truncated as the clock is, so every unit carries one sign; mixed signs throw\n\tconst total = Math.trunc(seconds);\n\n\treturn formatterFor(language, total === 0).format({\n\t\thours: Math.trunc(total / 3600),\n\t\tminutes: Math.trunc(total / 60) % 60,\n\t\tseconds: total % 60,\n\t});\n}\n"],"mappings":";AAAA,MAAM,6BAAa,IAAI,IAAiC;AAExD,SAAS,aAAa,UAAkB,QAAsC;CAC7E,MAAM,MAAM,GAAG,SAAS,GAAG,OAAO,MAAM;CACxC,MAAM,SAAS,WAAW,IAAI,GAAG;CACjC,IAAI,QAAQ,OAAO;CAGnB,MAAM,UAAU;EAAE,gBAAgB,SAAS,WAAW;EAAQ,OAAO;CAAO;CAC5E,IAAI;CAEJ,IAAI;EACH,YAAY,IAAI,KAAK,eAAe,YAAY,QAAW,OAAO;CACnE,QAAQ;EACP,YAAY,IAAI,KAAK,eAAe,QAAW,OAAO;CACvD;CACA,WAAW,IAAI,KAAK,SAAS;CAE7B,OAAO;AACR;AAEA,SAAgB,eAAe,SAAiB,WAAW,IAAY;CAEtE,MAAM,QAAQ,KAAK,MAAM,OAAO;CAEhC,OAAO,aAAa,UAAU,UAAU,CAAC,CAAC,CAAC,OAAO;EACjD,OAAO,KAAK,MAAM,QAAQ,IAAI;EAC9B,SAAS,KAAK,MAAM,QAAQ,EAAE,IAAI;EAClC,SAAS,QAAQ;CAClB,CAAC;AACF"}
1
+ {"version":3,"file":"spoken-duration.js","names":[],"sources":["../../../src/lib/spoken-duration.ts"],"sourcesContent":["import { formatClock } from '#lib/clock.ts';\n\nconst formatters = new Map<string, Intl.DurationFormat>();\n\nfunction formatterFor(language: string, isZero: boolean): Intl.DurationFormat {\n\tconst key = `${language}|${String(isZero)}`;\n\tconst cached = formatters.get(key);\n\tif (cached) return cached;\n\n\t// All-zero units format to an empty string unless the seconds are forced\n\tconst options = { secondsDisplay: isZero ? 'always' : 'auto', style: 'long' } as const;\n\tlet formatter: Intl.DurationFormat;\n\n\ttry {\n\t\tformatter = new Intl.DurationFormat(language || undefined, options);\n\t} catch {\n\t\tformatter = new Intl.DurationFormat(undefined, options);\n\t}\n\tformatters.set(key, formatter);\n\n\treturn formatter;\n}\n\nexport function spokenDuration(seconds: number, language = ''): string {\n\tif (!('DurationFormat' in Intl)) return formatClock(seconds);\n\n\t// Truncated as the clock is, so every unit carries one sign; mixed signs throw\n\tconst total = Math.trunc(seconds);\n\n\treturn formatterFor(language, total === 0).format({\n\t\thours: Math.trunc(total / 3600),\n\t\tminutes: Math.trunc(total / 60) % 60,\n\t\tseconds: total % 60,\n\t});\n}\n"],"mappings":";;;AAEA,MAAM,6BAAa,IAAI,IAAiC;AAExD,SAAS,aAAa,UAAkB,QAAsC;CAC7E,MAAM,MAAM,GAAG,SAAS,GAAG,OAAO,MAAM;CACxC,MAAM,SAAS,WAAW,IAAI,GAAG;CACjC,IAAI,QAAQ,OAAO;CAGnB,MAAM,UAAU;EAAE,gBAAgB,SAAS,WAAW;EAAQ,OAAO;CAAO;CAC5E,IAAI;CAEJ,IAAI;EACH,YAAY,IAAI,KAAK,eAAe,YAAY,QAAW,OAAO;CACnE,QAAQ;EACP,YAAY,IAAI,KAAK,eAAe,QAAW,OAAO;CACvD;CACA,WAAW,IAAI,KAAK,SAAS;CAE7B,OAAO;AACR;AAEA,SAAgB,eAAe,SAAiB,WAAW,IAAY;CACtE,IAAI,EAAE,oBAAoB,OAAO,OAAO,YAAY,OAAO;CAG3D,MAAM,QAAQ,KAAK,MAAM,OAAO;CAEhC,OAAO,aAAa,UAAU,UAAU,CAAC,CAAC,CAAC,OAAO;EACjD,OAAO,KAAK,MAAM,QAAQ,IAAI;EAC9B,SAAS,KAAK,MAAM,QAAQ,EAAE,IAAI;EAClC,SAAS,QAAQ;CAClB,CAAC;AACF"}
@@ -1,3 +1,5 @@
1
+ import { clampProportion } from "./math.js";
2
+
1
3
  //#region src/lib/waveform-bands.ts
2
4
  function fullScaleOf({ fullScale, levels }) {
3
5
  if (fullScale !== void 0) return fullScale;
@@ -32,33 +34,44 @@ function bandTable(bands) {
32
34
  weightOf: (level) => weights[level] ?? 0
33
35
  };
34
36
  }
35
- function mixGroup({ bandCount, levels, weightOf }, colours, [fromFrame, toFrame]) {
36
- let red = 0;
37
- let green = 0;
38
- let blue = 0;
39
- let total = 0;
40
- for (let band = 0; band < bandCount; band += 1) {
41
- const colour = colours[band];
42
- if (!colour) continue;
43
- let weight = 0;
44
- for (let bandFrame = fromFrame; bandFrame < toFrame; bandFrame += 1) weight += weightOf(levels[bandFrame * bandCount + band] ?? 0);
45
- red += weight * colour[0];
46
- green += weight * colour[1];
47
- blue += weight * colour[2];
48
- total += weight;
49
- }
50
- if (total === 0) return void 0;
51
- const scale = 255 / Math.max(red, green, blue, Number.MIN_VALUE);
52
- return [
53
- red * scale,
54
- green * scale,
55
- blue * scale,
56
- 255
57
- ];
37
+ function groupMixer({ bandCount, levels, weightOf }, colours, { contrast, normalize, tilt }) {
38
+ const gains = Array.from({ length: bandCount }, (_gain, band) => 10 ** (tilt * band / 20));
39
+ const amount = clampProportion(normalize);
40
+ return (fromFrame, toFrame) => {
41
+ let red = 0;
42
+ let green = 0;
43
+ let blue = 0;
44
+ let total = 0;
45
+ for (let band = 0; band < bandCount; band += 1) {
46
+ const colour = colours[band];
47
+ if (!colour) continue;
48
+ let weight = 0;
49
+ for (let bandFrame = fromFrame; bandFrame < toFrame; bandFrame += 1) weight += weightOf(levels[bandFrame * bandCount + band] ?? 0);
50
+ weight *= gains[band] ?? 1;
51
+ if (contrast !== 1) weight **= contrast;
52
+ red += weight * colour[0];
53
+ green += weight * colour[1];
54
+ blue += weight * colour[2];
55
+ total += weight;
56
+ }
57
+ if (total === 0) return;
58
+ const scale = 255 / Math.max(red, green, blue, Number.MIN_VALUE) * amount + (1 - amount) / total;
59
+ return [
60
+ red * scale,
61
+ green * scale,
62
+ blue * scale,
63
+ 255
64
+ ];
65
+ };
58
66
  }
59
67
  function bandStrip(table, colours, view) {
60
68
  if (view.pixelsPerSecond <= 0 || !colours.some(Boolean)) return void 0;
61
69
  const { bandCount, framesPerSecond, levels } = table;
70
+ const mixGroup = groupMixer(table, colours, view.mix ?? {
71
+ contrast: 1,
72
+ normalize: 1,
73
+ tilt: 0
74
+ });
62
75
  const pxPerFrame = view.pixelsPerSecond / framesPerSecond;
63
76
  const frames = Math.max(1, Math.round(1 / pxPerFrame));
64
77
  const groupWidth = frames * pxPerFrame;
@@ -70,7 +83,7 @@ function bandStrip(table, colours, view) {
70
83
  const pixels = new Uint8ClampedArray(count * 4);
71
84
  for (let index = 0; index < count; index += 1) {
72
85
  const fromFrame = (first + index) * frames;
73
- const mixed = mixGroup(table, colours, [fromFrame, fromFrame + frames]);
86
+ const mixed = mixGroup(fromFrame, fromFrame + frames);
74
87
  if (mixed) pixels.set(mixed, index * 4);
75
88
  }
76
89
  return {
@@ -1 +1 @@
1
- {"version":3,"file":"waveform-bands.js","names":[],"sources":["../../../src/lib/waveform-bands.ts"],"sourcesContent":["export interface WaveformBands {\n\t/** Bands per frame, interleaved in `levels`, lowest first */\n\tbandCount: number;\n\tframesPerSecond: number;\n\t/** Unset follows the array: 255, 65535 or 1 */\n\tfullScale?: number;\n\tlevels: Float32Array | Uint8Array | Uint16Array;\n\t/** Reads a level as decibels, from this just above zero up to 0 at full scale; unset means linear */\n\tminDecibels?: number;\n}\n\nexport type BandChannels = readonly [number, number, number];\n\nexport interface BandTable {\n\tbandCount: number;\n\tframesPerSecond: number;\n\tlevels: WaveformBands['levels'];\n\tweightOf: (level: number) => number;\n}\n\ninterface BandView {\n\tpixelsPerSecond: number;\n\tstartSeconds: number;\n\twidth: number;\n}\n\ninterface BandStrip {\n\tcount: number;\n\tgroupWidth: number;\n\tpixels: Uint8ClampedArray<ArrayBuffer>;\n\tx: number;\n}\n\nfunction fullScaleOf({ fullScale, levels }: WaveformBands): number {\n\tif (fullScale !== undefined) return fullScale;\n\tif (levels instanceof Uint8Array) return 255;\n\n\treturn levels instanceof Uint16Array ? 65_535 : 1;\n}\n\nfunction amplitudeReader(bands: WaveformBands): (level: number) => number {\n\tconst fullScale = fullScaleOf(bands);\n\tconst minDecibels = Math.min(0, bands.minDecibels ?? 0);\n\n\treturn (level) => {\n\t\tconst proportion = Math.min(1, level / fullScale);\n\t\tif (!(proportion > 0)) return 0;\n\n\t\treturn minDecibels < 0 ? 10 ** ((minDecibels * (1 - proportion)) / 20) : proportion;\n\t};\n}\n\nexport function bandTable(bands: undefined | WaveformBands): BandTable | undefined {\n\tif (!bands) return undefined;\n\n\tconst { bandCount, framesPerSecond, levels } = bands;\n\tif (!Number.isSafeInteger(bandCount) || bandCount < 1 || !(framesPerSecond > 0)) return undefined;\n\n\tconst amplitudeOf = amplitudeReader(bands);\n\tif (levels instanceof Float32Array) {\n\t\treturn { bandCount, framesPerSecond, levels, weightOf: amplitudeOf };\n\t}\n\n\tconst weights = Float32Array.from(\n\t\t{ length: levels instanceof Uint8Array ? 256 : 65_536 },\n\t\t(_weight, level) => amplitudeOf(level),\n\t);\n\n\treturn { bandCount, framesPerSecond, levels, weightOf: (level) => weights[level] ?? 0 };\n}\n\n// Hue only: the strongest channel goes to full, since loudness is the envelope's height\nfunction mixGroup(\n\t{ bandCount, levels, weightOf }: BandTable,\n\tcolours: ReadonlyArray<BandChannels | undefined>,\n\t[fromFrame, toFrame]: [number, number],\n): [number, number, number, number] | undefined {\n\tlet red = 0;\n\tlet green = 0;\n\tlet blue = 0;\n\tlet total = 0;\n\n\tfor (let band = 0; band < bandCount; band += 1) {\n\t\tconst colour = colours[band];\n\t\tif (!colour) continue;\n\n\t\tlet weight = 0;\n\n\t\tfor (let bandFrame = fromFrame; bandFrame < toFrame; bandFrame += 1) {\n\t\t\tweight += weightOf(levels[bandFrame * bandCount + band] ?? 0);\n\t\t}\n\t\tred += weight * colour[0];\n\t\tgreen += weight * colour[1];\n\t\tblue += weight * colour[2];\n\t\ttotal += weight;\n\t}\n\tif (total === 0) return undefined;\n\n\tconst scale = 255 / Math.max(red, green, blue, Number.MIN_VALUE);\n\n\treturn [red * scale, green * scale, blue * scale, 255];\n}\n\n// Keyed to absolute frames, as buckets are to pairs; keyed to the window, the colours shimmer as it scrolls\nexport function bandStrip(\n\ttable: BandTable,\n\tcolours: ReadonlyArray<BandChannels | undefined>,\n\tview: BandView,\n): BandStrip | undefined {\n\tif (view.pixelsPerSecond <= 0 || !colours.some(Boolean)) return undefined;\n\n\tconst { bandCount, framesPerSecond, levels } = table;\n\tconst pxPerFrame = view.pixelsPerSecond / framesPerSecond;\n\tconst frames = Math.max(1, Math.round(1 / pxPerFrame));\n\tconst groupWidth = frames * pxPerFrame;\n\tconst openingFrame = view.startSeconds * framesPerSecond;\n\tconst groupsTotal = Math.ceil(levels.length / bandCount / frames);\n\t// A group past each end, so the smoothing at the image's own edges falls outside the window\n\tconst first = Math.max(0, Math.floor(openingFrame / frames) - 1);\n\tconst last = Math.min(\n\t\tgroupsTotal,\n\t\tMath.ceil((openingFrame + view.width / pxPerFrame) / frames) + 1,\n\t);\n\tconst count = last - first;\n\tif (count < 1) return undefined;\n\n\tconst pixels = new Uint8ClampedArray(count * 4);\n\n\tfor (let index = 0; index < count; index += 1) {\n\t\tconst fromFrame = (first + index) * frames;\n\t\tconst mixed = mixGroup(table, colours, [fromFrame, fromFrame + frames]);\n\n\t\tif (mixed) pixels.set(mixed, index * 4);\n\t}\n\n\treturn { count, groupWidth, pixels, x: (first * frames - openingFrame) * pxPerFrame };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"waveform-bands.js","names":[],"sources":["../../../src/lib/waveform-bands.ts"],"sourcesContent":["import { clampProportion } from '#lib/math.ts';\n\nexport interface WaveformBands {\n\t/** Bands per frame, interleaved in `levels`, lowest first */\n\tbandCount: number;\n\tframesPerSecond: number;\n\t/** Unset follows the array: 255, 65535 or 1 */\n\tfullScale?: number;\n\tlevels: Float32Array | Uint8Array | Uint16Array;\n\t/** Reads a level as decibels, from this just above zero up to 0 at full scale; unset means linear */\n\tminDecibels?: number;\n}\n\nexport type BandChannels = readonly [number, number, number];\n\nexport interface BandTable {\n\tbandCount: number;\n\tframesPerSecond: number;\n\tlevels: WaveformBands['levels'];\n\tweightOf: (level: number) => number;\n}\n\nexport interface BandMix {\n\tcontrast: number;\n\tnormalize: number;\n\ttilt: number;\n}\n\ninterface BandView {\n\tmix?: BandMix;\n\tpixelsPerSecond: number;\n\tstartSeconds: number;\n\twidth: number;\n}\n\ninterface BandStrip {\n\tcount: number;\n\tgroupWidth: number;\n\tpixels: Uint8ClampedArray<ArrayBuffer>;\n\tx: number;\n}\n\nfunction fullScaleOf({ fullScale, levels }: WaveformBands): number {\n\tif (fullScale !== undefined) return fullScale;\n\tif (levels instanceof Uint8Array) return 255;\n\n\treturn levels instanceof Uint16Array ? 65_535 : 1;\n}\n\nfunction amplitudeReader(bands: WaveformBands): (level: number) => number {\n\tconst fullScale = fullScaleOf(bands);\n\tconst minDecibels = Math.min(0, bands.minDecibels ?? 0);\n\n\treturn (level) => {\n\t\tconst proportion = Math.min(1, level / fullScale);\n\t\tif (!(proportion > 0)) return 0;\n\n\t\treturn minDecibels < 0 ? 10 ** ((minDecibels * (1 - proportion)) / 20) : proportion;\n\t};\n}\n\nexport function bandTable(bands: undefined | WaveformBands): BandTable | undefined {\n\tif (!bands) return undefined;\n\n\tconst { bandCount, framesPerSecond, levels } = bands;\n\tif (!Number.isSafeInteger(bandCount) || bandCount < 1 || !(framesPerSecond > 0)) return undefined;\n\n\tconst amplitudeOf = amplitudeReader(bands);\n\tif (levels instanceof Float32Array) {\n\t\treturn { bandCount, framesPerSecond, levels, weightOf: amplitudeOf };\n\t}\n\n\tconst weights = Float32Array.from(\n\t\t{ length: levels instanceof Uint8Array ? 256 : 65_536 },\n\t\t(_weight, level) => amplitudeOf(level),\n\t);\n\n\treturn { bandCount, framesPerSecond, levels, weightOf: (level) => weights[level] ?? 0 };\n}\n\n// Loudness is the envelope's height, so the mix is hue: scaled to its strongest channel, or averaged to stay inside the palette\nfunction groupMixer(\n\t{ bandCount, levels, weightOf }: BandTable,\n\tcolours: ReadonlyArray<BandChannels | undefined>,\n\t{ contrast, normalize, tilt }: BandMix,\n): (fromFrame: number, toFrame: number) => [number, number, number, number] | undefined {\n\tconst gains = Array.from({ length: bandCount }, (_gain, band) => 10 ** ((tilt * band) / 20));\n\tconst amount = clampProportion(normalize);\n\n\treturn (fromFrame, toFrame) => {\n\t\tlet red = 0;\n\t\tlet green = 0;\n\t\tlet blue = 0;\n\t\tlet total = 0;\n\n\t\tfor (let band = 0; band < bandCount; band += 1) {\n\t\t\tconst colour = colours[band];\n\t\t\tif (!colour) continue;\n\n\t\t\tlet weight = 0;\n\n\t\t\tfor (let bandFrame = fromFrame; bandFrame < toFrame; bandFrame += 1) {\n\t\t\t\tweight += weightOf(levels[bandFrame * bandCount + band] ?? 0);\n\t\t\t}\n\t\t\tweight *= gains[band] ?? 1;\n\t\t\tif (contrast !== 1) weight **= contrast;\n\t\t\tred += weight * colour[0];\n\t\t\tgreen += weight * colour[1];\n\t\t\tblue += weight * colour[2];\n\t\t\ttotal += weight;\n\t\t}\n\t\tif (total === 0) return;\n\n\t\tconst full = 255 / Math.max(red, green, blue, Number.MIN_VALUE);\n\t\tconst scale = full * amount + (1 - amount) / total;\n\n\t\treturn [red * scale, green * scale, blue * scale, 255];\n\t};\n}\n\n// Keyed to absolute frames, as buckets are to pairs; keyed to the window, the colours shimmer as it scrolls\nexport function bandStrip(\n\ttable: BandTable,\n\tcolours: ReadonlyArray<BandChannels | undefined>,\n\tview: BandView,\n): BandStrip | undefined {\n\tif (view.pixelsPerSecond <= 0 || !colours.some(Boolean)) return undefined;\n\n\tconst { bandCount, framesPerSecond, levels } = table;\n\tconst mixGroup = groupMixer(table, colours, view.mix ?? { contrast: 1, normalize: 1, tilt: 0 });\n\tconst pxPerFrame = view.pixelsPerSecond / framesPerSecond;\n\tconst frames = Math.max(1, Math.round(1 / pxPerFrame));\n\tconst groupWidth = frames * pxPerFrame;\n\tconst openingFrame = view.startSeconds * framesPerSecond;\n\tconst groupsTotal = Math.ceil(levels.length / bandCount / frames);\n\t// A group past each end, so the smoothing at the image's own edges falls outside the window\n\tconst first = Math.max(0, Math.floor(openingFrame / frames) - 1);\n\tconst last = Math.min(\n\t\tgroupsTotal,\n\t\tMath.ceil((openingFrame + view.width / pxPerFrame) / frames) + 1,\n\t);\n\tconst count = last - first;\n\tif (count < 1) return undefined;\n\n\tconst pixels = new Uint8ClampedArray(count * 4);\n\n\tfor (let index = 0; index < count; index += 1) {\n\t\tconst fromFrame = (first + index) * frames;\n\t\tconst mixed = mixGroup(fromFrame, fromFrame + frames);\n\n\t\tif (mixed) pixels.set(mixed, index * 4);\n\t}\n\n\treturn { count, groupWidth, pixels, x: (first * frames - openingFrame) * pxPerFrame };\n}\n"],"mappings":"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"}
@@ -9,7 +9,10 @@ export declare const waveformColours: {
9
9
  readonly wave: "--_sonic-waveform-wave";
10
10
  };
11
11
  export declare const waveformNumbers: {
12
+ readonly bandContrast: "--_sonic-waveform-band-contrast";
13
+ readonly bandNormalize: "--_sonic-waveform-band-normalize";
12
14
  readonly bandOpacity: "--_sonic-waveform-band-opacity";
15
+ readonly bandTilt: "--_sonic-waveform-band-tilt";
13
16
  readonly edgeShade: "--_sonic-waveform-edge-shade";
14
17
  };
15
18
  //#endregion
@@ -14,7 +14,10 @@ const waveformColours = {
14
14
  wave: "--_sonic-waveform-wave"
15
15
  };
16
16
  const waveformNumbers = {
17
+ bandContrast: "--_sonic-waveform-band-contrast",
18
+ bandNormalize: "--_sonic-waveform-band-normalize",
17
19
  bandOpacity: "--_sonic-waveform-band-opacity",
20
+ bandTilt: "--_sonic-waveform-band-tilt",
18
21
  edgeShade: "--_sonic-waveform-edge-shade"
19
22
  };
20
23
  const amplitudeMargin = .94;
@@ -71,7 +74,15 @@ function bandChannels(strip, colour) {
71
74
  ];
72
75
  }
73
76
  function paintTint(context, scene, tint) {
74
- const strip = bandStrip(tint.table, tint.colours, scene);
77
+ const { bandContrast, bandNormalize, bandTilt } = scene.numbers;
78
+ const strip = bandStrip(tint.table, tint.colours, {
79
+ ...scene,
80
+ mix: {
81
+ contrast: bandContrast,
82
+ normalize: bandNormalize,
83
+ tilt: bandTilt
84
+ }
85
+ });
75
86
  if (!strip) return;
76
87
  const { canvas } = tint.strip;
77
88
  const { count } = strip;
@@ -1 +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 { BandChannels, BandTable } from '#lib/waveform-bands.ts';\nimport type { WaveformPeaks } from '#lib/waveform-buckets.ts';\n\nimport { isKind } from '#lib/marker-band.ts';\nimport { clamp } from '#lib/math.ts';\nimport { bandStrip } from '#lib/waveform-bands.ts';\nimport { waveformBuckets } from '#lib/waveform-buckets.ts';\n\ntype Colour = keyof typeof waveformColours;\n\ntype Numeric = keyof typeof waveformNumbers;\n\ninterface Scene extends View {\n\tcolours: Record<Colour, string>;\n\tnumbers: Record<Numeric, number>;\n}\n\nexport interface WaveformTint {\n\tcolours: ReadonlyArray<BandChannels | undefined>;\n\tstrip: CanvasRenderingContext2D;\n\ttable: BandTable;\n}\n\ninterface Drawn {\n\tbands: undefined | WaveformTint;\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\tshade: '--_sonic-waveform-shade',\n\tshadeClear: '--_sonic-waveform-shade-clear',\n\twave: '--_sonic-waveform-wave',\n} as const;\n\nexport const waveformNumbers = {\n\tbandOpacity: '--_sonic-waveform-band-opacity',\n\tedgeShade: '--_sonic-waveform-edge-shade',\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\n// A painted pixel is the one reading that resolves any colour syntax to channels\nexport function bandChannels(\n\tstrip: CanvasRenderingContext2D,\n\tcolour: string,\n): BandChannels | undefined {\n\tstrip.clearRect(0, 0, 1, 1);\n\tstrip.fillStyle = colour;\n\tstrip.fillRect(0, 0, 1, 1);\n\n\tconst [red = 0, green = 0, blue = 0, alpha = 0] = strip.getImageData(0, 0, 1, 1).data;\n\n\treturn alpha === 0 ? undefined : [red, green, blue];\n}\n\nfunction paintTint(context: CanvasRenderingContext2D, scene: Scene, tint: WaveformTint): void {\n\tconst strip = bandStrip(tint.table, tint.colours, scene);\n\tif (!strip) return;\n\n\tconst { canvas } = tint.strip;\n\tconst { count } = strip;\n\n\tif (canvas.width < count) canvas.width = count;\n\ttint.strip.putImageData(new ImageData(strip.pixels, count, 1), 0, 0);\n\tcontext.globalAlpha = scene.numbers.bandOpacity;\n\tcontext.drawImage(canvas, 0, 0, count, 1, strip.x, 0, count * strip.groupWidth, scene.height);\n}\n\nfunction paintEdgeShade(context: CanvasRenderingContext2D, scene: Scene): void {\n\tconst { shade, shadeClear } = scene.colours;\n\tconst gradient = context.createLinearGradient(0, 0, 0, scene.height);\n\n\tgradient.addColorStop(0, shade);\n\tgradient.addColorStop(0.5, shadeClear);\n\tgradient.addColorStop(1, shade);\n\tcontext.globalAlpha = scene.numbers.edgeShade;\n\tcontext.fillStyle = gradient;\n\tcontext.fillRect(0, 0, scene.width, scene.height);\n}\n\nfunction paintWave(context: CanvasRenderingContext2D, scene: Scene, drawn: Drawn): void {\n\tconst buckets = drawn.peaks ? waveformBuckets(drawn.peaks, scene) : [];\n\tconst [first] = buckets;\n\tif (!first) return;\n\n\tconst center = scene.height / 2;\n\tconst amplitudePx = center * amplitudeMargin;\n\tconst { bandOpacity, edgeShade } = scene.numbers;\n\n\tcontext.fillStyle = scene.colours.wave;\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\t// The flat envelope is the mask: from here only the pixels it covers take colour\n\tcontext.globalCompositeOperation = 'source-atop';\n\tif (drawn.bands && bandOpacity > 0) paintTint(context, scene, drawn.bands);\n\tif (edgeShade > 0) paintEdgeShade(context, scene);\n\tcontext.globalAlpha = 1;\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\tpaintWave(context, scene, drawn);\n\t// Laid under the wave, so the nearest is painted first\n\tcontext.globalCompositeOperation = 'destination-over';\n\tpaintPlaceholders(context, scene);\n\tcontext.fillStyle = grid;\n\tif (to > from) context.fillRect(from, Math.round((height - line) / 2), to - from, line);\n\tpaintHatch(context, scene, [0, from]);\n\tpaintHatch(context, scene, [to, width]);\n\tcontext.globalCompositeOperation = 'source-over';\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
+ {"version":3,"file":"waveform-scene.js","names":[],"sources":["../../../src/lib/waveform-scene.ts"],"sourcesContent":["import type { View } from '#lib/frame-timeline.ts';\nimport type { BandChannels, BandTable } from '#lib/waveform-bands.ts';\nimport type { WaveformPeaks } from '#lib/waveform-buckets.ts';\n\nimport { isKind } from '#lib/marker-band.ts';\nimport { clamp } from '#lib/math.ts';\nimport { bandStrip } from '#lib/waveform-bands.ts';\nimport { waveformBuckets } from '#lib/waveform-buckets.ts';\n\ntype Colour = keyof typeof waveformColours;\n\ntype Numeric = keyof typeof waveformNumbers;\n\ninterface Scene extends View {\n\tcolours: Record<Colour, string>;\n\tnumbers: Record<Numeric, number>;\n}\n\nexport interface WaveformTint {\n\tcolours: ReadonlyArray<BandChannels | undefined>;\n\tstrip: CanvasRenderingContext2D;\n\ttable: BandTable;\n}\n\ninterface Drawn {\n\tbands: undefined | WaveformTint;\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\tshade: '--_sonic-waveform-shade',\n\tshadeClear: '--_sonic-waveform-shade-clear',\n\twave: '--_sonic-waveform-wave',\n} as const;\n\nexport const waveformNumbers = {\n\tbandContrast: '--_sonic-waveform-band-contrast',\n\tbandNormalize: '--_sonic-waveform-band-normalize',\n\tbandOpacity: '--_sonic-waveform-band-opacity',\n\tbandTilt: '--_sonic-waveform-band-tilt',\n\tedgeShade: '--_sonic-waveform-edge-shade',\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\n// A painted pixel is the one reading that resolves any colour syntax to channels\nexport function bandChannels(\n\tstrip: CanvasRenderingContext2D,\n\tcolour: string,\n): BandChannels | undefined {\n\tstrip.clearRect(0, 0, 1, 1);\n\tstrip.fillStyle = colour;\n\tstrip.fillRect(0, 0, 1, 1);\n\n\tconst [red = 0, green = 0, blue = 0, alpha = 0] = strip.getImageData(0, 0, 1, 1).data;\n\n\treturn alpha === 0 ? undefined : [red, green, blue];\n}\n\nfunction paintTint(context: CanvasRenderingContext2D, scene: Scene, tint: WaveformTint): void {\n\tconst { bandContrast, bandNormalize, bandTilt } = scene.numbers;\n\tconst strip = bandStrip(tint.table, tint.colours, {\n\t\t...scene,\n\t\tmix: { contrast: bandContrast, normalize: bandNormalize, tilt: bandTilt },\n\t});\n\tif (!strip) return;\n\n\tconst { canvas } = tint.strip;\n\tconst { count } = strip;\n\n\tif (canvas.width < count) canvas.width = count;\n\ttint.strip.putImageData(new ImageData(strip.pixels, count, 1), 0, 0);\n\tcontext.globalAlpha = scene.numbers.bandOpacity;\n\tcontext.drawImage(canvas, 0, 0, count, 1, strip.x, 0, count * strip.groupWidth, scene.height);\n}\n\nfunction paintEdgeShade(context: CanvasRenderingContext2D, scene: Scene): void {\n\tconst { shade, shadeClear } = scene.colours;\n\tconst gradient = context.createLinearGradient(0, 0, 0, scene.height);\n\n\tgradient.addColorStop(0, shade);\n\tgradient.addColorStop(0.5, shadeClear);\n\tgradient.addColorStop(1, shade);\n\tcontext.globalAlpha = scene.numbers.edgeShade;\n\tcontext.fillStyle = gradient;\n\tcontext.fillRect(0, 0, scene.width, scene.height);\n}\n\nfunction paintWave(context: CanvasRenderingContext2D, scene: Scene, drawn: Drawn): void {\n\tconst buckets = drawn.peaks ? waveformBuckets(drawn.peaks, scene) : [];\n\tconst [first] = buckets;\n\tif (!first) return;\n\n\tconst center = scene.height / 2;\n\tconst amplitudePx = center * amplitudeMargin;\n\tconst { bandOpacity, edgeShade } = scene.numbers;\n\n\tcontext.fillStyle = scene.colours.wave;\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\t// The flat envelope is the mask: from here only the pixels it covers take colour\n\tcontext.globalCompositeOperation = 'source-atop';\n\tif (drawn.bands && bandOpacity > 0) paintTint(context, scene, drawn.bands);\n\tif (edgeShade > 0) paintEdgeShade(context, scene);\n\tcontext.globalAlpha = 1;\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\tpaintWave(context, scene, drawn);\n\t// Laid under the wave, so the nearest is painted first\n\tcontext.globalCompositeOperation = 'destination-over';\n\tpaintPlaceholders(context, scene);\n\tcontext.fillStyle = grid;\n\tif (to > from) context.fillRect(from, Math.round((height - line) / 2), to - from, line);\n\tpaintHatch(context, scene, [0, from]);\n\tpaintHatch(context, scene, [to, width]);\n\tcontext.globalCompositeOperation = 'source-over';\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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"}
@@ -112,11 +112,13 @@
112
112
  );
113
113
 
114
114
  color: color-mix(in oklab, var(--_sonic-lit) 55%, var(--_sonic-ink));
115
+ }
115
116
 
116
117
  @media (forced-colors: active) {
118
+ :where([armed]) > .sonic-button:not(:disabled, [aria-disabled='true']) {
117
119
  --_sonic-keycap-glow: transparent;
118
- }
119
120
  }
121
+ }
120
122
 
121
123
  /* Before the latched rule too; a button with an LED blinks the LED and keeps its legend */
122
124
  :where([busy]) > .sonic-button:not(:disabled, [aria-disabled='true'], :has(.sonic-led)) {
@@ -153,21 +155,28 @@
153
155
  )
154
156
  )
155
157
  );
158
+ }
156
159
 
157
- & .sonic-button-cap > [data-sonic-when='released'] {
160
+ .sonic-button:is([aria-pressed='true'],[aria-expanded='true'],:where([momentary]:state(pressed)) > *) .sonic-button-cap > [data-sonic-when='released'] {
158
161
  display: none;
159
162
  }
160
163
 
161
164
  @media (forced-colors: active) {
165
+
166
+ .sonic-button:is(
167
+ [aria-pressed='true'],
168
+ [aria-expanded='true'],
169
+ :where([momentary]:state(pressed)) > *
170
+ ) {
162
171
  --_sonic-skirt: var(--_sonic-lit);
163
172
  --_sonic-face: var(--_sonic-lit);
164
173
  --_sonic-ink: HighlightText;
174
+ }
165
175
 
166
- &:is(:disabled, [aria-disabled='true']) {
176
+ .sonic-button:is([aria-pressed='true'],[aria-expanded='true'],:where([momentary]:state(pressed)) > *):is(:disabled,[aria-disabled='true']) {
167
177
  --_sonic-ink: ButtonFace;
168
178
  }
169
179
  }
170
- }
171
180
 
172
181
  .sonic-button:is(:active, [data-sonic-pressed], :where([momentary]:state(pressed)) > *):where(
173
182
  :not(:disabled, [aria-disabled='true'])
@@ -232,11 +241,14 @@
232
241
  --_sonic-blank-edge-off,
233
242
  var(--_sonic-blank-edge-lit, color-mix(in oklab, currentcolor 35%, transparent))
234
243
  );
244
+ }
235
245
 
236
246
  @media (forced-colors: active) {
247
+
248
+ .sonic-button:not(:has(> .sonic-button-cap > :not(.sonic-led))) {
237
249
  --_sonic-blank-edge: transparent;
238
- }
239
250
  }
251
+ }
240
252
 
241
253
  .sonic-button-cap {
242
254
  --_sonic-keycap-corner: max(0px, var(--_sonic-button-corner) - var(--_sonic-button-gap));
@@ -259,19 +271,20 @@
259
271
  inset: var(--_sonic-button-gap);
260
272
  gap: calc(var(--_sonic-button-size) * var(--_sonic-button-led-ratio) / 2);
261
273
  place-content: center;
274
+ }
262
275
 
263
- & > .sonic-led {
276
+ .sonic-button-cap > .sonic-led {
264
277
  --_sonic-led-size: var(
265
278
  --sonic-led-size,
266
279
  calc(var(--_sonic-button-size) * var(--_sonic-button-led-ratio))
267
280
  );
268
281
  }
269
282
 
270
- & > :not(.sonic-led) {
283
+ .sonic-button-cap > :not(.sonic-led) {
271
284
  filter: var(--_sonic-engraving) var(--_sonic-button-legend-glow,);
272
285
  }
273
286
 
274
- & > :not(svg, .sonic-led) {
287
+ .sonic-button-cap > :not(svg,.sonic-led) {
275
288
  padding-inline: calc(var(--_sonic-button-size) * var(--_sonic-button-padding-ratio));
276
289
  font-size: var(
277
290
  --_sonic-button-font-own,
@@ -284,7 +297,7 @@
284
297
  white-space: nowrap;
285
298
  }
286
299
 
287
- & > svg {
300
+ .sonic-button-cap > svg {
288
301
  inline-size: auto;
289
302
  block-size: calc(var(--_sonic-button-size) * var(--_sonic-button-icon-ratio));
290
303
  aspect-ratio: auto 1;
@@ -295,7 +308,6 @@
295
308
  );
296
309
  mask-repeat: no-repeat;
297
310
  }
298
- }
299
311
 
300
312
  .sonic-button-cap:has(> :not(.sonic-led)) {
301
313
  position: relative;
@@ -329,11 +341,18 @@
329
341
  [busy] > .sonic-button:not(:disabled, [aria-disabled='true']) .sonic-led::after {
330
342
  animation: sonic-button-busy 500ms ease-in-out var(--sonic-button-busy-delay, 0s) infinite
331
343
  alternate;
344
+ }
332
345
 
333
346
  @media (prefers-reduced-motion: reduce) {
347
+
348
+ [busy]
349
+ > .sonic-button:not(:disabled, [aria-disabled='true'], :has(.sonic-led))
350
+ > .sonic-button-cap
351
+ > *,
352
+ [busy] > .sonic-button:not(:disabled, [aria-disabled='true']) .sonic-led::after {
334
353
  animation: none;
335
- }
336
354
  }
355
+ }
337
356
 
338
357
  @media (forced-colors: active) {
339
358
  .sonic-button-cap > :not(.sonic-led) {
@@ -148,8 +148,9 @@
148
148
  .sonic-dial-scale {
149
149
  font-size: calc(var(--_sonic-dial-size) * var(--_sonic-dial-scale-font-ratio));
150
150
  line-height: 1cap;
151
+ }
151
152
 
152
- & > * {
153
+ .sonic-dial-scale > * {
153
154
  --_sonic-dial-scale-angle: calc(
154
155
  var(--_sonic-dial-start) + var(--_sonic-scale-at, 0) * var(--_sonic-dial-sweep)
155
156
  );
@@ -165,7 +166,7 @@
165
166
  calc(-50% - var(--_sonic-dial-scale-cos) * (50% + 0.15cap));
166
167
  }
167
168
 
168
- & > .sonic-scale-tick {
169
+ .sonic-dial-scale > .sonic-scale-tick {
169
170
  --_sonic-dial-tick-length: calc(var(--_sonic-dial-scale-band) * 0.4);
170
171
 
171
172
  inline-size: var(--_sonic-unit);
@@ -174,7 +175,6 @@
174
175
  translate: calc(-50% + var(--_sonic-dial-scale-sin) * var(--_sonic-dial-tick-length) / 2)
175
176
  calc(-50% - var(--_sonic-dial-scale-cos) * var(--_sonic-dial-tick-length) / 2);
176
177
  }
177
- }
178
178
 
179
179
  :is(.sonic-dial-ring, .sonic-dial-modulation) {
180
180
  --_sonic-arc-width: var(--_sonic-dial-ring-width);
@@ -254,16 +254,16 @@
254
254
  display: block;
255
255
  background: none;
256
256
  mask: none;
257
+ }
257
258
 
258
- &::before,
259
- &::after {
259
+ :is(:state(modulated) > .sonic-dial > .sonic-dial-modulation)::before,:is(:state(modulated) > .sonic-dial > .sonic-dial-modulation)::after {
260
260
  position: absolute;
261
261
  inset: 0;
262
262
  content: '';
263
263
  border-radius: 50%;
264
264
  }
265
265
 
266
- &::before {
266
+ :is(:state(modulated) > .sonic-dial > .sonic-dial-modulation)::before {
267
267
  background:
268
268
  conic-gradient(
269
269
  from var(--_sonic-dial-start),
@@ -282,7 +282,7 @@
282
282
  mask: var(--_sonic-arc-mask);
283
283
  }
284
284
 
285
- &::after {
285
+ :is(:state(modulated) > .sonic-dial > .sonic-dial-modulation)::after {
286
286
  background: linear-gradient(
287
287
  var(--_sonic-dial-modulation-tick),
288
288
  var(--_sonic-dial-modulation-tick)
@@ -290,7 +290,6 @@
290
290
  50% 0 / var(--_sonic-unit) var(--_sonic-dial-ring-width) no-repeat;
291
291
  rotate: calc(var(--_sonic-dial-start) + var(--_sonic-dial-modulation-value-angle));
292
292
  }
293
- }
294
293
 
295
294
  .sonic-dial-cap {
296
295
  position: absolute;
@@ -302,15 +301,15 @@
302
301
  var(--_sonic-drop-shade),
303
302
  var(--_sonic-rim),
304
303
  var(--_sonic-focus-shadow);
304
+ }
305
305
 
306
- &::before,
307
- &::after {
306
+ .sonic-dial-cap::before,.sonic-dial-cap::after {
308
307
  position: absolute;
309
308
  content: '';
310
309
  border-radius: 50%;
311
310
  }
312
311
 
313
- &::before {
312
+ .sonic-dial-cap::before {
314
313
  inset: 0;
315
314
  background: repeating-conic-gradient(
316
315
  rgb(from var(--_sonic-shade) r g b / calc(0.3 * var(--_sonic-relief))) 0,
@@ -322,7 +321,7 @@
322
321
  rotate: var(--_sonic-dial-angle);
323
322
  }
324
323
 
325
- &::after {
324
+ .sonic-dial-cap::after {
326
325
  inset: 16%;
327
326
  background:
328
327
  conic-gradient(
@@ -343,7 +342,6 @@
343
342
  var(--_sonic-face);
344
343
  box-shadow: var(--_sonic-face-shadow);
345
344
  }
346
- }
347
345
 
348
346
  .sonic-dial-indicator {
349
347
  position: absolute;
@@ -351,8 +349,9 @@
351
349
  pointer-events: none;
352
350
  border-radius: 50%;
353
351
  rotate: calc(var(--_sonic-dial-start) + var(--_sonic-dial-angle));
352
+ }
354
353
 
355
- &::before {
354
+ .sonic-dial-indicator::before {
356
355
  --_sonic-dial-indicator-width: calc(
357
356
  var(--_sonic-dial-size) * var(--_sonic-dial-indicator-ratio)
358
357
  );
@@ -380,7 +379,6 @@
380
379
  );
381
380
  translate: -50% 0;
382
381
  }
383
- }
384
382
 
385
383
  :disabled > .sonic-dial > .sonic-dial-indicator::before {
386
384
  background: var(--_sonic-ink-disabled);
@@ -65,13 +65,13 @@
65
65
  inset: 0;
66
66
  overflow: clip;
67
67
  pointer-events: none;
68
+ }
68
69
 
69
- &[data-sonic-part='curve'] {
70
+ .sonic-envelope-bracket[data-sonic-part='curve'] {
70
71
  --_sonic-point-size: calc(
71
72
  2 * var(--_sonic-envelope-size) * var(--_sonic-envelope-curve-ratio)
72
73
  );
73
74
  }
74
- }
75
75
 
76
76
  .sonic-envelope-fill {
77
77
  fill: color-mix(in oklab, var(--_sonic-lit) 18%, transparent);
@@ -19,8 +19,9 @@
19
19
  block-size: var(--_sonic-led-size);
20
20
  background: var(--_sonic-lens-bezel);
21
21
  border-radius: 50%;
22
+ }
22
23
 
23
- &::after {
24
+ .sonic-led::after {
24
25
  position: absolute;
25
26
  inset: calc(var(--_sonic-led-size) * (1 - var(--_sonic-led-lens-ratio)) / 2);
26
27
  content: '';
@@ -29,19 +30,20 @@
29
30
  }
30
31
 
31
32
  @media (forced-colors: active) {
33
+ .sonic-led {
32
34
  --_sonic-lit-ok: Mark;
33
35
  --_sonic-lit-warning: Highlight;
34
36
  --_sonic-lit-danger: Mark;
35
37
 
36
38
  forced-color-adjust: none;
37
39
  background: none;
40
+ }
38
41
 
39
- &::after {
42
+ .sonic-led::after {
40
43
  background: GrayText;
41
44
  box-shadow: 0 0 0 1px CanvasText;
42
45
  }
43
46
  }
44
- }
45
47
 
46
48
  .sonic-led[data-sonic-lit='ok'] {
47
49
  --_sonic-lit: var(--_sonic-lit-ok);
@@ -56,23 +58,18 @@
56
58
  }
57
59
 
58
60
  /* Before the lit rule, which a latched button's LED also matches */
59
- .sonic-led:is(
60
- [data-sonic-lit='idle'],
61
- :where(:is([armed], [busy]) > .sonic-button:not(:disabled, [aria-disabled='true'])) *
62
- ) {
63
- &::after {
61
+ .sonic-led:is([data-sonic-lit='idle'],:where(:is([armed],[busy]) > .sonic-button:not(:disabled,[aria-disabled='true'])) *)::after {
64
62
  background:
65
63
  var(--_sonic-lens-glint),
66
64
  color-mix(in oklab, var(--_sonic-lit) 35%, var(--_sonic-lens-glass));
67
65
  }
68
66
 
69
67
  @media (forced-colors: active) {
70
- &::after {
68
+ .sonic-led:is([data-sonic-lit='idle'],:where(:is([armed],[busy]) > .sonic-button:not(:disabled,[aria-disabled='true'])) *)::after {
71
69
  background: GrayText;
72
70
  box-shadow: 0 0 0 1px var(--_sonic-lit);
73
71
  }
74
72
  }
75
- }
76
73
 
77
74
  .sonic-led:is(
78
75
  [data-sonic-lit]:where(:not([data-sonic-lit='idle'])),
@@ -96,8 +93,10 @@
96
93
  transparent
97
94
  ),
98
95
  var(--_sonic-lens-bezel);
96
+ }
99
97
 
100
- &::after {
98
+ .sonic-led:is([data-sonic-lit]:where(:not([data-sonic-lit='idle'])),:where(.sonic-button:is([aria-pressed='true'],[aria-expanded='true']),[momentary]:state(pressed) > .sonic-button,.sonic-segmented-option[aria-checked='true'],.sonic-toggle:has( > [role='switch'][aria-checked='true'], > [aria-checked='true'] [data-sonic-lit]))
99
+ *,:where(:state(at-origin):not(:disabled) > .sonic-led))::after {
101
100
  background: radial-gradient(
102
101
  closest-side,
103
102
  color-mix(
@@ -110,14 +109,30 @@
110
109
  }
111
110
 
112
111
  @media (forced-colors: active) {
112
+
113
+ .sonic-led:is(
114
+ [data-sonic-lit]:where(:not([data-sonic-lit='idle'])),
115
+ :where(
116
+ .sonic-button:is([aria-pressed='true'], [aria-expanded='true']),
117
+ [momentary]:state(pressed) > .sonic-button,
118
+ .sonic-segmented-option[aria-checked='true'],
119
+ .sonic-toggle:has(
120
+ > [role='switch'][aria-checked='true'],
121
+ > [aria-checked='true'] [data-sonic-lit]
122
+ )
123
+ )
124
+ *,
125
+ :where(:state(at-origin):not(:disabled) > .sonic-led)
126
+ ) {
113
127
  background: none;
128
+ }
114
129
 
115
- &::after {
130
+ .sonic-led:is([data-sonic-lit]:where(:not([data-sonic-lit='idle'])),:where(.sonic-button:is([aria-pressed='true'],[aria-expanded='true']),[momentary]:state(pressed) > .sonic-button,.sonic-segmented-option[aria-checked='true'],.sonic-toggle:has( > [role='switch'][aria-checked='true'], > [aria-checked='true'] [data-sonic-lit]))
131
+ *,:where(:state(at-origin):not(:disabled) > .sonic-led))::after {
116
132
  background: var(--_sonic-lit);
117
133
  box-shadow: 0 0 0 1px CanvasText;
118
134
  }
119
135
  }
120
- }
121
136
 
122
137
  :is(.sonic-button:is(:disabled, [aria-disabled='true']), .sonic-segmented-option:disabled)
123
138
  .sonic-led {
@@ -97,13 +97,13 @@
97
97
 
98
98
  :is(.sonic-button, .sonic-segmented-option, .sonic-switch-position, .sonic-toggle-position) {
99
99
  --_sonic-target-outset: min(0px, calc((100% - var(--sonic-target-size, 100%)) / 2));
100
+ }
100
101
 
101
- &::after {
102
+ :is(.sonic-button,.sonic-segmented-option,.sonic-switch-position,.sonic-toggle-position)::after {
102
103
  position: absolute;
103
104
  inset: var(--_sonic-target-outset);
104
105
  content: '';
105
106
  }
106
- }
107
107
 
108
108
  @media not (forced-colors: active) {
109
109
  :is(
@@ -146,12 +146,12 @@
146
146
  ):focus-visible {
147
147
  outline: 2px solid transparent;
148
148
  outline-offset: var(--sonic-focus-outline-offset, -2px);
149
+ }
149
150
 
150
- &:not([data-sonic-pointer-focus]) {
151
+ :is(.sonic-dial,.sonic-button,.sonic-number,.sonic-segmented-option,.sonic-slider,.sonic-waveform,.sonic-wavestrip,.sonic-xy-axis):focus-visible:not([data-sonic-pointer-focus]) {
151
152
  --_sonic-cap: var(--_sonic-cap-hover);
152
153
  --_sonic-focus-shadow: var(--_sonic-focus-glow);
153
154
  }
154
- }
155
155
 
156
156
  :is(
157
157
  .sonic-number,
@@ -229,11 +229,11 @@
229
229
  .sonic-wavestrip
230
230
  ):focus-visible {
231
231
  outline-offset: 2px;
232
+ }
232
233
 
233
- &[data-sonic-pointer-focus] {
234
+ [data-sonic-pointer-focus]:is(.sonic-dial,.sonic-button,.sonic-number,.sonic-segmented-option,.sonic-slider,.sonic-waveform,.sonic-wavestrip):focus-visible {
234
235
  outline-style: none;
235
236
  }
236
- }
237
237
 
238
238
  :is(
239
239
  .sonic-dial-cap,