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