@xsynaptic/sonic-ui 0.17.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/README.md +122 -19
- package/dist/default/elements/button.js +2 -5
- package/dist/default/elements/dial.js +2 -7
- package/dist/default/elements/display-element.js +9 -12
- package/dist/default/elements/envelope.js +31 -78
- package/dist/default/elements/field-gesture.js +81 -0
- package/dist/default/elements/meter.js +4 -1
- package/dist/default/elements/number.js +0 -7
- 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/segmented.js +0 -5
- package/dist/default/elements/slider.js +11 -20
- package/dist/default/elements/sonic-element.js +3 -1
- package/dist/default/elements/split.js +14 -13
- package/dist/default/elements/switch.js +31 -47
- package/dist/default/elements/toggle.js +20 -49
- package/dist/default/elements/value-element.js +72 -94
- package/dist/default/elements/value-gestures.js +22 -22
- package/dist/default/elements/wave-element.js +16 -20
- package/dist/default/elements/waveform.js +42 -241
- package/dist/default/elements/wavestrip.js +10 -5
- package/dist/default/elements/xy.js +67 -119
- package/dist/default/index.js +2 -1
- package/dist/default/lib/canvas-surface.js +25 -0
- package/dist/default/lib/clock.js +23 -0
- package/dist/default/lib/crossfade.js +1 -0
- package/dist/default/lib/envelope-curve.js +1 -0
- 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/spoken-duration.js +30 -0
- package/dist/default/lib/value-model.js +62 -0
- package/dist/default/lib/waveform-buckets.js +7 -1
- package/dist/default/lib/waveform-scene.js +108 -0
- package/dist/default/lib/zoom-gesture.js +97 -1
- package/dist/dev/elements/button.d.ts +2 -0
- package/dist/dev/elements/button.js +14 -5
- package/dist/dev/elements/button.js.map +1 -1
- package/dist/dev/elements/dial.d.ts +2 -0
- package/dist/dev/elements/dial.js +16 -7
- 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 +11 -9
- package/dist/dev/elements/display-element.js.map +1 -1
- package/dist/dev/elements/envelope.js +48 -78
- 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 +5 -1
- package/dist/dev/elements/meter.js.map +1 -1
- package/dist/dev/elements/number.js +13 -7
- package/dist/dev/elements/number.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/segmented.js +12 -5
- package/dist/dev/elements/segmented.js.map +1 -1
- package/dist/dev/elements/slider.d.ts +4 -2
- package/dist/dev/elements/slider.js +25 -20
- package/dist/dev/elements/slider.js.map +1 -1
- package/dist/dev/elements/sonic-element.d.ts +13 -2
- package/dist/dev/elements/sonic-element.js +63 -11
- package/dist/dev/elements/sonic-element.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 +38 -47
- package/dist/dev/elements/switch.js.map +1 -1
- package/dist/dev/elements/toggle.js +32 -49
- package/dist/dev/elements/toggle.js.map +1 -1
- package/dist/dev/elements/value-element.d.ts +11 -5
- package/dist/dev/elements/value-element.js +72 -94
- 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 +8 -5
- package/dist/dev/elements/wave-element.js +30 -20
- package/dist/dev/elements/wave-element.js.map +1 -1
- package/dist/dev/elements/waveform.d.ts +20 -22
- package/dist/dev/elements/waveform.js +42 -241
- 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 +81 -119
- package/dist/dev/elements/xy.js.map +1 -1
- package/dist/dev/index.d.ts +5 -3
- package/dist/dev/index.js +2 -1
- 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/clock.d.ts +5 -0
- package/dist/dev/lib/clock.js +24 -0
- package/dist/dev/lib/clock.js.map +1 -0
- 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/mirror-children.js.map +1 -1
- package/dist/dev/lib/owned-control.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/spoken-duration.js +31 -0
- package/dist/dev/lib/spoken-duration.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 +2 -1
- package/dist/dev/lib/waveform-buckets.js +7 -1
- 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/skins/amber.css +1 -0
- package/dist/skins/ivory.css +1 -0
- package/dist/skins/lime.css +1 -0
- package/dist/skins/slate.css +1 -0
- package/dist/styles/button.css +45 -11
- package/dist/styles/dial.css +10 -9
- package/dist/styles/envelope.css +10 -4
- package/dist/styles/led.css +5 -1
- package/dist/styles/material/arc.css +3 -2
- package/dist/styles/material/bracket.css +2 -2
- package/dist/styles/material/cap.css +33 -12
- package/dist/styles/material/core.css +4 -0
- package/dist/styles/material/glass.css +1 -1
- package/dist/styles/material/groove.css +4 -3
- package/dist/styles/material/keycap.css +3 -2
- package/dist/styles/material/lens.css +1 -1
- package/dist/styles/material/pane.css +5 -4
- package/dist/styles/material/readout.css +6 -6
- package/dist/styles/material/well.css +2 -2
- package/dist/styles/meter.css +2 -7
- package/dist/styles/number.css +5 -1
- package/dist/styles/panel.css +4 -3
- package/dist/styles/ring.css +1 -1
- package/dist/styles/screen.css +5 -1
- package/dist/styles/segmented.css +17 -4
- package/dist/styles/slider.css +9 -3
- package/dist/styles/spectrum.css +16 -3
- package/dist/styles/switch.css +35 -10
- package/dist/styles/toggle.css +14 -7
- package/dist/styles/waveform.css +17 -5
- package/dist/styles/wavestrip.css +11 -3
- package/dist/styles/xy.css +9 -5
- package/package.json +1 -6
- package/dist/default/dat.js +0 -51
- package/dist/default/lib/asked-value.js +0 -21
- package/dist/default/lib/fill-watch.js +0 -29
- package/dist/dev/dat.d.ts +0 -19
- package/dist/dev/dat.js +0 -52
- package/dist/dev/dat.js.map +0 -1
- 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":";;;AAmDA,SAAS,cAAsB;CAC9B,OAAO,WAAW,oBAAoB;AACvC;AAGA,SAAS,UACR,OACA,KACiC;CACjC,MAAM,SACL,+BAA+B,QAAQ,MAAM,0BAA0B,KAAK;CAC7E,MAAM,MAAM,YAAY;CACxB,IAAI,CAAC,UAAU,KAAK,IAAI,OAAO,aAAa,IAAI,aAAa,GAAG,IAAI,GAAG,OAAO;CAE9E,OAAO,KAAK,IAAI,OAAO,YAAY,IAAI,YAAY,GAAG,IAAI,IAAI,SAAY;AAC3E;AAGA,SAAS,SACR,YACA,MACsB;CAGtB,OAAO,OAAO,YACb,OAAO,QAAQA,UAAK,CAAC,CAAC,SAAS,CAAC,MAAM,cACrC,aAAa,SAAY,CAAC,IAAI,CAAC,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CACtD,CACD;AACD;AAEA,IAAM,gBAAN,MAIqB;CACpB,IAAI,YAAqB;EACxB,OAAO,KAAK;CACb;CAEA,IAAI,OAAoB;EACvB,OAAO,KAAK;CACb;CAGA,AAAS;CAET;CAEA;CAEA,WAAW;CAEX,mBAAmB;CAEnB,aAAa;CAEb;CAEA,AAAS;CAET,AAAS,iBAAiB,WAAW,kCAAkC;CAEvE,QAAqB;EAAE,KAAK,YAAY;EAAG,QAAQ;EAAG,OAAO;CAAE;CAE/D,YAAY,SAAkD;EAC7D,MAAM,EAAE,QAAQ,WAAW;EAE3B,KAAK,WAAW;EAChB,KAAK,WAAW,OAAO,WAAW,IAAI,KAAK;EAC3C,KAAK,gBAAgB;EACrB,KAAK,aAAa;EAClB,KAAK,aAAa;EAClB,KAAK,iBAAiB;EACtB,KAAK,eAAe,iBACnB,gBACM;GACL,KAAK,QAAQ;EACd,GACA,EAAE,OAAO,CACV;EAEA,OAAO,iBACN,uBACM;GACL,KAAK,QAAQ;EACd,GACA,EAAE,OAAO,CACV;EACA,OAAO,iBACN,eACM;GACL,IAAI,KAAK,WAAW,QAAW,qBAAqB,KAAK,MAAM;GAC/D,KAAK,SAAS;EACf,GACA,EAAE,MAAM,KAAK,CACd;EACA,KAAK,UAAU;CAChB;CAEA,aAAmB;EAClB,KAAK,WAAW;EAChB,KAAK,UAAU;CAChB;CAEA,UAAgB;EACf,KAAK,mBAAmB;EACxB,KAAK,WAAW;CACjB;CAEA,eAAqB;EACpB,KAAK,UAAU;CAChB;CAEA,kBAAwB;EACvB,MAAM,EAAE,QAAQ,SAAS,UAAU,CAAC,MAAM,KAAK;EAC/C,MAAM,EAAE,UAAU;EAClB,MAAM,UAAU,CAAC,GAAG,OAAO,OAAO,OAAO,GAAG,GAAG,OAAO,OAAO,OAAO,CAAC;EAErE,MAAM,YAAY,uBAAuB,QAAQ,KAAK,IAAI,CAAC;EAC3D,MAAM,YAAY,uBAAuB,SAAS;EAClD,MAAM,YAAY,8BAA8B,YAAY;EAC5D,MAAM,YAAY,uBAAuB,gBAAgB;CAC1D;CAEA,WAA+E;EAC9E,IAAI,KAAK,SAAS,CAAC,KAAK,kBAAkB,OAAO;GAAE,WAAW;GAAO,MAAM,KAAK;EAAM;EAEtF,OAAO;GAAE,WAAW;GAAM,MAAM,KAAK,MAAM;EAAE;CAC9C;CAEA,MAAM,SAAuB;EAC5B,KAAK,SAAS;EAEd,MAAM,OAAO,KAAK;EAClB,IAAI,KAAK,UAAU,KAAK,KAAK,WAAW,GAAG;EAE3C,MAAM,EAAE,QAAQ,UAAU,KAAK;EAC/B,MAAM,EAAE,WAAW,SAAS,KAAK,SAAS;EAC1C,MAAM,UAAU,KAAK;EAErB,KAAK,QAAQ;EACb,KAAK,WAAW;EAChB,KAAK,mBAAmB;EAExB,IAAI,OAAO,UAAU,KAAK,OAAO,OAAO,QAAQ,KAAK;EACrD,IAAI,OAAO,WAAW,KAAK,QAAQ,OAAO,SAAS,KAAK;EACxD,IAAI,KAAK,UAAU,MAAM,KAAK,UAAU;GAAE;GAAS;GAAS;GAAW;GAAM;EAAK,CAAC;CACpF;CAEA,SAAS,OAAkC;EAC1C,MAAM,CAAC,OAAO,MAAM;EACpB,IAAI,CAAC,KAAK;EAEV,MAAM,SAAS,UAAU,OAAO,GAAG;EAEnC,IAAI,QAAQ;GACX,KAAK,UAAU;GACf,KAAK,QAAQ,OAAO,YAAY,OAAO,SAAS;GAChD;EACD;EAEA,KAAK,UAAU;GAAE,QAAQ,IAAI;GAAW,OAAO,IAAI;EAAW;EAC9D,KAAK,SAAS;CACf;CAEA,eAAqB;EACpB,MAAM,EAAE,QAAQ,WAAW,KAAK;EAChC,MAAM,WAAW,IAAI,gBAAgB,YAAY;GAChD,MAAM,QAAQ,QAAQ,GAAG,EAAE;GAE3B,IAAI,OAAO,KAAK,SAAS,KAAK;EAC/B,CAAC;EAED,IAAI;GACH,SAAS,QAAQ,QAAQ,EAAE,KAAK,2BAA2B,CAAC;EAC7D,QAAQ;GAEP,SAAS,QAAQ,QAAQ,EAAE,KAAK,cAAc,CAAC;EAChD;EACA,OAAO,iBACN,eACM;GACL,SAAS,WAAW;EACrB,GACA,EAAE,MAAM,KAAK,CACd;CACD;CAEA,eAAqB;EACpB,MAAM,EAAE,QAAQ,WAAW,KAAK;EAChC,MAAM,WAAW,IAAI,sBAAsB,YAAY;GACtD,MAAM,QAAQ,QAAQ,GAAG,EAAE;GAC3B,IAAI,CAAC,OAAO;GAEZ,MAAM,aAAa,KAAK;GAExB,KAAK,aAAa,MAAM;GACxB,IAAI,CAAC,MAAM,kBAAkB,KAAK,WAAW,QAAW;IACvD,qBAAqB,KAAK,MAAM;IAChC,KAAK,SAAS;GACf;GAGA,IAAI,CAAC,cAAc,MAAM,gBAAgB,KAAK,QAAQ;EACvD,CAAC;EAED,SAAS,QAAQ,MAAM;EACvB,OAAO,iBACN,eACM;GACL,SAAS,WAAW;EACrB,GACA,EAAE,MAAM,KAAK,CACd;CACD;CAEA,QAA8C;EAC7C,MAAM,EAAE,QAAQ,SAAS,UAAU,CAAC,GAAG,UAAU,CAAC,MAAM,KAAK;EAC7D,MAAM,SAAS,iBAAiB,MAAM;EAEtC,OAAO;GACN,SAAS,SAAS,UAAU,aAC3B,OACE,iBAAiB,QAAQ,CAAC,CAC1B,KAAK,CAAC,CACN,WAAW,sBAAsB,OAAO,KAAK,CAChD;GACA,iBAAiB,KAAK,eAAe;GACrC,SAAS,SAAyB,UAAU,aAAa,eAAe,QAAQ,UAAU,CAAC,CAAC;GAC5F,SAAS,SAA0B,UAAU,aAC5C,eAAe,QAAQ,UAAU,CAAC,CACnC;EACD;CACD;CAEA,WAAiB;EAChB,MAAM,MAAM,KAAK;EACjB,IAAI,CAAC,KAAK;GACT,KAAK,QAAQ,KAAK,MAAM,OAAO,KAAK,MAAM,MAAM;GAChD;EACD;EAEA,MAAM,MAAM,YAAY;EAExB,KAAK,QAAQ,KAAK,MAAM,IAAI,QAAQ,GAAG,GAAG,KAAK,MAAM,IAAI,SAAS,GAAG,CAAC;CACvE;CAEA,QAAQ,OAAe,QAAsB;EAC5C,MAAM,WAAW,KAAK;EACtB,MAAM,MAAM,YAAY;EACxB,IAAI,SAAS,UAAU,SAAS,SAAS,WAAW,UAAU,SAAS,QAAQ,KAAK;EAEpF,KAAK,QAAQ;GAAE;GAAK;GAAQ;EAAM;EAClC,KAAK,SAAS,SAAS,KAAK,KAAK;EACjC,IAAI,SAAS,UAAU,KAAK,SAAS,WAAW,GAAG,KAAK,QAAQ;OAC3D,KAAK,WAAW;CACtB;CAEA,YAAkB;EACjB,IAAI,KAAK,SAAS,OAAO,WAAW,KAAK,WAAW,UAAa,CAAC,KAAK,YAAY;EAEnF,KAAK,SAAS,uBAAuB,YAAY;GAChD,KAAK,MAAM,OAAO;EACnB,CAAC;CACF;CAEA,mBAAyB;EACxB,MAAM,EAAE,WAAW,KAAK;EAExB,WAAW,gBAAgB,OAAO,YAAY,CAAC,EAAE,MAAM,CAAC,CAAC,iBACxD,gBACM;GACL,KAAK,iBAAiB;GACtB,KAAK,SAAS;EACf,GACA;GAAE,MAAM;GAAM;EAAO,CACtB;CACD;AACD;AAEA,SAAgB,YAId,SAA2D;CAC5D,OAAO,IAAI,cAAc,OAAO;AACjC"}
|
|
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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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
//#region src/lib/clock.ts
|
|
2
|
+
const sign = /^[-−]/;
|
|
3
|
+
function formatClock(seconds) {
|
|
4
|
+
const total = Math.floor(Math.abs(seconds));
|
|
5
|
+
const hours = Math.floor(total / 3600);
|
|
6
|
+
const minutes = Math.floor(total / 60) % 60;
|
|
7
|
+
const rest = String(total % 60).padStart(2, "0");
|
|
8
|
+
const prefix = seconds < 0 ? "−" : "";
|
|
9
|
+
if (hours === 0) return `${prefix}${String(minutes)}:${rest}`;
|
|
10
|
+
return `${prefix}${String(hours)}:${String(minutes).padStart(2, "0")}:${rest}`;
|
|
11
|
+
}
|
|
12
|
+
function parseClock(text) {
|
|
13
|
+
const clock = text.trim();
|
|
14
|
+
let seconds = 0;
|
|
15
|
+
for (const part of clock.replace(sign, "").split(":")) {
|
|
16
|
+
if (part.trim() === "") return NaN;
|
|
17
|
+
seconds = seconds * 60 + Number(part);
|
|
18
|
+
}
|
|
19
|
+
return sign.test(clock) ? -seconds : seconds;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
//#endregion
|
|
23
|
+
export { formatClock, parseClock };
|
|
24
|
+
//# sourceMappingURL=clock.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clock.js","names":[],"sources":["../../../src/lib/clock.ts"],"sourcesContent":["const sign = /^[-−]/;\n\nexport function formatClock(seconds: number): string {\n\tconst total = Math.floor(Math.abs(seconds));\n\tconst hours = Math.floor(total / 3600);\n\tconst minutes = Math.floor(total / 60) % 60;\n\tconst rest = String(total % 60).padStart(2, '0');\n\tconst prefix = seconds < 0 ? '−' : '';\n\n\tif (hours === 0) return `${prefix}${String(minutes)}:${rest}`;\n\n\treturn `${prefix}${String(hours)}:${String(minutes).padStart(2, '0')}:${rest}`;\n}\n\n// An emptied entry or an empty part reads as NaN, which the control ignores; `Number('')` is 0\nexport function parseClock(text: string): number {\n\tconst clock = text.trim();\n\tlet seconds = 0;\n\n\tfor (const part of clock.replace(sign, '').split(':')) {\n\t\tif (part.trim() === '') return NaN;\n\n\t\tseconds = seconds * 60 + Number(part);\n\t}\n\n\treturn sign.test(clock) ? -seconds : seconds;\n}\n"],"mappings":";AAAA,MAAM,OAAO;AAEb,SAAgB,YAAY,SAAyB;CACpD,MAAM,QAAQ,KAAK,MAAM,KAAK,IAAI,OAAO,CAAC;CAC1C,MAAM,QAAQ,KAAK,MAAM,QAAQ,IAAI;CACrC,MAAM,UAAU,KAAK,MAAM,QAAQ,EAAE,IAAI;CACzC,MAAM,OAAO,OAAO,QAAQ,EAAE,CAAC,CAAC,SAAS,GAAG,GAAG;CAC/C,MAAM,SAAS,UAAU,IAAI,MAAM;CAEnC,IAAI,UAAU,GAAG,OAAO,GAAG,SAAS,OAAO,OAAO,EAAE,GAAG;CAEvD,OAAO,GAAG,SAAS,OAAO,KAAK,EAAE,GAAG,OAAO,OAAO,CAAC,CAAC,SAAS,GAAG,GAAG,EAAE,GAAG;AACzE;AAGA,SAAgB,WAAW,MAAsB;CAChD,MAAM,QAAQ,KAAK,KAAK;CACxB,IAAI,UAAU;CAEd,KAAK,MAAM,QAAQ,MAAM,QAAQ,MAAM,EAAE,CAAC,CAAC,MAAM,GAAG,GAAG;EACtD,IAAI,KAAK,KAAK,MAAM,IAAI,OAAO;EAE/B,UAAU,UAAU,KAAK,OAAO,IAAI;CACrC;CAEA,OAAO,KAAK,KAAK,KAAK,IAAI,CAAC,UAAU;AACtC"}
|
|
@@ -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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mirror-children.js","names":[],"sources":["../../../src/lib/mirror-children.ts"],"sourcesContent":["import type { MirrorSlots } from '#lib/render.ts';\n\nimport { appendOnce, checkChildren } from '#lib/owned-control.ts';\nimport { attachSlots } from '#lib/render.ts';\n\
|
|
1
|
+
{"version":3,"file":"mirror-children.js","names":[],"sources":["../../../src/lib/mirror-children.ts"],"sourcesContent":["import type { MirrorSlots } from '#lib/render.ts';\n\nimport { appendOnce, checkChildren } from '#lib/owned-control.ts';\nimport { attachSlots } from '#lib/render.ts';\n\ndeclare const __DEV__: boolean;\n\nconst checkedMirrors = new WeakSet<object>();\n\nconst hostSlots = new WeakMap<HTMLElement, MirrorSlots>();\n\nconst interactive = 'a[href], button, input, select, textarea, [tabindex]';\n\ninterface Mirror {\n\tcontrol: Element;\n\tcopy: (original: ChildNode) => Node | undefined;\n\tisCopied?: (child: Node) => boolean;\n\tisPassed?: (child: Node) => boolean;\n\tplace: (copies: Array<Node>) => void;\n}\n\nfunction unmirrorable(original: Node): Element | undefined {\n\tif (!(original instanceof Element)) return undefined;\n\n\treturn [original, ...original.querySelectorAll('*')].find(\n\t\t(element) => element.matches(interactive) || element.localName.includes('-'),\n\t);\n}\n\nfunction checkMirrored(host: HTMLElement, originals: Array<Node>): void {\n\tif (!__DEV__ || checkedMirrors.has(host.constructor)) return;\n\n\tconst [found] = originals.flatMap((original) => unmirrorable(original) ?? []);\n\tif (!found) return;\n\n\tcheckedMirrors.add(host.constructor);\n\tconsole.warn(\n\t\t`<${host.localName}> copies its children into the control, so the <${found.localName}> inside it loses its listeners and state; keep mirrored children static`,\n\t);\n}\n\nfunction childHolding(host: HTMLElement, node: Node): Node | undefined {\n\tlet current: Node | null = node;\n\n\twhile (current && current.parentNode !== host) current = current.parentNode;\n\n\treturn current ?? undefined;\n}\n\nfunction isCopiedChange(\n\thost: HTMLElement,\n\trecord: MutationRecord,\n\t{ control, isCopied }: Required<Pick<Mirror, 'control' | 'isCopied'>>,\n): boolean {\n\tif (record.target === host) {\n\t\treturn (\n\t\t\trecord.removedNodes.length > 0 || [...record.addedNodes].some((node) => node !== control)\n\t\t);\n\t}\n\n\tconst child = childHolding(host, record.target);\n\n\tif (child === undefined) return false;\n\n\treturn isCopied(child) || (record.type === 'attributes' && record.target === child);\n}\n\nexport function mirrorChildren(\n\thost: HTMLElement,\n\t{ control, copy, isCopied = () => true, isPassed = () => false, place }: Mirror,\n\tsignal: AbortSignal,\n): void {\n\tconst slots = hostSlots.get(host) ?? attachSlots(host);\n\tlet copies = new Map<ChildNode, Node | undefined>();\n\n\thostSlots.set(host, slots);\n\tconst mirror = (touched?: Set<Node | undefined>): void => {\n\t\tappendOnce(host, control);\n\n\t\tconst children = [...host.childNodes].filter((child) => child !== control);\n\t\tconst slotted = children.filter(\n\t\t\t(child): child is Element | Text => child instanceof Element || child instanceof Text,\n\t\t);\n\t\tconst kept = copies;\n\t\tconst originals = children.filter((child) => isCopied(child));\n\n\t\tif (__DEV__) {\n\t\t\tcheckMirrored(host, originals);\n\t\t\tcheckChildren(\n\t\t\t\thost,\n\t\t\t\tchildren.filter((child) => !isCopied(child) && !isPassed(child)),\n\t\t\t);\n\t\t}\n\t\tcopies = new Map(\n\t\t\toriginals.map((original) => [\n\t\t\t\toriginal,\n\t\t\t\ttouched && !touched.has(original) && kept.has(original)\n\t\t\t\t\t? kept.get(original)\n\t\t\t\t\t: copy(original),\n\t\t\t]),\n\t\t);\n\t\tplace([...copies.values()].filter((made) => made !== undefined));\n\t\tslots.shown.assign(...slotted.filter((child) => !isCopied(child)), control);\n\t\tslots.kept.assign(...slotted.filter((child) => isCopied(child)));\n\t};\n\t// Each child is watched, never the control, so a value write wakes nothing\n\tconst watch = (): void => {\n\t\tobserver.disconnect();\n\t\tobserver.observe(host, { childList: true });\n\t\tfor (const child of host.childNodes) {\n\t\t\tif (child === control) continue;\n\n\t\t\tobserver.observe(child, {\n\t\t\t\tattributes: true,\n\t\t\t\tcharacterData: true,\n\t\t\t\tchildList: true,\n\t\t\t\tsubtree: true,\n\t\t\t});\n\t\t}\n\t};\n\tconst observer = new MutationObserver((records) => {\n\t\tconst changes = records.filter((record) => isCopiedChange(host, record, { control, isCopied }));\n\t\tif (changes.length === 0) return;\n\n\t\tif (changes.some((record) => record.target === host)) {\n\t\t\tmirror();\n\t\t\twatch();\n\t\t} else mirror(new Set(changes.map((record) => childHolding(host, record.target))));\n\t});\n\n\tmirror();\n\twatch();\n\tsignal.addEventListener(\n\t\t'abort',\n\t\t() => {\n\t\t\tobserver.disconnect();\n\t\t},\n\t\t{ once: true },\n\t);\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"owned-control.js","names":[],"sources":["../../../src/lib/owned-control.ts"],"sourcesContent":["
|
|
1
|
+
{"version":3,"file":"owned-control.js","names":[],"sources":["../../../src/lib/owned-control.ts"],"sourcesContent":["declare const __DEV__: boolean;\n\nconst checkedChildren = new WeakSet<object>();\n\nfunction isContent(child: Node): boolean {\n\tif (child instanceof Element) return true;\n\n\treturn child instanceof Text && child.data.trim() !== '';\n}\n\nexport function appendOnce(host: HTMLElement, control: Element): void {\n\tconst [hook] = control.classList;\n\tconst stale = [...host.children].filter(\n\t\t(child) => child !== control && hook !== undefined && child.classList.contains(hook),\n\t);\n\n\tfor (const child of stale) child.remove();\n\tif (control.parentNode !== host) host.append(control);\n}\n\nexport function checkChildren(host: HTMLElement, undocumented: Array<Node>): void {\n\tif (!__DEV__ || checkedChildren.has(host.constructor)) return;\n\n\tconst found = undocumented.find((child) => isContent(child));\n\tif (!found) return;\n\n\tcheckedChildren.add(host.constructor);\n\tconsole.warn(\n\t\t`<${host.localName}> uses only the children it documents, so the ${\n\t\t\tfound instanceof Element ? `<${found.localName}>` : 'text'\n\t\t} inside it is unsupported`,\n\t);\n}\n"],"mappings":";AAEA,MAAM,kCAAkB,IAAI,QAAgB;AAE5C,SAAS,UAAU,OAAsB;CACxC,IAAI,iBAAiB,SAAS,OAAO;CAErC,OAAO,iBAAiB,QAAQ,MAAM,KAAK,KAAK,MAAM;AACvD;AAEA,SAAgB,WAAW,MAAmB,SAAwB;CACrE,MAAM,CAAC,QAAQ,QAAQ;CACvB,MAAM,QAAQ,CAAC,GAAG,KAAK,QAAQ,CAAC,CAAC,QAC/B,UAAU,UAAU,WAAW,SAAS,UAAa,MAAM,UAAU,SAAS,IAAI,CACpF;CAEA,KAAK,MAAM,SAAS,OAAO,MAAM,OAAO;CACxC,IAAI,QAAQ,eAAe,MAAM,KAAK,OAAO,OAAO;AACrD;AAEA,SAAgB,cAAc,MAAmB,cAAiC;CACjF,IAAgB,gBAAgB,IAAI,KAAK,WAAW,GAAG;CAEvD,MAAM,QAAQ,aAAa,MAAM,UAAU,UAAU,KAAK,CAAC;CAC3D,IAAI,CAAC,OAAO;CAEZ,gBAAgB,IAAI,KAAK,WAAW;CACpC,QAAQ,KACP,IAAI,KAAK,UAAU,gDAClB,iBAAiB,UAAU,IAAI,MAAM,UAAU,KAAK,OACpD,0BACF;AACD"}
|
|
@@ -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"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
//#region src/lib/spoken-duration.ts
|
|
2
|
+
const formatters = /* @__PURE__ */ new Map();
|
|
3
|
+
function formatterFor(language, isZero) {
|
|
4
|
+
const key = `${language}|${String(isZero)}`;
|
|
5
|
+
const cached = formatters.get(key);
|
|
6
|
+
if (cached) return cached;
|
|
7
|
+
const options = {
|
|
8
|
+
secondsDisplay: isZero ? "always" : "auto",
|
|
9
|
+
style: "long"
|
|
10
|
+
};
|
|
11
|
+
let formatter;
|
|
12
|
+
try {
|
|
13
|
+
formatter = new Intl.DurationFormat(language || void 0, options);
|
|
14
|
+
} catch {
|
|
15
|
+
formatter = new Intl.DurationFormat(void 0, options);
|
|
16
|
+
}
|
|
17
|
+
formatters.set(key, formatter);
|
|
18
|
+
return formatter;
|
|
19
|
+
}
|
|
20
|
+
function spokenDuration(seconds, language = "") {
|
|
21
|
+
const total = Math.trunc(seconds);
|
|
22
|
+
return formatterFor(language, total === 0).format({
|
|
23
|
+
hours: Math.trunc(total / 3600),
|
|
24
|
+
minutes: Math.trunc(total / 60) % 60,
|
|
25
|
+
seconds: total % 60
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
//#endregion
|
|
30
|
+
export { spokenDuration };
|
|
31
|
+
//# sourceMappingURL=spoken-duration.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spoken-duration.js","names":[],"sources":["../../../src/lib/spoken-duration.ts"],"sourcesContent":["const formatters = new Map<string, Intl.DurationFormat>();\n\nfunction formatterFor(language: string, isZero: boolean): Intl.DurationFormat {\n\tconst key = `${language}|${String(isZero)}`;\n\tconst cached = formatters.get(key);\n\tif (cached) return cached;\n\n\t// All-zero units format to an empty string unless the seconds are forced\n\tconst options = { secondsDisplay: isZero ? 'always' : 'auto', style: 'long' } as const;\n\tlet formatter: Intl.DurationFormat;\n\n\ttry {\n\t\tformatter = new Intl.DurationFormat(language || undefined, options);\n\t} catch {\n\t\tformatter = new Intl.DurationFormat(undefined, options);\n\t}\n\tformatters.set(key, formatter);\n\n\treturn formatter;\n}\n\nexport function spokenDuration(seconds: number, language = ''): string {\n\t// Truncated as the clock is, so every unit carries one sign; mixed signs throw\n\tconst total = Math.trunc(seconds);\n\n\treturn formatterFor(language, total === 0).format({\n\t\thours: Math.trunc(total / 3600),\n\t\tminutes: Math.trunc(total / 60) % 60,\n\t\tseconds: total % 60,\n\t});\n}\n"],"mappings":";AAAA,MAAM,6BAAa,IAAI,IAAiC;AAExD,SAAS,aAAa,UAAkB,QAAsC;CAC7E,MAAM,MAAM,GAAG,SAAS,GAAG,OAAO,MAAM;CACxC,MAAM,SAAS,WAAW,IAAI,GAAG;CACjC,IAAI,QAAQ,OAAO;CAGnB,MAAM,UAAU;EAAE,gBAAgB,SAAS,WAAW;EAAQ,OAAO;CAAO;CAC5E,IAAI;CAEJ,IAAI;EACH,YAAY,IAAI,KAAK,eAAe,YAAY,QAAW,OAAO;CACnE,QAAQ;EACP,YAAY,IAAI,KAAK,eAAe,QAAW,OAAO;CACvD;CACA,WAAW,IAAI,KAAK,SAAS;CAE7B,OAAO;AACR;AAEA,SAAgB,eAAe,SAAiB,WAAW,IAAY;CAEtE,MAAM,QAAQ,KAAK,MAAM,OAAO;CAEhC,OAAO,aAAa,UAAU,UAAU,CAAC,CAAC,CAAC,OAAO;EACjD,OAAO,KAAK,MAAM,QAAQ,IAAI;EAC9B,SAAS,KAAK,MAAM,QAAQ,EAAE,IAAI;EAClC,SAAS,QAAQ;CAClB,CAAC;AACF"}
|
|
@@ -1 +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"}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
//#region src/lib/waveform-buckets.d.ts
|
|
2
|
+
/** Interleaved min, max per channel per pair; `fullScale` unset follows the array: 128, 32768 or 1 */
|
|
2
3
|
export interface WaveformPeaks {
|
|
3
4
|
channels?: number;
|
|
4
|
-
fullScale
|
|
5
|
+
fullScale?: number;
|
|
5
6
|
pairsPerSecond: number;
|
|
6
7
|
samples: Float32Array | Int8Array | Int16Array;
|
|
7
8
|
}
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
//#region src/lib/waveform-buckets.ts
|
|
2
|
+
function fullScaleOf({ fullScale, samples }) {
|
|
3
|
+
if (fullScale !== void 0) return fullScale;
|
|
4
|
+
if (samples instanceof Int8Array) return 128;
|
|
5
|
+
return samples instanceof Int16Array ? 32768 : 1;
|
|
6
|
+
}
|
|
2
7
|
function foldPairs(peaks, fromPair, toPair) {
|
|
3
8
|
const { samples } = peaks;
|
|
4
9
|
const channels = Math.max(1, peaks.channels ?? 1);
|
|
5
10
|
const stride = channels * 2;
|
|
6
11
|
const pairsTotal = Math.floor(samples.length / stride);
|
|
12
|
+
const fullScale = fullScaleOf(peaks);
|
|
7
13
|
let low = 0;
|
|
8
14
|
let high = 0;
|
|
9
15
|
for (let pair = Math.max(0, fromPair); pair < Math.min(pairsTotal, toPair); pair += 1) for (let channel = 0; channel < channels; channel += 1) {
|
|
@@ -11,7 +17,7 @@ function foldPairs(peaks, fromPair, toPair) {
|
|
|
11
17
|
low = Math.min(low, samples[at] ?? 0);
|
|
12
18
|
high = Math.max(high, samples[at + 1] ?? 0);
|
|
13
19
|
}
|
|
14
|
-
return [low /
|
|
20
|
+
return [low / fullScale, high / fullScale];
|
|
15
21
|
}
|
|
16
22
|
function waveformBuckets(peaks, view) {
|
|
17
23
|
const { pairsPerSecond } = peaks;
|