@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
|
@@ -22,6 +22,7 @@ var CanvasSurface = class {
|
|
|
22
22
|
}
|
|
23
23
|
#context;
|
|
24
24
|
#cssBox;
|
|
25
|
+
#fillWatch;
|
|
25
26
|
#frame;
|
|
26
27
|
#isDirty = true;
|
|
27
28
|
#isRebuildWanted = true;
|
|
@@ -51,6 +52,7 @@ var CanvasSurface = class {
|
|
|
51
52
|
signal.addEventListener("abort", () => {
|
|
52
53
|
if (this.#frame !== void 0) cancelAnimationFrame(this.#frame);
|
|
53
54
|
this.#frame = void 0;
|
|
55
|
+
this.#stopFill();
|
|
54
56
|
}, { once: true });
|
|
55
57
|
this.#schedule();
|
|
56
58
|
}
|
|
@@ -65,6 +67,25 @@ var CanvasSurface = class {
|
|
|
65
67
|
requestFrame() {
|
|
66
68
|
this.#schedule();
|
|
67
69
|
}
|
|
70
|
+
setFill(isFilling) {
|
|
71
|
+
const { fill, signal } = this.#options;
|
|
72
|
+
if (!fill || signal.aborted || isFilling === (this.#fillWatch !== void 0)) return;
|
|
73
|
+
const { style } = fill.control;
|
|
74
|
+
this.#stopFill();
|
|
75
|
+
if (!isFilling) {
|
|
76
|
+
style.removeProperty(fill.sizeProperty);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const started = new ResizeObserver((entries) => {
|
|
80
|
+
const box = entries.at(-1)?.borderBoxSize[0];
|
|
81
|
+
if (!box) return;
|
|
82
|
+
requestAnimationFrame(() => {
|
|
83
|
+
if (this.#fillWatch === started) style.setProperty(fill.sizeProperty, `${String(box.blockSize)}px`);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
this.#fillWatch = started;
|
|
87
|
+
started.observe(fill.control);
|
|
88
|
+
}
|
|
68
89
|
#armTransitions() {
|
|
69
90
|
const { canvas, colours, numbers = {} } = this.#options;
|
|
70
91
|
const { style } = canvas;
|
|
@@ -190,6 +211,10 @@ var CanvasSurface = class {
|
|
|
190
211
|
this.#draw(frameMs);
|
|
191
212
|
});
|
|
192
213
|
}
|
|
214
|
+
#stopFill() {
|
|
215
|
+
this.#fillWatch?.disconnect();
|
|
216
|
+
this.#fillWatch = void 0;
|
|
217
|
+
}
|
|
193
218
|
#watchResolution() {
|
|
194
219
|
const { signal } = this.#options;
|
|
195
220
|
matchMedia(`(resolution: ${String(deviceRatio())}dppx)`).addEventListener("change", () => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"canvas-surface.js","names":["named"],"sources":["../../../src/lib/canvas-surface.ts"],"sourcesContent":["import { readPxProperty } from '#lib/read-px-property.ts';\n\ntype PrivateProperty = `--_sonic-${string}`;\n\nexport interface SurfaceSize {\n\tdpr: number;\n\theight: number;\n\twidth: number;\n}\n\nexport interface SurfaceLook<\n\tColour extends string,\n\tLength extends string = never,\n\tNumeric extends string = never,\n> {\n\tcolours: Record<Colour, string>;\n\tisReducedMotion: boolean;\n\tlengths: Record<Length, number>;\n\tnumbers: Record<Numeric, number>;\n}\n\nexport interface SurfaceFrame<\n\tColour extends string,\n\tLength extends string = never,\n\tNumeric extends string = never,\n> {\n\tframeMs: number;\n\tisDirty: boolean;\n\tisRebuilt: boolean;\n\tlook: SurfaceLook<Colour, Length, Numeric>;\n\tsize: SurfaceSize;\n}\n\nexport interface Surface {\n\tinvalidate: () => void;\n\treadonly isVisible: boolean;\n\trebuild: () => void;\n\trequestFrame: () => void;\n\treadonly size: SurfaceSize;\n}\n\ninterface SurfaceOptions<Colour extends string, Length extends string, Numeric extends string> {\n\tcanvas: HTMLCanvasElement;\n\tcolours: Record<Colour, PrivateProperty>;\n\tlengths?: Record<Length, PrivateProperty>;\n\tnumbers?: Record<Numeric, PrivateProperty>;\n\tpaint: (context: CanvasRenderingContext2D, frame: SurfaceFrame<Colour, Length, Numeric>) => void;\n\tresize?: (size: SurfaceSize) => void;\n\tsignal: AbortSignal;\n}\n\nfunction deviceRatio(): number {\n\treturn globalThis.devicePixelRatio || 1;\n}\n\n// WebKit reports none, and an emulated device scale reports it in CSS px, so it counts only where it agrees\nfunction deviceBox(\n\tentry: ResizeObserverEntry,\n\tcss: ResizeObserverSize,\n): ResizeObserverSize | undefined {\n\tconst device =\n\t\t'devicePixelContentBoxSize' in entry ? entry.devicePixelContentBoxSize[0] : undefined;\n\tconst dpr = deviceRatio();\n\tif (!device || Math.abs(device.inlineSize - css.inlineSize * dpr) > 1) return undefined;\n\n\treturn Math.abs(device.blockSize - css.blockSize * dpr) > 1 ? undefined : device;\n}\n\n// `Object.fromEntries` widens the keys to `string`\nfunction readEach<Name extends string, Value>(\n\tproperties: Partial<Record<Name, PrivateProperty>>,\n\tread: (property: PrivateProperty) => Value,\n): Record<Name, Value> {\n\tconst named: Readonly<Record<string, PrivateProperty | undefined>> = properties;\n\n\treturn Object.fromEntries(\n\t\tObject.entries(named).flatMap(([name, property]) =>\n\t\t\tproperty === undefined ? [] : [[name, read(property)]],\n\t\t),\n\t) as Record<Name, Value>;\n}\n\nclass CanvasSurface<\n\tColour extends string,\n\tLength extends string,\n\tNumeric extends string,\n> implements Surface {\n\tget isVisible(): boolean {\n\t\treturn this.#isVisible;\n\t}\n\n\tget size(): SurfaceSize {\n\t\treturn this.#size;\n\t}\n\n\t// happy-dom has no canvas context\n\treadonly #context: CanvasRenderingContext2D | undefined;\n\n\t#cssBox: undefined | { height: number; width: number };\n\n\t#frame: number | undefined;\n\n\t#isDirty = true;\n\n\t#isRebuildWanted = true;\n\n\t#isVisible = true;\n\n\t#look: SurfaceLook<Colour, Length, Numeric> | undefined;\n\n\treadonly #options: SurfaceOptions<Colour, Length, Numeric>;\n\n\treadonly #reducedMotion = matchMedia('(prefers-reduced-motion: reduce)');\n\n\t#size: SurfaceSize = { dpr: deviceRatio(), height: 0, width: 0 };\n\n\tconstructor(options: SurfaceOptions<Colour, Length, Numeric>) {\n\t\tconst { canvas, signal } = options;\n\n\t\tthis.#options = options;\n\t\tthis.#context = canvas.getContext('2d') ?? undefined;\n\t\tthis.#armTransitions();\n\t\tthis.#observeSize();\n\t\tthis.#observeView();\n\t\tthis.#watchResolution();\n\t\tthis.#reducedMotion.addEventListener(\n\t\t\t'change',\n\t\t\t() => {\n\t\t\t\tthis.rebuild();\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t\t// `transitionstart` drops events when a change replaces a pending transition in WebKit\n\t\tcanvas.addEventListener(\n\t\t\t'transitionrun',\n\t\t\t() => {\n\t\t\t\tthis.rebuild();\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t\tsignal.addEventListener(\n\t\t\t'abort',\n\t\t\t() => {\n\t\t\t\tif (this.#frame !== undefined) cancelAnimationFrame(this.#frame);\n\t\t\t\tthis.#frame = undefined;\n\t\t\t},\n\t\t\t{ once: true },\n\t\t);\n\t\tthis.#schedule();\n\t}\n\n\tinvalidate(): void {\n\t\tthis.#isDirty = true;\n\t\tthis.#schedule();\n\t}\n\n\trebuild(): void {\n\t\tthis.#isRebuildWanted = true;\n\t\tthis.invalidate();\n\t}\n\n\trequestFrame(): void {\n\t\tthis.#schedule();\n\t}\n\n\t#armTransitions(): void {\n\t\tconst { canvas, colours, numbers = {} } = this.#options;\n\t\tconst { style } = canvas;\n\t\tconst watched = [...Object.values(colours), ...Object.values(numbers)];\n\n\t\tstyle.setProperty('transition-property', watched.join(', '));\n\t\tstyle.setProperty('transition-duration', '0.001ms');\n\t\tstyle.setProperty('transition-timing-function', 'step-start');\n\t\tstyle.setProperty('transition-behavior', 'allow-discrete');\n\t}\n\n\t#current(): { isRebuilt: boolean; look: SurfaceLook<Colour, Length, Numeric> } {\n\t\tif (this.#look && !this.#isRebuildWanted) return { isRebuilt: false, look: this.#look };\n\n\t\treturn { isRebuilt: true, look: this.#read() };\n\t}\n\n\t#draw(frameMs: number): void {\n\t\tthis.#frame = undefined;\n\n\t\tconst size = this.#size;\n\t\tif (size.width === 0 || size.height === 0) return;\n\n\t\tconst { canvas, paint } = this.#options;\n\t\tconst { isRebuilt, look } = this.#current();\n\t\tconst isDirty = this.#isDirty;\n\n\t\tthis.#look = look;\n\t\tthis.#isDirty = false;\n\t\tthis.#isRebuildWanted = false;\n\t\t// Assigning either clears the canvas and its state\n\t\tif (canvas.width !== size.width) canvas.width = size.width;\n\t\tif (canvas.height !== size.height) canvas.height = size.height;\n\t\tif (this.#context) paint(this.#context, { frameMs, isDirty, isRebuilt, look, size });\n\t}\n\n\t#measure(entry: ResizeObserverEntry): void {\n\t\tconst [box] = entry.contentBoxSize;\n\t\tif (!box) return;\n\n\t\tconst device = deviceBox(entry, box);\n\n\t\tif (device) {\n\t\t\tthis.#cssBox = undefined;\n\t\t\tthis.#resize(device.inlineSize, device.blockSize);\n\t\t\treturn;\n\t\t}\n\n\t\tthis.#cssBox = { height: box.blockSize, width: box.inlineSize };\n\t\tthis.#rescale();\n\t}\n\n\t#observeSize(): void {\n\t\tconst { canvas, signal } = this.#options;\n\t\tconst observer = new ResizeObserver((entries) => {\n\t\t\tconst entry = entries.at(-1);\n\n\t\t\tif (entry) this.#measure(entry);\n\t\t});\n\n\t\ttry {\n\t\t\tobserver.observe(canvas, { box: 'device-pixel-content-box' });\n\t\t} catch {\n\t\t\t// WebKit throws a TypeError on the option\n\t\t\tobserver.observe(canvas, { box: 'content-box' });\n\t\t}\n\t\tsignal.addEventListener(\n\t\t\t'abort',\n\t\t\t() => {\n\t\t\t\tobserver.disconnect();\n\t\t\t},\n\t\t\t{ once: true },\n\t\t);\n\t}\n\n\t#observeView(): void {\n\t\tconst { canvas, signal } = this.#options;\n\t\tconst observer = new IntersectionObserver((entries) => {\n\t\t\tconst entry = entries.at(-1);\n\t\t\tif (!entry) return;\n\n\t\t\tconst wasVisible = this.#isVisible;\n\n\t\t\tthis.#isVisible = entry.isIntersecting;\n\t\t\tif (!entry.isIntersecting && this.#frame !== undefined) {\n\t\t\t\tcancelAnimationFrame(this.#frame);\n\t\t\t\tthis.#frame = undefined;\n\t\t\t}\n\n\t\t\t// Chromium fires no transition inside a skipped `content-visibility` subtree\n\t\t\tif (!wasVisible && entry.isIntersecting) this.rebuild();\n\t\t});\n\n\t\tobserver.observe(canvas);\n\t\tsignal.addEventListener(\n\t\t\t'abort',\n\t\t\t() => {\n\t\t\t\tobserver.disconnect();\n\t\t\t},\n\t\t\t{ once: true },\n\t\t);\n\t}\n\n\t#read(): SurfaceLook<Colour, Length, Numeric> {\n\t\tconst { canvas, colours, lengths = {}, numbers = {} } = this.#options;\n\t\tconst styles = getComputedStyle(canvas);\n\n\t\treturn {\n\t\t\tcolours: readEach(colours, (property) =>\n\t\t\t\tstyles\n\t\t\t\t\t.getPropertyValue(property)\n\t\t\t\t\t.trim()\n\t\t\t\t\t.replaceAll('currentcolor', () => styles.color),\n\t\t\t),\n\t\t\tisReducedMotion: this.#reducedMotion.matches,\n\t\t\tlengths: readEach<Length, number>(lengths, (property) => readPxProperty(styles, property, 0)),\n\t\t\tnumbers: readEach<Numeric, number>(numbers, (property) =>\n\t\t\t\treadPxProperty(styles, property, 0),\n\t\t\t),\n\t\t};\n\t}\n\n\t#rescale(): void {\n\t\tconst box = this.#cssBox;\n\t\tif (!box) {\n\t\t\tthis.#resize(this.#size.width, this.#size.height);\n\t\t\treturn;\n\t\t}\n\n\t\tconst dpr = deviceRatio();\n\n\t\tthis.#resize(Math.round(box.width * dpr), Math.round(box.height * dpr));\n\t}\n\n\t#resize(width: number, height: number): void {\n\t\tconst previous = this.#size;\n\t\tconst dpr = deviceRatio();\n\t\tif (previous.width === width && previous.height === height && previous.dpr === dpr) return;\n\n\t\tthis.#size = { dpr, height, width };\n\t\tthis.#options.resize?.(this.#size);\n\t\tif (previous.width === 0 || previous.height === 0) this.rebuild();\n\t\telse this.invalidate();\n\t}\n\n\t#schedule(): void {\n\t\tif (this.#options.signal.aborted || this.#frame !== undefined || !this.#isVisible) return;\n\n\t\tthis.#frame = requestAnimationFrame((frameMs) => {\n\t\t\tthis.#draw(frameMs);\n\t\t});\n\t}\n\n\t#watchResolution(): void {\n\t\tconst { signal } = this.#options;\n\n\t\tmatchMedia(`(resolution: ${String(deviceRatio())}dppx)`).addEventListener(\n\t\t\t'change',\n\t\t\t() => {\n\t\t\t\tthis.#watchResolution();\n\t\t\t\tthis.#rescale();\n\t\t\t},\n\t\t\t{ once: true, signal },\n\t\t);\n\t}\n}\n\nexport function bindSurface<\n\tColour extends string,\n\tLength extends string = never,\n\tNumeric extends string = never,\n>(options: SurfaceOptions<Colour, Length, Numeric>): Surface {\n\treturn new CanvasSurface(options);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"canvas-surface.js","names":["named"],"sources":["../../../src/lib/canvas-surface.ts"],"sourcesContent":["import { readPxProperty } from '#lib/read-px-property.ts';\n\ntype PrivateProperty = `--_sonic-${string}`;\n\nexport interface SurfaceSize {\n\tdpr: number;\n\theight: number;\n\twidth: number;\n}\n\nexport interface SurfaceLook<\n\tColour extends string,\n\tLength extends string = never,\n\tNumeric extends string = never,\n> {\n\tcolours: Record<Colour, string>;\n\tisReducedMotion: boolean;\n\tlengths: Record<Length, number>;\n\tnumbers: Record<Numeric, number>;\n}\n\nexport interface SurfaceFrame<\n\tColour extends string,\n\tLength extends string = never,\n\tNumeric extends string = never,\n> {\n\tframeMs: number;\n\tisDirty: boolean;\n\tisRebuilt: boolean;\n\tlook: SurfaceLook<Colour, Length, Numeric>;\n\tsize: SurfaceSize;\n}\n\nexport interface Surface {\n\tinvalidate: () => void;\n\treadonly isVisible: boolean;\n\trebuild: () => void;\n\trequestFrame: () => void;\n\tsetFill: (isFilling: boolean) => void;\n\treadonly size: SurfaceSize;\n}\n\ninterface SurfaceOptions<Colour extends string, Length extends string, Numeric extends string> {\n\tcanvas: HTMLCanvasElement;\n\tcolours: Record<Colour, PrivateProperty>;\n\tfill?: { control: HTMLElement; sizeProperty: PrivateProperty };\n\tlengths?: Record<Length, PrivateProperty>;\n\tnumbers?: Record<Numeric, PrivateProperty>;\n\tpaint: (context: CanvasRenderingContext2D, frame: SurfaceFrame<Colour, Length, Numeric>) => void;\n\tresize?: (size: SurfaceSize) => void;\n\tsignal: AbortSignal;\n}\n\nfunction deviceRatio(): number {\n\treturn globalThis.devicePixelRatio || 1;\n}\n\n// WebKit reports none, and an emulated device scale reports it in CSS px, so it counts only where it agrees\nfunction deviceBox(\n\tentry: ResizeObserverEntry,\n\tcss: ResizeObserverSize,\n): ResizeObserverSize | undefined {\n\tconst device =\n\t\t'devicePixelContentBoxSize' in entry ? entry.devicePixelContentBoxSize[0] : undefined;\n\tconst dpr = deviceRatio();\n\tif (!device || Math.abs(device.inlineSize - css.inlineSize * dpr) > 1) return undefined;\n\n\treturn Math.abs(device.blockSize - css.blockSize * dpr) > 1 ? undefined : device;\n}\n\n// `Object.fromEntries` widens the keys to `string`\nfunction readEach<Name extends string, Value>(\n\tproperties: Partial<Record<Name, PrivateProperty>>,\n\tread: (property: PrivateProperty) => Value,\n): Record<Name, Value> {\n\tconst named: Readonly<Record<string, PrivateProperty | undefined>> = properties;\n\n\treturn Object.fromEntries(\n\t\tObject.entries(named).flatMap(([name, property]) =>\n\t\t\tproperty === undefined ? [] : [[name, read(property)]],\n\t\t),\n\t) as Record<Name, Value>;\n}\n\nclass CanvasSurface<\n\tColour extends string,\n\tLength extends string,\n\tNumeric extends string,\n> implements Surface {\n\tget isVisible(): boolean {\n\t\treturn this.#isVisible;\n\t}\n\n\tget size(): SurfaceSize {\n\t\treturn this.#size;\n\t}\n\n\t// happy-dom has no canvas context\n\treadonly #context: CanvasRenderingContext2D | undefined;\n\n\t#cssBox: undefined | { height: number; width: number };\n\n\t#fillWatch: ResizeObserver | undefined;\n\n\t#frame: number | undefined;\n\n\t#isDirty = true;\n\n\t#isRebuildWanted = true;\n\n\t#isVisible = true;\n\n\t#look: SurfaceLook<Colour, Length, Numeric> | undefined;\n\n\treadonly #options: SurfaceOptions<Colour, Length, Numeric>;\n\n\treadonly #reducedMotion = matchMedia('(prefers-reduced-motion: reduce)');\n\n\t#size: SurfaceSize = { dpr: deviceRatio(), height: 0, width: 0 };\n\n\tconstructor(options: SurfaceOptions<Colour, Length, Numeric>) {\n\t\tconst { canvas, signal } = options;\n\n\t\tthis.#options = options;\n\t\tthis.#context = canvas.getContext('2d') ?? undefined;\n\t\tthis.#armTransitions();\n\t\tthis.#observeSize();\n\t\tthis.#observeView();\n\t\tthis.#watchResolution();\n\t\tthis.#reducedMotion.addEventListener(\n\t\t\t'change',\n\t\t\t() => {\n\t\t\t\tthis.rebuild();\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t\t// `transitionstart` drops events when a change replaces a pending transition in WebKit\n\t\tcanvas.addEventListener(\n\t\t\t'transitionrun',\n\t\t\t() => {\n\t\t\t\tthis.rebuild();\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t\tsignal.addEventListener(\n\t\t\t'abort',\n\t\t\t() => {\n\t\t\t\tif (this.#frame !== undefined) cancelAnimationFrame(this.#frame);\n\t\t\t\tthis.#frame = undefined;\n\t\t\t\tthis.#stopFill();\n\t\t\t},\n\t\t\t{ once: true },\n\t\t);\n\t\tthis.#schedule();\n\t}\n\n\tinvalidate(): void {\n\t\tthis.#isDirty = true;\n\t\tthis.#schedule();\n\t}\n\n\trebuild(): void {\n\t\tthis.#isRebuildWanted = true;\n\t\tthis.invalidate();\n\t}\n\n\trequestFrame(): void {\n\t\tthis.#schedule();\n\t}\n\n\tsetFill(isFilling: boolean): void {\n\t\tconst { fill, signal } = this.#options;\n\t\tif (!fill || signal.aborted || isFilling === (this.#fillWatch !== undefined)) return;\n\n\t\tconst { style } = fill.control;\n\n\t\tthis.#stopFill();\n\t\tif (!isFilling) {\n\t\t\tstyle.removeProperty(fill.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 (this.#fillWatch === started) {\n\t\t\t\t\tstyle.setProperty(fill.sizeProperty, `${String(box.blockSize)}px`);\n\t\t\t\t}\n\t\t\t});\n\t\t});\n\n\t\tthis.#fillWatch = started;\n\t\tstarted.observe(fill.control);\n\t}\n\n\t#armTransitions(): void {\n\t\tconst { canvas, colours, numbers = {} } = this.#options;\n\t\tconst { style } = canvas;\n\t\tconst watched = [...Object.values(colours), ...Object.values(numbers)];\n\n\t\tstyle.setProperty('transition-property', watched.join(', '));\n\t\tstyle.setProperty('transition-duration', '0.001ms');\n\t\tstyle.setProperty('transition-timing-function', 'step-start');\n\t\tstyle.setProperty('transition-behavior', 'allow-discrete');\n\t}\n\n\t#current(): { isRebuilt: boolean; look: SurfaceLook<Colour, Length, Numeric> } {\n\t\tif (this.#look && !this.#isRebuildWanted) return { isRebuilt: false, look: this.#look };\n\n\t\treturn { isRebuilt: true, look: this.#read() };\n\t}\n\n\t#draw(frameMs: number): void {\n\t\tthis.#frame = undefined;\n\n\t\tconst size = this.#size;\n\t\tif (size.width === 0 || size.height === 0) return;\n\n\t\tconst { canvas, paint } = this.#options;\n\t\tconst { isRebuilt, look } = this.#current();\n\t\tconst isDirty = this.#isDirty;\n\n\t\tthis.#look = look;\n\t\tthis.#isDirty = false;\n\t\tthis.#isRebuildWanted = false;\n\t\t// Assigning either clears the canvas and its state\n\t\tif (canvas.width !== size.width) canvas.width = size.width;\n\t\tif (canvas.height !== size.height) canvas.height = size.height;\n\t\tif (this.#context) paint(this.#context, { frameMs, isDirty, isRebuilt, look, size });\n\t}\n\n\t#measure(entry: ResizeObserverEntry): void {\n\t\tconst [box] = entry.contentBoxSize;\n\t\tif (!box) return;\n\n\t\tconst device = deviceBox(entry, box);\n\n\t\tif (device) {\n\t\t\tthis.#cssBox = undefined;\n\t\t\tthis.#resize(device.inlineSize, device.blockSize);\n\t\t\treturn;\n\t\t}\n\n\t\tthis.#cssBox = { height: box.blockSize, width: box.inlineSize };\n\t\tthis.#rescale();\n\t}\n\n\t#observeSize(): void {\n\t\tconst { canvas, signal } = this.#options;\n\t\tconst observer = new ResizeObserver((entries) => {\n\t\t\tconst entry = entries.at(-1);\n\n\t\t\tif (entry) this.#measure(entry);\n\t\t});\n\n\t\ttry {\n\t\t\tobserver.observe(canvas, { box: 'device-pixel-content-box' });\n\t\t} catch {\n\t\t\t// WebKit throws a TypeError on the option\n\t\t\tobserver.observe(canvas, { box: 'content-box' });\n\t\t}\n\t\tsignal.addEventListener(\n\t\t\t'abort',\n\t\t\t() => {\n\t\t\t\tobserver.disconnect();\n\t\t\t},\n\t\t\t{ once: true },\n\t\t);\n\t}\n\n\t#observeView(): void {\n\t\tconst { canvas, signal } = this.#options;\n\t\tconst observer = new IntersectionObserver((entries) => {\n\t\t\tconst entry = entries.at(-1);\n\t\t\tif (!entry) return;\n\n\t\t\tconst wasVisible = this.#isVisible;\n\n\t\t\tthis.#isVisible = entry.isIntersecting;\n\t\t\tif (!entry.isIntersecting && this.#frame !== undefined) {\n\t\t\t\tcancelAnimationFrame(this.#frame);\n\t\t\t\tthis.#frame = undefined;\n\t\t\t}\n\n\t\t\t// Chromium fires no transition inside a skipped `content-visibility` subtree\n\t\t\tif (!wasVisible && entry.isIntersecting) this.rebuild();\n\t\t});\n\n\t\tobserver.observe(canvas);\n\t\tsignal.addEventListener(\n\t\t\t'abort',\n\t\t\t() => {\n\t\t\t\tobserver.disconnect();\n\t\t\t},\n\t\t\t{ once: true },\n\t\t);\n\t}\n\n\t#read(): SurfaceLook<Colour, Length, Numeric> {\n\t\tconst { canvas, colours, lengths = {}, numbers = {} } = this.#options;\n\t\tconst styles = getComputedStyle(canvas);\n\n\t\treturn {\n\t\t\tcolours: readEach(colours, (property) =>\n\t\t\t\tstyles\n\t\t\t\t\t.getPropertyValue(property)\n\t\t\t\t\t.trim()\n\t\t\t\t\t.replaceAll('currentcolor', () => styles.color),\n\t\t\t),\n\t\t\tisReducedMotion: this.#reducedMotion.matches,\n\t\t\tlengths: readEach<Length, number>(lengths, (property) => readPxProperty(styles, property, 0)),\n\t\t\tnumbers: readEach<Numeric, number>(numbers, (property) =>\n\t\t\t\treadPxProperty(styles, property, 0),\n\t\t\t),\n\t\t};\n\t}\n\n\t#rescale(): void {\n\t\tconst box = this.#cssBox;\n\t\tif (!box) {\n\t\t\tthis.#resize(this.#size.width, this.#size.height);\n\t\t\treturn;\n\t\t}\n\n\t\tconst dpr = deviceRatio();\n\n\t\tthis.#resize(Math.round(box.width * dpr), Math.round(box.height * dpr));\n\t}\n\n\t#resize(width: number, height: number): void {\n\t\tconst previous = this.#size;\n\t\tconst dpr = deviceRatio();\n\t\tif (previous.width === width && previous.height === height && previous.dpr === dpr) return;\n\n\t\tthis.#size = { dpr, height, width };\n\t\tthis.#options.resize?.(this.#size);\n\t\tif (previous.width === 0 || previous.height === 0) this.rebuild();\n\t\telse this.invalidate();\n\t}\n\n\t#schedule(): void {\n\t\tif (this.#options.signal.aborted || this.#frame !== undefined || !this.#isVisible) return;\n\n\t\tthis.#frame = requestAnimationFrame((frameMs) => {\n\t\t\tthis.#draw(frameMs);\n\t\t});\n\t}\n\n\t#stopFill(): void {\n\t\tthis.#fillWatch?.disconnect();\n\t\tthis.#fillWatch = undefined;\n\t}\n\n\t#watchResolution(): void {\n\t\tconst { signal } = this.#options;\n\n\t\tmatchMedia(`(resolution: ${String(deviceRatio())}dppx)`).addEventListener(\n\t\t\t'change',\n\t\t\t() => {\n\t\t\t\tthis.#watchResolution();\n\t\t\t\tthis.#rescale();\n\t\t\t},\n\t\t\t{ once: true, signal },\n\t\t);\n\t}\n}\n\nexport function bindSurface<\n\tColour extends string,\n\tLength extends string = never,\n\tNumeric extends string = never,\n>(options: SurfaceOptions<Colour, Length, Numeric>): Surface {\n\treturn new CanvasSurface(options);\n}\n"],"mappings":"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"}
|
|
@@ -7,6 +7,7 @@ interface CrossfadeGains {
|
|
|
7
7
|
a: number;
|
|
8
8
|
b: number;
|
|
9
9
|
}
|
|
10
|
+
/** `position` runs from 0, all a, to 1, all b; `sharpness` from 0 to 1 shortens the overlap */
|
|
10
11
|
export declare function crossfadeGains(position: number, options?: CrossfadeOptions): CrossfadeGains;
|
|
11
12
|
//#endregion
|
|
12
13
|
//# sourceMappingURL=crossfade.d.ts.map
|
|
@@ -5,6 +5,7 @@ function progress(side, span) {
|
|
|
5
5
|
if (span === 0) return side > 0 ? 1 : 0;
|
|
6
6
|
return Math.min(1, side / span);
|
|
7
7
|
}
|
|
8
|
+
/** `position` runs from 0, all a, to 1, all b; `sharpness` from 0 to 1 shortens the overlap */
|
|
8
9
|
function crossfadeGains(position, options = {}) {
|
|
9
10
|
const at = clampProportion(position);
|
|
10
11
|
const span = 1 - clampProportion(options.sharpness ?? 0);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"crossfade.js","names":[],"sources":["../../../src/lib/crossfade.ts"],"sourcesContent":["import { clampProportion } from '#lib/math.ts';\n\ninterface CrossfadeOptions {\n\tlaw?: 'linear' | 'power';\n\tsharpness?: number;\n}\n\ninterface CrossfadeGains {\n\ta: number;\n\tb: number;\n}\n\nfunction progress(side: number, span: number): number {\n\tif (span === 0) return side > 0 ? 1 : 0;\n\n\treturn Math.min(1, side / span);\n}\n\nexport function crossfadeGains(position: number, options: CrossfadeOptions = {}): CrossfadeGains {\n\tconst at = clampProportion(position);\n\tconst span = 1 - clampProportion(options.sharpness ?? 0);\n\tconst gain = (side: number): number => {\n\t\tconst reached = progress(side, span);\n\n\t\treturn options.law === 'linear' ? reached : Math.sin((reached * Math.PI) / 2);\n\t};\n\n\treturn { a: gain(1 - at), b: gain(at) };\n}\n"],"mappings":";;;AAYA,SAAS,SAAS,MAAc,MAAsB;CACrD,IAAI,SAAS,GAAG,OAAO,OAAO,IAAI,IAAI;CAEtC,OAAO,KAAK,IAAI,GAAG,OAAO,IAAI;AAC/B
|
|
1
|
+
{"version":3,"file":"crossfade.js","names":[],"sources":["../../../src/lib/crossfade.ts"],"sourcesContent":["import { clampProportion } from '#lib/math.ts';\n\ninterface CrossfadeOptions {\n\tlaw?: 'linear' | 'power';\n\tsharpness?: number;\n}\n\ninterface CrossfadeGains {\n\ta: number;\n\tb: number;\n}\n\nfunction progress(side: number, span: number): number {\n\tif (span === 0) return side > 0 ? 1 : 0;\n\n\treturn Math.min(1, side / span);\n}\n\n/** `position` runs from 0, all a, to 1, all b; `sharpness` from 0 to 1 shortens the overlap */\nexport function crossfadeGains(position: number, options: CrossfadeOptions = {}): CrossfadeGains {\n\tconst at = clampProportion(position);\n\tconst span = 1 - clampProportion(options.sharpness ?? 0);\n\tconst gain = (side: number): number => {\n\t\tconst reached = progress(side, span);\n\n\t\treturn options.law === 'linear' ? reached : Math.sin((reached * Math.PI) / 2);\n\t};\n\n\treturn { a: gain(1 - at), b: gain(at) };\n}\n"],"mappings":";;;AAYA,SAAS,SAAS,MAAc,MAAsB;CACrD,IAAI,SAAS,GAAG,OAAO,OAAO,IAAI,IAAI;CAEtC,OAAO,KAAK,IAAI,GAAG,OAAO,IAAI;AAC/B;;AAGA,SAAgB,eAAe,UAAkB,UAA4B,CAAC,GAAmB;CAChG,MAAM,KAAK,gBAAgB,QAAQ;CACnC,MAAM,OAAO,IAAI,gBAAgB,QAAQ,aAAa,CAAC;CACvD,MAAM,QAAQ,SAAyB;EACtC,MAAM,UAAU,SAAS,MAAM,IAAI;EAEnC,OAAO,QAAQ,QAAQ,WAAW,UAAU,KAAK,IAAK,UAAU,KAAK,KAAM,CAAC;CAC7E;CAEA,OAAO;EAAE,GAAG,KAAK,IAAI,EAAE;EAAG,GAAG,KAAK,EAAE;CAAE;AACvC"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
//#region src/lib/envelope-curve.ts
|
|
2
2
|
const flatCurve = .001;
|
|
3
|
+
/** `position` is 0 to 1 along a stage; a `curve` of 0 is a straight line */
|
|
3
4
|
function envelopeCurve(position, curve) {
|
|
4
5
|
if (Math.abs(curve) < flatCurve) return position;
|
|
5
6
|
return Math.expm1(curve * position) / Math.expm1(curve);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"envelope-curve.js","names":[],"sources":["../../../src/lib/envelope-curve.ts"],"sourcesContent":["const flatCurve = 0.001;\n\nexport function envelopeCurve(position: number, curve: number): number {\n\tif (Math.abs(curve) < flatCurve) return position;\n\n\treturn Math.expm1(curve * position) / Math.expm1(curve);\n}\n"],"mappings":";AAAA,MAAM,YAAY
|
|
1
|
+
{"version":3,"file":"envelope-curve.js","names":[],"sources":["../../../src/lib/envelope-curve.ts"],"sourcesContent":["const flatCurve = 0.001;\n\n/** `position` is 0 to 1 along a stage; a `curve` of 0 is a straight line */\nexport function envelopeCurve(position: number, curve: number): number {\n\tif (Math.abs(curve) < flatCurve) return position;\n\n\treturn Math.expm1(curve * position) / Math.expm1(curve);\n}\n"],"mappings":";AAAA,MAAM,YAAY;;AAGlB,SAAgB,cAAc,UAAkB,OAAuB;CACtE,IAAI,KAAK,IAAI,KAAK,IAAI,WAAW,OAAO;CAExC,OAAO,KAAK,MAAM,QAAQ,QAAQ,IAAI,KAAK,MAAM,KAAK;AACvD"}
|
|
@@ -7,15 +7,16 @@ import { writeKind } from "./marker-band.js";
|
|
|
7
7
|
function lineX(seconds, view) {
|
|
8
8
|
return (seconds - view.startSeconds) / view.windowSeconds * view.widthPx;
|
|
9
9
|
}
|
|
10
|
-
function parkedPlacement(labels, index, [view, arrivingX]) {
|
|
10
|
+
function parkedPlacement(labels, index, [view, arrivingX, [startX, endX]]) {
|
|
11
11
|
const parked = labels[index];
|
|
12
12
|
if (!parked) return void 0;
|
|
13
13
|
const next = labels[index + 1];
|
|
14
14
|
const x = Math.max(view.parkPx, lineX(parked.start, view) + view.insetPx);
|
|
15
|
-
const
|
|
15
|
+
const fadePx = startX - endX;
|
|
16
|
+
const leftPx = next ? lineX(next.start, view) - endX : Infinity;
|
|
16
17
|
return {
|
|
17
18
|
index,
|
|
18
|
-
opacity:
|
|
19
|
+
opacity: fadePx > 0 ? clamp(leftPx / fadePx, 0, 1) : 1,
|
|
19
20
|
shownPx: Math.min(parked.widthPx, arrivingX - x, view.widthPx - x),
|
|
20
21
|
x
|
|
21
22
|
};
|
|
@@ -23,12 +24,23 @@ function parkedPlacement(labels, index, [view, arrivingX]) {
|
|
|
23
24
|
function isDrawable(length) {
|
|
24
25
|
return Number.isFinite(length) && length > 0;
|
|
25
26
|
}
|
|
27
|
+
function fadeLines(view) {
|
|
28
|
+
const playheadX = lineX(view.playheadSeconds, view);
|
|
29
|
+
if (view.isPaged) return [playheadX, playheadX];
|
|
30
|
+
const stretchPx = view.parkPx - view.insetPx - playheadX;
|
|
31
|
+
return [playheadX + clamp(view.fadeStart, 0, 1) * stretchPx, playheadX + clamp(view.fadeEnd, 0, 1) * stretchPx];
|
|
32
|
+
}
|
|
26
33
|
function layoutRider(labels, view) {
|
|
27
34
|
if (!isDrawable(view.windowSeconds) || !isDrawable(view.widthPx)) return {};
|
|
28
|
-
const
|
|
35
|
+
const fade = fadeLines(view);
|
|
36
|
+
const parkedIndex = labels.findLastIndex((label) => lineX(label.start, view) <= fade[1]);
|
|
29
37
|
const next = labels[parkedIndex + 1];
|
|
30
38
|
const arrivingX = next ? lineX(next.start, view) + view.insetPx : Infinity;
|
|
31
|
-
const parked = parkedPlacement(labels, parkedIndex, [
|
|
39
|
+
const parked = parkedPlacement(labels, parkedIndex, [
|
|
40
|
+
view,
|
|
41
|
+
arrivingX,
|
|
42
|
+
fade
|
|
43
|
+
]);
|
|
32
44
|
const arriving = next && arrivingX < view.widthPx ? {
|
|
33
45
|
index: parkedIndex + 1,
|
|
34
46
|
opacity: 1,
|
|
@@ -44,11 +56,16 @@ function labelSlot(element) {
|
|
|
44
56
|
return {
|
|
45
57
|
element,
|
|
46
58
|
kind: void 0,
|
|
59
|
+
label: void 0,
|
|
47
60
|
opacity: NaN,
|
|
48
61
|
shownPx: NaN,
|
|
49
62
|
x: NaN
|
|
50
63
|
};
|
|
51
64
|
}
|
|
65
|
+
function writeContent(element, { text, write }) {
|
|
66
|
+
element.textContent = write ? "" : text;
|
|
67
|
+
write?.(element);
|
|
68
|
+
}
|
|
52
69
|
function writePlacement(slot, { opacity, shownPx, x }, widthPx) {
|
|
53
70
|
const { style } = slot.element;
|
|
54
71
|
if (slot.x !== x) {
|
|
@@ -69,7 +86,10 @@ function writeLabel(slot, [labels, colourProperty], placement) {
|
|
|
69
86
|
const label = placement && placement.shownPx > 0 ? labels[placement.index] : void 0;
|
|
70
87
|
if (element.hidden !== !label) element.hidden = !label;
|
|
71
88
|
if (!label || !placement) return;
|
|
72
|
-
if (
|
|
89
|
+
if (slot.label !== label) {
|
|
90
|
+
slot.label = label;
|
|
91
|
+
writeContent(element, label);
|
|
92
|
+
}
|
|
73
93
|
element.toggleAttribute("data-sonic-dimmed", label.isDimmed);
|
|
74
94
|
if (slot.kind !== label.kind) {
|
|
75
95
|
slot.kind = label.kind;
|
|
@@ -77,11 +97,11 @@ function writeLabel(slot, [labels, colourProperty], placement) {
|
|
|
77
97
|
}
|
|
78
98
|
writePlacement(slot, placement, label.widthPx);
|
|
79
99
|
}
|
|
80
|
-
function measureWidths(control, className,
|
|
81
|
-
const probes =
|
|
100
|
+
function measureWidths(control, className, labels) {
|
|
101
|
+
const probes = labels.map((label) => {
|
|
82
102
|
const probe = document.createElement("div");
|
|
83
103
|
probe.className = className;
|
|
84
|
-
probe
|
|
104
|
+
writeContent(probe, label);
|
|
85
105
|
return probe;
|
|
86
106
|
});
|
|
87
107
|
control.append(...probes);
|
|
@@ -90,18 +110,20 @@ function measureWidths(control, className, texts) {
|
|
|
90
110
|
return widths;
|
|
91
111
|
}
|
|
92
112
|
function createLabelRider(control, options) {
|
|
93
|
-
const { className, colourProperty,
|
|
113
|
+
const { className, colourProperty, fadeProperties, insetProperty, parkProperty } = options;
|
|
94
114
|
const parked = labelSlot(requireChild(control, `.${className}`, HTMLElement));
|
|
95
115
|
const arriving = labelSlot(requireChild(control, `.${className} + .${className}`, HTMLElement));
|
|
96
|
-
let
|
|
116
|
+
let fadeStart = 0;
|
|
117
|
+
let fadeEnd = 1;
|
|
97
118
|
let insetPx = 0;
|
|
98
119
|
let parkPx = 0;
|
|
99
120
|
let measured = [];
|
|
100
121
|
return {
|
|
101
122
|
measure: (labels) => {
|
|
102
|
-
const widths = measureWidths(control, className, labels
|
|
123
|
+
const widths = measureWidths(control, className, labels);
|
|
103
124
|
const styles = getComputedStyle(control);
|
|
104
|
-
|
|
125
|
+
fadeStart = readPxProperty(styles, fadeProperties[0], 0);
|
|
126
|
+
fadeEnd = readPxProperty(styles, fadeProperties[1], 1);
|
|
105
127
|
insetPx = readPxProperty(styles, insetProperty, 0);
|
|
106
128
|
parkPx = readPxProperty(styles, parkProperty, 0);
|
|
107
129
|
measured = labels.map((label, index) => ({
|
|
@@ -112,7 +134,8 @@ function createLabelRider(control, options) {
|
|
|
112
134
|
place: (window) => {
|
|
113
135
|
const layout = layoutRider(measured, {
|
|
114
136
|
...window,
|
|
115
|
-
|
|
137
|
+
fadeEnd,
|
|
138
|
+
fadeStart,
|
|
116
139
|
insetPx,
|
|
117
140
|
parkPx
|
|
118
141
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"marker-rider.js","names":[],"sources":["../../../src/lib/marker-rider.ts"],"sourcesContent":["import { writeKind } from '#lib/marker-band.ts';\nimport { clamp } from '#lib/math.ts';\nimport { readPxProperty } from '#lib/read-px-property.ts';\nimport { requireChild } from '#lib/render.ts';\n\ninterface RiderWindow {\n\tplayheadSeconds: number;\n\tstartSeconds: number;\n\twidthPx: number;\n\twindowSeconds: number;\n}\n\nexport interface RiderView extends RiderWindow {\n\tfadeShare: number;\n\tinsetPx: number;\n\tparkPx: number;\n}\n\nexport interface RiderLabel {\n\tisDimmed: boolean;\n\tkind?: string;\n\tstart: number;\n\ttext: string;\n\twidthPx: number;\n}\n\ninterface LabelPlacement {\n\tindex: number;\n\topacity: number;\n\tshownPx: number;\n\tx: number;\n}\n\ninterface RiderLayout {\n\tarriving?: LabelPlacement;\n\tparked?: LabelPlacement;\n}\n\ninterface LabelSlot {\n\telement: HTMLElement;\n\tkind: string | undefined;\n\topacity: number;\n\tshownPx: number;\n\tx: number;\n}\n\nfunction lineX(seconds: number, view: RiderView): number {\n\treturn ((seconds - view.startSeconds) / view.windowSeconds) * view.widthPx;\n}\n\nfunction parkedPlacement(\n\tlabels: ReadonlyArray<RiderLabel>,\n\tindex: number,\n\t[view, arrivingX]: [RiderView, number],\n): LabelPlacement | undefined {\n\tconst parked = labels[index];\n\tif (!parked) return undefined;\n\n\tconst next = labels[index + 1];\n\tconst x = Math.max(view.parkPx, lineX(parked.start, view) + view.insetPx);\n\tconst approachPx = next ? lineX(next.start, view) - lineX(view.playheadSeconds, view) : Infinity;\n\n\treturn {\n\t\tindex,\n\t\topacity: view.fadeShare > 0 ? clamp(approachPx / (view.widthPx * view.fadeShare), 0, 1) : 1,\n\t\tshownPx: Math.min(parked.widthPx, arrivingX - x, view.widthPx - x),\n\t\tx,\n\t};\n}\n\nfunction isDrawable(length: number): boolean {\n\treturn Number.isFinite(length) && length > 0;\n}\n\nexport function layoutRider(labels: ReadonlyArray<RiderLabel>, view: RiderView): RiderLayout {\n\tif (!isDrawable(view.windowSeconds) || !isDrawable(view.widthPx)) return {};\n\n\tconst parkedIndex = labels.findLastIndex((label) => label.start <= view.playheadSeconds);\n\tconst next = labels[parkedIndex + 1];\n\tconst arrivingX = next ? lineX(next.start, view) + view.insetPx : Infinity;\n\tconst parked = parkedPlacement(labels, parkedIndex, [view, arrivingX]);\n\tconst arriving: LabelPlacement | undefined =\n\t\tnext && arrivingX < view.widthPx\n\t\t\t? {\n\t\t\t\t\tindex: parkedIndex + 1,\n\t\t\t\t\topacity: 1,\n\t\t\t\t\tshownPx: Math.min(next.widthPx, view.widthPx - arrivingX),\n\t\t\t\t\tx: arrivingX,\n\t\t\t\t}\n\t\t\t: undefined;\n\n\treturn { ...(arriving ? { arriving } : {}), ...(parked ? { parked } : {}) };\n}\n\nfunction labelSlot(element: HTMLElement): LabelSlot {\n\treturn { element, kind: undefined, opacity: NaN, shownPx: NaN, x: NaN };\n}\n\nfunction writePlacement(\n\tslot: LabelSlot,\n\t{ opacity, shownPx, x }: LabelPlacement,\n\twidthPx: number,\n): void {\n\tconst { style } = slot.element;\n\n\tif (slot.x !== x) {\n\t\tslot.x = x;\n\t\tstyle.setProperty('translate', `${x.toFixed(2)}px`);\n\t}\n\tif (slot.shownPx !== shownPx) {\n\t\tslot.shownPx = shownPx;\n\t\tstyle.setProperty(\n\t\t\t'clip-path',\n\t\t\tshownPx < widthPx ? `inset(0 calc(100% - ${shownPx.toFixed(2)}px) 0 0)` : '',\n\t\t);\n\t}\n\tif (slot.opacity !== opacity) {\n\t\tslot.opacity = opacity;\n\t\tstyle.setProperty('opacity', opacity < 1 ? opacity.toFixed(3) : '');\n\t}\n}\n\nfunction writeLabel(\n\tslot: LabelSlot,\n\t[labels, colourProperty]: [ReadonlyArray<RiderLabel>, string],\n\tplacement: LabelPlacement | undefined,\n): void {\n\tconst { element } = slot;\n\tconst label = placement && placement.shownPx > 0 ? labels[placement.index] : undefined;\n\n\tif (element.hidden !== !label) element.hidden = !label;\n\tif (!label || !placement) return;\n\n\tif (element.textContent !== label.text) element.textContent = label.text;\n\telement.toggleAttribute('data-sonic-dimmed', label.isDimmed);\n\tif (slot.kind !== label.kind) {\n\t\tslot.kind = label.kind;\n\t\twriteKind(element, label.kind, colourProperty);\n\t}\n\twritePlacement(slot, placement, label.widthPx);\n}\n\nfunction measureWidths(\n\tcontrol: HTMLElement,\n\tclassName: string,\n\ttexts: Array<string>,\n): Array<number> {\n\tconst probes = texts.map((text) => {\n\t\tconst probe = document.createElement('div');\n\n\t\tprobe.className = className;\n\t\tprobe.textContent = text;\n\n\t\treturn probe;\n\t});\n\n\tcontrol.append(...probes);\n\n\tconst widths = probes.map((probe) => probe.getBoundingClientRect().width);\n\n\tfor (const probe of probes) probe.remove();\n\n\treturn widths;\n}\n\nexport function createLabelRider(\n\tcontrol: HTMLElement,\n\toptions: {\n\t\tclassName: string;\n\t\tcolourProperty: `--_sonic-${string}`;\n\t\tfadeProperty: `--_sonic-${string}`;\n\t\tinsetProperty: `--_sonic-${string}`;\n\t\tparkProperty: `--_sonic-${string}`;\n\t},\n): {\n\tmeasure: (labels: ReadonlyArray<Omit<RiderLabel, 'widthPx'>>) => void;\n\tplace: (window: RiderWindow) => void;\n} {\n\tconst { className, colourProperty, fadeProperty, insetProperty, parkProperty } = options;\n\tconst parked = labelSlot(requireChild(control, `.${className}`, HTMLElement));\n\tconst arriving = labelSlot(requireChild(control, `.${className} + .${className}`, HTMLElement));\n\tlet fadeShare = 0.25;\n\tlet insetPx = 0;\n\tlet parkPx = 0;\n\tlet measured: Array<RiderLabel> = [];\n\n\treturn {\n\t\tmeasure: (labels) => {\n\t\t\tconst widths = measureWidths(\n\t\t\t\tcontrol,\n\t\t\t\tclassName,\n\t\t\t\tlabels.map(({ text }) => text),\n\t\t\t);\n\n\t\t\tconst styles = getComputedStyle(control);\n\n\t\t\tfadeShare = readPxProperty(styles, fadeProperty, 0.25);\n\t\t\tinsetPx = readPxProperty(styles, insetProperty, 0);\n\t\t\tparkPx = readPxProperty(styles, parkProperty, 0);\n\t\t\tmeasured = labels\n\t\t\t\t.map((label, index) => ({ ...label, widthPx: widths[index] ?? 0 }))\n\t\t\t\t.toSorted((first, second) => first.start - second.start);\n\t\t},\n\t\tplace: (window) => {\n\t\t\tconst layout = layoutRider(measured, { ...window, fadeShare, insetPx, parkPx });\n\n\t\t\twriteLabel(parked, [measured, colourProperty], layout.parked);\n\t\t\twriteLabel(arriving, [measured, colourProperty], layout.arriving);\n\t\t},\n\t};\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"marker-rider.js","names":[],"sources":["../../../src/lib/marker-rider.ts"],"sourcesContent":["import { writeKind } from '#lib/marker-band.ts';\nimport { clamp } from '#lib/math.ts';\nimport { readPxProperty } from '#lib/read-px-property.ts';\nimport { requireChild } from '#lib/render.ts';\n\ninterface RiderWindow {\n\tisPaged: boolean;\n\tplayheadSeconds: number;\n\tstartSeconds: number;\n\twidthPx: number;\n\twindowSeconds: number;\n}\n\nexport interface RiderView extends RiderWindow {\n\tfadeEnd: number;\n\tfadeStart: number;\n\tinsetPx: number;\n\tparkPx: number;\n}\n\nexport interface RiderLabel {\n\tisDimmed: boolean;\n\tkind?: string;\n\tstart: number;\n\ttext: string;\n\twidthPx: number;\n\twrite?: (element: HTMLElement) => void;\n}\n\ninterface LabelPlacement {\n\tindex: number;\n\topacity: number;\n\tshownPx: number;\n\tx: number;\n}\n\ninterface RiderLayout {\n\tarriving?: LabelPlacement;\n\tparked?: LabelPlacement;\n}\n\ninterface LabelSlot {\n\telement: HTMLElement;\n\tkind: string | undefined;\n\tlabel: RiderLabel | undefined;\n\topacity: number;\n\tshownPx: number;\n\tx: number;\n}\n\nfunction lineX(seconds: number, view: RiderView): number {\n\treturn ((seconds - view.startSeconds) / view.windowSeconds) * view.widthPx;\n}\n\nfunction parkedPlacement(\n\tlabels: ReadonlyArray<RiderLabel>,\n\tindex: number,\n\t[view, arrivingX, [startX, endX]]: [RiderView, number, [number, number]],\n): LabelPlacement | undefined {\n\tconst parked = labels[index];\n\tif (!parked) return undefined;\n\n\tconst next = labels[index + 1];\n\tconst x = Math.max(view.parkPx, lineX(parked.start, view) + view.insetPx);\n\tconst fadePx = startX - endX;\n\tconst leftPx = next ? lineX(next.start, view) - endX : Infinity;\n\n\treturn {\n\t\tindex,\n\t\topacity: fadePx > 0 ? clamp(leftPx / fadePx, 0, 1) : 1,\n\t\tshownPx: Math.min(parked.widthPx, arrivingX - x, view.widthPx - x),\n\t\tx,\n\t};\n}\n\nfunction isDrawable(length: number): boolean {\n\treturn Number.isFinite(length) && length > 0;\n}\n\n// Where the next line is as the fade starts and ends; 0 at the playhead, 1 with its label on the park\nfunction fadeLines(view: RiderView): [number, number] {\n\tconst playheadX = lineX(view.playheadSeconds, view);\n\t// Nothing rides in on a still page, so the label changes as the playhead crosses\n\tif (view.isPaged) return [playheadX, playheadX];\n\n\tconst stretchPx = view.parkPx - view.insetPx - playheadX;\n\n\treturn [\n\t\tplayheadX + clamp(view.fadeStart, 0, 1) * stretchPx,\n\t\tplayheadX + clamp(view.fadeEnd, 0, 1) * stretchPx,\n\t];\n}\n\nexport function layoutRider(labels: ReadonlyArray<RiderLabel>, view: RiderView): RiderLayout {\n\tif (!isDrawable(view.windowSeconds) || !isDrawable(view.widthPx)) return {};\n\n\tconst fade = fadeLines(view);\n\tconst parkedIndex = labels.findLastIndex((label) => lineX(label.start, view) <= fade[1]);\n\tconst next = labels[parkedIndex + 1];\n\tconst arrivingX = next ? lineX(next.start, view) + view.insetPx : Infinity;\n\tconst parked = parkedPlacement(labels, parkedIndex, [view, arrivingX, fade]);\n\tconst arriving: LabelPlacement | undefined =\n\t\tnext && arrivingX < view.widthPx\n\t\t\t? {\n\t\t\t\t\tindex: parkedIndex + 1,\n\t\t\t\t\topacity: 1,\n\t\t\t\t\tshownPx: Math.min(next.widthPx, view.widthPx - arrivingX),\n\t\t\t\t\tx: arrivingX,\n\t\t\t\t}\n\t\t\t: undefined;\n\n\treturn { ...(arriving ? { arriving } : {}), ...(parked ? { parked } : {}) };\n}\n\nfunction labelSlot(element: HTMLElement): LabelSlot {\n\treturn { element, kind: undefined, label: undefined, opacity: NaN, shownPx: NaN, x: NaN };\n}\n\nfunction writeContent(element: HTMLElement, { text, write }: Omit<RiderLabel, 'widthPx'>): void {\n\telement.textContent = write ? '' : text;\n\twrite?.(element);\n}\n\nfunction writePlacement(\n\tslot: LabelSlot,\n\t{ opacity, shownPx, x }: LabelPlacement,\n\twidthPx: number,\n): void {\n\tconst { style } = slot.element;\n\n\tif (slot.x !== x) {\n\t\tslot.x = x;\n\t\tstyle.setProperty('translate', `${x.toFixed(2)}px`);\n\t}\n\tif (slot.shownPx !== shownPx) {\n\t\tslot.shownPx = shownPx;\n\t\tstyle.setProperty(\n\t\t\t'clip-path',\n\t\t\tshownPx < widthPx ? `inset(0 calc(100% - ${shownPx.toFixed(2)}px) 0 0)` : '',\n\t\t);\n\t}\n\tif (slot.opacity !== opacity) {\n\t\tslot.opacity = opacity;\n\t\tstyle.setProperty('opacity', opacity < 1 ? opacity.toFixed(3) : '');\n\t}\n}\n\nfunction writeLabel(\n\tslot: LabelSlot,\n\t[labels, colourProperty]: [ReadonlyArray<RiderLabel>, string],\n\tplacement: LabelPlacement | undefined,\n): void {\n\tconst { element } = slot;\n\tconst label = placement && placement.shownPx > 0 ? labels[placement.index] : undefined;\n\n\tif (element.hidden !== !label) element.hidden = !label;\n\tif (!label || !placement) return;\n\n\tif (slot.label !== label) {\n\t\tslot.label = label;\n\t\twriteContent(element, label);\n\t}\n\telement.toggleAttribute('data-sonic-dimmed', label.isDimmed);\n\tif (slot.kind !== label.kind) {\n\t\tslot.kind = label.kind;\n\t\twriteKind(element, label.kind, colourProperty);\n\t}\n\twritePlacement(slot, placement, label.widthPx);\n}\n\nfunction measureWidths(\n\tcontrol: HTMLElement,\n\tclassName: string,\n\tlabels: ReadonlyArray<Omit<RiderLabel, 'widthPx'>>,\n): Array<number> {\n\tconst probes = labels.map((label) => {\n\t\tconst probe = document.createElement('div');\n\n\t\tprobe.className = className;\n\t\twriteContent(probe, label);\n\n\t\treturn probe;\n\t});\n\n\tcontrol.append(...probes);\n\n\tconst widths = probes.map((probe) => probe.getBoundingClientRect().width);\n\n\tfor (const probe of probes) probe.remove();\n\n\treturn widths;\n}\n\nexport function createLabelRider(\n\tcontrol: HTMLElement,\n\toptions: {\n\t\tclassName: string;\n\t\tcolourProperty: `--_sonic-${string}`;\n\t\tfadeProperties: [start: `--_sonic-${string}`, end: `--_sonic-${string}`];\n\t\tinsetProperty: `--_sonic-${string}`;\n\t\tparkProperty: `--_sonic-${string}`;\n\t},\n): {\n\tmeasure: (labels: ReadonlyArray<Omit<RiderLabel, 'widthPx'>>) => void;\n\tplace: (window: RiderWindow) => void;\n} {\n\tconst { className, colourProperty, fadeProperties, insetProperty, parkProperty } = options;\n\tconst parked = labelSlot(requireChild(control, `.${className}`, HTMLElement));\n\tconst arriving = labelSlot(requireChild(control, `.${className} + .${className}`, HTMLElement));\n\tlet fadeStart = 0;\n\tlet fadeEnd = 1;\n\tlet insetPx = 0;\n\tlet parkPx = 0;\n\tlet measured: Array<RiderLabel> = [];\n\n\treturn {\n\t\tmeasure: (labels) => {\n\t\t\tconst widths = measureWidths(control, className, labels);\n\n\t\t\tconst styles = getComputedStyle(control);\n\n\t\t\tfadeStart = readPxProperty(styles, fadeProperties[0], 0);\n\t\t\tfadeEnd = readPxProperty(styles, fadeProperties[1], 1);\n\t\t\tinsetPx = readPxProperty(styles, insetProperty, 0);\n\t\t\tparkPx = readPxProperty(styles, parkProperty, 0);\n\t\t\tmeasured = labels\n\t\t\t\t.map((label, index) => ({ ...label, widthPx: widths[index] ?? 0 }))\n\t\t\t\t.toSorted((first, second) => first.start - second.start);\n\t\t},\n\t\tplace: (window) => {\n\t\t\tconst layout = layoutRider(measured, { ...window, fadeEnd, fadeStart, insetPx, parkPx });\n\n\t\t\twriteLabel(parked, [measured, colourProperty], layout.parked);\n\t\t\twriteLabel(arriving, [measured, colourProperty], layout.arriving);\n\t\t},\n\t};\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
//#region src/lib/peaks-request.d.ts
|
|
2
|
+
/** Return the same promise for a span already on its way; a new promise per call repaints forever */
|
|
3
|
+
export type PeaksRequest = (fromSeconds: number, toSeconds: number) => Iterable<Promise<unknown>> | Promise<unknown> | undefined;
|
|
4
|
+
//#endregion
|
|
5
|
+
//# sourceMappingURL=peaks-request.d.ts.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
//#region src/lib/peaks-request.ts
|
|
2
|
+
function createPeaksRequest(repaint) {
|
|
3
|
+
const awaited = /* @__PURE__ */ new WeakSet();
|
|
4
|
+
let askedKey;
|
|
5
|
+
let isAsking = false;
|
|
6
|
+
const changed = () => {
|
|
7
|
+
if (!isAsking) askedKey = void 0;
|
|
8
|
+
repaint();
|
|
9
|
+
};
|
|
10
|
+
const changedAfter = async (asked) => {
|
|
11
|
+
await Promise.allSettled([asked]);
|
|
12
|
+
changed();
|
|
13
|
+
};
|
|
14
|
+
const call = (request, wanted) => {
|
|
15
|
+
isAsking = true;
|
|
16
|
+
try {
|
|
17
|
+
return request?.(...wanted);
|
|
18
|
+
} finally {
|
|
19
|
+
isAsking = false;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
return {
|
|
23
|
+
ask: (request, wanted) => {
|
|
24
|
+
const key = wanted?.join(":");
|
|
25
|
+
if (!wanted || key === askedKey) return;
|
|
26
|
+
askedKey = key;
|
|
27
|
+
const asked = call(request, wanted);
|
|
28
|
+
const promises = asked instanceof Promise ? [asked] : asked ?? [];
|
|
29
|
+
for (const promise of promises) {
|
|
30
|
+
if (awaited.has(promise)) continue;
|
|
31
|
+
awaited.add(promise);
|
|
32
|
+
changedAfter(promise);
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
changed,
|
|
36
|
+
reset: () => {
|
|
37
|
+
askedKey = void 0;
|
|
38
|
+
repaint();
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
//#endregion
|
|
44
|
+
export { createPeaksRequest };
|
|
45
|
+
//# sourceMappingURL=peaks-request.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"peaks-request.js","names":[],"sources":["../../../src/lib/peaks-request.ts"],"sourcesContent":["/** Return the same promise for a span already on its way; a new promise per call repaints forever */\nexport type PeaksRequest = (\n\tfromSeconds: number,\n\ttoSeconds: number,\n) => Iterable<Promise<unknown>> | Promise<unknown> | undefined;\n\ninterface PeaksAsker {\n\task: (request: PeaksRequest | undefined, wanted: [number, number] | undefined) => void;\n\tchanged: () => void;\n\treset: () => void;\n}\n\nexport function createPeaksRequest(repaint: () => void): PeaksAsker {\n\tconst awaited = new WeakSet<Promise<unknown>>();\n\tlet askedKey: string | undefined;\n\tlet isAsking = false;\n\n\t// A write from inside the consumer's call repaints without asking for the same window again\n\tconst changed = (): void => {\n\t\tif (!isAsking) askedKey = undefined;\n\t\trepaint();\n\t};\n\tconst changedAfter = async (asked: Promise<unknown>): Promise<void> => {\n\t\tawait Promise.allSettled([asked]);\n\t\tchanged();\n\t};\n\n\tconst call = (request: PeaksRequest | undefined, wanted: [number, number]) => {\n\t\tisAsking = true;\n\t\ttry {\n\t\t\treturn request?.(...wanted);\n\t\t} finally {\n\t\t\tisAsking = false;\n\t\t}\n\t};\n\n\treturn {\n\t\task: (request, wanted) => {\n\t\t\tconst key = wanted?.join(':');\n\t\t\tif (!wanted || key === askedKey) return;\n\n\t\t\taskedKey = key;\n\n\t\t\tconst asked = call(request, wanted);\n\t\t\tconst promises = asked instanceof Promise ? [asked] : (asked ?? []);\n\n\t\t\tfor (const promise of promises) {\n\t\t\t\tif (awaited.has(promise)) continue;\n\n\t\t\t\tawaited.add(promise);\n\t\t\t\tvoid changedAfter(promise);\n\t\t\t}\n\t\t},\n\t\tchanged,\n\t\treset: () => {\n\t\t\taskedKey = undefined;\n\t\t\trepaint();\n\t\t},\n\t};\n}\n"],"mappings":";AAYA,SAAgB,mBAAmB,SAAiC;CACnE,MAAM,0BAAU,IAAI,QAA0B;CAC9C,IAAI;CACJ,IAAI,WAAW;CAGf,MAAM,gBAAsB;EAC3B,IAAI,CAAC,UAAU,WAAW;EAC1B,QAAQ;CACT;CACA,MAAM,eAAe,OAAO,UAA2C;EACtE,MAAM,QAAQ,WAAW,CAAC,KAAK,CAAC;EAChC,QAAQ;CACT;CAEA,MAAM,QAAQ,SAAmC,WAA6B;EAC7E,WAAW;EACX,IAAI;GACH,OAAO,UAAU,GAAG,MAAM;EAC3B,UAAU;GACT,WAAW;EACZ;CACD;CAEA,OAAO;EACN,MAAM,SAAS,WAAW;GACzB,MAAM,MAAM,QAAQ,KAAK,GAAG;GAC5B,IAAI,CAAC,UAAU,QAAQ,UAAU;GAEjC,WAAW;GAEX,MAAM,QAAQ,KAAK,SAAS,MAAM;GAClC,MAAM,WAAW,iBAAiB,UAAU,CAAC,KAAK,IAAK,SAAS,CAAC;GAEjE,KAAK,MAAM,WAAW,UAAU;IAC/B,IAAI,QAAQ,IAAI,OAAO,GAAG;IAE1B,QAAQ,IAAI,OAAO;IACnB,AAAK,aAAa,OAAO;GAC1B;EACD;EACA;EACA,aAAa;GACZ,WAAW;GACX,QAAQ;EACT;CACD;AACD"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { clamp } from "./math.js";
|
|
2
|
+
|
|
3
|
+
//#region src/lib/positions.ts
|
|
4
|
+
const keySteps = /* @__PURE__ */ new Map([
|
|
5
|
+
["ArrowDown", 1],
|
|
6
|
+
["ArrowLeft", -1],
|
|
7
|
+
["ArrowRight", 1],
|
|
8
|
+
["ArrowUp", -1]
|
|
9
|
+
]);
|
|
10
|
+
function indexOrUndefined(index) {
|
|
11
|
+
return index < 0 ? void 0 : index;
|
|
12
|
+
}
|
|
13
|
+
function isEnabled(position) {
|
|
14
|
+
return !position.isDisabled;
|
|
15
|
+
}
|
|
16
|
+
function enabledEnds(positions) {
|
|
17
|
+
return [positions.findIndex(isEnabled), positions.findLastIndex(isEnabled)];
|
|
18
|
+
}
|
|
19
|
+
function stepFrom(positions, from, { isWrapping, step }) {
|
|
20
|
+
const count = positions.length;
|
|
21
|
+
let index = from;
|
|
22
|
+
for (const _ of positions) {
|
|
23
|
+
const next = index + step;
|
|
24
|
+
index = isWrapping ? (next + count) % count : clamp(next, 0, count - 1);
|
|
25
|
+
if (positions[index]?.isDisabled === false) return index;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function keyTarget(positions, from, { isWrapping, key }) {
|
|
29
|
+
const [first, last] = enabledEnds(positions);
|
|
30
|
+
if (key === "Home") return indexOrUndefined(first);
|
|
31
|
+
if (key === "End") return indexOrUndefined(last);
|
|
32
|
+
const step = keySteps.get(key);
|
|
33
|
+
if (step === void 0) return void 0;
|
|
34
|
+
return stepFrom(positions, from, {
|
|
35
|
+
isWrapping,
|
|
36
|
+
step
|
|
37
|
+
}) ?? from;
|
|
38
|
+
}
|
|
39
|
+
function isMomentaryAt(positions, index) {
|
|
40
|
+
return (index === 0 || index === positions.length - 1) && positions.length > 1 && positions[index]?.isMomentary === true;
|
|
41
|
+
}
|
|
42
|
+
function pressTarget(positions, checked, pressed) {
|
|
43
|
+
return positions.length === 2 && checked >= 0 ? 1 - checked : pressed;
|
|
44
|
+
}
|
|
45
|
+
function soleStep(positions, from) {
|
|
46
|
+
const ways = [-1, 1].map((step) => stepFrom(positions, from, {
|
|
47
|
+
isWrapping: false,
|
|
48
|
+
step
|
|
49
|
+
})).filter((to) => to !== void 0 && to !== from);
|
|
50
|
+
return ways.length === 1 ? ways[0] : void 0;
|
|
51
|
+
}
|
|
52
|
+
function restOf(positions, held) {
|
|
53
|
+
if (!isMomentaryAt(positions, held)) return void 0;
|
|
54
|
+
return held === 0 ? 1 : held - 1;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
//#endregion
|
|
58
|
+
export { enabledEnds, isMomentaryAt, keyTarget, pressTarget, restOf, soleStep, stepFrom };
|
|
59
|
+
//# sourceMappingURL=positions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"positions.js","names":[],"sources":["../../../src/lib/positions.ts"],"sourcesContent":["import { clamp } from '#lib/math.ts';\n\nexport interface Position {\n\tisDisabled: boolean;\n\tisMomentary: boolean;\n}\n\ntype Positions = ReadonlyArray<Position>;\n\n// RTL is not mirrored\nconst keySteps = new Map([\n\t['ArrowDown', 1],\n\t['ArrowLeft', -1],\n\t['ArrowRight', 1],\n\t['ArrowUp', -1],\n]);\n\nfunction indexOrUndefined(index: number): number | undefined {\n\treturn index < 0 ? undefined : index;\n}\n\nfunction isEnabled(position: Position): boolean {\n\treturn !position.isDisabled;\n}\n\nexport function enabledEnds(positions: Positions): [number, number] {\n\treturn [positions.findIndex(isEnabled), positions.findLastIndex(isEnabled)];\n}\n\nexport function stepFrom(\n\tpositions: Positions,\n\tfrom: number,\n\t{ isWrapping, step }: { isWrapping: boolean; step: number },\n): number | undefined {\n\tconst count = positions.length;\n\tlet index = from;\n\n\tfor (const _ of positions) {\n\t\tconst next = index + step;\n\n\t\tindex = isWrapping ? (next + count) % count : clamp(next, 0, count - 1);\n\t\tif (positions[index]?.isDisabled === false) return index;\n\t}\n\n\treturn undefined;\n}\n\nexport function keyTarget(\n\tpositions: Positions,\n\tfrom: number,\n\t{ isWrapping, key }: { isWrapping: boolean; key: string },\n): number | undefined {\n\tconst [first, last] = enabledEnds(positions);\n\n\tif (key === 'Home') return indexOrUndefined(first);\n\tif (key === 'End') return indexOrUndefined(last);\n\n\tconst step = keySteps.get(key);\n\tif (step === undefined) return undefined;\n\n\treturn stepFrom(positions, from, { isWrapping, step }) ?? from;\n}\n\nexport function isMomentaryAt(positions: Positions, index: number): boolean {\n\tconst isEnd = index === 0 || index === positions.length - 1;\n\n\treturn isEnd && positions.length > 1 && positions[index]?.isMomentary === true;\n}\n\nexport function pressTarget(positions: Positions, checked: number, pressed: number): number {\n\treturn positions.length === 2 && checked >= 0 ? 1 - checked : pressed;\n}\n\nexport function soleStep(positions: Positions, from: number): number | undefined {\n\tconst ways = [-1, 1]\n\t\t.map((step) => stepFrom(positions, from, { isWrapping: false, step }))\n\t\t.filter((to) => to !== undefined && to !== from);\n\n\treturn ways.length === 1 ? ways[0] : undefined;\n}\n\nexport function restOf(positions: Positions, held: number): number | undefined {\n\tif (!isMomentaryAt(positions, held)) return undefined;\n\n\treturn held === 0 ? 1 : held - 1;\n}\n"],"mappings":";;;AAUA,MAAM,2BAAW,IAAI,IAAI;CACxB,CAAC,aAAa,CAAC;CACf,CAAC,aAAa,EAAE;CAChB,CAAC,cAAc,CAAC;CAChB,CAAC,WAAW,EAAE;AACf,CAAC;AAED,SAAS,iBAAiB,OAAmC;CAC5D,OAAO,QAAQ,IAAI,SAAY;AAChC;AAEA,SAAS,UAAU,UAA6B;CAC/C,OAAO,CAAC,SAAS;AAClB;AAEA,SAAgB,YAAY,WAAwC;CACnE,OAAO,CAAC,UAAU,UAAU,SAAS,GAAG,UAAU,cAAc,SAAS,CAAC;AAC3E;AAEA,SAAgB,SACf,WACA,MACA,EAAE,YAAY,QACO;CACrB,MAAM,QAAQ,UAAU;CACxB,IAAI,QAAQ;CAEZ,KAAK,MAAM,KAAK,WAAW;EAC1B,MAAM,OAAO,QAAQ;EAErB,QAAQ,cAAc,OAAO,SAAS,QAAQ,MAAM,MAAM,GAAG,QAAQ,CAAC;EACtE,IAAI,UAAU,MAAM,EAAE,eAAe,OAAO,OAAO;CACpD;AAGD;AAEA,SAAgB,UACf,WACA,MACA,EAAE,YAAY,OACO;CACrB,MAAM,CAAC,OAAO,QAAQ,YAAY,SAAS;CAE3C,IAAI,QAAQ,QAAQ,OAAO,iBAAiB,KAAK;CACjD,IAAI,QAAQ,OAAO,OAAO,iBAAiB,IAAI;CAE/C,MAAM,OAAO,SAAS,IAAI,GAAG;CAC7B,IAAI,SAAS,QAAW,OAAO;CAE/B,OAAO,SAAS,WAAW,MAAM;EAAE;EAAY;CAAK,CAAC,KAAK;AAC3D;AAEA,SAAgB,cAAc,WAAsB,OAAwB;CAG3E,QAFc,UAAU,KAAK,UAAU,UAAU,SAAS,MAE1C,UAAU,SAAS,KAAK,UAAU,MAAM,EAAE,gBAAgB;AAC3E;AAEA,SAAgB,YAAY,WAAsB,SAAiB,SAAyB;CAC3F,OAAO,UAAU,WAAW,KAAK,WAAW,IAAI,IAAI,UAAU;AAC/D;AAEA,SAAgB,SAAS,WAAsB,MAAkC;CAChF,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,CAClB,KAAK,SAAS,SAAS,WAAW,MAAM;EAAE,YAAY;EAAO;CAAK,CAAC,CAAC,CAAC,CACrE,QAAQ,OAAO,OAAO,UAAa,OAAO,IAAI;CAEhD,OAAO,KAAK,WAAW,IAAI,KAAK,KAAK;AACtC;AAEA,SAAgB,OAAO,WAAsB,MAAkC;CAC9E,IAAI,CAAC,cAAc,WAAW,IAAI,GAAG,OAAO;CAE5C,OAAO,SAAS,IAAI,IAAI,OAAO;AAChC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region src/lib/scrub.ts
|
|
2
|
+
function scrubRegions(state, value, mapping) {
|
|
3
|
+
const played = mapping.proportionOf(state.played);
|
|
4
|
+
const reached = mapping.proportionOf(value);
|
|
5
|
+
return {
|
|
6
|
+
played,
|
|
7
|
+
scrub: state.from !== void 0 && state.isRevealed && reached !== played ? reached : void 0
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
//#endregion
|
|
12
|
+
export { scrubRegions };
|
|
13
|
+
//# sourceMappingURL=scrub.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scrub.js","names":[],"sources":["../../../src/lib/scrub.ts"],"sourcesContent":["import type { ValueMapping } from '#lib/value-mapping.ts';\n\nexport interface ScrubState {\n\tfrom: number | undefined;\n\tisRevealed: boolean;\n\tplayed: number;\n}\n\nexport function scrubRegions(\n\tstate: ScrubState,\n\tvalue: number,\n\tmapping: ValueMapping,\n): { played: number; scrub: number | undefined } {\n\tconst played = mapping.proportionOf(state.played);\n\tconst reached = mapping.proportionOf(value);\n\tconst isScrubbing = state.from !== undefined && state.isRevealed && reached !== played;\n\n\treturn { played, scrub: isScrubbing ? reached : undefined };\n}\n"],"mappings":";AAQA,SAAgB,aACf,OACA,OACA,SACgD;CAChD,MAAM,SAAS,QAAQ,aAAa,MAAM,MAAM;CAChD,MAAM,UAAU,QAAQ,aAAa,KAAK;CAG1C,OAAO;EAAE;EAAQ,OAFG,MAAM,SAAS,UAAa,MAAM,cAAc,YAAY,SAE1C,UAAU;CAAU;AAC3D"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"time-regions.js","names":[],"sources":["../../../src/lib/time-regions.ts"],"sourcesContent":["
|
|
1
|
+
{"version":3,"file":"time-regions.js","names":[],"sources":["../../../src/lib/time-regions.ts"],"sourcesContent":["/** A media element's `buffered` or `seekable` as it is, or any list of start and end pairs */\nexport type TimeRegions = Iterable<readonly [number, number]> | TimeRanges;\n\nexport function readRegions(regions: TimeRegions): Array<[number, number]> {\n\tif ('start' in regions) {\n\t\treturn Array.from({ length: regions.length }, (_entry, index) => [\n\t\t\tregions.start(index),\n\t\t\tregions.end(index),\n\t\t]);\n\t}\n\n\treturn [...regions].map(([start, end]) => [start, end]);\n}\n\nexport function dueRegions(\n\tregions: ReadonlyArray<[number, number]>,\n\tlistedMs: ReadonlyMap<string, number>,\n\t[nowMs, delayMs]: [number, number],\n): Array<[number, number]> {\n\tif (delayMs <= 0) return [...regions];\n\n\treturn regions.filter((region) => nowMs - (listedMs.get(region.join(':')) ?? nowMs) >= delayMs);\n}\n\nexport function sortedRegions(regions: TimeRegions | undefined): Array<[number, number]> {\n\treturn regions ? readRegions(regions).toSorted((first, second) => first[0] - second[0]) : [];\n}\n"],"mappings":";AAGA,SAAgB,YAAY,SAA+C;CAC1E,IAAI,WAAW,SACd,OAAO,MAAM,KAAK,EAAE,QAAQ,QAAQ,OAAO,IAAI,QAAQ,UAAU,CAChE,QAAQ,MAAM,KAAK,GACnB,QAAQ,IAAI,KAAK,CAClB,CAAC;CAGF,OAAO,CAAC,GAAG,OAAO,CAAC,CAAC,KAAK,CAAC,OAAO,SAAS,CAAC,OAAO,GAAG,CAAC;AACvD;AAEA,SAAgB,WACf,SACA,UACA,CAAC,OAAO,UACkB;CAC1B,IAAI,WAAW,GAAG,OAAO,CAAC,GAAG,OAAO;CAEpC,OAAO,QAAQ,QAAQ,WAAW,SAAS,SAAS,IAAI,OAAO,KAAK,GAAG,CAAC,KAAK,UAAU,OAAO;AAC/F;AAEA,SAAgB,cAAc,SAA2D;CACxF,OAAO,UAAU,YAAY,OAAO,CAAC,CAAC,UAAU,OAAO,WAAW,MAAM,KAAK,OAAO,EAAE,IAAI,CAAC;AAC5F"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
//#region src/lib/units.d.ts
|
|
2
|
+
export type Decibels = number;
|
|
3
|
+
export type Milliseconds = number;
|
|
4
|
+
/** CSS pixels drawn for each second of audio */
|
|
5
|
+
export type PixelsPerSecond = number;
|
|
6
|
+
export type Seconds = number;
|
|
7
|
+
//#endregion
|
|
8
|
+
//# sourceMappingURL=units.d.ts.map
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { clamp } from "./math.js";
|
|
2
|
+
import { valueMapping } from "./value-mapping.js";
|
|
3
|
+
|
|
4
|
+
//#region src/lib/value-model.ts
|
|
5
|
+
function createValueModel(readSpec) {
|
|
6
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
7
|
+
let asked;
|
|
8
|
+
let bounds;
|
|
9
|
+
let landing;
|
|
10
|
+
let mapped;
|
|
11
|
+
let value = 0;
|
|
12
|
+
function mapping() {
|
|
13
|
+
if (mapped) return mapped;
|
|
14
|
+
mapped = valueMapping(readSpec());
|
|
15
|
+
return mapped;
|
|
16
|
+
}
|
|
17
|
+
function moveTo(next) {
|
|
18
|
+
if (!Number.isFinite(next)) return false;
|
|
19
|
+
const snapped = mapping().snap(next);
|
|
20
|
+
asked = snapped;
|
|
21
|
+
if (snapped === value) return false;
|
|
22
|
+
value = snapped;
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
input: (next) => moveTo(bounds ? clamp(next, ...bounds) : next),
|
|
27
|
+
land: (target, direction) => landing?.(target, direction) ?? target,
|
|
28
|
+
limit: () => bounds,
|
|
29
|
+
mapping,
|
|
30
|
+
notify: () => {
|
|
31
|
+
for (const listener of listeners) listener();
|
|
32
|
+
},
|
|
33
|
+
respec: (fallback, options) => {
|
|
34
|
+
mapped = void 0;
|
|
35
|
+
if (options?.forgetAsk === true) asked = void 0;
|
|
36
|
+
value = mapping().snap(asked ?? fallback);
|
|
37
|
+
},
|
|
38
|
+
setLanding: (resolve) => {
|
|
39
|
+
landing = resolve;
|
|
40
|
+
},
|
|
41
|
+
setLimit: (next) => {
|
|
42
|
+
bounds = next;
|
|
43
|
+
},
|
|
44
|
+
get value() {
|
|
45
|
+
return value;
|
|
46
|
+
},
|
|
47
|
+
watch: (listener) => {
|
|
48
|
+
listeners.add(listener);
|
|
49
|
+
return () => {
|
|
50
|
+
listeners.delete(listener);
|
|
51
|
+
};
|
|
52
|
+
},
|
|
53
|
+
write: (next) => {
|
|
54
|
+
const hasMoved = moveTo(next);
|
|
55
|
+
if (Number.isFinite(next)) asked = next;
|
|
56
|
+
return hasMoved;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
//#endregion
|
|
62
|
+
export { createValueModel };
|
|
63
|
+
//# sourceMappingURL=value-model.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"value-model.js","names":[],"sources":["../../../src/lib/value-model.ts"],"sourcesContent":["import type { ValueMapping, ValueSpec } from '#lib/value-mapping.ts';\n\nimport { clamp } from '#lib/math.ts';\nimport { valueMapping } from '#lib/value-mapping.ts';\n\ntype ValueLanding = (target: number, direction: -1 | 0 | 1) => number;\n\nexport interface ValueModel {\n\tinput: (next: number) => boolean;\n\tland: (target: number, direction: -1 | 0 | 1) => number;\n\tlimit: () => [number, number] | undefined;\n\tmapping: () => ValueMapping;\n\tnotify: () => void;\n\trespec: (fallback: number, options?: { forgetAsk?: boolean }) => void;\n\tsetLanding: (resolve: undefined | ValueLanding) => void;\n\tsetLimit: (bounds: [number, number] | undefined) => void;\n\treadonly value: number;\n\twatch: (listener: () => void) => () => void;\n\twrite: (next: number) => boolean;\n}\n\nexport function createValueModel(readSpec: () => ValueSpec): ValueModel {\n\tconst listeners = new Set<() => void>();\n\n\tlet asked: number | undefined;\n\tlet bounds: [number, number] | undefined;\n\tlet landing: undefined | ValueLanding;\n\t// Kept until `respec`, so a host observes every attribute its spec reads\n\tlet mapped: undefined | ValueMapping;\n\tlet value = 0;\n\n\tfunction mapping(): ValueMapping {\n\t\tif (mapped) return mapped;\n\n\t\tmapped = valueMapping(readSpec());\n\n\t\treturn mapped;\n\t}\n\n\t// eslint-disable-next-line unicorn/consistent-boolean-name -- a writer; the result says whether the value moved\n\tfunction moveTo(next: number): boolean {\n\t\tif (!Number.isFinite(next)) return false;\n\n\t\tconst snapped = mapping().snap(next);\n\n\t\tasked = snapped;\n\t\tif (snapped === value) return false;\n\n\t\tvalue = snapped;\n\n\t\treturn true;\n\t}\n\n\treturn {\n\t\tinput: (next) => moveTo(bounds ? clamp(next, ...bounds) : next),\n\t\tland: (target, direction) => landing?.(target, direction) ?? target,\n\t\tlimit: () => bounds,\n\t\tmapping,\n\t\tnotify: () => {\n\t\t\tfor (const listener of listeners) listener();\n\t\t},\n\t\trespec: (fallback, options) => {\n\t\t\tmapped = undefined;\n\t\t\tif (options?.forgetAsk === true) asked = undefined;\n\t\t\tvalue = mapping().snap(asked ?? fallback);\n\t\t},\n\t\tsetLanding: (resolve) => {\n\t\t\tlanding = resolve;\n\t\t},\n\t\tsetLimit: (next) => {\n\t\t\tbounds = next;\n\t\t},\n\t\tget value() {\n\t\t\treturn value;\n\t\t},\n\t\twatch: (listener) => {\n\t\t\tlisteners.add(listener);\n\n\t\t\treturn () => {\n\t\t\t\tlisteners.delete(listener);\n\t\t\t};\n\t\t},\n\t\twrite: (next) => {\n\t\t\tconst hasMoved = moveTo(next);\n\n\t\t\tif (Number.isFinite(next)) asked = next;\n\n\t\t\treturn hasMoved;\n\t\t},\n\t};\n}\n"],"mappings":";;;;AAqBA,SAAgB,iBAAiB,UAAuC;CACvE,MAAM,4BAAY,IAAI,IAAgB;CAEtC,IAAI;CACJ,IAAI;CACJ,IAAI;CAEJ,IAAI;CACJ,IAAI,QAAQ;CAEZ,SAAS,UAAwB;EAChC,IAAI,QAAQ,OAAO;EAEnB,SAAS,aAAa,SAAS,CAAC;EAEhC,OAAO;CACR;CAGA,SAAS,OAAO,MAAuB;EACtC,IAAI,CAAC,OAAO,SAAS,IAAI,GAAG,OAAO;EAEnC,MAAM,UAAU,QAAQ,CAAC,CAAC,KAAK,IAAI;EAEnC,QAAQ;EACR,IAAI,YAAY,OAAO,OAAO;EAE9B,QAAQ;EAER,OAAO;CACR;CAEA,OAAO;EACN,QAAQ,SAAS,OAAO,SAAS,MAAM,MAAM,GAAG,MAAM,IAAI,IAAI;EAC9D,OAAO,QAAQ,cAAc,UAAU,QAAQ,SAAS,KAAK;EAC7D,aAAa;EACb;EACA,cAAc;GACb,KAAK,MAAM,YAAY,WAAW,SAAS;EAC5C;EACA,SAAS,UAAU,YAAY;GAC9B,SAAS;GACT,IAAI,SAAS,cAAc,MAAM,QAAQ;GACzC,QAAQ,QAAQ,CAAC,CAAC,KAAK,SAAS,QAAQ;EACzC;EACA,aAAa,YAAY;GACxB,UAAU;EACX;EACA,WAAW,SAAS;GACnB,SAAS;EACV;EACA,IAAI,QAAQ;GACX,OAAO;EACR;EACA,QAAQ,aAAa;GACpB,UAAU,IAAI,QAAQ;GAEtB,aAAa;IACZ,UAAU,OAAO,QAAQ;GAC1B;EACD;EACA,QAAQ,SAAS;GAChB,MAAM,WAAW,OAAO,IAAI;GAE5B,IAAI,OAAO,SAAS,IAAI,GAAG,QAAQ;GAEnC,OAAO;EACR;CACD;AACD"}
|