@xsynaptic/sonic-ui 0.23.0 → 0.24.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # @xsynaptic/sonic-ui
2
2
 
3
+ ## 0.24.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Waveform: three number tokens shape the band mix, each watched, so a change repaints with no `repaint()`. Unset, the tint is unchanged.
8
+
9
+ - `--sonic-waveform-band-normalize`, 0 to 1 (default 1): at 0 the mix is the weighted average of the band colours and stays inside the palette
10
+ - `--sonic-waveform-band-contrast` (default 1): the exponent on each band's weight
11
+ - `--sonic-waveform-band-tilt` (default 0): decibels of gain per band above the lowest
12
+
3
13
  ## 0.23.0
4
14
 
5
15
  ### Minor Changes
@@ -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 {
@@ -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;
@@ -65,7 +65,10 @@ export declare class SonicWaveform extends SonicWaveElement<Colour, never, Numer
65
65
  };
66
66
  protected readonly lengths: {};
67
67
  protected readonly numbers: {
68
+ readonly bandContrast: "--_sonic-waveform-band-contrast";
69
+ readonly bandNormalize: "--_sonic-waveform-band-normalize";
68
70
  readonly bandOpacity: "--_sonic-waveform-band-opacity";
71
+ readonly bandTilt: "--_sonic-waveform-band-tilt";
69
72
  readonly edgeShade: "--_sonic-waveform-edge-shade";
70
73
  };
71
74
  protected readonly sheet = "waveform.css";
@@ -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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"}
@@ -54,6 +54,24 @@
54
54
  initial-value: 1;
55
55
  }
56
56
 
57
+ @property --_sonic-waveform-band-normalize {
58
+ syntax: '<number>';
59
+ inherits: true;
60
+ initial-value: 1;
61
+ }
62
+
63
+ @property --_sonic-waveform-band-contrast {
64
+ syntax: '<number>';
65
+ inherits: true;
66
+ initial-value: 1;
67
+ }
68
+
69
+ @property --_sonic-waveform-band-tilt {
70
+ syntax: '<number>';
71
+ inherits: true;
72
+ initial-value: 0;
73
+ }
74
+
57
75
  @property --_sonic-waveform-grid {
58
76
  syntax: '<color>';
59
77
  inherits: true;
@@ -122,6 +140,9 @@
122
140
  --_sonic-waveform-band-opacity: calc(
123
141
  var(--sonic-waveform-band-opacity, 1) * var(--_sonic-waveform-band-dim, 1)
124
142
  );
143
+ --_sonic-waveform-band-normalize: var(--sonic-waveform-band-normalize, 1);
144
+ --_sonic-waveform-band-contrast: var(--sonic-waveform-band-contrast, 1);
145
+ --_sonic-waveform-band-tilt: var(--sonic-waveform-band-tilt, 0);
125
146
  --_sonic-waveform-grid: var(--sonic-waveform-grid, var(--_sonic-etch));
126
147
  --_sonic-waveform-ends: var(--sonic-waveform-ends, var(--_sonic-etch));
127
148
  --_sonic-waveform-placeholder: var(--sonic-waveform-placeholder, var(--_sonic-etch));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xsynaptic/sonic-ui",
3
- "version": "0.23.0",
3
+ "version": "0.24.0",
4
4
  "description": "Native custom elements for audio interfaces, skinned with CSS custom properties",
5
5
  "keywords": [
6
6
  "custom-elements",